From 7c77c4a81e5cebfc4bad2ed18b0d1bdbd30e5146 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Wed, 11 Mar 2026 02:01:23 -0700 Subject: [PATCH] Fix per-frame particle descriptor set leak in M2 renderer Pre-allocate one stable VkDescriptorSet per particle emitter at model upload time (particleTexSets[]) instead of allocating a new set from materialDescPool_ every frame for each particle group. The per-frame path exhausted the 8192-set pool in ~14 s at 60 fps with 10 active particle emitters, causing GPU device-lost crashes. The old path is kept as an explicit fallback but should never be reached in practice. --- include/rendering/m2_renderer.hpp | 3 +- src/rendering/m2_renderer.cpp | 74 ++++++++++++++++++++++++------- 2 files changed, 59 insertions(+), 18 deletions(-) diff --git a/include/rendering/m2_renderer.hpp b/include/rendering/m2_renderer.hpp index e26583b5..3d79379f 100644 --- a/include/rendering/m2_renderer.hpp +++ b/include/rendering/m2_renderer.hpp @@ -127,7 +127,8 @@ struct M2ModelGPU { // Particle emitter data (kept from M2Model) std::vector particleEmitters; - std::vector particleTextures; // Resolved Vulkan textures per emitter + std::vector particleTextures; // Resolved Vulkan textures per emitter + std::vector particleTexSets; // Pre-allocated descriptor sets per emitter (stable, avoids per-frame alloc) // Texture transform data for UV animation std::vector textureTransforms; diff --git a/src/rendering/m2_renderer.cpp b/src/rendering/m2_renderer.cpp index 4a30274e..b9a52c3e 100644 --- a/src/rendering/m2_renderer.cpp +++ b/src/rendering/m2_renderer.cpp @@ -748,6 +748,11 @@ void M2Renderer::destroyModelGPU(M2ModelGPU& model) { if (batch.materialSet) { vkFreeDescriptorSets(device, materialDescPool_, 1, &batch.materialSet); batch.materialSet = VK_NULL_HANDLE; } if (batch.materialUBO) { vmaDestroyBuffer(alloc, batch.materialUBO, batch.materialUBOAlloc); batch.materialUBO = VK_NULL_HANDLE; } } + // Free pre-allocated particle texture descriptor sets + for (auto& pSet : model.particleTexSets) { + if (pSet) { vkFreeDescriptorSets(device, materialDescPool_, 1, &pSet); pSet = VK_NULL_HANDLE; } + } + model.particleTexSets.clear(); } void M2Renderer::destroyInstanceBones(M2Instance& inst) { @@ -1349,6 +1354,31 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { } } + // Pre-allocate one stable descriptor set per particle emitter to avoid per-frame allocation. + // This prevents materialDescPool_ exhaustion when many emitters are active each frame. + if (particleTexLayout_ && materialDescPool_ && !model.particleEmitters.empty()) { + VkDevice device = vkCtx_->getDevice(); + gpuModel.particleTexSets.resize(model.particleEmitters.size(), VK_NULL_HANDLE); + for (size_t ei = 0; ei < model.particleEmitters.size(); ei++) { + VkDescriptorSetAllocateInfo ai{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; + ai.descriptorPool = materialDescPool_; + ai.descriptorSetCount = 1; + ai.pSetLayouts = &particleTexLayout_; + if (vkAllocateDescriptorSets(device, &ai, &gpuModel.particleTexSets[ei]) == VK_SUCCESS) { + VkTexture* tex = gpuModel.particleTextures[ei]; + VkDescriptorImageInfo imgInfo = tex->descriptorInfo(); + VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}; + write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + write.dstSet = gpuModel.particleTexSets[ei]; + write.dstBinding = 0; + write.descriptorCount = 1; + write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + write.pImageInfo = &imgInfo; + vkUpdateDescriptorSets(device, 1, &write, 0, nullptr); + } + } + } + // Copy texture transform data for UV animation gpuModel.textureTransforms = model.textureTransforms; gpuModel.textureTransformLookup = model.textureTransformLookup; @@ -3415,6 +3445,7 @@ void M2Renderer::renderM2Particles(VkCommandBuffer cmd, VkDescriptorSet perFrame uint8_t blendType; uint16_t tilesX; uint16_t tilesY; + VkDescriptorSet preAllocSet = VK_NULL_HANDLE; // Pre-allocated stable set, avoids per-frame alloc std::vector vertexData; // 9 floats per particle }; std::unordered_map groups; @@ -3456,6 +3487,11 @@ void M2Renderer::renderM2Particles(VkCommandBuffer cmd, VkDescriptorSet perFrame group.blendType = em.blendingType; group.tilesX = tilesX; group.tilesY = tilesY; + // Capture pre-allocated descriptor set on first insertion for this key + if (group.preAllocSet == VK_NULL_HANDLE && + p.emitterIndex < static_cast(gpu.particleTexSets.size())) { + group.preAllocSet = gpu.particleTexSets[p.emitterIndex]; + } group.vertexData.push_back(p.position.x); group.vertexData.push_back(p.position.y); @@ -3499,23 +3535,27 @@ void M2Renderer::renderM2Particles(VkCommandBuffer cmd, VkDescriptorSet perFrame currentPipeline = desiredPipeline; } - // Allocate descriptor set for this group's texture - VkDescriptorSetAllocateInfo ai{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; - ai.descriptorPool = materialDescPool_; - ai.descriptorSetCount = 1; - ai.pSetLayouts = &particleTexLayout_; - VkDescriptorSet texSet = VK_NULL_HANDLE; - if (vkAllocateDescriptorSets(vkCtx_->getDevice(), &ai, &texSet) == VK_SUCCESS) { - VkTexture* tex = group.texture ? group.texture : whiteTexture_.get(); - VkDescriptorImageInfo imgInfo = tex->descriptorInfo(); - VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}; - write.dstSet = texSet; - write.dstBinding = 0; - write.descriptorCount = 1; - write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; - write.pImageInfo = &imgInfo; - vkUpdateDescriptorSets(vkCtx_->getDevice(), 1, &write, 0, nullptr); - + // Use pre-allocated stable descriptor set; fall back to per-frame alloc only if unavailable + VkDescriptorSet texSet = group.preAllocSet; + if (texSet == VK_NULL_HANDLE) { + // Fallback: allocate per-frame (pool exhaustion risk — should not happen in practice) + VkDescriptorSetAllocateInfo ai{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; + ai.descriptorPool = materialDescPool_; + ai.descriptorSetCount = 1; + ai.pSetLayouts = &particleTexLayout_; + if (vkAllocateDescriptorSets(vkCtx_->getDevice(), &ai, &texSet) == VK_SUCCESS) { + VkTexture* tex = group.texture ? group.texture : whiteTexture_.get(); + VkDescriptorImageInfo imgInfo = tex->descriptorInfo(); + VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}; + write.dstSet = texSet; + write.dstBinding = 0; + write.descriptorCount = 1; + write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + write.pImageInfo = &imgInfo; + vkUpdateDescriptorSets(vkCtx_->getDevice(), 1, &write, 0, nullptr); + } + } + if (texSet != VK_NULL_HANDLE) { vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, particlePipelineLayout_, 1, 1, &texSet, 0, nullptr); }