rustglslvulkanraytracingshader-storage-buffer

The intersection shader reads zeroes out of an SSBO


My intersection shader always reads just zeroes from the SSBO I have. I tried using debugPrintEXT, but it wouldn't print out from within the intersection shader. However, when I try to output any value via the ray payload as the hit colour, all I get is black colour all the time. Initially, I thought there was a problem with how I copied the data, but no, it is all just as good as in the example code, which works (from NVIDIA). Then, the validation layer throws no warnings/errors/nothing. Then I checked in NVIDIA NSight graphics what the buffer has, and it has the data I copied to it, exactly with the layout I wanted. Then I checked the SPIRV instructions, and all of them correctly refer to the offsets of the buffer! I am completely lost now: even the NVIDIA NSight sees the correct values but not the shader!

So, here is the shader code:

// Hitting a triangle.
#version 460 core
#extension GL_EXT_ray_tracing : enable

layout(location = 0) rayPayloadInEXT vec4 payload;
hitAttributeEXT vec2 attributes;
layout(push_constant) uniform rayParams {
  vec3 rayOrigin;
  vec3 rayDir;
  mat4 transformMatrix;
  float maxDistance;
};
struct PhongMaterial {
  float ambient;
  float diffuse;
  float specular;
  float shininess;
  float reflective;
  float transparency;
  float refraction;
  vec4 color;
  float reserved_1;
  vec4 reserved_2;
};

layout(set = 0, binding = 2) readonly buffer Materials {
  PhongMaterial m[];
}
materials;

void main() {
  PhongMaterial material = materials.m[0];
  payload = material.color;
}

Here is the layout of the structure in Rust:

#[repr(C)]
pub struct PhongMaterial {
    pub ambient: f32,
    pub diffuse: f32,
    pub specular: f32,
    pub shininess: f32,
    pub reflective: f32,
    pub transparency: f32,
    pub refraction: f32,
    pub color: ColorRGBA,
    reserved: [u8; 20],
}

The total byte size of the struct is 44 bytes. Aligning to a multiple of 16 results in 48 bytes copy when moving the data from the temporary CPU buffer to the GPU buffer. Here is the screenshot of NVIDIA NSight, which proves the shader must have the data: NSight screenshot Here in the screenshot of NSight, the values are exactly the ones I have in Rust. And here is the shader's SPIRV:

OpName %58 "PhongMaterial"
OpMemberDecorate %58 0 Offset 0
OpMemberDecorate %58 1 Offset 4
OpMemberDecorate %58 2 Offset 8
OpMemberDecorate %58 3 Offset 12
OpMemberDecorate %58 4 Offset 16
OpMemberDecorate %58 5 Offset 20
OpMemberDecorate %58 6 Offset 24
OpMemberDecorate %58 7 Offset 32
OpMemberDecorate %58 8 Offset 48
OpMemberDecorate %58 9 Offset 64

I don't get what is wrong. I tried to play with padding, but it just didn't help, nothing helped with regard to memory alignment. Even so, if it was an alignment issue, I think the shader should have accessed the values, at least the first ones, as those are laid out properly anyway.

Update: it seems that my closest hit shader doesn't even work when the read is performed to the buffer!

void main() {
  PhongMaterial material = materials.m[0];
  if (material.ambient <= 0.1) {
    payload = vec4(1.0, 0.0, 0.0, 1.0);
  } else if (material.ambient <= 0.5) {
    payload = vec4(0.0, 1.0, 0.0, 1.0);
  } else {
    payload = vec4(0.0, 0.0, 1.0, 1.0);
  }

Here, neither of the branch sections is walked into! The "payload" is not set entirely, so the colour I see is pitch black. However, if I don't try to access the "material" buffer and read values out of it, or just put an assignment to the "payload" after these if-elses, the colour I assign is visible:

  if (material.ambient <= 0.1) {
    payload = vec4(1.0, 0.0, 0.0, 1.0);
  } else if (material.ambient <= 0.5) {
    payload = vec4(0.0, 1.0, 0.0, 1.0);
  } else {
    payload = vec4(0.0, 0.0, 1.0, 1.0);
  }
  payload = vec4(1.0, 1.0, 0.0, 1.0);

Besides that, if I set the colour first to that, before the branching, the colour is black again, as if the payload wasn't set!

  payload = vec4(1.0, 1.0, 0.0, 1.0);
  if (material.ambient <= 0.1) {
    payload = vec4(1.0, 0.0, 0.0, 1.0);
  } else if (material.ambient <= 0.5) {
    payload = vec4(0.0, 1.0, 0.0, 1.0);
  } else {
    payload = vec4(0.0, 0.0, 1.0, 1.0);
  }

If I leave just one if and check for whatever number, it is always evaluated to true. Do I see some UB?

Another screenshot from the NSight showing that the buffer is correctly laid out, bound to the hit shader correctly as well: enter image description here

My current guess is that the shader binding table isn't properly set up for the hit shader. But then I don't understand why it is even invoked then.

To whoever decided to vote for closing: The reproduction steps must be clear to everyone who knows vulkan and rust and glsl: use a GPU buffer of the desired type. The NSight proves that it is done correctly, so this of the MCVE is unnecessary. The only thing not working correctly here is the shader, and the code for it serves as MCVE. The desired behaviour is obvious - the data should be read as it is in the buffer instead of zeroes all the time. If you have any questions - ask, don't vote for closing unless it is clear to you. Even if you are sure, notify the author about that instead of silently voting. People may provide any sort of information if asked, but I have already provided all the necessary information.


Solution

  • I solved the problem. There were actually two problems. One was the alignment issue - the colour vector was expected to be indeed at the offset of 32 bytes from the beginning of the chunk, as it was pointed out by @Nicol Bolas. However, this was rather a minor problem. A much larger problem was with the shader binding table. As you can see on my last screenshot in the question, there is no hit shader assigned to the shader binding table. This was a huge problem to debug as all the data is copied out just as correctly as it may get in my code, and the problem was not in how the data is copied to the SBT buffer but rather how the shader groups are created. In the book "Ray Tracing Gems II" it is said that the order of shaders for which we create shader groups doesn't matter. Well, on my machine, it is wrong. First of all, as NVIDIA NSight showed, I had no hit shader attached. Second of all, my miss shader was with index "1" in the group "2", which certainly was odd compared to other applications where the order of indexes and groups is always the same:

    Having read in the book that the order didn't matter, I changed the order of the shader groups I created: the second was the hit shader group instead of the miss shader group as everywhere else.

    As a Vulkan ray-tracing learner, I should point out that this quote from the Ray Tracing Gems II (page 251) book might be misinterpreted:

    ...First, there is no ordering requirement with respect to shader types in the SBT; ray generation, hit, and miss groups can come in any order.

    I interpreted it so that we also don't care how to create the shader groups as well. It seems like the shader records for the shader groups are always in the order of ray generation, miss and then hit groups and the order matters when we create the groups themselves for the ray tracing pipeline.

    One piece of advice for people having the same odd behaviour of shaders while using ray tracing pipeline and shader binding tables: if your shader behaves oddly (producing weird behaviour), even if it is invoked, or when you try to access any data except for the data passed between the shader stages, it is highly likely that the problem is with the shader binding table. Within the same book (Ray Tracing Gems II), it is pointed out that creating/using the SBT and the data for it is quite often done wrong and is a frequent cause of problems.