From ac3c90dd75c748d6482b6031bfcbd65d1e4e58c1 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 7 Mar 2026 22:47:07 -0800 Subject: [PATCH] Fix M2 animated instance flashing (deer/bird/critter pop-in) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: bonesDirty was a single bool shared across both double-buffered frame indices. When bones were copied to frame 0's SSBO and bonesDirty cleared, frame 1's newly-allocated SSBO would contain garbage/zeros and never get populated — causing animated M2 instances to flash invisible on alternating frames. Fix: Make bonesDirty per-frame-index (bool[2]) so each buffer independently tracks whether it needs bone data uploaded. When bones are recomputed, both indices are marked dirty. When uploaded during render, only the current frame index is cleared. New buffer allocations in prepareRender force their frame index dirty. --- include/rendering/m2_renderer.hpp | 2 +- src/rendering/m2_renderer.cpp | 18 ++++++++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/include/rendering/m2_renderer.hpp b/include/rendering/m2_renderer.hpp index 75a92565..ee7d6ebf 100644 --- a/include/rendering/m2_renderer.hpp +++ b/include/rendering/m2_renderer.hpp @@ -194,7 +194,7 @@ struct M2Instance { // Frame-skip optimization (update distant animations less frequently) uint8_t frameSkipCounter = 0; - bool bonesDirty = false; // Set when bones recomputed, cleared after upload + bool bonesDirty[2] = {false, false}; // Per-frame-index: set when bones recomputed, cleared after upload // Per-instance bone SSBO (double-buffered) ::VkBuffer boneBuffer[2] = {}; diff --git a/src/rendering/m2_renderer.cpp b/src/rendering/m2_renderer.cpp index cbe26302..eed9a025 100644 --- a/src/rendering/m2_renderer.cpp +++ b/src/rendering/m2_renderer.cpp @@ -1687,7 +1687,7 @@ uint32_t M2Renderer::createInstance(uint32_t modelId, const glm::vec3& position, for (const auto& existing : instances) { if (existing.modelId == modelId && !existing.boneMatrices.empty()) { instance.boneMatrices = existing.boneMatrices; - instance.bonesDirty = true; + instance.bonesDirty[0] = instance.bonesDirty[1] = true; break; } } @@ -1791,7 +1791,7 @@ uint32_t M2Renderer::createInstanceWithMatrix(uint32_t modelId, const glm::mat4& for (const auto& existing : instances) { if (existing.modelId == modelId && !existing.boneMatrices.empty()) { instance.boneMatrices = existing.boneMatrices; - instance.bonesDirty = true; + instance.bonesDirty[0] = instance.bonesDirty[1] = true; break; } } @@ -1951,7 +1951,7 @@ static void computeBoneMatrices(const M2ModelGPU& model, M2Instance& instance) { instance.boneMatrices[i] = local; } } - instance.bonesDirty = true; + instance.bonesDirty[0] = instance.bonesDirty[1] = true; } void M2Renderer::update(float deltaTime, const glm::vec3& cameraPos, const glm::mat4& viewProjection) { @@ -2237,6 +2237,11 @@ void M2Renderer::prepareRender(uint32_t frameIndex, const Camera& camera) { &instance.boneBuffer[frameIndex], &instance.boneAlloc[frameIndex], &allocInfo); instance.boneMapped[frameIndex] = allocInfo.pMappedData; + // Force dirty so current boneMatrices get copied into this + // newly-allocated buffer during render (prevents garbage/zero + // bones when the other frame index already cleared bonesDirty). + instance.bonesDirty[frameIndex] = true; + instance.boneSet[frameIndex] = allocateBoneSet(); if (instance.boneSet[frameIndex]) { VkDescriptorBufferInfo bufInfo{}; @@ -2426,12 +2431,13 @@ void M2Renderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const } bool useBones = needsBones; if (useBones) { - // Upload bone matrices only when recomputed (skip frame-skipped instances) - if (instance.bonesDirty && instance.boneMapped[frameIndex]) { + // Upload bone matrices only when recomputed (per-frame-index tracking + // ensures both double-buffered SSBOs get the latest bone data) + if (instance.bonesDirty[frameIndex] && instance.boneMapped[frameIndex]) { int numBones = std::min(static_cast(instance.boneMatrices.size()), 128); memcpy(instance.boneMapped[frameIndex], instance.boneMatrices.data(), numBones * sizeof(glm::mat4)); - instance.bonesDirty = false; + instance.bonesDirty[frameIndex] = false; } // Bind bone descriptor set (set 2)