When I use a struct containing an array as a ray tracing payload in Vulkan (using rayPayloadEXT/rayPayloadInEXT with VK_KHR_ray_tracing_pipeline), unexpected behavior occurs.
I have created a simple test program for reproducing this unexpected behavior: https://github.com/chrismile/TestVulkanPayloadArray
Let me shortly describe the problem here.
When a payload struct containing an array like “struct RayPayload { vec4 colors[2]; };” is used, then reading and writing to the array using index variables can trigger faulty behavior. For reading from/writing to the payload from the closest hit shader, the following two statements should be equivalent, but they aren’t in practice. The first one fails to overwrite the value in the array.
Case a): Incorrect result
for (int i = 0; i < 1; i++) {
swap(newColor, payload.colors[i]);
}
Case b): Correct result
for (int i = 0; i < 1; i++) {
swap(newColor, payload.colors[0]);
}
I tested this both on Windows (driver 497.29) and Linux/Ubuntu 20.04 (driver 470.86) on an RTX 3090, and the results are the same. I can’t explain this behavior with anything but a bug in the NVIDIA Vulkan drivers, as the generated SPIR-V code looks as expected in both cases.
You can find the full shader code below.
- Expected behavior: Green background (miss shader), red geometry (closest hit shader).
- Faulty behavior: Green background (miss shader), blue geometry (write in closest hit shader failed).
-- RayGen
#version 460
#extension GL_EXT_ray_tracing : require
layout (binding = 0) uniform CameraSettings {
mat4 inverseViewMatrix;
mat4 inverseProjectionMatrix;
} camera;
layout(binding = 1, rgba8) uniform image2D outputImage;
layout(binding = 2) uniform accelerationStructureEXT topLevelAS;
struct RayPayload {
vec4 colors[2];
};
layout(location = 0) rayPayloadEXT RayPayload payload;
void main() {
vec2 fragNdc = 2.0 * ((vec2(gl_LaunchIDEXT.xy) + vec2(0.5)) / vec2(gl_LaunchSizeEXT.xy)) - 1.0;
vec3 rayOrigin = (camera.inverseViewMatrix * vec4(0.0, 0.0, 0.0, 1.0)).xyz;
vec3 rayTarget = (camera.inverseProjectionMatrix * vec4(fragNdc.xy, 1.0, 1.0)).xyz;
vec3 rayDirection = (camera.inverseViewMatrix * vec4(normalize(rayTarget.xyz), 0.0)).xyz;
for (int i = 0; i < 2; i++) {
payload.colors[i] = vec4(0.0, 0.0, 1.0, 1.0);
}
float tMin = 0.0001f;
float tMax = 1000.0f;
traceRayEXT(topLevelAS, gl_RayFlagsOpaqueEXT, 0xFF, 0, 0, 0, rayOrigin, tMin, rayDirection, tMax, 0);
vec4 color = payload.colors[0];
imageStore(outputImage, ivec2(gl_LaunchIDEXT.xy), color);
}
-- Miss
#version 460
#extension GL_EXT_ray_tracing : require
struct RayPayload {
vec4 colors[2];
};
layout(location = 0) rayPayloadInEXT RayPayload payload;
void main() {
payload.colors[0] = vec4(0.0, 1.0, 0.0, 1.0);
}
-- ClosestHit
#version 460
#extension GL_EXT_ray_tracing : require
struct RayPayload {
vec4 colors[2];
};
layout(location = 0) rayPayloadInEXT RayPayload payload;
void swap(inout vec4 a, inout vec4 b) {
vec4 temp = a;
a = b;
b = temp;
}
void main() {
vec4 newColor = vec4(1.0, 0.0, 0.0, 1.0);
for (int i = 0; i < 1; i++) {
// Doesn't work.
swap(newColor, payload.colors[i]);
// Works.
//swap(newColor, payload.colors[0]);
// Works.
//payload.colors[i] = newColor;
// Doesn't work.
//vec4 temp = payload.colors[i];
//payload.colors[i] = newColor;
//newColor = temp;
// Doesn't work.
//vec4 temp = newColor;
//newColor = payload.colors[i];
//payload.colors[i] = temp;
}
}