Fix GPU resource leaks and re-entrant world loading for instance transitions

Reset descriptor pools in CharacterRenderer/M2Renderer/WMORenderer on map
change to prevent VK_ERROR_DEVICE_LOST from pool exhaustion. Defer re-entrant
SMSG_NEW_WORLD during active world load to avoid recursive cleanup crashes.
Gate swim bubbles on swimming state, skip redundant shadow pipeline re-init,
add WOWEE_SKIP_* env vars for render isolation debugging.
This commit is contained in:
Kelsi 2026-03-02 08:06:35 -08:00
parent 19652ae521
commit 48eb0b70a3
9 changed files with 255 additions and 47 deletions

View file

@ -383,6 +383,85 @@ void CharacterRenderer::shutdown() {
vkCtx_ = nullptr;
}
void CharacterRenderer::clear() {
if (!vkCtx_) return;
LOG_INFO("CharacterRenderer::clear instances=", instances.size(),
" models=", models.size());
vkDeviceWaitIdle(vkCtx_->getDevice());
VkDevice device = vkCtx_->getDevice();
VmaAllocator alloc = vkCtx_->getAllocator();
// Destroy GPU resources for all models
for (auto& pair : models) {
destroyModelGPU(pair.second);
}
// Destroy bone buffers for all instances
for (auto& pair : instances) {
destroyInstanceBones(pair.second);
}
// Clear texture cache (VkTexture unique_ptrs auto-destroy)
textureCache.clear();
textureHasAlphaByPtr_.clear();
textureColorKeyBlackByPtr_.clear();
textureCacheBytes_ = 0;
textureCacheCounter_ = 0;
loggedTextureLoadFails_.clear();
// Clear composite and failed caches
compositeCache_.clear();
failedTextureCache_.clear();
// Recreate default textures (needed by loadModel/loadTexture fallbacks)
whiteTexture_.reset();
transparentTexture_.reset();
flatNormalTexture_.reset();
{
uint8_t white[] = {255, 255, 255, 255};
whiteTexture_ = std::make_unique<VkTexture>();
whiteTexture_->upload(*vkCtx_, white, 1, 1, VK_FORMAT_R8G8B8A8_UNORM, false);
whiteTexture_->createSampler(device, VK_FILTER_NEAREST, VK_FILTER_NEAREST, VK_SAMPLER_ADDRESS_MODE_REPEAT);
}
{
uint8_t transparent[] = {0, 0, 0, 0};
transparentTexture_ = std::make_unique<VkTexture>();
transparentTexture_->upload(*vkCtx_, transparent, 1, 1, VK_FORMAT_R8G8B8A8_UNORM, false);
transparentTexture_->createSampler(device, VK_FILTER_NEAREST, VK_FILTER_NEAREST, VK_SAMPLER_ADDRESS_MODE_REPEAT);
}
{
uint8_t flatNormal[] = {128, 128, 255, 128};
flatNormalTexture_ = std::make_unique<VkTexture>();
flatNormalTexture_->upload(*vkCtx_, flatNormal, 1, 1, VK_FORMAT_R8G8B8A8_UNORM, false);
flatNormalTexture_->createSampler(device, VK_FILTER_NEAREST, VK_FILTER_NEAREST, VK_SAMPLER_ADDRESS_MODE_REPEAT);
}
models.clear();
instances.clear();
// Release deferred transient material UBOs
for (int i = 0; i < 2; i++) {
for (const auto& b : transientMaterialUbos_[i]) {
if (b.first) {
vmaDestroyBuffer(alloc, b.first, b.second);
}
}
transientMaterialUbos_[i].clear();
}
// Reset descriptor pools (don't destroy — reuse for new allocations)
for (int i = 0; i < 2; i++) {
if (materialDescPools_[i]) {
vkResetDescriptorPool(device, materialDescPools_[i], 0);
}
}
if (boneDescPool_) {
vkResetDescriptorPool(device, boneDescPool_, 0);
}
}
void CharacterRenderer::destroyModelGPU(M2ModelGPU& gpuModel) {
if (!vkCtx_) return;
VmaAllocator alloc = vkCtx_->getAllocator();

View file

@ -3254,6 +3254,35 @@ void M2Renderer::clear() {
for (auto& inst : instances) {
destroyInstanceBones(inst);
}
// Reset descriptor pools so new allocations succeed after reload.
// destroyModelGPU/destroyInstanceBones don't free individual sets,
// so the pools fill up across map changes without this reset.
VkDevice device = vkCtx_->getDevice();
if (materialDescPool_) {
vkResetDescriptorPool(device, materialDescPool_, 0);
// Re-allocate the glow texture descriptor set (pre-allocated during init,
// invalidated by pool reset).
if (glowTexture_ && particleTexLayout_) {
VkDescriptorSetAllocateInfo ai{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO};
ai.descriptorPool = materialDescPool_;
ai.descriptorSetCount = 1;
ai.pSetLayouts = &particleTexLayout_;
glowTexDescSet_ = VK_NULL_HANDLE;
if (vkAllocateDescriptorSets(device, &ai, &glowTexDescSet_) == VK_SUCCESS) {
VkDescriptorImageInfo imgInfo = glowTexture_->descriptorInfo();
VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET};
write.dstSet = glowTexDescSet_;
write.dstBinding = 0;
write.descriptorCount = 1;
write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
write.pImageInfo = &imgInfo;
vkUpdateDescriptorSets(device, 1, &write, 0, nullptr);
}
}
}
if (boneDescPool_) {
vkResetDescriptorPool(device, boneDescPool_, 0);
}
}
models.clear();
instances.clear();

