Metal GPU缓冲区与CPU不同步:多物体渲染异常问题
我一眼就看出问题所在了——你遇到的是动态Uniform Buffer的异步更新与绘制调用的同步冲突,具体来说:
你当前的代码在一帧内只调用了一次updateDynamicBufferState(),这意味着两个立方体的Uniform数据都被写入了动态缓冲区的同一块内存区域。由于Metal的渲染命令是异步执行的,当你修改uniforms[0]准备绘制右侧立方体时,GPU可能还没完成左侧立方体的绘制(甚至还没读取对应的Uniform数据),这就导致第二次的修改直接覆盖了第一次的数据。最终GPU执行两次绘制时,读取的都是最后一次写入的右侧立方体矩阵,所以两个立方体重叠在一起,看起来只有第二个显示。
而用setVertexBytes时不会有这个问题,是因为每次调用都会把数据直接拷贝到命令缓冲区中,每个绘制调用的数据是独立的,不会被后续修改覆盖。
修复步骤
核心思路是:为每个物体分配动态缓冲区中独立的内存区域,并在绘制前切换到对应的偏移量。
1. 调整Uniform Buffer的管理逻辑
首先在Renderer类中添加一个方法,用于获取下一帧/下一个物体可用的Uniform缓冲区偏移:
private func nextUniformOffset() -> Int { let currentOffset = alignedUniformsSize * uniformBufferIndex // 循环更新索引,确保在maxBuffersInFlight范围内循环 uniformBufferIndex = (uniformBufferIndex + 1) % maxBuffersInFlight return currentOffset }
注意:如果你的一帧内需要渲染的物体数量超过
maxBuffersInFlight(当前是3),需要增大maxBuffersInFlight的值,避免覆盖之前帧的Uniform数据。
2. 修改绘制代码,为每个物体独立设置Uniform
在draw(in:)方法中,把原来的左右立方体绘制代码替换为:
// 左侧立方体 var modelMatrix = matrix4x4_translation(-3, 0, 0) var viewMatrix = matrix4x4_translation(0.0, 0.0, -8.0) // 获取独立的Uniform偏移 let leftUniformOffset = nextUniformOffset() // 获取对应内存的指针并写入数据 let leftUniforms = UnsafeMutableRawPointer(dynamicUniformBuffer.contents() + leftUniformOffset) .bindMemory(to: Uniforms.self, capacity: 1) leftUniforms.pointee.modelViewMatrix = simd_mul(viewMatrix, modelMatrix) print(leftUniforms.pointee) // 切换到左侧立方体的Uniform缓冲区 renderEncoder.setVertexBuffer(dynamicUniformBuffer, offset: leftUniformOffset, index: BufferIndex.uniforms.rawValue) renderEncoder.setFragmentBuffer(dynamicUniformBuffer, offset: leftUniformOffset, index: BufferIndex.uniforms.rawValue) // 绘制左侧立方体 for submesh in mesh.submeshes { renderEncoder.drawIndexedPrimitives(type: submesh.primitiveType, indexCount: submesh.indexCount, indexType: submesh.indexType, indexBuffer: submesh.indexBuffer.buffer, indexBufferOffset: submesh.indexBuffer.offset) } // 右侧立方体 modelMatrix = matrix4x4_translation(3, 0, 0) viewMatrix = matrix4x4_translation(0.0, 0.0, -8.0) let rightUniformOffset = nextUniformOffset() let rightUniforms = UnsafeMutableRawPointer(dynamicUniformBuffer.contents() + rightUniformOffset) .bindMemory(to: Uniforms.self, capacity: 1) rightUniforms.pointee.modelViewMatrix = simd_mul(viewMatrix, modelMatrix) print(rightUniforms.pointee) // 切换到右侧立方体的Uniform缓冲区 renderEncoder.setVertexBuffer(dynamicUniformBuffer, offset: rightUniformOffset, index: BufferIndex.uniforms.rawValue) renderEncoder.setFragmentBuffer(dynamicUniformBuffer, offset: rightUniformOffset, index: BufferIndex.uniforms.rawValue) // 绘制右侧立方体 for submesh in mesh.submeshes { renderEncoder.drawIndexedPrimitives(type: submesh.primitiveType, indexCount: submesh.indexCount, indexType: submesh.indexType, indexBuffer: submesh.indexBuffer.buffer, indexBufferOffset: submesh.indexBuffer.offset) }
3. 调整初始化与状态更新逻辑
原来的updateDynamicBufferState()方法可以保留,但要注意在draw(in:)开头调用它时,会初始化第一帧的uniformBufferIndex,然后我们的nextUniformOffset()会继续递增索引,确保每个物体的Uniform数据都在独立区域。
验证修复效果
修改后再用GPU帧捕获工具查看两次绘制调用的Uniform Buffer偏移量,你会发现它们指向了缓冲区的不同位置,数据分别对应左右两个立方体的矩阵。此时两个立方体应该会正常显示在屏幕两侧。
内容的提问来源于stack exchange,提问作者hyperknot

