Vulkan ray-tracing any-hit: (ssboValue & payloadValue) == 0 is true even though each operand is nonzero, only after a g…
Vulkan ray-tracing any-hit: (ssboValue & payloadValue) == 0 is true even though each operand is nonzero, only after a glslang / Vulkan SDK 1.4.335 upgrade
I maintain a Vulkan ray-traced shadow path in a fork of the Godot engine (https://github.com/vorvek/Faster-Godot). After moving the fork to a newer engine base, which bumped glslang to the Vulkan SDK 1.4.335 toolchain, all ray-traced shadows silently disappeared. The raster shadow-map path is unaffected. GLSL is compiled to SPIR-V by glslang at runtime; GPU is an NVIDIA RTX 4080S.
How the shadows work. A shadow ray is traced toward the light with TerminateOnFirstHit | SkipClosestHitShader. An any-hit shader runs for each geometry the ray crosses and decides whether that geometry is an occluder. The suspect filter:
void main() {
uint geometry_idx = gl_InstanceCustomIndexEXT;
GeometryData geom = geometries[geometry_idx]; // std430 SSBO load
bool shadow_ray = is_shadow_ray(payload.packed_bounces_flags);
if (shadow_ray && (geom.layer_mask & payload.rng_state) == 0u) {
ignoreIntersectionEXT; // skip this occluder
return;
}
// ... alpha-test path ...
}
geom.layer_mask(uint) is read from astd430 readonly buffer { GeometryData geometries[]; }, indexed bygl_InstanceCustomIndexEXT.payload.rng_state(uint) carries the light'sshadow_caster_mask; it is packed into the ray payload right before the shadowtraceRayEXTand read back here.- By default both masks are
0xFFFFFFFF, so the AND is nonzero and every occluder casts a shadow.
Symptom: every occluder is skipped, so nothing casts a shadow. A headless render reports the luma of the region that should be shadowed: 0.0 = correct (dark), ~0.69 = broken (fully lit). The ray-traced mode reports ~0.69; raster reports 0.0.
What I verified (every run: freshly recompiled shaders, and I delete the on-disk shader cache before each run, and confirm the generated/compiled shader contains the exact edit):
- No struct layout mismatch. glslang's own reflection reports every field at the exact byte offset the C++ side writes (
shadow_caster_mask@80,layer_mask@108,position@0,emission@16, ...). The 1.4.335 diff only added bfloat types; the std430 vec3/vec4 rules are unchanged. - C++ values are correct. Logged at runtime: light
shadow_caster_mask = 0xFFFFFFFF, occluderlayer_mask = 1. The buffer is uploaded with a plain full-struct memcpy. - The payload carry works. A sentinel packed into
payload.rng_stateround-trips into the any-hit unchanged. - The any-hit really runs on the occluder (changing its condition changes the result), so the shadow ray is traversing the occluder.
The experiments (only the any-hit condition changes between builds; a = geom.layer_mask, b = payload.rng_state):
condition (… ignoreIntersectionEXT; if true) |
result | implies |
|---|---|---|
(a & b) == 0u (original) |
no shadow | (a & b) == 0 |
a == 0u |
shadow OK | a != 0 |
b != 0xFFFFFFFFu |
shadow OK | b == 0xFFFFFFFF |
So independently a != 0 and b == 0xFFFFFFFF, yet (a & b) == 0 evaluates true. That is impossible for fixed values. Materializing both into locals first
uint a = geom.layer_mask;
uint b = payload.rng_state;
if ((a & b) == 0u) { ignoreIntersectionEXT; return; }
does not change the outcome (glslang folds it to the same SPIR-V). Reading either operand alone is correct; only the combined bitwise-AND of the SSBO load and the ray-payload load comes out as zero, and only after the toolchain bump.
Ruled out: stale shader cache (deleted before each run), stale generated shader (verified each build), struct layout divergence (reflection), wrong C++ values (logged), payload not carrying (sentinel round-trips).
Question: what mechanism makes (ssboValue & payloadValue) == 0 evaluate true on the GPU when each operand reads correctly in isolation, appearing only after a glslang / Vulkan SDK 1.4.335 upgrade and only for the unmodified combined expression? Is this a known glslang SPIR-V codegen bug for bitwise-AND of a buffer load and a rayPayloadInEXT load, an NVIDIA driver fold, or a ray-payload aliasing issue I'm missing? What is the robust fix?
Full shaders are under servers/rendering/renderer_rd/shaders/raytracing/ in the repo above (scene_raytracing_raygen.glsl any-hit, raytracing_lights_inc.glsl shadow trace, raytracing_inc.glsl payload struct).