View file

@ -3165,6 +3165,10 @@ void Renderer::renderOverlay(const glm::vec4& color) {
void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) {
(void)world;
// GPU crash diagnostic: skip ALL world rendering to isolate crash source
static const bool skipAll = (std::getenv("WOWEE_SKIP_ALL_RENDER") != nullptr);
if (skipAll) return;
auto renderStart = std::chrono::steady_clock::now();
lastTerrainRenderMs = 0.0;
lastWMORenderMs = 0.0;
@ -3208,6 +3212,11 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) {
skySystem->render(currentCmd, perFrameSet, *camera, skyParams);
}
// GPU crash diagnostic: skip individual renderers to isolate which one faults
static const bool skipWMO = (std::getenv("WOWEE_SKIP_WMO") != nullptr);
static const bool skipChars = (std::getenv("WOWEE_SKIP_CHARS") != nullptr);
static const bool skipM2 = (std::getenv("WOWEE_SKIP_M2") != nullptr);
// Terrain (opaque pass)
if (terrainRenderer && camera && terrainEnabled) {
auto terrainStart = std::chrono::steady_clock::now();
@ -3217,7 +3226,7 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) {
}
// WMO buildings (opaque, drawn before characters so selection circle sits on top)
if (wmoRenderer && camera) {
if (wmoRenderer && camera && !skipWMO) {
auto wmoStart = std::chrono::steady_clock::now();
wmoRenderer->render(currentCmd, perFrameSet, *camera);
lastWMORenderMs = std::chrono::duration<double, std::milli>(
@ -3228,12 +3237,12 @@ void Renderer::renderWorld(game::World* world, game::GameHandler* gameHandler) {
renderSelectionCircle(view, projection);
// Characters (after selection circle so units draw over the ring)
if (characterRenderer && camera) {
if (characterRenderer && camera && !skipChars) {
characterRenderer->render(currentCmd, perFrameSet, *camera);
}
// M2 doodads, creatures, glow sprites, particles
if (m2Renderer && camera) {
if (m2Renderer && camera && !skipM2) {
if (cameraController) {
m2Renderer->setInsideInterior(cameraController->isInsideWMO());
m2Renderer->setOnTaxi(cameraController->isOnTaxi());
@ -3393,21 +3402,21 @@ bool Renderer::loadTestTerrain(pipeline::AssetManager* assetManager, const std::
if (!wmoRenderer) {
wmoRenderer = std::make_unique<WMORenderer>();
wmoRenderer->initialize(vkCtx, perFrameSetLayout, assetManager);
if (shadowRenderPass != VK_NULL_HANDLE) {
wmoRenderer->initializeShadow(shadowRenderPass);
}
}
// Initialize shadow pipelines (Phase 7/8)
if (wmoRenderer && shadowRenderPass != VK_NULL_HANDLE) {
wmoRenderer->initializeShadow(shadowRenderPass);
}
if (m2Renderer && shadowRenderPass != VK_NULL_HANDLE) {
// Initialize shadow pipelines for M2 if not yet done
if (m2Renderer && shadowRenderPass != VK_NULL_HANDLE && !m2Renderer->hasShadowPipeline()) {
m2Renderer->initializeShadow(shadowRenderPass);
}
if (!characterRenderer) {
characterRenderer = std::make_unique<CharacterRenderer>();
characterRenderer->initialize(vkCtx, perFrameSetLayout, assetManager);
}
if (characterRenderer && shadowRenderPass != VK_NULL_HANDLE) {
characterRenderer->initializeShadow(shadowRenderPass);
if (shadowRenderPass != VK_NULL_HANDLE) {
characterRenderer->initializeShadow(shadowRenderPass);
}
}
// Create and initialize terrain manager
@ -3862,6 +3871,8 @@ void Renderer::renderReflectionPass() {
}
void Renderer::renderShadowPass() {
static const bool skipShadows = (std::getenv("WOWEE_SKIP_SHADOWS") != nullptr);
if (skipShadows) return;
if (!shadowsEnabled || shadowDepthImage == VK_NULL_HANDLE) return;
if (currentCmd == VK_NULL_HANDLE) return;

View file

@ -557,7 +557,9 @@ void SwimEffects::update(const Camera& camera, const CameraController& cc,
}
// --- Bubble spawning ---
bool underwater = camWaterH && camPos.z < *camWaterH;
// Require swimming state to prevent spurious bubbles on login/teleport
// when camera may briefly appear below a water surface before grounding.
bool underwater = swimming && camWaterH && camPos.z < *camWaterH;
if (underwater) {
float bubbleRate = 20.0f;
bubbleSpawnAccum += bubbleRate * deltaTime;

View file

@ -1059,6 +1059,11 @@ void WMORenderer::clearAll() {
if (entry.texture) entry.texture->destroy(device, allocator);
if (entry.normalHeightMap) entry.normalHeightMap->destroy(device, allocator);
}
// Reset descriptor pool so new allocations succeed after reload
if (materialDescPool_) {
vkResetDescriptorPool(device, materialDescPool_, 0);
}
}
loadedModels.clear();