From f098983c9fddea90d4319a2589723ea84926cc78 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Mon, 13 Jul 2026 04:41:14 -0400 Subject: [PATCH] [Perf] (MG_Backend/DirectVulkan): skip the per-draw sampled-texture walk when the bound set is unchanged (texture-bind generation + program state version); CollectSampledTextures 5.0%->0.2%, fps 228->249 --- .../DirectVulkan/Renderer/VulkanRenderer.cpp | 31 +++++++++++++++++-- .../DirectVulkan/Renderer/VulkanRenderer.h | 14 +++++++++ MobileGL/MG_State/GLState/Core.h | 4 +++ .../GLState/TextureState/TextureState.cpp | 4 +++ .../GLState/TextureState/TextureState.h | 10 ++++++ 5 files changed, 60 insertions(+), 3 deletions(-) diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index 058b0b33..daaff1df 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -3389,6 +3389,9 @@ void main() { // Begin command recording if not yet if (!frame.isCommandRecording) { m_frameContext.BeginCommandRecording(); + // New command buffer: a program/FBO address from a previous frame may have been + // recycled, so start the sampled-set skip cache fresh this frame. + m_lastSampledSetValid = false; } auto* activeRenderPass = VkRenderPassManager::GetActiveRenderPass(); @@ -3397,9 +3400,31 @@ void main() { // which probably indicates it's been gone through codepath like `fbo attach` -> `clear` -> `fbo detach`, and // without draws in between to give it a chance to materialize such clear. // Deal with this situation here. - auto& sampledTextures = m_sampledTexturesScratch; // cleared by CollectSampledTextures - Bool hasSampledTextures = m_uniformManager->CollectSampledTextures(program, programObj, sampledTextures); - MOBILEGL_ASSERT(hasSampledTextures, "%s: CollectSampledTextures failed", __func__); + // Reuse the previous draw's sampled-texture list when the set is provably unchanged (same + // program+state+transform and no bind/unbind/delete since), skipping the per-draw GL walk. + // The layout/feedback/transition loops below still run on the list every draw, so this only + // elides re-resolving *which* textures are sampled, never their layout handling. + auto& sampledTextures = m_sampledTexturesScratch; + { + const Uint programIndex = program.GetExternalIndex(); + const Uint32 programVersion = program.GetBackendStateVersion(); + const Uint64 bindGeneration = MG_State::pGLContext->GetTextureBindGeneration(); + const Bool sampledSetUnchanged = + m_lastSampledSetValid && m_lastSampledSetProgramIndex == programIndex && + m_lastSampledSetProgramVersion == programVersion && + m_lastSampledSetTransformFlags == transformFlags && + m_lastSampledSetBindGeneration == bindGeneration; + if (!sampledSetUnchanged) { + const Bool hasSampledTextures = + m_uniformManager->CollectSampledTextures(program, programObj, sampledTextures); + MOBILEGL_ASSERT(hasSampledTextures, "%s: CollectSampledTextures failed", __func__); + m_lastSampledSetValid = true; + m_lastSampledSetProgramIndex = programIndex; + m_lastSampledSetProgramVersion = programVersion; + m_lastSampledSetTransformFlags = transformFlags; + m_lastSampledSetBindGeneration = bindGeneration; + } + } MGLOG_D("SetupDraw: program=%u drawFbo=%u sampledTextureCount=%zu activeRenderPass=%s", program.GetExternalIndex(), drawFbo ? drawFbo->GetExternalIndex() : 0u, sampledTextures.size(), activeRenderPass ? "true" : "false"); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index a035a05d..6ac7199d 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -403,6 +403,20 @@ namespace MobileGL::MG_Backend::DirectVulkan { DepthMipmapResources m_depthMipmapResources; 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 + + // 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. + Bool m_lastSampledSetValid = false; + Uint m_lastSampledSetProgramIndex = 0; + Uint32 m_lastSampledSetProgramVersion = 0; + ProgramFactory::CompileOptionFlags m_lastSampledSetTransformFlags = {}; + Uint64 m_lastSampledSetBindGeneration = 0; + // Per-draw scratch buffers (clear keeps capacity) — these paths run for every // draw call and must not allocate. Vector m_sampledTexturesScratch; diff --git a/MobileGL/MG_State/GLState/Core.h b/MobileGL/MG_State/GLState/Core.h index ae472162..ad65734a 100644 --- a/MobileGL/MG_State/GLState/Core.h +++ b/MobileGL/MG_State/GLState/Core.h @@ -102,6 +102,10 @@ namespace MobileGL { const ImageTextureBinding& GetImageTextureBinding(Int unit) const; void NoteTextureUnitTouched(Int unit) { m_textureState.NoteUnitTouched(unit); } Int GetMaxTouchedTextureUnit() const { return m_textureState.GetMaxTouchedUnit(); } + // Monotonic counter bumped whenever a texture bind/unbind/delete changes which + // texture is bound at a unit; lets a backend skip re-resolving an unchanged + // per-draw sampled-texture set. + Uint64 GetTextureBindGeneration() const { return m_textureState.GetTextureBindGeneration(); } Bool ValidateTextureName(Uint index) const; Bool ValidateTextureObject(Uint index) const; Int GetActiveTextureUnit() const; diff --git a/MobileGL/MG_State/GLState/TextureState/TextureState.cpp b/MobileGL/MG_State/GLState/TextureState/TextureState.cpp index 64586bc6..31617504 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureState.cpp +++ b/MobileGL/MG_State/GLState/TextureState/TextureState.cpp @@ -104,6 +104,10 @@ namespace MobileGL::MG_State::GLState { imageBinding.Bind(nullptr, 0, GL_FALSE, 0, GL_READ_ONLY, GL_R8); } } + // Deleting a texture unbinds it from every unit above; treat that as a binding + // change so a cached sampled-texture set (which may hold this raw pointer) is + // re-resolved instead of dangling. + BumpTextureBindGeneration(); m_textureObjects.erase(index); } m_indexGenerator.Delete(index); diff --git a/MobileGL/MG_State/GLState/TextureState/TextureState.h b/MobileGL/MG_State/GLState/TextureState/TextureState.h index b9ef9b68..7653d7bd 100644 --- a/MobileGL/MG_State/GLState/TextureState/TextureState.h +++ b/MobileGL/MG_State/GLState/TextureState/TextureState.h @@ -63,10 +63,20 @@ namespace MobileGL::MG_State::GLState { // can stop there instead of walking all MAX_TEXTURE_IMAGE_UNITS units. void NoteUnitTouched(Int unit) { if (unit > m_maxTouchedUnit && unit < MAX_TEXTURE_IMAGE_UNITS) m_maxTouchedUnit = unit; + // Every texture/sampler bind entry point (glBindTexture / glBindTextureUnit / + // glBindTextures / glBindSampler) routes through here, so bumping the generation here + // - plus in MarkTextureObjectForDeletion for delete-unbind - covers every change to + // which texture is bound at which unit. A backend that has cached the per-draw + // sampled-texture set can compare this against a snapshot to skip re-resolving it when + // no bind changed (the block atlas + lightmap stay bound across a whole terrain batch). + ++m_textureBindGeneration; } Int GetMaxTouchedUnit() const { return m_maxTouchedUnit; } + Uint64 GetTextureBindGeneration() const { return m_textureBindGeneration; } + void BumpTextureBindGeneration() { ++m_textureBindGeneration; } private: + Uint64 m_textureBindGeneration = 0; Int m_maxTouchedUnit = -1; Int m_activeTextureUnit = 0; Array m_textureUnits;