From 9ed5dcdc27bdc8c7da3959e8b76c5efde2a73fa5 Mon Sep 17 00:00:00 2001 From: Chris Healy Date: Sat, 28 Feb 2026 15:16:13 -0800 Subject: [PATCH] GPU: Use inversesqrt for light distance and half-vector calculations Replace length()/normalize() with inversesqrt() pattern for better performance on mobile GPUs. The inversesqrt instruction is a single hardware operation, while length() requires sqrt(dot()) and normalize() requires inversesqrt internally plus a multiply. Changes: 1. Light distance calculation: Before: distance = length(toLight); toLight /= distance; After: distSq = dot(toLight, toLight); invDist = inversesqrt(distSq); distance = distSq * invDist; toLight *= invDist; 2. Specular half-vector normalization: Before: ldot = dot(normalize(toLight + vec3(0,0,1)), worldnormal); After: halfVec = toLight + vec3(0,0,1); halfInvLen = inversesqrt(dot(halfVec, halfVec)); ldot = dot(halfVec, worldnormal) * halfInvLen; Also reuses distSq in attenuation calculation to avoid recomputing distance*distance. Offline shader compiler results (vertex shader with 4-light ubershader): Mali (malioc): G31 (Bifrost): 44.90 -> 40.50 cycles (-9.8%) G52 (Bifrost): 14.97 -> 13.50 cycles (-9.8%) G310 (Valhall): 13.88 -> 12.88 cycles (-7.2%) G57 (Valhall): 7.60 -> 6.97 cycles (-8.3%) PowerVR (Profiling Compilers): Series 8 GE8300: 322 -> 302 ALU cycles (-6.2%) Series 9 GM9445: 296 -> 280 ALU cycles (-5.4%) Series B BXM: 560 -> 540 instructions (-3.6%) Signed-off-by: Chris Healy --- Common/GPU/ShaderWriter.cpp | 5 ++++- GPU/Common/VertexShaderGenerator.cpp | 32 ++++++++++++++++++---------- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/Common/GPU/ShaderWriter.cpp b/Common/GPU/ShaderWriter.cpp index 19901e0938..ecdee31eb1 100644 --- a/Common/GPU/ShaderWriter.cpp +++ b/Common/GPU/ShaderWriter.cpp @@ -34,7 +34,8 @@ const char * const hlsl_preamble_fs = "#define mediump\n" "#define highp\n" "#define fract frac\n" -"#define mod(x, y) fmod(x, y)\n"; +"#define mod(x, y) fmod(x, y)\n" +"#define inversesqrt rsqrt\n"; static const char * const hlsl_d3d11_preamble_fs = "#define DISCARD discard\n" @@ -61,6 +62,7 @@ static const char * const hlsl_preamble_gs = "#define lowp\n" "#define mediump\n" "#define highp\n" +"#define inversesqrt rsqrt\n" "\n"; static const char * const vulkan_glsl_preamble_gs = @@ -84,6 +86,7 @@ static const char * const hlsl_preamble_vs = "#define lowp\n" "#define mediump\n" "#define highp\n" +"#define inversesqrt rsqrt\n" "\n"; static const char * const semanticNames[] = { diff --git a/GPU/Common/VertexShaderGenerator.cpp b/GPU/Common/VertexShaderGenerator.cpp index b49abbf00e..ced3375970 100644 --- a/GPU/Common/VertexShaderGenerator.cpp +++ b/GPU/Common/VertexShaderGenerator.cpp @@ -923,6 +923,8 @@ bool GenerateVertexShader(const VShaderID &id, char *buffer, const ShaderLanguag } if (distanceNeeded) { WRITE(p, " float distance;\n"); + WRITE(p, " float distSq;\n"); + WRITE(p, " float invDist;\n"); WRITE(p, " lowp float lightScale;\n"); } WRITE(p, " mediump float ldot;\n"); @@ -961,9 +963,11 @@ bool GenerateVertexShader(const VShaderID &id, char *buffer, const ShaderLanguag p.F(" toLight = u_lightpos%s;\n", iStr); p.C(" if (type != 0x0u) {\n"); // GE_LIGHTTYPE_DIRECTIONAL p.F(" toLight -= worldpos;\n"); - p.F(" distance = length(toLight);\n"); - p.F(" toLight /= distance;\n"); - p.F(" attenuation = clamp(1.0 / dot(u_lightatt%s, vec3(1.0, distance, distance*distance)), 0.0, 1.0);\n", iStr); + p.F(" float distSq = dot(toLight, toLight);\n"); + p.F(" float invDist = inversesqrt(distSq);\n"); + p.F(" distance = distSq * invDist;\n"); + p.F(" toLight *= invDist;\n"); + p.F(" attenuation = clamp(1.0 / dot(u_lightatt%s, vec3(1.0, distance, distSq)), 0.0, 1.0);\n", iStr); p.C(" if (type == 0x01u) {\n"); // GE_LIGHTTYPE_POINT p.C(" lightScale = attenuation;\n"); p.C(" } else {\n"); // type must be 0x02 - GE_LIGHTTYPE_SPOT @@ -984,8 +988,9 @@ bool GenerateVertexShader(const VShaderID &id, char *buffer, const ShaderLanguag p.F(" diffuse = (u_lightdiffuse%s * diffuseColor) * max(ldot, 0.0);\n", iStr); p.C(" if (comp == 0x1u && ldot >= 0.0) {\n"); // do specular. note - must allow for the >= case, since the u_matspecular.a <= 0.0 case relies on it. p.C(" if (u_matspecular.a > 0.0) {\n"); - p.C(" ldot = dot(normalize(toLight + vec3(0.0, 0.0, 1.0)), worldnormal);\n"); - p.C(" ldot = pow(max(ldot, 0.0), u_matspecular.a);\n"); + p.C(" vec3 halfVec = toLight + vec3(0.0, 0.0, 1.0);\n"); + p.C(" float halfInvLen = inversesqrt(dot(halfVec, halfVec));\n"); + p.C(" ldot = pow(max(dot(halfVec, worldnormal) * halfInvLen, 0.0), u_matspecular.a);\n"); p.C(" } else {\n"); p.C(" ldot = 1.0;\n"); p.C(" }\n"); @@ -1013,8 +1018,12 @@ bool GenerateVertexShader(const VShaderID &id, char *buffer, const ShaderLanguag p.F(" toLight = u_lightpos%s;\n", iStr); } else { p.F(" toLight = u_lightpos%s - worldpos;\n", iStr); - p.C(" distance = length(toLight);\n"); - p.C(" toLight /= distance;\n"); + // Use inversesqrt() for better performance on mobile GPUs. + // distance = sqrt(distSq), and toLight * inversesqrt(distSq) normalizes it. + p.C(" distSq = dot(toLight, toLight);\n"); + p.C(" invDist = inversesqrt(distSq);\n"); + p.C(" distance = distSq * invDist;\n"); + p.C(" toLight *= invDist;\n"); } bool doSpecular = comp == GE_LIGHTCOMP_BOTH; @@ -1039,13 +1048,13 @@ bool GenerateVertexShader(const VShaderID &id, char *buffer, const ShaderLanguag timesLightScale = ""; break; case GE_LIGHTTYPE_POINT: - p.F(" lightScale = clamp(1.0 / dot(u_lightatt%s, vec3(1.0, distance, distance*distance)), 0.0, 1.0);\n", iStr); + p.F(" lightScale = clamp(1.0 / dot(u_lightatt%s, vec3(1.0, distance, distSq)), 0.0, 1.0);\n", iStr); break; case GE_LIGHTTYPE_SPOT: case GE_LIGHTTYPE_UNKNOWN: p.F(" angle = dot(u_lightdir%s, toLight);\n", iStr, iStr); p.F(" if (angle >= u_lightangle_spotCoef%s.x) {\n", iStr); - p.F(" lightScale = clamp(1.0 / dot(u_lightatt%s, vec3(1.0, distance, distance*distance)), 0.0, 1.0) * (u_lightangle_spotCoef%s.y <= 0.0 ? 1.0 : pow(max(angle, 0.0), u_lightangle_spotCoef%s.y));\n", iStr, iStr, iStr); + p.F(" lightScale = clamp(1.0 / dot(u_lightatt%s, vec3(1.0, distance, distSq)), 0.0, 1.0) * (u_lightangle_spotCoef%s.y <= 0.0 ? 1.0 : pow(max(angle, 0.0), u_lightangle_spotCoef%s.y));\n", iStr, iStr, iStr); p.C(" } else {\n"); p.C(" lightScale = 0.0;\n"); p.C(" }\n"); @@ -1059,8 +1068,9 @@ bool GenerateVertexShader(const VShaderID &id, char *buffer, const ShaderLanguag if (doSpecular) { p.C(" if (ldot >= 0.0) {\n"); p.C(" if (u_matspecular.a > 0.0) {\n"); - p.C(" ldot = dot(normalize(toLight + vec3(0.0, 0.0, 1.0)), worldnormal);\n"); - p.C(" ldot = pow(max(ldot, 0.0), u_matspecular.a);\n"); + p.C(" vec3 halfVec = toLight + vec3(0.0, 0.0, 1.0);\n"); + p.C(" float halfInvLen = inversesqrt(dot(halfVec, halfVec));\n"); + p.C(" ldot = pow(max(dot(halfVec, worldnormal) * halfInvLen, 0.0), u_matspecular.a);\n"); p.C(" } else {\n"); p.C(" ldot = 1.0;\n"); p.C(" }\n");