From 46585366528aa4d8b653d097f9a1ed294059e440 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Mon, 20 Jul 2026 01:42:57 -0400 Subject: [PATCH] [Perf] (DirectVulkan): keep the render pass alive for steady-state storage-image draws and skip storage-image collection for programs without them --- .../DirectVulkan/Renderer/ProgramFactory.cpp | 1 + .../DirectVulkan/Renderer/ProgramFactory.h | 7 ++++++ .../Renderer/VkTextureManager.cpp | 18 +++++++++++++++ .../DirectVulkan/Renderer/VkTextureManager.h | 5 +++++ .../DirectVulkan/Renderer/VulkanRenderer.cpp | 22 ++++++++++++++++++- 5 files changed, 52 insertions(+), 1 deletion(-) diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp index 3786b61f..5cce9401 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp @@ -1808,6 +1808,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { layoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; } else if (kind == DescriptorBindingKind::StorageImage) { layoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; + entry.hasStorageImages = true; } else { layoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; } diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.h index 6dba8852..2021c81e 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.h @@ -67,6 +67,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { Vector storageImageUsesBindingFormatByBinding; Vector storageBlockNameByBinding; Vector storageBlockIndexByBinding; + // Set once during ReflectLayout so the per-draw path can skip the whole + // storage-image preparation for the overwhelming majority of programs. + Bool hasStorageImages = false; Int globalUboBinding = -1; Uint32 activeVertexInputLocationMask = 0; Array vertexInputTypes{}; @@ -99,6 +102,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { std::move(other.storageImageUsesBindingFormatByBinding); storageBlockNameByBinding = std::move(other.storageBlockNameByBinding); storageBlockIndexByBinding = std::move(other.storageBlockIndexByBinding); + hasStorageImages = other.hasStorageImages; globalUboBinding = other.globalUboBinding; activeVertexInputLocationMask = other.activeVertexInputLocationMask; vertexInputTypes = other.vertexInputTypes; @@ -110,6 +114,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { other.hash = 0; other.descriptorSetLayout = VK_NULL_HANDLE; other.pipelineLayout = VK_NULL_HANDLE; + other.hasStorageImages = false; other.globalUboBinding = -1; other.activeVertexInputLocationMask = 0; other.activeFragmentOutputLocationMask = 0; @@ -139,6 +144,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { std::move(other.storageImageUsesBindingFormatByBinding); storageBlockNameByBinding = std::move(other.storageBlockNameByBinding); storageBlockIndexByBinding = std::move(other.storageBlockIndexByBinding); + hasStorageImages = other.hasStorageImages; globalUboBinding = other.globalUboBinding; activeVertexInputLocationMask = other.activeVertexInputLocationMask; vertexInputTypes = other.vertexInputTypes; @@ -150,6 +156,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { other.hash = 0; other.descriptorSetLayout = VK_NULL_HANDLE; other.pipelineLayout = VK_NULL_HANDLE; + other.hasStorageImages = false; other.globalUboBinding = -1; other.activeVertexInputLocationMask = 0; other.activeFragmentOutputLocationMask = 0; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp index 78f6fddc..4cd818e8 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp @@ -1140,6 +1140,24 @@ namespace MobileGL::MG_Backend::DirectVulkan { return ok; } + Bool VkTextureManager::NeedsStorageImagePreparation(MG_State::GLState::ITextureObject& texture) const { + const auto it = m_textureResources.find(MakeTextureIdentity(&texture)); + if (it == m_textureResources.end()) { + return true; + } + const TextureResource& resource = it->second; + if (resource.image == VK_NULL_HANDLE || resource.layout != VK_IMAGE_LAYOUT_GENERAL) { + return true; + } + // Mirror SyncTexture's cross-draw skip condition: any version drift means the sync + // path may upload or rebuild, both of which need the render pass ended first. + const auto* mipTexture = MG_State::GLState::AsMipmapTexture(&texture); + const Uint32 mipLevelCount = mipTexture != nullptr ? mipTexture->GetMipmapLevelCount() : 0u; + return resource.syncedContentVersion != texture.GetContentVersion() || + resource.syncedTextureParamsVersion != texture.GetTextureParamsVersion() || + resource.syncedMipLevelCount != mipLevelCount; + } + Bool VkTextureManager::TransitionImageLayout(VkCommandBuffer commandBuffer, VkImage image, VkImageLayout& trackedLayout, VkImageLayout newLayout, VkPipelineStageFlags srcStageMask, VkPipelineStageFlags dstStageMask, diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h index 9e3a3567..a3b13f52 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h @@ -284,6 +284,11 @@ public: VkImageLayout newLayout); Bool TransitionTextureForSampling(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture); Bool TransitionTextureForStorageImage(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture); + // Non-mutating probe for the per-draw storage-image fast path: true when preparing this + // texture as a storage image may need work that is illegal inside a render pass (resource + // creation, dirty-content upload, or a layout transition to GENERAL). Unknown state reports + // true - a false positive merely ends the render pass, a false negative would skip a barrier. + Bool NeedsStorageImagePreparation(MG_State::GLState::ITextureObject& texture) const; static VkImageAspectFlags ResolveSampledImageViewAspectMask(VkImageAspectFlags imageAspect); static VkFormat ResolveSampledImageViewFormat(VkFormat imageFormat, SamplerNumericDomain numericDomain); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index c8383236..c9d08148 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -3552,6 +3552,9 @@ void main() { VkCommandBuffer commandBuffer, const MG_State::GLState::ProgramObject& program, const ProgramFactory::VkProgramObject& programObj) { + if (!programObj.hasStorageImages) { + return true; + } auto& storageTextures = m_storageImageTexturesScratch; if (!m_uniformManager->CollectStorageImageTextures(program, programObj, storageTextures)) { MGLOG_E("%s: failed to collect storage images for program=%u", @@ -3562,6 +3565,24 @@ void main() { return true; } + // Steady-state fast path: when every collected texture is already resident in GENERAL + // with no pending clear and no dirty content, the loop below has nothing to record, so + // keep the render pass alive instead of splitting it on every storage-image draw (on + // tiled GPUs each split is a full tile load/store). GL makes cross-draw image-store + // coherence the app's job (glMemoryBarrier), so no implicit barrier is owed here. + Bool anyNeedsPreparation = false; + for (auto* texture : storageTextures) { + MOBILEGL_ASSERT(texture != nullptr, "%s: collected a null storage texture", __func__); + if (m_textureManager->NeedsStorageImagePreparation(*texture) || + m_clearManager->HasPendingClear(texture)) { + anyNeedsPreparation = true; + break; + } + } + if (!anyNeedsPreparation) { + return true; + } + // Image uploads, deferred-clear materialization, and layout barriers are illegal inside // a classic render pass. Do this before sampler preparation as well: a texture used by // both a sampler and an image must stay in GENERAL, and both descriptors must name that @@ -3571,7 +3592,6 @@ void main() { } for (auto* texture : storageTextures) { - MOBILEGL_ASSERT(texture != nullptr, "%s: collected a null storage texture", __func__); if (!MaterializePendingClearForTexture(commandBuffer, *texture)) { MGLOG_E("%s: failed to materialize pending clear for storage textureId=%d", __func__, texture->GetExternalIndex());