Should you use one uniform buffer per material instance, or re-use but update the same? Or use different descriptor sets per material instance?

I'm working on rewriting my engines renderer from OpenGL4 to Vulkan, however I've found myself at a crossroads.

When I'm rendering a mesh, I need to pass material attributes to the shader. This would have to be done with a uniform buffer and a descriptor set, however should I keep one buffer for each material instance, or should I just just have one and update it between calls? Or should I down-right use different descriptor sets for each material instance?

What kind of device/driver limits exist for the number of uniform buffers/descriptor sets you can have, except device memory?

I also need transform matrices, however thinking about using push constants for those (if they can fit in 128 bytes, i.e. two 4x4 matrices).

Posted May 15, 2019, 1:37 PM UTC

98
fiber2•

Check out Dynamic Uniform Buffers.

Right from Vulkan 1.0, vkCmdBindDescriptorSets had the pDynamicOffsets parameter.

The docs for vkCmdBindDescriptorSets.

The docs for maxDescriptorSetUniformBuffers.

u/SaschaWillems has a nice example on his github about Dynamic Uniform Buffers.

  • The device may only allow simultaneous use of 8 Dynamic Offsets across all shader stages in a pipeline. But if you use UPDATE_AFTER_BIND then it doesn't count against that limit.

  • Getting the alignment right is tricky. Look carefully at u/SaschaWillems example.

What this lets you do is make one descriptor set. It points to the start of the uniform buffer.

Then you can call vkCmdBindDescriptorSets as many times as you want but just change the offset to point to the material you need.

This even lets you do more advanced stuff. You could dynamically stream the material parameters into the uniform buffer - doing a partial copy from the host to the GPU overwriting sections of the uniform buffer that you are sure are not being used by the GPU. Having that offset parameter gives you a lot of flexibility without having to make a different descriptor set.

4
fsvantessonOP•

Very cool, thank you!

1
Enkidu420•

I put my MVP and model matrix in the push constant, but I convert the model matrix to 4x3 since model matrix is not a projection matrix, then i use 4 more bytes for a texture index and have 3 leftover ints/floats if I ever need them.

2
fsvantessonOP•

Texture index? Do you somehow also expose a texture array or something through the descriptor set then?

1
Enkidu420•

Yeah. I think its one descriptor set, bound once, with one descriptor per image. In the shader its declared as layout(set = 1, binding = 0) uniform sampler2D colorTex[4]; (or however many you need)

1
yonderbagel•

I did something like this before moving to another approach, but I never ended up testing if it made much of a difference how well-packed the push constant was.

Do you have any tests showing that having your push constants as small as possible makes a difference (or is a bottleneck), or were you just doing it because it's satisfying to pack data more efficiently (that's why I did it without ever actually testing)?

1
Enkidu420•

The only other uniform data I need is bone transforms, so I can get away with just that and the push constant. Performance wise it’s probably no different but it simplifies the code a lot, than if I needed to manage a separate memory and buffer and ranges for each instance of object.

1
pragmojo•

In general it's probably not optimal to bind a separate uniform per-instance.

What I tend to do in general is bind an array of uniforms per frame for all the objects I will render with a given shader. So for instance, that might be an array of model transforms and material ID's. Then with each draw call, I will pass an index into that array as a push-constant, and do a lookup in the vertex shader to get the relevant model transform.

2