fix(rendering): wait all frame fences before freeing shared descriptor sets

deferAfterFrameFence only waits for one frame slot's fence, but shared
resources (material descriptor sets, vertex/index buffers) are bound by
both in-flight frames' command buffers. On AMD RADV this caused
vkFreeDescriptorSets errors and eventual SIGSEGV.

Add deferAfterAllFrameFences: queues to every frame slot with a shared
counter so cleanup runs exactly once, after the last slot is fenced.
Use it for WMO, terrain, water, and character model shared resources.
Per-frame bone sets keep using deferAfterFrameFence (already correct).

Also fix character renderer vertex format: R8G8B8A8_UINT -> _SINT to
match shader's ivec4 input (RADV validation rejects the mismatch).
This commit is contained in:
Kelsi 2026-04-03 19:48:43 -07:00
parent def821055b
commit 3ac8c4d95f
6 changed files with 31 additions and 10 deletions

View file

@ -246,7 +246,7 @@ bool CharacterRenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFram
std::vector<VkVertexInputAttributeDescription> charAttrs = {
{0, 0, VK_FORMAT_R32G32B32_SFLOAT, static_cast<uint32_t>(offsetof(CharVertexGPU, position))},
{1, 0, VK_FORMAT_R8G8B8A8_UNORM, static_cast<uint32_t>(offsetof(CharVertexGPU, boneWeights))},
{2, 0, VK_FORMAT_R8G8B8A8_UINT, static_cast<uint32_t>(offsetof(CharVertexGPU, boneIndices))},
{2, 0, VK_FORMAT_R8G8B8A8_SINT, static_cast<uint32_t>(offsetof(CharVertexGPU, boneIndices))},
{3, 0, VK_FORMAT_R32G32B32_SFLOAT, static_cast<uint32_t>(offsetof(CharVertexGPU, normal))},
{4, 0, VK_FORMAT_R32G32_SFLOAT, static_cast<uint32_t>(offsetof(CharVertexGPU, texCoords))},
{5, 0, VK_FORMAT_R32G32B32A32_SFLOAT, static_cast<uint32_t>(offsetof(CharVertexGPU, tangent))},
@ -492,7 +492,7 @@ void CharacterRenderer::destroyModelGPU(M2ModelGPU& gpuModel, bool defer) {
if (ib) vmaDestroyBuffer(alloc, ib, ibAlloc);
} else if (vb || ib) {
// Streaming path: in-flight command buffers may still reference these
vkCtx_->deferAfterFrameFence([alloc, vb, vbAlloc, ib, ibAlloc]() {
vkCtx_->deferAfterAllFrameFences([alloc, vb, vbAlloc, ib, ibAlloc]() {
if (vb) vmaDestroyBuffer(alloc, vb, vbAlloc);
if (ib) vmaDestroyBuffer(alloc, ib, ibAlloc);
});
@ -2663,7 +2663,7 @@ bool CharacterRenderer::initializeShadow(VkRenderPass shadowRenderPass) {
// Character vertex format (CharVertexGPU): stride = 56 bytes
// loc 0: vec3 aPos (R32G32B32_SFLOAT, offset 0)
// loc 1: vec4 aBoneWeights (R8G8B8A8_UNORM, offset 12)
// loc 2: ivec4 aBoneIndices (R8G8B8A8_UINT, offset 16)
// loc 2: ivec4 aBoneIndices (R8G8B8A8_SINT, offset 16)
// loc 3: vec2 aTexCoord (R32G32_SFLOAT, offset 32)
VkVertexInputBindingDescription vertBind{};
vertBind.binding = 0;
@ -2672,7 +2672,7 @@ bool CharacterRenderer::initializeShadow(VkRenderPass shadowRenderPass) {
std::vector<VkVertexInputAttributeDescription> vertAttrs = {
{0, 0, VK_FORMAT_R32G32B32_SFLOAT, static_cast<uint32_t>(offsetof(CharVertexGPU, position))},
{1, 0, VK_FORMAT_R8G8B8A8_UNORM, static_cast<uint32_t>(offsetof(CharVertexGPU, boneWeights))},
{2, 0, VK_FORMAT_R8G8B8A8_UINT, static_cast<uint32_t>(offsetof(CharVertexGPU, boneIndices))},
{2, 0, VK_FORMAT_R8G8B8A8_SINT, static_cast<uint32_t>(offsetof(CharVertexGPU, boneIndices))},
{3, 0, VK_FORMAT_R32G32_SFLOAT, static_cast<uint32_t>(offsetof(CharVertexGPU, texCoords))},
};
@ -3336,7 +3336,7 @@ void CharacterRenderer::recreatePipelines() {
std::vector<VkVertexInputAttributeDescription> charAttrs = {
{0, 0, VK_FORMAT_R32G32B32_SFLOAT, static_cast<uint32_t>(offsetof(CharVertexGPU, position))},
{1, 0, VK_FORMAT_R8G8B8A8_UNORM, static_cast<uint32_t>(offsetof(CharVertexGPU, boneWeights))},
{2, 0, VK_FORMAT_R8G8B8A8_UINT, static_cast<uint32_t>(offsetof(CharVertexGPU, boneIndices))},
{2, 0, VK_FORMAT_R8G8B8A8_SINT, static_cast<uint32_t>(offsetof(CharVertexGPU, boneIndices))},
{3, 0, VK_FORMAT_R32G32B32_SFLOAT, static_cast<uint32_t>(offsetof(CharVertexGPU, normal))},
{4, 0, VK_FORMAT_R32G32_SFLOAT, static_cast<uint32_t>(offsetof(CharVertexGPU, texCoords))},
{5, 0, VK_FORMAT_R32G32B32A32_SFLOAT, static_cast<uint32_t>(offsetof(CharVertexGPU, tangent))},

View file

@ -1061,8 +1061,8 @@ void TerrainRenderer::destroyChunkGPU(TerrainChunkGPU& chunk) {
chunk.materialSet = VK_NULL_HANDLE;
chunk.ownedAlphaTextures.clear();
vkCtx->deferAfterFrameFence([device, allocator, vertexBuffer, vertexAlloc, indexBuffer, indexAlloc,
paramsUBO, paramsAlloc, pool, materialSet, alphaTextures]() {
vkCtx->deferAfterAllFrameFences([device, allocator, vertexBuffer, vertexAlloc, indexBuffer, indexAlloc,
paramsUBO, paramsAlloc, pool, materialSet, alphaTextures]() {
if (vertexBuffer) {
AllocatedBuffer ab{}; ab.buffer = vertexBuffer; ab.allocation = vertexAlloc;
destroyBuffer(allocator, ab);

View file

@ -194,6 +194,23 @@ void VkContext::deferAfterFrameFence(std::function<void()>&& fn) {
deferredCleanup_[currentFrame].push_back(std::move(fn));
}
void VkContext::deferAfterAllFrameFences(std::function<void()>&& fn) {
// Shared resources (material descriptor sets, vertex/index buffers) are
// bound by every in-flight frame's command buffer. deferAfterFrameFence
// only waits for ONE slot's fence — the other slot may still be executing.
// Add to every slot; a shared counter ensures the lambda runs exactly once,
// after the LAST slot has been fenced.
auto counter = std::make_shared<uint32_t>(MAX_FRAMES_IN_FLIGHT);
auto sharedFn = std::make_shared<std::function<void()>>(std::move(fn));
for (uint32_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {
deferredCleanup_[i].push_back([counter, sharedFn]() {
if (--(*counter) == 0) {
(*sharedFn)();
}
});
}
}
void VkContext::runDeferredCleanup(uint32_t frameIndex) {
auto& q = deferredCleanup_[frameIndex];
if (q.empty()) return;

View file

@ -1405,8 +1405,8 @@ void WaterRenderer::destroyWaterMesh(WaterSurface& surface) {
surface.materialAlloc = VK_NULL_HANDLE;
surface.materialSet = VK_NULL_HANDLE;
vkCtx->deferAfterFrameFence([device, allocator, vertexBuffer, vertexAlloc, indexBuffer, indexAlloc,
materialUBO, materialAlloc, pool, materialSet]() {
vkCtx->deferAfterAllFrameFences([device, allocator, vertexBuffer, vertexAlloc, indexBuffer, indexAlloc,
materialUBO, materialAlloc, pool, materialSet]() {
if (vertexBuffer) {
AllocatedBuffer ab{}; ab.buffer = vertexBuffer; ab.allocation = vertexAlloc;
destroyBuffer(allocator, ab);

View file

@ -1972,7 +1972,7 @@ void WMORenderer::destroyGroupGPU(GroupResources& group, bool defer) {
}
VkDescriptorPool pool = materialDescPool_;
vkCtx_->deferAfterFrameFence([device, allocator, pool, vb, vbAlloc, ib, ibAlloc,
vkCtx_->deferAfterAllFrameFences([device, allocator, pool, vb, vbAlloc, ib, ibAlloc,
mats = std::move(mats)]() {
if (vb) vmaDestroyBuffer(allocator, vb, vbAlloc);
if (ib) vmaDestroyBuffer(allocator, ib, ibAlloc);