From fc4cd980f2b2a4a16cbe1834d53a2ea658db5ea9 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Wed, 29 Jul 2026 07:07:50 -0400 Subject: [PATCH] [Fix] (DirectVulkan): bound image mutability so Adreno keeps UBWC compression - Every storage-capable colour texture was created MUTABLE_FORMAT, and Adreno gives up bandwidth compression on an image that may be viewed as any format in its compatibility class. MC's main render target therefore ran uncompressed; in a fill-bound scene that is the whole frame budget. Measured on Adreno 650, MC 26.2, same scene and camera, device cooled to 38-40C before each run: 65.3 -> 80.9 fps (+23.9%), GPU busy ~93% in both. - VK_KHR_image_format_list (enabled when present) fixes it without giving up mutability: VkImageFormatListCreateInfo names the exact formats a view may use, so the driver can keep the image compressed. The set must be exhaustive or the result is undefined - for sampled views it is exactly what ResolveSampledImageViewFormat can return over the three numeric domains. - glBindImageTexture may name any compatible format, which cannot be enumerated ahead of time, so a texture bound to an image unit gets no format list. That is what VK_IMAGE_USAGE_STORAGE_BIT becoming on-demand is for: it makes "unmarked" mean "will never receive an arbitrary-format storage view", which is what makes the list sound. Removing STORAGE is worth nothing on its own (65.4 fps, measured) - only the mutability bound pays. - MarkStorageImageTexture runs over every collected image-unit texture before the probe loop in PrepareStorageImageTextures, because that loop stops at the first texture needing work and would leave the rest unmarked. The mark makes NeedsStorageImagePreparation report true, which is what ends the render pass, so the recreate lands outside it. - storageUsageResolved separates "not upgraded yet" from "this format can never carry STORAGE", so a format whose optimalTilingFeatures lack STORAGE_IMAGE cannot ask for a recreate that will never happen. SyncTexture's cross-draw early-out also has to break on a pending upgrade or the recreate never runs. - An upgrade recreates the image and carries its contents forward through PreserveTextureContentsOnRecreate, which submits its own command buffer and waits. Whatever the frame already recorded into the old image is still unsubmitted, so that copy would read pre-frame content and this frame's rendering into the texture would be lost - exactly the render-target-then- image-unit case. PrepareStorageImageTextures now flushes first; it takes the FrameData rather than a command buffer because the flush retires the current one, and drops the sampled-descriptor-set memo that described it. --- .../Renderer/VkTextureManager.cpp | 119 +++++++++++++++--- .../DirectVulkan/Renderer/VkTextureManager.h | 32 +++++ .../DirectVulkan/Renderer/VulkanRenderer.cpp | 69 ++++++++-- .../DirectVulkan/Renderer/VulkanRenderer.h | 8 +- 4 files changed, 204 insertions(+), 24 deletions(-) diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp index 5a7dab65..a8252d5b 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp @@ -587,6 +587,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { m_allocator = initInfo.allocator; m_commandPool = initInfo.commandPool; m_graphicsQueue = initInfo.graphicsQueue; + m_imageFormatListSupported = initInfo.imageFormatListSupported; m_currentFrameIndex = 0; m_deferredReleases.clear(); m_deferredReleases.resize(initInfo.frameCount); @@ -609,6 +610,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { DestroyDeferredReleases(); m_textureResources.clear(); m_aliveObjects.clear(); + m_storageImageTextures.clear(); m_device = VK_NULL_HANDLE; m_physicalDevice = VK_NULL_HANDLE; @@ -656,6 +658,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { m_textureResources.erase(resourceIt); } m_aliveObjects.erase(identity); + m_storageImageTextures.erase(identity); } void VkTextureManager::PruneStaleTextureAliases(MG_State::GLState::ITextureObject* texture) { @@ -1189,8 +1192,25 @@ namespace MobileGL::MG_Backend::DirectVulkan { return ok; } + void VkTextureManager::MarkStorageImageTexture(MG_State::GLState::ITextureObject& texture) { + m_storageImageTextures.insert(MakeTextureIdentity(&texture)); + } + + Bool VkTextureManager::NeedsStorageUsageUpgrade(MG_State::GLState::ITextureObject& texture) const { + const TextureIdentity identity = MakeTextureIdentity(&texture); + if (m_storageImageTextures.find(identity) == m_storageImageTextures.end()) { + return false; + } + const auto it = m_textureResources.find(identity); + // No image yet: the first sync creates it with STORAGE straight away, so there is nothing + // to preserve and nothing to order against. + return it != m_textureResources.end() && it->second.image != VK_NULL_HANDLE && + !it->second.storageUsageResolved; + } + Bool VkTextureManager::NeedsStorageImagePreparation(MG_State::GLState::ITextureObject& texture) const { - const auto it = m_textureResources.find(MakeTextureIdentity(&texture)); + const TextureIdentity identity = MakeTextureIdentity(&texture); + const auto it = m_textureResources.find(identity); if (it == m_textureResources.end()) { return true; } @@ -1198,6 +1218,12 @@ namespace MobileGL::MG_Backend::DirectVulkan { if (resource.image == VK_NULL_HANDLE || resource.layout != VK_IMAGE_LAYOUT_GENERAL) { return true; } + // The image predates this texture's first image-unit binding, so it was created without + // STORAGE usage and has to be recreated - which is illegal inside a render pass. + if (!resource.storageUsageResolved && + m_storageImageTextures.find(identity) != m_storageImageTextures.end()) { + 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); @@ -1304,7 +1330,13 @@ namespace MobileGL::MG_Backend::DirectVulkan { const auto* syncingMipTexture = MG_State::GLState::AsMipmapTexture(&texture); const Uint32 syncingMipLevelCount = syncingMipTexture != nullptr ? syncingMipTexture->GetMipmapLevelCount() : 0u; - if (outResource.image != VK_NULL_HANDLE && + // A pending storage-usage upgrade also has to bust the skip: nothing about the texture's + // content or params changed, but the image itself must be recreated with STORAGE usage + // before it can back an image-unit descriptor. + const Bool storageUpgradePending = + !outResource.storageUsageResolved && + m_storageImageTextures.find(MakeTextureIdentity(&texture)) != m_storageImageTextures.end(); + if (outResource.image != VK_NULL_HANDLE && !storageUpgradePending && outResource.syncedContentVersion == syncingContentVersion && outResource.syncedTextureParamsVersion == texture.GetTextureParamsVersion() && outResource.syncedMipLevelCount == syncingMipLevelCount) { @@ -1415,16 +1447,44 @@ namespace MobileGL::MG_Backend::DirectVulkan { const VkImageAspectFlags aspect = GetAspectMaskForFormat(format); VkFormatProperties formatProperties{}; vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &formatProperties); - const Bool supportsStorageImage = + // Only textures that have actually been bound to a GL image unit get STORAGE usage (and + // the MUTABLE_FORMAT it drags in for format-reinterpreting image views). Requesting it + // for every storage-capable colour texture costs real bandwidth: Adreno cannot keep UBWC + // compression on an image that may be written through a storage descriptor, so the whole + // render target - MC's included - runs uncompressed. MarkStorageImageTexture upgrades a + // texture before its first image-unit draw, and the usage below feeds the compatibility + // check so the upgrade recreates the image. + const Bool markedAsStorageImage = + m_storageImageTextures.find(MakeTextureIdentity( + const_cast(&texture))) != m_storageImageTextures.end(); + // Storage-image CAPABILITY (does the format allow it at all) is deliberately separate from + // whether this texture actually needs the usage. MUTABLE_FORMAT keys off capability, as + // before: format-reinterpreting views are not a storage-only concern - the SAMPLED path + // needs them too (GetOrCreateSampledImageView bails out without it, see ~line 892), so + // tying MUTABLE_FORMAT to the image-unit mark would break sampled format reinterpretation + // for every texture that never becomes a storage image. + const Bool storageImageCapable = !isMultisampleTexture && (aspect & VK_IMAGE_ASPECT_COLOR_BIT) != 0 && (formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT) != 0; + const Bool supportsStorageImage = storageImageCapable && markedAsStorageImage; VkImageCreateFlags imageCreateFlags = shapeInfo.imageFlags; - if (supportsStorageImage && IsMutableStorageImageFormat(format) && + if (storageImageCapable && IsMutableStorageImageFormat(format) && m_mutableFormatUnsupported.find(format) == m_mutableFormatUnsupported.end()) { imageCreateFlags |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT; } + VkImageUsageFlags desiredUsage = + VK_IMAGE_USAGE_SAMPLED_BIT | + (supportsStorageImage ? VK_IMAGE_USAGE_STORAGE_BIT : 0) | + ((aspect & VK_IMAGE_ASPECT_COLOR_BIT) ? VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT : 0) | + (((aspect & VK_IMAGE_ASPECT_DEPTH_BIT) || (aspect & VK_IMAGE_ASPECT_STENCIL_BIT)) ? + VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT : + 0); + if (!isMultisampleTexture) { + desiredUsage |= VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT; + } + const Bool compatible = resource.image != VK_NULL_HANDLE && resource.format == format && resource.extent.width == static_cast(texelSize.x()) && resource.extent.height == static_cast(texelSize.y()) && @@ -1433,6 +1493,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { resource.viewType == shapeInfo.viewType && resource.sampleCount == resolvedSampleCount && resource.imageCreateFlags == imageCreateFlags && + resource.usageFlags == desiredUsage && resource.mipLevels == backingMipLevels; if (compatible) { if (resource.perMipViews.size() != backingMipLevels) { @@ -1441,6 +1502,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { if (resource.perMipSampledViews.size() != backingMipLevels) { resource.perMipSampledViews.resize(backingMipLevels, VK_NULL_HANDLE); } + // Keeping the image is itself the answer to the mark: either it already carries + // STORAGE, or this format can never carry it. Either way there is nothing left to + // recreate, so stop reporting the texture as needing preparation. + resource.storageUsageResolved = markedAsStorageImage; return true; } @@ -1455,7 +1520,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { resource.sampleCount == resolvedSampleCount && resource.imageCreateFlags == imageCreateFlags && resolvedSampleCount == VK_SAMPLE_COUNT_1_BIT && - resource.mipLevels < backingMipLevels && + // '<=' rather than '<': a storage-usage upgrade recreates the image with an + // unchanged mip count, and its contents (a render target's pixels live only on the + // GPU) still have to survive. The vkCmdCopyImage below copies min(mipLevels). + resource.mipLevels <= backingMipLevels && resource.layout != VK_IMAGE_LAYOUT_UNDEFINED; std::unique_ptr preservedResource; @@ -1477,16 +1545,37 @@ namespace MobileGL::MG_Backend::DirectVulkan { imageInfo.format = format; imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL; imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; - imageInfo.usage = VK_IMAGE_USAGE_SAMPLED_BIT | - (supportsStorageImage ? VK_IMAGE_USAGE_STORAGE_BIT : 0) | - ((aspect & VK_IMAGE_ASPECT_COLOR_BIT) ? VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT : 0) | - (((aspect & VK_IMAGE_ASPECT_DEPTH_BIT) || (aspect & VK_IMAGE_ASPECT_STENCIL_BIT)) ? - VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT : - 0); - if (!isMultisampleTexture) { - imageInfo.usage |= VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT; - } + imageInfo.usage = desiredUsage; imageInfo.samples = resolvedSampleCount; + + // Bound the mutability. A blindly-mutable image has to be laid out so that ANY format in + // its compatibility class can be viewed, which costs bandwidth compression on tilers; + // naming the exact set instead lets the driver keep it. Only safe when that set really is + // exhaustive, so it is restricted to textures that are not image-unit bound: sampled views + // can only ever ask for ResolveSampledImageViewFormat's output, whereas glBindImageTexture + // may name any compatible format, which nothing here can enumerate ahead of time. + Vector viewFormats; + VkImageFormatListCreateInfo formatListInfo{}; + if (m_imageFormatListSupported && !supportsStorageImage && + (imageInfo.flags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) != 0) { + viewFormats.push_back(format); + for (const SamplerNumericDomain domain : {SamplerNumericDomain::Float, + SamplerNumericDomain::SignedInteger, + SamplerNumericDomain::UnsignedInteger}) { + const VkFormat viewFormat = ResolveSampledImageViewFormat(format, domain); + if (viewFormat == VK_FORMAT_UNDEFINED) { + continue; + } + if (std::find(viewFormats.begin(), viewFormats.end(), viewFormat) == viewFormats.end()) { + viewFormats.push_back(viewFormat); + } + } + formatListInfo.sType = VK_STRUCTURE_TYPE_IMAGE_FORMAT_LIST_CREATE_INFO; + formatListInfo.viewFormatCount = static_cast(viewFormats.size()); + formatListInfo.pViewFormats = viewFormats.data(); + imageInfo.pNext = &formatListInfo; + } + if (isMultisampleTexture || (imageInfo.flags & VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT) != 0) { VkImageFormatProperties imageFormatProperties{}; VkResult imageFormatResult = vkGetPhysicalDeviceImageFormatProperties( @@ -1543,6 +1632,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { resource.viewType = shapeInfo.viewType; resource.sampleCount = resolvedSampleCount; resource.imageCreateFlags = imageCreateFlags; + resource.usageFlags = imageInfo.usage; + resource.storageUsageResolved = markedAsStorageImage; resource.syncedTextureParamsVersion = 0; if (preservedResource) { diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h index c56567c2..4c47f15e 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h @@ -53,6 +53,9 @@ public: VkCommandPool commandPool = VK_NULL_HANDLE; VkQueue graphicsQueue = VK_NULL_HANDLE; Uint32 frameCount = 0; + // VK_KHR_image_format_list is enabled: MUTABLE_FORMAT images can name the exact set of + // formats they will be viewed as, which is what lets a tiler keep them compressed. + Bool imageFormatListSupported = false; }; struct TextureResource { @@ -157,6 +160,17 @@ public: VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D; VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT; VkImageCreateFlags imageCreateFlags = 0; + // Usage the live image was created with. STORAGE is only requested for textures that + // have actually been bound to a GL image unit, because on Adreno a storage-capable + // image loses UBWC bandwidth compression; a later image binding upgrades the usage + // and recreates the image, so the resolved usage has to be part of the compatibility + // check that decides whether the existing image can be kept. + VkImageUsageFlags usageFlags = 0; + // True once this image was (re)resolved while the texture was already marked as an + // image-unit texture. Distinguishes "not upgraded yet" from "cannot be upgraded" + // (a format whose optimalTilingFeatures lack STORAGE_IMAGE never gains the bit), so + // NeedsStorageImagePreparation cannot ask for a recreate that will never happen. + Bool storageUsageResolved = false; Uint16 syncedTextureParamsVersion = 0; // Snapshot of ITextureObject::GetContentVersion() at the last successful sync; // lets SyncTexture skip the whole re-check/re-upload when content is unchanged. @@ -190,6 +204,8 @@ public: std::swap(this->viewType, that.viewType); std::swap(this->sampleCount, that.sampleCount); std::swap(this->imageCreateFlags, that.imageCreateFlags); + std::swap(this->usageFlags, that.usageFlags); + std::swap(this->storageUsageResolved, that.storageUsageResolved); std::swap(this->syncedTextureParamsVersion, that.syncedTextureParamsVersion); std::swap(this->syncedContentVersion, that.syncedContentVersion); std::swap(this->syncedMipLevelCount, that.syncedMipLevelCount); @@ -251,6 +267,8 @@ public: viewType = VK_IMAGE_VIEW_TYPE_2D; sampleCount = VK_SAMPLE_COUNT_1_BIT; imageCreateFlags = 0; + usageFlags = 0; + storageUsageResolved = false; syncedTextureParamsVersion = 0; syncedContentVersion = 0; syncedMipLevelCount = 0; @@ -289,6 +307,17 @@ public: VkImageLayout newLayout); Bool TransitionTextureForSampling(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture); Bool TransitionTextureForStorageImage(VkCommandBuffer commandBuffer, MG_State::GLState::ITextureObject& texture); + // Records that this texture is bound to a GL image unit, so its image must carry + // VK_IMAGE_USAGE_STORAGE_BIT. Must be called before NeedsStorageImagePreparation, and + // therefore before the render pass is committed: an image that has to be upgraded is + // recreated, which is illegal inside a render pass. Sticky for the texture's lifetime - + // GL lets an image binding come and go, and re-creating the image every time it does + // would cost far more than the compression it wins back. + void MarkStorageImageTexture(MG_State::GLState::ITextureObject& texture); + // True when this texture is marked but its live image predates the mark, i.e. the next sync + // will recreate it with STORAGE usage and copy the old contents forward. Callers use this to + // submit their pending recording first, so that copy cannot read pre-flush content. + Bool NeedsStorageUsageUpgrade(MG_State::GLState::ITextureObject& texture) const; // 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 @@ -375,6 +404,7 @@ private: VmaAllocator m_allocator = nullptr; VkCommandPool m_commandPool = VK_NULL_HANDLE; VkQueue m_graphicsQueue = VK_NULL_HANDLE; + Bool m_imageFormatListSupported = false; Uint32 m_currentFrameIndex = 0; Uint8 m_gcCounter = 0; @@ -398,6 +428,8 @@ private: std::unordered_set m_mutableFormatUnsupported; std::unordered_map, TextureIdentityHash> m_aliveObjects; std::unordered_map m_textureResources; + // Textures that have been bound to a GL image unit (see MarkStorageImageTexture). + std::unordered_set m_storageImageTextures; Vector> m_deferredReleases; Vector> m_deferredViewReleases; }; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index 54a26963..4cce10f7 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -2491,7 +2491,7 @@ void main() { MOBILEGL_ASSERT(m_textureManager != nullptr, "VkTextureManager creation failed."); succeeded = m_textureManager->Initialize( {m_device, m_physicalDevice.handle, m_allocator, m_commandPool, m_graphicsQueue, - m_frameContext.GetFrameCount()}); + m_frameContext.GetFrameCount(), m_imageFormatListExtensionEnabled}); MOBILEGL_ASSERT(succeeded, "VkTextureManager initialization failed."); m_clearManager = MakeUnique(); MOBILEGL_ASSERT(m_clearManager != nullptr, "VkClearManager creation failed."); @@ -4193,7 +4193,7 @@ void main() { } Bool VulkanRenderer::PrepareStorageImageTextures( - VkCommandBuffer commandBuffer, + FrameContext::FrameData& frame, const MG_State::GLState::ProgramObject& program, const ProgramFactory::VkProgramObject& programObj) { if (!programObj.hasStorageImages) { @@ -4214,9 +4214,18 @@ void main() { // 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; + // Record every image-unit binding before probing anything: a texture whose image was + // created without STORAGE usage (the default - it costs UBWC compression on Adreno) + // needs a recreate, and the probe below is what ends the render pass so that recreate + // lands here rather than mid-pass. This cannot be folded into the probe loop, which + // stops at the first texture that needs work and would leave the rest unmarked. for (auto* texture : storageTextures) { MOBILEGL_ASSERT(texture != nullptr, "%s: collected a null storage texture", __func__); + m_textureManager->MarkStorageImageTexture(*texture); + } + + Bool anyNeedsPreparation = false; + for (auto* texture : storageTextures) { if (m_textureManager->NeedsStorageImagePreparation(*texture) || m_clearManager->HasPendingClear(texture)) { anyNeedsPreparation = true; @@ -4227,21 +4236,51 @@ void main() { return true; } + // A first-time storage-usage upgrade recreates the image and carries the old contents + // forward with an out-of-band, immediately-submitted copy (PreserveTextureContentsOnRecreate). + // Whatever this frame already recorded into the old image is still sitting unsubmitted in + // this command buffer, so that copy would read pre-frame content and this frame's rendering + // into the texture would be lost - precisely the render-target-then-image-unit case this + // whole path exists for. Submit what is recorded first; the copy then queues behind it. + Bool anyNeedsStorageUpgrade = false; + for (auto* texture : storageTextures) { + if (m_textureManager->NeedsStorageUsageUpgrade(*texture)) { + anyNeedsStorageUpgrade = true; + break; + } + } + if (anyNeedsStorageUpgrade && HasPendingRecordedWork()) { + if (FlushPendingCommands()) { + // Fresh command buffer: the sampled-descriptor-set memo describes bindings that + // only existed in the retired one. FlushPendingCommands drops the pipeline memo + // itself; this is the other command-buffer-scoped cache. + m_lastSampledSetValid = false; + } else { + // Best effort: the upgrade still produces a correct image, only its preserved + // contents may predate this frame's writes. Dropping the draw would be worse. + MGLOG_E("%s: flush before a storage-usage image upgrade failed; preserved contents " + "may be stale for one frame", __func__); + } + } + if (!frame.isCommandRecording) { + m_frameContext.BeginCommandRecording(); + } + // 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 // same layout independent of SPIR-V reflection/binding order. if (VkRenderPassManager::GetActiveRenderPass() != nullptr) { - VkRenderPassManager::EndRenderPass(commandBuffer); + VkRenderPassManager::EndRenderPass(frame.commandBuffer); } for (auto* texture : storageTextures) { - if (!MaterializePendingClearForTexture(commandBuffer, *texture)) { + if (!MaterializePendingClearForTexture(frame.commandBuffer, *texture)) { MGLOG_E("%s: failed to materialize pending clear for storage textureId=%d", __func__, texture->GetExternalIndex()); return false; } - if (!m_textureManager->TransitionTextureForStorageImage(commandBuffer, *texture)) { + if (!m_textureManager->TransitionTextureForStorageImage(frame.commandBuffer, *texture)) { MGLOG_E("%s: failed to prepare storage textureId=%d", __func__, texture->GetExternalIndex()); return false; @@ -4286,7 +4325,7 @@ void main() { m_lastSampledSetValid = false; } - if (!PrepareStorageImageTextures(frame.commandBuffer, program, programObj)) { + if (!PrepareStorageImageTextures(frame, program, programObj)) { MGLOG_E("SetupDraw skipped: storage image preparation failed"); return false; } @@ -4511,7 +4550,7 @@ void main() { VkRenderPassManager::EndRenderPass(frame.commandBuffer); } - if (!PrepareStorageImageTextures(frame.commandBuffer, program, programObj)) { + if (!PrepareStorageImageTextures(frame, program, programObj)) { MGLOG_E("DispatchCompute skipped: storage image preparation failed"); return; } @@ -4551,7 +4590,7 @@ void main() { VkRenderPassManager::EndRenderPass(frame.commandBuffer); } - if (!PrepareStorageImageTextures(frame.commandBuffer, program, programObj)) { + if (!PrepareStorageImageTextures(frame, program, programObj)) { MGLOG_E("DispatchComputeIndirect skipped: storage image preparation failed"); return; } @@ -8157,6 +8196,18 @@ void main() { const Vector availableExtensions = EnumerateDeviceExtensions(m_physicalDevice.handle); ResolveOptionalDeviceExtensions(availableExtensions, enabledDeviceExtensions); + + // VK_KHR_image_format_list lets a MUTABLE_FORMAT image declare exactly which formats it + // may be viewed as. Adreno drops UBWC bandwidth compression on a blindly-mutable image + // (measured: 65 -> 80 fps in MC 26.2 once mutability is not requested); an explicit, + // compression-compatible format list is the portable way to keep both. + m_imageFormatListExtensionEnabled = + IsExtensionSupported(availableExtensions, VK_KHR_IMAGE_FORMAT_LIST_EXTENSION_NAME); + if (m_imageFormatListExtensionEnabled) { + enabledDeviceExtensions.push_back(VK_KHR_IMAGE_FORMAT_LIST_EXTENSION_NAME); + } + MGLOG_I("VK_KHR_image_format_list enabled: %s", + m_imageFormatListExtensionEnabled ? "true" : "false"); MGLOG_I("VK_KHR_draw_indirect_count enabled: %s", m_drawIndirectCountExtensionEnabled ? "true" : "false"); m_indexTypeUint8ExtensionEnabled = false; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index 591aa60c..131a1ff3 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -576,8 +576,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { const RenderPassEntry& renderPassEntry); VkPipeline GetOrCreateComputePipeline(const ProgramFactory::VkProgramObject& programObj); void DestroyComputePipelines(); + // Takes the frame rather than a command buffer: a first-time storage-usage upgrade has to + // flush the pending recording (see the body), which retires the current command buffer. Bool PrepareStorageImageTextures( - VkCommandBuffer commandBuffer, + FrameContext::FrameData& frame, const MG_State::GLState::ProgramObject& program, const ProgramFactory::VkProgramObject& programObj); @@ -639,6 +641,10 @@ namespace MobileGL::MG_Backend::DirectVulkan { const PhysicalDevice& compareWithDevice, PhysicalDevice& outBetterDevice); static constexpr const char* s_validationLayerNames[] = {"VK_LAYER_KHRONOS_validation"}; + // VK_KHR_image_format_list: lets MUTABLE_FORMAT images declare their exact view-format + // set so the driver can keep bandwidth compression (see CreateLogicalDeviceAndQueues). + Bool m_imageFormatListExtensionEnabled = false; + static constexpr const char* s_deviceExtensionNames[] = {VK_KHR_SWAPCHAIN_EXTENSION_NAME}; static Bool CheckValidationLayerSupport();