fix(rendering): defer model buffer destruction and per-frame FXAA descriptors

CharacterRenderer::destroyModelGPU now defers vertex/index buffer
destruction when replacing models mid-stream, preventing use-after-free
on AMD RADV. FXAA descriptor sets are now per-frame to eliminate
write-read races between in-flight command buffers. Water reflection
descriptor update narrowed to current frame only.
This commit is contained in:
Kelsi 2026-04-03 19:17:55 -07:00
parent e19bf76d88
commit 40e72d535e
5 changed files with 71 additions and 43 deletions

View file

@ -472,11 +472,31 @@ void CharacterRenderer::createFallbackTextures(VkDevice device) {
}
}
void CharacterRenderer::destroyModelGPU(M2ModelGPU& gpuModel) {
void CharacterRenderer::destroyModelGPU(M2ModelGPU& gpuModel, bool defer) {
if (!vkCtx_) return;
VmaAllocator alloc = vkCtx_->getAllocator();
if (gpuModel.vertexBuffer) { vmaDestroyBuffer(alloc, gpuModel.vertexBuffer, gpuModel.vertexAlloc); gpuModel.vertexBuffer = VK_NULL_HANDLE; }
if (gpuModel.indexBuffer) { vmaDestroyBuffer(alloc, gpuModel.indexBuffer, gpuModel.indexAlloc); gpuModel.indexBuffer = VK_NULL_HANDLE; }
// Snapshot raw handles and null the model fields immediately
::VkBuffer vb = gpuModel.vertexBuffer;
VmaAllocation vbAlloc = gpuModel.vertexAlloc;
::VkBuffer ib = gpuModel.indexBuffer;
VmaAllocation ibAlloc = gpuModel.indexAlloc;
gpuModel.vertexBuffer = VK_NULL_HANDLE;
gpuModel.vertexAlloc = VK_NULL_HANDLE;
gpuModel.indexBuffer = VK_NULL_HANDLE;
gpuModel.indexAlloc = VK_NULL_HANDLE;
if (!defer) {
// Safe after vkDeviceWaitIdle (shutdown / clear paths)
if (vb) vmaDestroyBuffer(alloc, vb, vbAlloc);
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]() {
if (vb) vmaDestroyBuffer(alloc, vb, vbAlloc);
if (ib) vmaDestroyBuffer(alloc, ib, ibAlloc);
});
}
}
void CharacterRenderer::destroyInstanceBones(CharacterInstance& inst, bool defer) {
@ -1412,7 +1432,7 @@ bool CharacterRenderer::loadModel(const pipeline::M2Model& model, uint32_t id) {
if (models.find(id) != models.end()) {
core::Logger::getInstance().warning("Model ID ", id, " already loaded, replacing");
destroyModelGPU(models[id]);
destroyModelGPU(models[id], /*defer=*/true);
}
M2ModelGPU gpuModel;

View file

@ -198,7 +198,9 @@ bool PostProcessPipeline::executePostProcessing(VkCommandBuffer cmd, uint32_t im
// FSR3+FXAA combined: re-point FXAA's descriptor to the FSR3 temporal output
// so renderFXAAPass() applies spatial AA on the temporally-stabilized frame.
// This must happen outside the render pass (descriptor updates are CPU-side).
if (fxaa_.enabled && fxaa_.descSet && fxaa_.sceneSampler) {
// Use per-frame descriptor set to avoid race with in-flight command buffers.
uint32_t fxaaFrameIdx = vkCtx_->getCurrentFrame();
if (fxaa_.enabled && fxaa_.descSet[fxaaFrameIdx] && fxaa_.sceneSampler) {
VkImageView fsr3OutputView = VK_NULL_HANDLE;
if (fsr2_.useAmdBackend) {
if (fsr2_.amdFsr3FramegenRuntimeActive && fsr2_.framegenOutput.image)
@ -215,7 +217,7 @@ bool PostProcessPipeline::executePostProcessing(VkCommandBuffer cmd, uint32_t im
imgInfo.sampler = fxaa_.sceneSampler;
VkWriteDescriptorSet write{};
write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
write.dstSet = fxaa_.descSet;
write.dstSet = fxaa_.descSet[fxaaFrameIdx];
write.dstBinding = 0;
write.descriptorCount = 1;
write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
@ -257,23 +259,23 @@ bool PostProcessPipeline::executePostProcessing(VkCommandBuffer cmd, uint32_t im
// of RCAS sharpening. FXAA descriptor is temporarily pointed to the FSR3
// history buffer (which is already in SHADER_READ_ONLY_OPTIMAL). This gives
// FSR3 temporal stability + FXAA spatial edge smoothing ("ultra quality native").
if (fxaa_.enabled && fxaa_.pipeline && fxaa_.descSet) {
if (fxaa_.enabled && fxaa_.pipeline && fxaa_.descSet[fxaaFrameIdx]) {
renderFXAAPass();
} else {
// Draw RCAS sharpening from accumulated history buffer
renderFSR2Sharpen();
}
// Restore FXAA descriptor to its normal scene color source so standalone
// FXAA frames are not affected by the FSR3 history pointer set above.
if (fxaa_.enabled && fxaa_.descSet && fxaa_.sceneSampler && fxaa_.sceneColor.imageView) {
// Restore this frame's FXAA descriptor to its normal scene color source
// so standalone FXAA frames are not affected by the FSR3 history pointer.
if (fxaa_.enabled && fxaa_.descSet[fxaaFrameIdx] && fxaa_.sceneSampler && fxaa_.sceneColor.imageView) {
VkDescriptorImageInfo restoreInfo{};
restoreInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
restoreInfo.imageView = fxaa_.sceneColor.imageView;
restoreInfo.sampler = fxaa_.sceneSampler;
VkWriteDescriptorSet restoreWrite{};
restoreWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
restoreWrite.dstSet = fxaa_.descSet;
restoreWrite.dstSet = fxaa_.descSet[fxaaFrameIdx];
restoreWrite.dstBinding = 0;
restoreWrite.descriptorCount = 1;
restoreWrite.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
@ -1754,36 +1756,41 @@ bool PostProcessPipeline::initFXAAResources() {
layoutInfo.pBindings = &binding;
vkCreateDescriptorSetLayout(device, &layoutInfo, nullptr, &fxaa_.descSetLayout);
constexpr uint32_t setCount = FXAAState::DESC_SET_COUNT;
VkDescriptorPoolSize poolSize{};
poolSize.type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
poolSize.descriptorCount = 1;
poolSize.descriptorCount = setCount;
VkDescriptorPoolCreateInfo poolInfo{};
poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
poolInfo.maxSets = 1;
poolInfo.maxSets = setCount;
poolInfo.poolSizeCount = 1;
poolInfo.pPoolSizes = &poolSize;
vkCreateDescriptorPool(device, &poolInfo, nullptr, &fxaa_.descPool);
VkDescriptorSetLayout layouts[setCount];
for (uint32_t i = 0; i < setCount; i++) layouts[i] = fxaa_.descSetLayout;
VkDescriptorSetAllocateInfo dsAllocInfo{};
dsAllocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
dsAllocInfo.descriptorPool = fxaa_.descPool;
dsAllocInfo.descriptorSetCount = 1;
dsAllocInfo.pSetLayouts = &fxaa_.descSetLayout;
vkAllocateDescriptorSets(device, &dsAllocInfo, &fxaa_.descSet);
dsAllocInfo.descriptorSetCount = setCount;
dsAllocInfo.pSetLayouts = layouts;
vkAllocateDescriptorSets(device, &dsAllocInfo, fxaa_.descSet);
// Bind the resolved 1x sceneColor
// Bind the resolved 1x sceneColor to all per-frame sets
VkDescriptorImageInfo imgInfo{};
imgInfo.sampler = fxaa_.sceneSampler;
imgInfo.imageView = fxaa_.sceneColor.imageView;
imgInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
VkWriteDescriptorSet write{};
write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
write.dstSet = fxaa_.descSet;
write.dstBinding = 0;
write.descriptorCount = 1;
write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
write.pImageInfo = &imgInfo;
vkUpdateDescriptorSets(device, 1, &write, 0, nullptr);
for (uint32_t i = 0; i < setCount; i++) {
VkWriteDescriptorSet write{};
write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
write.dstSet = fxaa_.descSet[i];
write.dstBinding = 0;
write.descriptorCount = 1;
write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
write.pImageInfo = &imgInfo;
vkUpdateDescriptorSets(device, 1, &write, 0, nullptr);
}
// Pipeline layout — push constant holds vec4(rcpFrame.xy, sharpness, pad)
VkPushConstantRange pc{};
@ -1843,7 +1850,7 @@ void PostProcessPipeline::destroyFXAAResources() {
if (fxaa_.pipeline) { vkDestroyPipeline(device, fxaa_.pipeline, nullptr); fxaa_.pipeline = VK_NULL_HANDLE; }
if (fxaa_.pipelineLayout) { vkDestroyPipelineLayout(device, fxaa_.pipelineLayout, nullptr); fxaa_.pipelineLayout = VK_NULL_HANDLE; }
if (fxaa_.descPool) { vkDestroyDescriptorPool(device, fxaa_.descPool, nullptr); fxaa_.descPool = VK_NULL_HANDLE; fxaa_.descSet = VK_NULL_HANDLE; }
if (fxaa_.descPool) { vkDestroyDescriptorPool(device, fxaa_.descPool, nullptr); fxaa_.descPool = VK_NULL_HANDLE; for (auto& s : fxaa_.descSet) s = VK_NULL_HANDLE; }
if (fxaa_.descSetLayout) { vkDestroyDescriptorSetLayout(device, fxaa_.descSetLayout, nullptr); fxaa_.descSetLayout = VK_NULL_HANDLE; }
if (fxaa_.sceneFramebuffer) { vkDestroyFramebuffer(device, fxaa_.sceneFramebuffer, nullptr); fxaa_.sceneFramebuffer = VK_NULL_HANDLE; }
fxaa_.sceneSampler = VK_NULL_HANDLE; // Owned by VkContext sampler cache
@ -1857,9 +1864,10 @@ void PostProcessPipeline::renderFXAAPass() {
if (!fxaa_.pipeline || currentCmd_ == VK_NULL_HANDLE) return;
VkExtent2D ext = vkCtx_->getSwapchainExtent();
uint32_t fi = vkCtx_->getCurrentFrame();
vkCmdBindPipeline(currentCmd_, VK_PIPELINE_BIND_POINT_GRAPHICS, fxaa_.pipeline);
vkCmdBindDescriptorSets(currentCmd_, VK_PIPELINE_BIND_POINT_GRAPHICS,
fxaa_.pipelineLayout, 0, 1, &fxaa_.descSet, 0, nullptr);
fxaa_.pipelineLayout, 0, 1, &fxaa_.descSet[fi], 0, nullptr);
// Pass rcpFrame + sharpness + effect flag (vec4, 16 bytes).
// When FSR2/FSR3 is active alongside FXAA, forward FSR2's sharpness so the

View file

@ -1920,27 +1920,25 @@ void WaterRenderer::endReflectionPass(VkCommandBuffer cmd) {
vkCmdEndRenderPass(cmd);
reflectionColorLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
// Update all per-frame scene descriptor sets with the freshly rendered reflection texture
if (reflectionColorView && reflectionSampler) {
VkDescriptorImageInfo reflInfo{};
reflInfo.sampler = reflectionSampler;
reflInfo.imageView = reflectionColorView;
reflInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
// Update only the current frame's scene descriptor set with the reflection texture.
// Updating all frames would race with in-flight command buffers that have the
// other frame's descriptor set bound.
if (reflectionColorView && reflectionSampler && vkCtx) {
uint32_t fi = vkCtx->getCurrentFrame() % SCENE_HISTORY_FRAMES;
if (sceneHistory[fi].sceneSet) {
VkDescriptorImageInfo reflInfo{};
reflInfo.sampler = reflectionSampler;
reflInfo.imageView = reflectionColorView;
reflInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
std::vector<VkWriteDescriptorSet> writes;
for (uint32_t f = 0; f < SCENE_HISTORY_FRAMES; f++) {
if (!sceneHistory[f].sceneSet) continue;
VkWriteDescriptorSet write{};
write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
write.dstSet = sceneHistory[f].sceneSet;
write.dstSet = sceneHistory[fi].sceneSet;
write.dstBinding = 2;
write.descriptorCount = 1;
write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
write.pImageInfo = &reflInfo;
writes.push_back(write);
}
if (!writes.empty()) {
vkUpdateDescriptorSets(vkCtx->getDevice(), static_cast<uint32_t>(writes.size()), writes.data(), 0, nullptr);
vkUpdateDescriptorSets(vkCtx->getDevice(), 1, &write, 0, nullptr);
}
}
}