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 <cphealy@gmail.com>
This commit is contained in:
Chris Healy authored and cphealy committed 2026-03-08 19:13:41 -07:00
1 parent 161cf0952d
commit 9ed5dcdc27
2 files changed
+25 -12

No files matched your search

+4 -1
View File
@@ -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[] = {
+21 -11
View File
@@ -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");