From dbae4eda10677813a660e4d76366f787691e366b Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Tue, 11 Aug 2026 10:24:59 -0400 Subject: [PATCH] [Fix, Test] (MG_Backend/DirectVulkan): depth blits to or from the default framebuffer convert their rect out of GL's bottom-origin space, like the colour blit does --- .../DirectVulkan/Renderer/VulkanRenderer.cpp | 18 ++++- .../DepthStencilReadbackScenario.cpp | 69 +++++++++++++++++++ 2 files changed, 86 insertions(+), 1 deletion(-) diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index e22e5925..718b7ec0 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -7766,7 +7766,15 @@ void main() { MOBILEGL_ASSERT(ok, "%s: failed to transition depth destination image", __func__); } - if (depthBlitScales) { + // The default framebuffer is stored display-side-up, so a rect aimed at it (or read + // from it) has to be converted out of GL's bottom-origin space - the same conversion + // the colour blit below applies. vkCmdCopyImage cannot express it (it has no second + // offset to invert), so a default-framebuffer side forces the vkCmdBlitImage form even + // at equal size. Without this a scissored depth blit into the default framebuffer + // wrote the MIRRORED band: KHR-GL*.framebuffer_blit.scissor_blit clips to the lower + // left quadrant, and the depth landed in the upper one. + const Bool depthBlitNeedsOrientation = readIsDefaultFbo || drawIsDefaultFbo; + if (depthBlitScales || depthBlitNeedsOrientation) { // vkCmdCopyImage cannot resize; NEAREST is the only filter Vulkan allows for a // depth/stencil blit anyway, and the GL front end already rejects the others. VkImageBlit blitRegion{}; @@ -7782,6 +7790,14 @@ void main() { blitRegion.dstSubresource.layerCount = dstBinding.layerCount; blitRegion.dstOffsets[0] = {dstX0, dstY0, 0}; blitRegion.dstOffsets[1] = {dstX1, dstY1, 1}; + if (readIsDefaultFbo) { + ApplyNativeBlitDefaultFramebufferSourceTransform(m_swapchainObject.GetPreTransform(), srcBinding, + blitRegion); + } + if (drawIsDefaultFbo) { + ApplyNativeBlitDefaultFramebufferTransform(m_swapchainObject.GetPreTransform(), dstBinding, + blitRegion); + } vkCmdBlitImage(frame.commandBuffer, srcBinding.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, dstBinding.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, diff --git a/MobileGL/MG_IntegrationTest/Scenarios/DepthStencilReadbackScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/DepthStencilReadbackScenario.cpp index 4ed38d51..6a41efae 100644 --- a/MobileGL/MG_IntegrationTest/Scenarios/DepthStencilReadbackScenario.cpp +++ b/MobileGL/MG_IntegrationTest/Scenarios/DepthStencilReadbackScenario.cpp @@ -179,6 +179,75 @@ namespace MGITest { gl.EndFrame(); } + // A depth blit INTO the default framebuffer has to convert its rect out of GL's bottom-origin + // space, exactly as the colour blit does. The colour path had that conversion and the + // depth path did not, so a scissored depth blit landed in the mirrored band - which is the + // whole of KHR-GL*.framebuffer_blit.scissor_blit once the readback above works well enough to + // see it (before that the test died on the poison values and never reached the blit). + TEST_F(DepthStencilReadbackScenario, AScissoredDepthBlitIntoTheDefaultFramebufferLandsInTheScissorBox) { + if (!Ready()) return; + if (!BackendReadsDepthStencil()) { + GTEST_SKIP() << "backend " << Gl().BackendName() << " has no depth readback path"; + } + HeadlessGL& gl = Gl(); + const int width = gl.Width(); + const int height = gl.Height(); + ASSERT_GE(width, 8); + ASSERT_GE(height, 8); + + // Source: a user framebuffer whose depth is uniformly 0.75. + GLuint fbo = 0, colorTex = 0, depthTex = 0; + glGenFramebuffers(1, &fbo); + glBindFramebuffer(GL_FRAMEBUFFER, fbo); + glGenTextures(1, &colorTex); + glBindTexture(GL_TEXTURE_2D, colorTex); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, colorTex, 0); + glGenTextures(1, &depthTex); + glBindTexture(GL_TEXTURE_2D, depthTex); + glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH24_STENCIL8, width, height, 0, GL_DEPTH_STENCIL, + GL_UNSIGNED_INT_24_8, nullptr); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, depthTex, 0); + ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), GLenum(GL_FRAMEBUFFER_COMPLETE)); + glViewport(0, 0, width, height); + glDisable(GL_SCISSOR_TEST); + glDepthMask(GL_TRUE); + glClearDepth(0.75); + glClear(GL_DEPTH_BUFFER_BIT); + + // Destination: the default framebuffer, depth 0 everywhere. + BindDefaultFramebuffer(); + glViewport(0, 0, width, height); + glClearDepth(0.0); + glClear(GL_DEPTH_BUFFER_BIT); + + // Blit the whole rect, but scissored to the BOTTOM-LEFT quadrant in GL coordinates. + glEnable(GL_SCISSOR_TEST); + glScissor(0, 0, width / 2, height / 2); + glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); + glBlitFramebuffer(0, 0, width, height, 0, 0, width, height, GL_DEPTH_BUFFER_BIT, GL_NEAREST); + glDisable(GL_SCISSOR_TEST); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + EXPECT_EQ(FirstGLError(), 0u); + + const float inside = ReadDepthAt(width / 4, height / 4); + const float above = ReadDepthAt(width / 4, height - 1 - height / 4); + EXPECT_EQ(FirstGLError(), 0u); + EXPECT_NEAR(inside, 0.75f, 1.0f / 4096.0f) + << "GL (" << (width / 4) << ", " << (height / 4) << ") is inside the scissor box and should hold the " + << "blitted 0.75, but read back " << inside; + EXPECT_NEAR(above, 0.0f, 1.0f / 4096.0f) + << "GL (" << (width / 4) << ", " << (height - 1 - height / 4) + << ") is ABOVE the scissor box and must still hold the cleared 0.0, but read back " << above + << " (0.75 there means the depth blit landed in the mirrored band)"; + + glDeleteTextures(1, &depthTex); + glDeleteTextures(1, &colorTex); + glDeleteFramebuffers(1, &fbo); + gl.EndFrame(); + } + // The control: the same read against a user framebuffer, which never went through the // declined path. It is what makes a failure above specific to the default framebuffer. TEST_F(DepthStencilReadbackScenario, UserFramebufferDepthClearIsVisibleToReadPixels) {