How does memory and alignment work when passing a UBO?

Hello!

In my C++ code I am trying to send a struct A and a variable B to the fragment shader as a UBO (inside the descriptor set, or course).

  • A” has a bunch of member variables of different size (int, floats, glm::vec3), which makes a total size of 84 bytes.
  • B” has a size of 12 bytes (glm::vec3). Both together make a total of (84 + 12) 96 bytes. Since the “minimum UBO offset alignment” here should be 256 bytes, I reserve that amount of memory for my UBO, copy A and B inside and send this UBO buffer to the shader.

In my shader (my GLSL code) I created the struct A again, and received the data in the following uniform:

layout(set = 0, binding = 1) uniform dataBlock {
     A light;
     B camPos;
} obj;

However, in the shader, when I try to access “camPos” (like in obj.camPos.x) I got wrong values. Then, I thought that this could be due to the alignment of “light” in the shader. Therefore, using the trial and error method, I finally added some padding bytes at the end of my C++ A struct (28 bytes more, making a total of 112). This way, in my GLSL code, I can successfully access the correct values at camPos.

In case it’s relevant, A has the following members: one int, six floats, and five glm::vec3. On the other hand, B is a glm::vec3. In C++, A and B look like this:

struct A
{
	A();
	int a;
	glm::vec3 b;
	glm::vec3 c;
	glm::vec3 d;
	glm::vec3 e;
	glm::vec3 f;
	float g;
	float h;
	float i;
	float j;
	float k;
};

glm::vec3 x;

However, I don’t understand what exactly is happening here. Why are those 24 additional bytes required for correctly accessing B (and not 12, for example)? It shouldn’t be required to use the trial and error method to find the correct padding. What is the logic behind this?

PD:
Ok, I could solve my question. I found out how the shader is aligning my struct. The padding it is applying behaves as follows:

struct A
{
	A();
 	int a;
	float padding1[3];
	glm::vec3 b;
	float padding2[1];
	glm::vec3 c;
	float padding3[1];
	glm::vec3 d;
	float padding4[1];
	glm::vec3 e;
	float padding5[1];
	glm::vec3 f;
	float g;
	float h;
	float i;
	float j;
	float k;
};

It took some hours of testing to find this. Now I can see why I couldn’t access easily some of the members since the padding was a bit different than I expected (for example, I didn’t expect the last members not to be padded).
Note that A size in the shader is 112, which is a multiple of 16 (i.e. 7 = 112 / 16).

Please don’t use vec3 within UBOs/SSBOs. Just use vec4s; it will save you a lot of trouble.

Also, you need to align your members correctly. And it would really help if you actually showed the code instead of describing it.

Thanks for your advise. What rules of alignment should I follow? So, I should align vec3 with 16 bytes, but what about floats, ints or doubles?
I have included the code about A and B. I hope it helps.

As Alfonse said, let’s see that code. Particularly the GLSL. Not just that “uniform” decl, but also the dependent types/structs as well (A,B), and any layout default overrides if present.

I suspect you should read up on the std140 and std430 layouts in the spec, as it sounds like you’re using the former (though you didn’t know it). The following is a good starting point:

…but consider that you’re coding for Vulkan not OpenGL, so shared and packed aren’t supported, and std430 might be an option for uniform blocks if your implementation supports it:

Pay close attention to the vec3 warnings in both the std140 and std430 descriptions.

Note that in GL_KHR_vulkan_glsl, we have:

but again, your implementation may support an extension to allow std430 on uniform blocks as an alternative to the std140 default.