From 37111ae992f91989cd58f6b42d8e336f8b71a766 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 30 Jul 2026 09:40:54 -0400 Subject: [PATCH] [Perf] (DirectVulkan): snapshot-gated consecutive-draw fast path skips SetupDraw re-resolution --- .../DirectVulkan/Renderer/VkClearManager.h | 6 + .../Renderer/VkRenderPassManager.h | 7 + .../DirectVulkan/Renderer/VkTextureManager.h | 3 + .../DirectVulkan/Renderer/VulkanRenderer.cpp | 174 ++++++++++++++++++ .../DirectVulkan/Renderer/VulkanRenderer.h | 41 +++++ 5 files changed, 231 insertions(+) diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkClearManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkClearManager.h index e6739193..3d4b8c2e 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkClearManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkClearManager.h @@ -121,6 +121,12 @@ namespace MobileGL::MG_Backend::DirectVulkan { SharedPtr& outTexture); Uint8 m_gcCounter = 0; + public: + // Lock-free probe for the consecutive-draw fast path: any pending clear + // forces the full SetupDraw path (which materializes/consumes it). + Bool HasAnyPendingClears() const { return m_pendingCount.load(std::memory_order_relaxed) != 0; } + + private: mutable std::mutex m_mutex; // Lock-free mirror of m_pendingClears.size(), maintained under m_mutex // by every mutation. The per-draw probes (HasPendingClear/GetPending*) diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.h index e1b7cc21..84dee790 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.h @@ -238,6 +238,13 @@ namespace MobileGL::MG_Backend::DirectVulkan { // image recreation. Uint64 m_renderbufferImageEpoch = 1; + public: + // Bumped whenever a renderbuffer backing is (re)created; consecutive-draw + // snapshots include it so an attachment respecify forces a re-resolve. + Uint64 GetRenderbufferImageEpoch() const { return m_renderbufferImageEpoch; } + + private: + // Per-draw fast-path memo for GetOrCreateRenderPass (dirty-flag state tracking): when the // framebuffer state is provably unchanged since the last resolution, the active render pass // is reused WITHOUT recomputing the expensive per-draw hash. Invalidated by FBO switch / diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h index 33b683d8..b375bc05 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h @@ -28,6 +28,9 @@ public: // manager keys its per-draw fast path on this so an attachment's image recreation // invalidates the cached render pass (dirty-flag tracking; portable to Vulkan 1.1). Uint64 GetTextureImageEpoch() const { return m_textureImageEpoch; } + // Bumped whenever any tracked texture resource is erased; cached + // TextureResource pointers are valid only while this is unchanged. + Uint64 GetResourceEraseEpoch() const { return m_resourceEraseEpoch; } struct TextureIdentity { MG_State::GLState::ITextureObject* texture = nullptr; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index 97a8d458..612b90dc 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -4444,6 +4444,129 @@ void main() { return true; } + + Bool VulkanRenderer::TrySetupDrawFastPath(FrameContext::FrameData& frame, GLenum mode, + Flags aspects, const DrawCmdParam& drawParams, + const IndexBufferView* pIndexBufferView) { + const SetupDrawSnapshot& snap = m_setupDrawSnapshot; + if (!snap.valid || !frame.isCommandRecording) { + return false; + } + if (snap.aspects != aspects.GetRaw() || snap.mode != mode) { + return false; + } + if (m_clearManager->HasAnyPendingClears()) { + return false; + } + const auto* activeRenderPass = VkRenderPassManager::GetActiveRenderPass(); + if (activeRenderPass == nullptr || activeRenderPass->hash != snap.renderPassHash || + snap.imageIndex != m_imageIndexAcquired) { + return false; + } + const auto& program = *MG_State::pGLContext->GetCurrentProgram(); + if (program.GetLifetimeId() != snap.programLifetimeId || + program.GetBackendStateVersion() != snap.programVersion) { + return false; + } + const auto& vao = *MG_State::pGLContext->GetBoundVertexArray(); + if (static_cast(&vao) != snap.vao || vao.GetConfigVersion() != snap.vaoConfigVersion) { + return false; + } + const auto& drawFbo = + MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject(); + if (static_cast(drawFbo.get()) != snap.drawFbo || + drawFbo->GetObjectVersion() != snap.fboVersion) { + return false; + } + if (MG_State::pGLContext->GetRenderStateParametersVersion() != snap.renderStateVersion || + MG_State::pGLContext->GetTextureBindGeneration() != snap.bindGeneration) { + return false; + } + if (GetShaderTransformFlags(m_swapchainObject.GetPreTransform()).GetRaw() != snap.baseTransformFlags) { + return false; + } + if (m_textureManager->GetResourceEraseEpoch() != snap.textureEraseEpoch || + m_textureManager->GetTextureImageEpoch() != snap.textureImageEpoch || + m_renderPassManager->GetRenderbufferImageEpoch() != snap.renderbufferImageEpoch) { + return false; + } + + // Same sampled set as the snapshotting draw (program/bind keys above); + // verify content and params are untouched and every layout is still + // sampleable, then stamp recording use exactly as the full path would. + // A feedback case (sampled texture written by the active pass) fails the + // layout check and falls back to the full path's end-pass handling. + const auto& sampledTextures = m_sampledTexturesScratch; + const auto& sampledResources = m_sampledResourcesScratch; + if (sampledResources.size() != sampledTextures.size()) { + return false; + } + Uint64 contentSum = 0; + Uint64 paramsSum = 0; + for (SizeT i = 0; i < sampledTextures.size(); ++i) { + const auto* sampledTexture = sampledTextures[i]; + if (sampledTexture == nullptr) { + continue; + } + const auto* resource = sampledResources[i]; + if (resource == nullptr || !IsValidSampledImageLayout(resource->layout)) { + return false; + } + contentSum += sampledTexture->GetContentVersion(); + paramsSum += sampledTexture->GetTextureParamsVersion(); + } + if (contentSum != snap.sampledContentSum || paramsSum != snap.sampledParamsSum) { + return false; + } + for (SizeT i = 0; i < sampledTextures.size(); ++i) { + if (sampledTextures[i] != nullptr && sampledResources[i] != nullptr) { + m_textureManager->StampResourceRecordingUse(*sampledResources[i]); + } + } + + // Everything the full path would re-resolve is provably unchanged; run + // only the per-draw tail. + if (!g_dynamicStateShadow.graphicsPipelineValid || + g_dynamicStateShadow.graphicsPipeline != snap.pipeline) { + vkCmdBindPipeline(frame.commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, snap.pipeline); + g_dynamicStateShadow.graphicsPipelineValid = true; + g_dynamicStateShadow.graphicsPipeline = snap.pipeline; + } + const auto& programObj = m_programFactory->GetOrCreateProgram( + program, ProgramFactory::CompileOptionFlags(snap.resolvedTransformFlags)); + if (!m_uniformManager->BindProgramUniformBuffers(frame.commandBuffer, program, programObj, + m_frameContext.GetCurrentFrameIndex())) { + return false; + } + if (!UploadAndBindVertexBuffers(frame.commandBuffer, vao, programObj, drawParams, pIndexBufferView)) { + return false; + } + if (aspects & DrawSetupAspect::IndexBuffer) { + const Bool idxUploadOk = UploadAndBindIndexBuffer(frame, vao, pIndexBufferView); + MOBILEGL_ASSERT(idxUploadOk, "SetupDraw fast path: failed to upload index buffer"); + } + ApplyGLViewportState(frame.commandBuffer, snap.renderPassExtent, m_swapchainObject.GetPreTransform(), + snap.drawFboIsDefault); + ApplyBlendConstants(frame.commandBuffer); + ApplyPolygonOffsetState(frame.commandBuffer); + ApplyLineWidthState(frame.commandBuffer); + ApplyStencilState(frame.commandBuffer); + const Bool scissorEnabled = MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::ScissorTest); + VkRect2D scissor{}; + if (scissorEnabled) { + const auto& scissorBox = MG_State::pGLContext->GetScissorBox(); + scissor = snap.drawFboIsDefault + ? MakeDefaultFramebufferScissorRect(scissorBox, snap.renderPassExtent, + m_swapchainObject.GetPreTransform()) + : MakeClampedScissorRect(scissorBox, snap.renderPassExtent); + } else { + scissor.offset = {0, 0}; + scissor.extent = { (Uint)snap.renderPassExtent.x(), (Uint)snap.renderPassExtent.y() }; + } + ShadowedSetScissor(frame.commandBuffer, scissor); + return true; + } + Bool VulkanRenderer::SetupDraw(FrameContext::FrameData& frame, GLenum mode, Flags aspects, const DrawCmdParam& drawParams, const IndexBufferView* pIndexBufferView) { @@ -4452,6 +4575,12 @@ void main() { // otherwise each re-run the full SyncTexture path on the same textures. VkTextureManager::DrawSyncScope drawSyncScope(*m_textureManager); m_textureManager->CollectGarbage(); + if (TrySetupDrawFastPath(frame, mode, aspects, drawParams, pIndexBufferView)) { + return true; + } + // The fast path declined: whatever it saw may be stale. The full path + // below re-resolves everything and refreshes the snapshot on success. + m_setupDrawSnapshot.valid = false; const auto& drawFbo = MG_State::pGLContext->GetFramebufferBindingSlot(FramebufferTarget::Draw).GetBoundObject(); if (drawFbo != nullptr && IsUnsupportedFramebufferForDirectVulkan(*drawFbo)) { @@ -4790,6 +4919,48 @@ void main() { scissor.extent = { (Uint)renderPassEntry->extent.x(), (Uint)renderPassEntry->extent.y() }; } ShadowedSetScissor(frame.commandBuffer, scissor); + + // Snapshot the fully resolved configuration for the consecutive-draw + // fast path (see TrySetupDrawFastPath). + { + auto& snap = m_setupDrawSnapshot; + const auto* nowActiveRenderPass = VkRenderPassManager::GetActiveRenderPass(); + if (nowActiveRenderPass != nullptr && !programObj.hasStorageImages) { + snap.valid = true; + snap.aspects = aspects.GetRaw(); + snap.mode = mode; + snap.programLifetimeId = program.GetLifetimeId(); + snap.programVersion = program.GetBackendStateVersion(); + snap.vao = &vao; + snap.vaoConfigVersion = vao.GetConfigVersion(); + snap.drawFbo = drawFbo.get(); + snap.fboVersion = drawFbo->GetObjectVersion(); + snap.drawFboIsDefault = drawFbo->IsDefaultFramebuffer(); + snap.renderStateVersion = MG_State::pGLContext->GetRenderStateParametersVersion(); + snap.bindGeneration = MG_State::pGLContext->GetTextureBindGeneration(); + snap.baseTransformFlags = GetShaderTransformFlags(m_swapchainObject.GetPreTransform()).GetRaw(); + snap.resolvedTransformFlags = transformFlags.GetRaw(); + snap.renderPassHash = nowActiveRenderPass->hash; + snap.imageIndex = m_imageIndexAcquired; + snap.textureEraseEpoch = m_textureManager->GetResourceEraseEpoch(); + snap.textureImageEpoch = m_textureManager->GetTextureImageEpoch(); + snap.renderbufferImageEpoch = m_renderPassManager->GetRenderbufferImageEpoch(); + snap.renderPassExtent = renderPassEntry->extent; + snap.pipeline = pipeline; + Uint64 snapContentSum = 0; + Uint64 snapParamsSum = 0; + for (const auto* sampledTexture : sampledTextures) { + if (sampledTexture != nullptr) { + snapContentSum += sampledTexture->GetContentVersion(); + snapParamsSum += sampledTexture->GetTextureParamsVersion(); + } + } + snap.sampledContentSum = snapContentSum; + snap.sampledParamsSum = snapParamsSum; + } else { + snap.valid = false; + } + } return true; } @@ -7809,6 +7980,7 @@ void main() { void VulkanRenderer::OnFrameCommandRecordingBegan(VkCommandBuffer commandBuffer) { // Dynamic state does not survive a command-buffer boundary. ResetDynamicStateShadow(); + m_setupDrawSnapshot.valid = false; if (m_uniformManager) { m_uniformManager->OnCommandBufferBoundary(); } @@ -7932,6 +8104,7 @@ void main() { // A recreated pipeline could reuse a freed handle value and alias // the bind-dedup shadow; force the next draw to re-bind. g_dynamicStateShadow.graphicsPipelineValid = false; + m_setupDrawSnapshot.valid = false; } m_vertexInputStateFactory->OnFrameBoundary(); m_samplerManager->OnFrameBoundary(); @@ -8959,6 +9132,7 @@ void main() { } InvalidatePipelineMemo(); // pipelines freed -> the memoized handle would dangle g_dynamicStateShadow.graphicsPipelineValid = false; + m_setupDrawSnapshot.valid = false; DestroyComputePipelines(); if (m_frameContext.GetFrameCount() > 0) { m_frameContext.GetCurrent().isCommandRecording = false; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index ef8676a7..14c8385a 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -151,6 +151,14 @@ namespace MobileGL::MG_Backend::DirectVulkan { Bool SetupDraw(FrameContext::FrameData& frame, GLenum mode, Flags aspects, const DrawCmdParam& drawParams, const IndexBufferView* pIndexBufferView = nullptr); + // ANGLE-style consecutive-draw fast path: SetupDraw snapshots the fully + // resolved draw configuration; the next draw whose cheap version/identity + // checks all match skips the resolution half (LOD probe, sampled-set + // walk, render-pass and pipeline resolution) and jumps straight to the + // per-draw tail. Returns false (leaving no side effects that the full + // path cannot redo idempotently) whenever anything might have changed. + Bool TrySetupDrawFastPath(FrameContext::FrameData& frame, GLenum mode, Flags aspects, + const DrawCmdParam& drawParams, const IndexBufferView* pIndexBufferView); void ClearAttachmentsOnActiveRenderPass(VkCommandBuffer commandBuffer, const RenderPassEntry& compatibleRenderPassEntry); @@ -521,6 +529,39 @@ namespace MobileGL::MG_Backend::DirectVulkan { ProgramFactory::CompileOptionFlags m_lastLodBaseFlags = {}; ProgramFactory::CompileOptionFlags m_lastLodResultFlags = {}; + // Snapshot behind TrySetupDrawFastPath. Values only: the program and + // render-pass caches are open-addressing maps whose entries move on + // insert, so no pointers into them are cached; the pipeline handle is + // protected by the command-buffer-boundary reset plus the mid-frame + // pipeline-destruction resets, and monotonic epochs guard everything + // that can be destroyed or recreated between draws. + struct SetupDrawSnapshot { + Bool valid = false; + Uint8 aspects = 0; + GLenum mode = 0; + Uint64 programLifetimeId = 0; + Uint32 programVersion = 0; + const void* vao = nullptr; + Uint32 vaoConfigVersion = 0; + const void* drawFbo = nullptr; + Uint16 fboVersion = 0; + Bool drawFboIsDefault = false; + Uint renderStateVersion = 0; + Uint64 bindGeneration = 0; + Uint32 baseTransformFlags = 0; + Uint32 resolvedTransformFlags = 0; + Uint64 renderPassHash = 0; + Uint32 imageIndex = 0; + Uint64 textureEraseEpoch = 0; + Uint64 textureImageEpoch = 0; + Uint64 renderbufferImageEpoch = 0; + Uint64 sampledContentSum = 0; + Uint64 sampledParamsSum = 0; + IntVec2 renderPassExtent = {0, 0}; + VkPipeline pipeline = VK_NULL_HANDLE; + }; + SetupDrawSnapshot m_setupDrawSnapshot; + // Per-draw scratch buffers (clear keeps capacity) — these paths run for every // draw call and must not allocate. Vector m_sampledTexturesScratch;