From 7fe5247626c790b7bf75338776d4797e70893958 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Mon, 13 Jul 2026 04:58:05 -0400 Subject: [PATCH] [Fix] (MG_Backend/DirectVulkan): key the sampled-set walk-skip on a program lifetime id, not the recyclable GL name, so a deleted+recreated program can't false-hit the cache --- .../DirectVulkan/Renderer/VulkanRenderer.cpp | 6 +++--- .../MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h | 9 +++++---- .../MG_State/GLState/ProgramState/ProgramObject.cpp | 7 +++++++ MobileGL/MG_State/GLState/ProgramState/ProgramObject.h | 10 +++++++++- 4 files changed, 24 insertions(+), 8 deletions(-) diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index daaff1df..f62b2313 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -3406,11 +3406,11 @@ void main() { // elides re-resolving *which* textures are sampled, never their layout handling. auto& sampledTextures = m_sampledTexturesScratch; { - const Uint programIndex = program.GetExternalIndex(); + const Uint64 programLifetimeId = program.GetLifetimeId(); const Uint32 programVersion = program.GetBackendStateVersion(); const Uint64 bindGeneration = MG_State::pGLContext->GetTextureBindGeneration(); const Bool sampledSetUnchanged = - m_lastSampledSetValid && m_lastSampledSetProgramIndex == programIndex && + m_lastSampledSetValid && m_lastSampledSetProgramLifetimeId == programLifetimeId && m_lastSampledSetProgramVersion == programVersion && m_lastSampledSetTransformFlags == transformFlags && m_lastSampledSetBindGeneration == bindGeneration; @@ -3419,7 +3419,7 @@ void main() { m_uniformManager->CollectSampledTextures(program, programObj, sampledTextures); MOBILEGL_ASSERT(hasSampledTextures, "%s: CollectSampledTextures failed", __func__); m_lastSampledSetValid = true; - m_lastSampledSetProgramIndex = programIndex; + m_lastSampledSetProgramLifetimeId = programLifetimeId; m_lastSampledSetProgramVersion = programVersion; m_lastSampledSetTransformFlags = transformFlags; m_lastSampledSetBindGeneration = bindGeneration; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index 6ac7199d..8fbf9034 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -404,15 +404,16 @@ namespace MobileGL::MG_Backend::DirectVulkan { Vector m_deferredDepthMipmapCleanup; // Skip the per-draw CollectSampledTextures walk (~5% of the render thread) when the sampled - // texture SET is provably unchanged from the previous draw: same program (external index + + // texture SET is provably unchanged from the previous draw: same program (lifetime id + // backend-state version, which covers sampler-uniform reassignment / relink) and transform // flags, and no texture bind/unbind/delete since (GetTextureBindGeneration). On a hit, // m_sampledTexturesScratch still holds the previous draw's list and steps 2-4 (feedback / // layout probe / transition) re-run on it, so layout correctness is unaffected - only the GL - // walk is skipped. Reset per command-buffer recording so a reused program/FBO address can't - // outlive a frame. + // walk is skipped. The program lifetime id (never reused, unlike the GL name) and the + // monotonic bind generation make the key ABA-proof; the per-command-buffer reset is a cheap + // belt-and-suspenders. Bool m_lastSampledSetValid = false; - Uint m_lastSampledSetProgramIndex = 0; + Uint64 m_lastSampledSetProgramLifetimeId = 0; Uint32 m_lastSampledSetProgramVersion = 0; ProgramFactory::CompileOptionFlags m_lastSampledSetTransformFlags = {}; Uint64 m_lastSampledSetBindGeneration = 0; diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp index 3ee65d94..8abe3b5d 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp @@ -7,6 +7,7 @@ // End of Source File Header #include "ProgramObject.h" +#include #include #include #include @@ -122,6 +123,12 @@ namespace { } namespace MobileGL::MG_State::GLState { + static std::atomic s_nextProgramLifetimeId = 1; + + Uint64 ProgramObject::AllocateLifetimeId() { + return s_nextProgramLifetimeId.fetch_add(1, std::memory_order_relaxed); + } + void ProgramObject::ResetLinkArtifacts() { // Relinking regenerates the SPIR-V, so any backend-cached state keyed on // m_backendStateVersion (e.g. the content-hash memo) must be invalidated, diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h index a90c3e7e..3687e05d 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h @@ -16,7 +16,7 @@ namespace MobileGL::MG_State::GLState { class ProgramObject { public: - ProgramObject(Uint externalIndex) : m_externalIndex(externalIndex) {} + ProgramObject(Uint externalIndex) : m_externalIndex(externalIndex), m_lifetimeId(AllocateLifetimeId()) {} bool ShaderIsAttached(const SharedPtr& shader); bool AttachShader(const SharedPtr& shader); SizeT DetachShader(const SharedPtr& shader); @@ -340,6 +340,11 @@ namespace MobileGL::MG_State::GLState { } Uint GetExternalIndex() const { return m_externalIndex; } + // Globally-unique, never-reused id for this program object's lifetime. Unlike the GL + // name (external index), which is freed to a LIFO list and immediately handed back by + // the next glCreateProgram, this distinguishes a deleted-and-recreated program from the + // original, so an identity cache can't false-hit on name recycling. + Uint64 GetLifetimeId() const { return m_lifetimeId; } private: void ResetLinkArtifacts(); @@ -349,7 +354,10 @@ namespace MobileGL::MG_State::GLState { void AddDefaultFragmentShaderIfMissing(); Bool ValidateFragmentOutputLocations(); + static Uint64 AllocateLifetimeId(); + const Uint m_externalIndex = 0; + const Uint64 m_lifetimeId = 0; Vector> m_shaders; Vector> m_detachedShaders; // Store detached shaders and remove on next link