From f0c021176791f0bb64d7a5a3993c6085206d7f84 Mon Sep 17 00:00:00 2001 From: BZLZHH Date: Sat, 1 Aug 2026 01:15:57 -0400 Subject: [PATCH] [Fix] (DirectVulkan): raw sRGB attachment writes while FRAMEBUFFER_SRGB is off GL renders into sRGB color attachments RAW when GL_FRAMEBUFFER_SRGB is disabled (the core-profile default), but Vulkan sRGB attachments always encode on write - one decode went missing whenever a rendered-into sRGB texture was sampled again (multisampled sRGB targets in texture_size_promotion and texture_swizzle idx27/28 ms cases). Attachment views (textures and renderbuffers) now reinterpret sRGB images through their UNORM twin while the capability is off, switching back when enabled: images get MUTABLE_FORMAT, the attachment-view cache keys the view format, renderbuffers carry a second view, and the render pass hash includes the capability state. Sampled views keep decoding. The VkTextureManager.cpp half of this rides with the next commit. --- .../Renderer/VkRenderPassManager.cpp | 40 +++++++++++++++++-- .../Renderer/VkRenderPassManager.h | 4 ++ .../DirectVulkan/Renderer/VkTextureManager.h | 8 +++- MobileGL/MG_Backend/DirectVulkan/VkIncludes.h | 19 +++++++++ 4 files changed, 66 insertions(+), 5 deletions(-) diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp index d0c4b00c..fd6bf6a3 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp @@ -156,6 +156,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { if (view != VK_NULL_HANDLE) { vkDestroyImageView(device, view, nullptr); } + if (unormTwinView != VK_NULL_HANDLE) { + vkDestroyImageView(device, unormTwinView, nullptr); + } if (image != VK_NULL_HANDLE && allocation != nullptr) { vmaDestroyImage(allocator, image, allocation); } @@ -163,6 +166,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { image = VK_NULL_HANDLE; allocation = nullptr; view = VK_NULL_HANDLE; + unormTwinView = VK_NULL_HANDLE; layout = VK_IMAGE_LAYOUT_UNDEFINED; format = VK_FORMAT_UNDEFINED; aspect = VK_IMAGE_ASPECT_NONE; @@ -221,10 +225,12 @@ namespace MobileGL::MG_Backend::DirectVulkan { if (resource.image == VK_NULL_HANDLE && resource.view == VK_NULL_HANDLE) { return; } - m_deferredRenderbufferReleases.push_back({resource.image, resource.allocation, resource.view, m_frameCounter}); + m_deferredRenderbufferReleases.push_back( + {resource.image, resource.allocation, resource.view, resource.unormTwinView, m_frameCounter}); resource.image = VK_NULL_HANDLE; resource.allocation = nullptr; resource.view = VK_NULL_HANDLE; + resource.unormTwinView = VK_NULL_HANDLE; } void VkRenderPassManager::CollectDeferredRenderbufferReleases(Bool destroyAll) { @@ -239,6 +245,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { if (release.view != VK_NULL_HANDLE) { vkDestroyImageView(m_device, release.view, nullptr); } + if (release.unormTwinView != VK_NULL_HANDLE) { + vkDestroyImageView(m_device, release.unormTwinView, nullptr); + } if (release.image != VK_NULL_HANDLE) { vmaDestroyImage(m_allocator, release.image, release.allocation); } @@ -381,6 +390,12 @@ namespace MobileGL::MG_Backend::DirectVulkan { imageInfo.usage = imageUsage; imageInfo.samples = sampleCount; imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + // sRGB renderbuffers attach through their UNORM twin while GL_FRAMEBUFFER_SRGB + // is disabled, which needs a format-reinterpreting second view. + const Bool hasUnormTwin = ResolveSrgbAttachmentWriteFormat(format, false) != format; + if (hasUnormTwin) { + imageInfo.flags |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT; + } VkImageFormatProperties imageFormatProperties{}; const VkResult imageFormatResult = vkGetPhysicalDeviceImageFormatProperties( @@ -415,6 +430,11 @@ namespace MobileGL::MG_Backend::DirectVulkan { viewInfo.subresourceRange.layerCount = 1; VK_VERIFY(vkCreateImageView(m_device, &viewInfo, nullptr, &resource.view), "vkCreateImageView(renderbuffer)"); + if (hasUnormTwin) { + viewInfo.format = ResolveSrgbAttachmentWriteFormat(format, false); + VK_VERIFY(vkCreateImageView(m_device, &viewInfo, nullptr, &resource.unormTwinView), + "vkCreateImageView(renderbuffer unorm twin)"); + } resource.layout = VK_IMAGE_LAYOUT_UNDEFINED; resource.format = format; @@ -518,6 +538,11 @@ namespace MobileGL::MG_Backend::DirectVulkan { if (isDefaultFbo) { XXHASH_VERIFY(XXH64_update(m_hashState, &swapchainImageIndex, sizeof(swapchainImageIndex))); } + // sRGB attachments switch between their sRGB and UNORM-twin views with this + // capability (ResolveSrgbAttachmentWriteFormat), changing the render pass formats. + const Bool framebufferSrgbEnabled = + MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb); + XXHASH_VERIFY(XXH64_update(m_hashState, &framebufferSrgbEnabled, sizeof(framebufferSrgbEnabled))); auto& drawBuffers = fbo.GetDrawBuffers(); XXHASH_VERIFY(XXH64_update(m_hashState, drawBuffers.data(), drawBuffers.size() * sizeof(drawBuffers[0]))); auto readBuffer = fbo.GetReadBuffer(); @@ -865,8 +890,12 @@ namespace MobileGL::MG_Backend::DirectVulkan { } const VkImageLayout trackedRbLayout = rbResource->layout; + const Bool rbFramebufferSrgb = + MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb); + const VkFormat rbAttachmentFormat = + ResolveSrgbAttachmentWriteFormat(rbResource->format, rbFramebufferSrgb); rbDesc.flags = 0; - rbDesc.format = rbResource->format; + rbDesc.format = rbAttachmentFormat; rbDesc.samples = rbResource->sampleCount; rbDesc.loadOp = rbHasClear ? VK_ATTACHMENT_LOAD_OP_CLEAR : (trackedRbLayout == VK_IMAGE_LAYOUT_UNDEFINED ? VK_ATTACHMENT_LOAD_OP_DONT_CARE @@ -901,7 +930,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { .finalLayout = rbDesc.finalLayout, }); textureResources.emplace_back(nullptr); - attachmentViews.emplace_back(rbResource->view); + attachmentViews.emplace_back(rbAttachmentFormat != rbResource->format ? rbResource->unormTwinView + : rbResource->view); MOBILEGL_ASSERT(attachmentViews.back() != VK_NULL_HANDLE, "GetOrCreateRenderPass: renderbuffer view missing at color attachment %d", i); @@ -990,7 +1020,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { MOBILEGL_ASSERT(textureResource, "GetOrCreateRenderPass: SyncTextureAndGetDescriptor failed at color attachment %d", i); textureResources.emplace_back(textureResource); - desc.format = textureResource->format; + desc.format = ResolveSrgbAttachmentWriteFormat( + textureResource->format, + MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb)); attachmentSampleCount = textureResource->sampleCount; trackedColorLayout = textureResource->layout; trackedAttachmentLayouts.emplace_back(TrackedAttachmentLayoutInfo { diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.h index b844b9ab..5b4d97a5 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.h @@ -274,6 +274,9 @@ namespace MobileGL::MG_Backend::DirectVulkan { VkImage image = VK_NULL_HANDLE; VmaAllocation allocation = nullptr; VkImageView view = VK_NULL_HANDLE; + // UNORM reinterpretation of an sRGB image, used as the attachment view while + // GL_FRAMEBUFFER_SRGB is disabled (raw writes). Null for non-sRGB formats. + VkImageView unormTwinView = VK_NULL_HANDLE; VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED; VkFormat format = VK_FORMAT_UNDEFINED; VkImageAspectFlags aspect = VK_IMAGE_ASPECT_NONE; @@ -307,6 +310,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { VkImage image = VK_NULL_HANDLE; VmaAllocation allocation = nullptr; VkImageView view = VK_NULL_HANDLE; + VkImageView unormTwinView = VK_NULL_HANDLE; Uint64 deferredAtFrame = 0; }; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h index c1566f61..a3e3e1ed 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.h @@ -67,12 +67,16 @@ public: Uint32 baseArrayLayer = 0; Uint32 layerCount = 1; VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D; + // May differ from the image format: sRGB images attach through their UNORM + // twin while GL_FRAMEBUFFER_SRGB is disabled. + VkFormat viewFormat = VK_FORMAT_UNDEFINED; Bool operator==(const AttachmentViewKey& other) const { return mipLevel == other.mipLevel && baseArrayLayer == other.baseArrayLayer && layerCount == other.layerCount && - viewType == other.viewType; + viewType == other.viewType && + viewFormat == other.viewFormat; } }; @@ -83,6 +87,8 @@ public: hash ^= std::hash{}(key.layerCount) + 0x9e3779b9u + (hash << 6) + (hash >> 2); hash ^= std::hash{}(static_cast(key.viewType)) + 0x9e3779b9u + (hash << 6) + (hash >> 2); + hash ^= std::hash{}(static_cast(key.viewFormat)) + + 0x9e3779b9u + (hash << 6) + (hash >> 2); return hash; } }; diff --git a/MobileGL/MG_Backend/DirectVulkan/VkIncludes.h b/MobileGL/MG_Backend/DirectVulkan/VkIncludes.h index 9048764b..e3d1f983 100644 --- a/MobileGL/MG_Backend/DirectVulkan/VkIncludes.h +++ b/MobileGL/MG_Backend/DirectVulkan/VkIncludes.h @@ -52,6 +52,25 @@ namespace MobileGL::MG_Backend::DirectVulkan { } } // namespace MobileGL::MG_Backend::DirectVulkan +namespace MobileGL::MG_Backend::DirectVulkan { + // GL renders into sRGB color attachments RAW while GL_FRAMEBUFFER_SRGB is disabled + // (the core-profile default); Vulkan sRGB attachments always encode on write. The + // attachment view (and render pass format) therefore drops to the UNORM twin + // whenever the capability is off. Sampled views keep the sRGB format (decode on + // sample is unconditional in GL). + inline VkFormat ResolveSrgbAttachmentWriteFormat(VkFormat format, bool framebufferSrgbEnabled) { + if (framebufferSrgbEnabled) return format; + switch (format) { + case VK_FORMAT_R8G8B8A8_SRGB: + return VK_FORMAT_R8G8B8A8_UNORM; + case VK_FORMAT_B8G8R8A8_SRGB: + return VK_FORMAT_B8G8R8A8_UNORM; + default: + return format; + } + } +} // namespace MobileGL::MG_Backend::DirectVulkan + // The context line (__VA_ARGS__ = its own format string + args) must be a SEPARATE log // call: appending its format to the base format while its arguments precede the base // arguments makes every conversion read the wrong slot (a %s pulling an int crashes).