Metal: Fix dynamic uniform buffer offset corruption when rebinding sets

When the same uniform set is bound multiple times within a render pass
(e.g., OPAQUE pass then ALPHA pass with different instance buffers),
the dynamic offset bits were being OR'd together, corrupting the packed
frame indices.

For example, if OPAQUE binds set 1 with frame_idx=1 (0x10) and ALPHA
binds set 1 with frame_idx=2 (0x20), the OR produces 0x30 instead of
the correct 0x20, causing the shader to read from the wrong buffer
offset.

This fix clears the relevant bits for each set being bound before
OR'ing the new values, ensuring only the latest frame indices are used.

Fixes rendering artifacts in the mobile renderer which uses two dynamic
uniforms (uniform buffer + storage buffer) in set 1, unlike the clustered
renderer which only has one.
This commit is contained in:
Stuart Carnie
2026-01-09 13:23:27 +11:00
parent 6f15a05b6c
commit 541f62617f

View File

@@ -722,8 +722,6 @@ void MDCommandBuffer::encodeRenderCommandEncoderWithDescriptor(MTLRenderPassDesc
void MDCommandBuffer::render_bind_uniform_sets(VectorView<RDD::UniformSetID> p_uniform_sets, RDD::ShaderID p_shader, uint32_t p_first_set_index, uint32_t p_set_count, uint32_t p_dynamic_offsets) {
DEV_ASSERT(type == MDCommandBufferStateType::Render);
render.dynamic_offsets |= p_dynamic_offsets;
if (uint32_t new_size = p_first_set_index + p_set_count; render.uniform_sets.size() < new_size) {
uint32_t s = render.uniform_sets.size();
render.uniform_sets.resize(new_size);
@@ -734,6 +732,20 @@ void MDCommandBuffer::render_bind_uniform_sets(VectorView<RDD::UniformSetID> p_u
const MDShader *shader = (const MDShader *)p_shader.id;
DynamicOffsetLayout layout = shader->dynamic_offset_layout;
// Clear bits for sets being rebound before OR'ing new values.
// This prevents corruption when the same set is bound multiple times
// with different frame indices (e.g., OPAQUE pass then ALPHA pass).
for (uint32_t i = 0; i < p_set_count && render.dynamic_offsets != 0; i++) {
uint32_t set_index = p_first_set_index + i;
uint32_t count = layout.get_count(set_index);
if (count > 0) {
uint32_t shift = layout.get_offset_index_shift(set_index);
uint32_t mask = ((1u << (count * 4u)) - 1u) << shift;
render.dynamic_offsets &= ~mask;
}
}
render.dynamic_offsets |= p_dynamic_offsets;
for (size_t i = 0; i < p_set_count; ++i) {
MDUniformSet *set = (MDUniformSet *)(p_uniform_sets[i].id);
@@ -1620,8 +1632,6 @@ void MDCommandBuffer::ComputeState::reset() {
void MDCommandBuffer::compute_bind_uniform_sets(VectorView<RDD::UniformSetID> p_uniform_sets, RDD::ShaderID p_shader, uint32_t p_first_set_index, uint32_t p_set_count, uint32_t p_dynamic_offsets) {
DEV_ASSERT(type == MDCommandBufferStateType::Compute);
compute.dynamic_offsets |= p_dynamic_offsets;
if (uint32_t new_size = p_first_set_index + p_set_count; compute.uniform_sets.size() < new_size) {
uint32_t s = compute.uniform_sets.size();
compute.uniform_sets.resize(new_size);
@@ -1632,6 +1642,20 @@ void MDCommandBuffer::compute_bind_uniform_sets(VectorView<RDD::UniformSetID> p_
const MDShader *shader = (const MDShader *)p_shader.id;
DynamicOffsetLayout layout = shader->dynamic_offset_layout;
// Clear bits for sets being rebound before OR'ing new values.
// This prevents corruption when the same set is bound multiple times
// with different frame indices.
for (uint32_t i = 0; i < p_set_count && compute.dynamic_offsets != 0; i++) {
uint32_t set_index = p_first_set_index + i;
uint32_t count = layout.get_count(set_index);
if (count > 0) {
uint32_t shift = layout.get_offset_index_shift(set_index);
uint32_t mask = ((1u << (count * 4u)) - 1u) << shift;
compute.dynamic_offsets &= ~mask;
}
}
compute.dynamic_offsets |= p_dynamic_offsets;
for (size_t i = 0; i < p_set_count; ++i) {
MDUniformSet *set = (MDUniformSet *)(p_uniform_sets[i].id);