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();