diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index a1bc1d6a..e22e5925 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -7194,6 +7194,96 @@ void main() { return true; } + // The aspects a depth/stencil format actually carries. VkTextureManager keeps its own copy of + // this private, and the swapchain's depth/stencil image has no TextureResource to ask. + static VkImageAspectFlags GetDepthStencilAspectMaskForFormat(VkFormat format) { + switch (format) { + case VK_FORMAT_D16_UNORM: + case VK_FORMAT_X8_D24_UNORM_PACK32: + case VK_FORMAT_D32_SFLOAT: + return VK_IMAGE_ASPECT_DEPTH_BIT; + case VK_FORMAT_D16_UNORM_S8_UINT: + case VK_FORMAT_D24_UNORM_S8_UINT: + case VK_FORMAT_D32_SFLOAT_S8_UINT: + return VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT; + case VK_FORMAT_S8_UINT: + return VK_IMAGE_ASPECT_STENCIL_BIT; + default: + return VK_IMAGE_ASPECT_NONE; + } + } + + // The depth/stencil half of MaterializePendingClearForDefaultFramebuffer. Separate only + // because the image, the aspects and the clear value are all different from the colour one; + // the reason it exists is the same - a readback with no intervening draw has no render pass + // to fold the parked clear into. + Bool VulkanRenderer::MaterializePendingDepthStencilClearForDefaultFramebuffer( + VkCommandBuffer commandBuffer, const MG_State::GLState::FramebufferAttachmentObject& attachment, + const ClearAttachmentPayload& payload) { + const VkImage depthStencilImage = m_swapchainObject.GetDepthStencilImage(m_imageIndexAcquired); + if (depthStencilImage == VK_NULL_HANDLE) { + return false; + } + const VkImageAspectFlags imageAspects = + GetDepthStencilAspectMaskForFormat(m_swapchainObject.GetDepthStencilFormat()); + VkImageAspectFlags clearAspects = 0; + if ((payload.mask & GL_DEPTH_BUFFER_BIT) != 0) clearAspects |= (imageAspects & VK_IMAGE_ASPECT_DEPTH_BIT); + if ((payload.mask & GL_STENCIL_BUFFER_BIT) != 0) clearAspects |= (imageAspects & VK_IMAGE_ASPECT_STENCIL_BIT); + if (clearAspects == 0) { + // Nothing this image can express; drop the pending clear rather than leave it to a + // later render pass that would load it against an aspect that does not exist. + m_clearManager->PopPendingClear(attachment); + return true; + } + + VkImageLayout currentLayout = m_swapchainObject.GetDepthStencilImageLayout(m_imageIndexAcquired); + VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; + VkAccessFlags srcAccessMask = 0; + GetImageTransitionSourceState(currentLayout, srcStageMask, srcAccessMask); + VkImageLayout clearLayout = currentLayout; + if (!VkTextureManager::TransitionImageLayout(commandBuffer, depthStencilImage, clearLayout, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, srcStageMask, + VK_PIPELINE_STAGE_TRANSFER_BIT, srcAccessMask, + VK_ACCESS_TRANSFER_WRITE_BIT, imageAspects)) { + return false; + } + + VkClearDepthStencilValue clearValue{}; + clearValue.depth = payload.depth; + clearValue.stencil = payload.stencil; + VkImageSubresourceRange range{}; + range.aspectMask = clearAspects; + range.baseMipLevel = 0; + range.levelCount = 1; + range.baseArrayLayer = 0; + range.layerCount = 1; + vkCmdClearDepthStencilImage(commandBuffer, depthStencilImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &clearValue, + 1, &range); + + VkImageLayout settledLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + VkPipelineStageFlags dstStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; + VkAccessFlags dstAccessMask = 0; + GetImageTransitionDestinationState(VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, dstStageMask, + dstAccessMask); + if (!VkTextureManager::TransitionImageLayout(commandBuffer, depthStencilImage, settledLayout, + VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, + VK_PIPELINE_STAGE_TRANSFER_BIT, dstStageMask, + VK_ACCESS_TRANSFER_WRITE_BIT, dstAccessMask, imageAspects)) { + return false; + } + m_swapchainObject.SetDepthStencilImageLayout(m_imageIndexAcquired, + VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL); + // The image now holds real values, so the next render pass must LOAD them rather than + // treat the attachment as undefined and discard the clear that just executed. + m_swapchainObject.SetDepthStencilContentDefined(m_imageIndexAcquired, true); + + m_clearManager->PopPendingClear(attachment); + MGLOG_D("MaterializePendingClearForDefaultFramebuffer: swapchain depth/stencil image %u pending clear " + "materialized (aspects=0x%x)", + m_imageIndexAcquired, static_cast(clearAspects)); + return true; + } + // A glClear on the DEFAULT framebuffer is parked as a pending clear and folded into the next // render pass's loadOp. With no draw in between there is no render pass, so a readback that // followed such a clear blitted the untouched swapchain image and returned the PREVIOUS @@ -7217,15 +7307,14 @@ void main() { if (!m_clearManager->GetPendingClear(attachment, payload)) { return true; } - if ((payload.mask & GL_COLOR_BUFFER_BIT) == 0) { - // Depth/stencil on the default framebuffer keeps the loadOp route; the readback - // path for it declines default framebuffers outright (ReadDepthStencilPixels). - return true; - } MOBILEGL_ASSERT(VkRenderPassManager::GetActiveRenderPass() == nullptr || commandBuffer != m_frameContext.GetCurrent().commandBuffer, "MaterializePendingClearForDefaultFramebuffer requires no active render pass"); + if ((payload.mask & GL_COLOR_BUFFER_BIT) == 0) { + return MaterializePendingDepthStencilClearForDefaultFramebuffer(commandBuffer, attachment, payload); + } + const VkImage swapchainImage = m_swapchainObject.GetImage(m_imageIndexAcquired); if (swapchainImage == VK_NULL_HANDLE) { return false; @@ -8750,10 +8839,6 @@ void main() { void VulkanRenderer::ReadDepthStencilPixels(MG_State::GLState::FramebufferObject& readFbo, GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels) { - if (readFbo.IsDefaultFramebuffer()) { - MGLOG_E("DirectVulkan::ReadDepthStencilPixels skipped: default framebuffer readback is unsupported"); - return; - } if (width <= 0 || height <= 0) { return; } @@ -8764,10 +8849,13 @@ void main() { // framebuffers lacking either, so resolving via the depth attachment is enough. const auto attachmentType = wantDepth ? MobileGL::FramebufferAttachmentType::Depth : MobileGL::FramebufferAttachmentType::Stencil; - const auto& attachment = readFbo.GetAttachment(attachmentType); - if (!attachment.IsValid() || attachment.IsEmpty()) { - MGLOG_E("DirectVulkan::ReadDepthStencilPixels skipped: no depth/stencil attachment image"); - return; + const Bool readIsDefaultFbo = readFbo.IsDefaultFramebuffer(); + if (!readIsDefaultFbo) { + const auto& attachment = readFbo.GetAttachment(attachmentType); + if (!attachment.IsValid() || attachment.IsEmpty()) { + MGLOG_E("DirectVulkan::ReadDepthStencilPixels skipped: no depth/stencil attachment image"); + return; + } } auto& frame = m_frameContext.GetCurrent(); @@ -8778,6 +8866,47 @@ void main() { VkRenderPassManager::EndRenderPass(frame.commandBuffer); } + // The default framebuffer's depth/stencil lives in the swapchain, not in an + // attachment object: its placeholder ITextureObject describes the format but backs no + // image, so the branches below would have synced (and read back) an unrelated one. + // Declining outright is what made every glReadPixels(GL_DEPTH_COMPONENT/ + // GL_STENCIL_INDEX) of the default framebuffer leave the caller's buffer untouched - + // the whole KHR-GL*.framebuffer_blit family checks exactly that before it blits. + if (readIsDefaultFbo) { + const VkImage swapchainDepthImage = m_swapchainObject.GetDepthStencilImage(m_imageIndexAcquired); + if (swapchainDepthImage == VK_NULL_HANDLE) { + MGLOG_E("DirectVulkan::ReadDepthStencilPixels skipped: the default framebuffer has no " + "depth/stencil image"); + return; + } + // Per aspect, because the default framebuffer carries a SEPARATE placeholder + // attachment for depth and for stencil (MG_Impl/Init.cpp) and each parks its own + // pending clear; materializing only one would read the other back un-cleared. + if (wantDepth) { + const Bool clearReady = MaterializePendingClearForDefaultFramebuffer( + frame.commandBuffer, readFbo, MobileGL::FramebufferAttachmentType::Depth); + MOBILEGL_ASSERT(clearReady, + "ReadDepthStencilPixels: failed to materialize the default framebuffer's pending " + "depth clear"); + } + if (wantStencil) { + const Bool clearReady = MaterializePendingClearForDefaultFramebuffer( + frame.commandBuffer, readFbo, MobileGL::FramebufferAttachmentType::Stencil); + MOBILEGL_ASSERT(clearReady, + "ReadDepthStencilPixels: failed to materialize the default framebuffer's pending " + "stencil clear"); + } + const VkFormat swapchainDepthFormat = m_swapchainObject.GetDepthStencilFormat(); + VkImageLayout trackedLayout = m_swapchainObject.GetDepthStencilImageLayout(m_imageIndexAcquired); + ReadDepthStencilImageToClient(swapchainDepthImage, swapchainDepthFormat, &trackedLayout, + GetDepthStencilAspectMaskForFormat(swapchainDepthFormat), 0, 0, x, y, + width, height, format, type, pixels, + /*defaultFramebufferOrientation=*/true); + m_swapchainObject.SetDepthStencilImageLayout(m_imageIndexAcquired, trackedLayout); + return; + } + + const auto& attachment = readFbo.GetAttachment(attachmentType); VkImage image = VK_NULL_HANDLE; VkFormat vkFormat = VK_FORMAT_UNDEFINED; VkImageLayout* trackedLayout = nullptr; @@ -8828,7 +8957,8 @@ void main() { void VulkanRenderer::ReadDepthStencilImageToClient(VkImage image, VkFormat vkFormat, VkImageLayout* trackedLayout, VkImageAspectFlags imageAspect, Uint32 mipLevel, Uint32 baseArrayLayer, GLint x, GLint y, GLsizei width, - GLsizei height, GLenum format, GLenum type, void* pixels) { + GLsizei height, GLenum format, GLenum type, void* pixels, + Bool defaultFramebufferOrientation) { const Bool wantDepth = format != GL_STENCIL_INDEX; const Bool wantStencil = format != GL_DEPTH_COMPONENT; auto& frame = m_frameContext.GetCurrent(); @@ -8893,6 +9023,21 @@ void main() { VK_PIPELINE_STAGE_TRANSFER_BIT, srcAccessMask, VK_ACCESS_TRANSFER_READ_BIT, imageAspect, mipLevel, 1); MOBILEGL_ASSERT(ok, "%s: failed to transition depth-stencil source image", __func__); + // The swapchain's depth/stencil image is stored display-side-up like its colour twin, so + // the GL rect has to be mapped into that space before the copy and the copied rows + // re-oriented afterwards - the same two halves the colour ReadPixels path applies. + Int32 copyOffsetX = x; + Int32 copyOffsetY = y; + if (defaultFramebufferOrientation) { + const VkExtent2D defaultFboExtent = m_swapchainObject.GetExtent(); + const DefaultFramebufferRectMapping mapping = + GetDefaultFramebufferRectMapping(m_swapchainObject.GetPreTransform()); + copyOffsetX = MapDefaultFramebufferRectAxis(x, width, static_cast(defaultFboExtent.width), + mapping.mirrorX); + copyOffsetY = MapDefaultFramebufferRectAxis(y, height, static_cast(defaultFboExtent.height), + mapping.flipY); + } + VkBufferImageCopy regions[2]{}; Uint32 regionCount = 0; if (wantDepth) { @@ -8902,7 +9047,7 @@ void main() { region.imageSubresource.mipLevel = mipLevel; region.imageSubresource.baseArrayLayer = baseArrayLayer; region.imageSubresource.layerCount = 1; - region.imageOffset = {x, y, 0}; + region.imageOffset = {copyOffsetX, copyOffsetY, 0}; region.imageExtent = {static_cast(width), static_cast(height), 1}; } if (wantStencil) { @@ -8912,7 +9057,7 @@ void main() { region.imageSubresource.mipLevel = mipLevel; region.imageSubresource.baseArrayLayer = baseArrayLayer; region.imageSubresource.layerCount = 1; - region.imageOffset = {x, y, 0}; + region.imageOffset = {copyOffsetX, copyOffsetY, 0}; region.imageExtent = {static_cast(width), static_cast(height), 1}; } vkCmdCopyImageToBuffer(frame.commandBuffer, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, readback.GetHandle(), @@ -8937,6 +9082,38 @@ void main() { const Uint8* depthSrc = mapped; const Uint8* stencilSrc = mapped + stencilOffset; + // Re-orient the copied band per aspect, before any repacking reads it: the depth and + // stencil aspects were copied into their own tightly packed sub-buffers, so each is a + // plain width x height image of its own texel size. + Vector remappedDepth; + Vector remappedStencil; + if (defaultFramebufferOrientation) { + const VkSurfaceTransformFlagBitsKHR preTransform = m_swapchainObject.GetPreTransform(); + Bool remapped = true; + if (wantDepth && depthCopyBytes > 0) { + remappedDepth.resize(pixelCount * depthCopyBytes); + remapped = RemapDefaultFboReadbackToGLOrientation(depthSrc, static_cast(width), + static_cast(height), preTransform, + depthCopyBytes, remappedDepth.data()); + } + if (remapped && wantStencil) { + remappedStencil.resize(pixelCount); + remapped = RemapDefaultFboReadbackToGLOrientation(stencilSrc, static_cast(width), + static_cast(height), preTransform, 1, + remappedStencil.data()); + } + if (remapped) { + if (!remappedDepth.empty()) depthSrc = remappedDepth.data(); + if (!remappedStencil.empty()) stencilSrc = remappedStencil.data(); + } else { + // Only a quarter-turn pre-transform reaches this, and nothing in this renderer + // models one. MGLOG_I because the INFO builds are the ones that run conformance. + MGLOG_I("DirectVulkan::ReadDepthStencilPixels: default-FBO remap declined (w=%d h=%d " + "preTransform=%d); falling back to raw readback", + width, height, static_cast(preTransform)); + } + } + const auto depthValueAt = [&](SizeT i) -> Float { switch (vkFormat) { case VK_FORMAT_D16_UNORM: { diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index 17daf23c..803a7c4f 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -211,10 +211,15 @@ namespace MobileGL::MG_Backend::DirectVulkan { GLsizei height, GLenum format, GLenum type, void* pixels); // Copy-and-repack core shared by depth-stencil ReadPixels and GetTexImage; // expects command recording to be active and any render pass already ended. + // + // `defaultFramebufferOrientation` is set only when the source is the swapchain's + // depth/stencil image, which this renderer stores display-side-up: the copy rect then + // has to be mapped out of GL's bottom-origin space and the copied rows re-oriented on + // the way back, exactly as the colour ReadPixels path does. void ReadDepthStencilImageToClient(VkImage image, VkFormat vkFormat, VkImageLayout* trackedLayout, VkImageAspectFlags imageAspect, Uint32 mipLevel, Uint32 baseArrayLayer, GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, - void* pixels); + void* pixels, Bool defaultFramebufferOrientation = false); // Same-extent depth blit between images of different depth formats: host // round-trip with a per-texel re-encode (see BlitNamedFramebuffer). Bool BlitDepthAcrossFormats(FrameContext::FrameData& frame, VkImage srcImage, VkFormat srcFormat, @@ -1125,6 +1130,11 @@ namespace MobileGL::MG_Backend::DirectVulkan { Bool MaterializePendingClearForDefaultFramebuffer(VkCommandBuffer commandBuffer, MG_State::GLState::FramebufferObject& fbo, FramebufferAttachmentType attachmentType); + // Its depth/stencil half: a different image (the swapchain's depth/stencil twin), a + // different clear command and per-aspect masking. + Bool MaterializePendingDepthStencilClearForDefaultFramebuffer( + VkCommandBuffer commandBuffer, const MG_State::GLState::FramebufferAttachmentObject& attachment, + const ClearAttachmentPayload& payload); VkPipeline GetOrCreateBlitPipeline(const RenderPassEntry& renderPassEntry); Bool GenerateDepthMipmapWithShader(FrameContext::FrameData& frame, MG_State::GLState::ITextureObject& texture, diff --git a/MobileGL/MG_IntegrationTest/CMakeLists.txt b/MobileGL/MG_IntegrationTest/CMakeLists.txt index f621b3e0..aa7e5ec0 100644 --- a/MobileGL/MG_IntegrationTest/CMakeLists.txt +++ b/MobileGL/MG_IntegrationTest/CMakeLists.txt @@ -58,6 +58,7 @@ add_executable(MobileGLIntegrationTest Scenarios/PixelStoreSweepScenario.cpp Scenarios/FragCoordOriginScenario.cpp Scenarios/ClearThenReadPixelsScenario.cpp + Scenarios/DepthStencilReadbackScenario.cpp ) target_include_directories(MobileGLIntegrationTest PRIVATE diff --git a/MobileGL/MG_IntegrationTest/Scenarios/DepthStencilReadbackScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/DepthStencilReadbackScenario.cpp new file mode 100644 index 00000000..4ed38d51 --- /dev/null +++ b/MobileGL/MG_IntegrationTest/Scenarios/DepthStencilReadbackScenario.cpp @@ -0,0 +1,228 @@ +// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/DepthStencilReadbackScenario.cpp +// Copyright (c) 2025-2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header +// +// Scenario - glReadPixels OF DEPTH AND STENCIL FROM THE DEFAULT FRAMEBUFFER. +// +// DirectVulkan's depth/stencil readback used to decline the default framebuffer outright +// (`ReadDepthStencilPixels` returned at its first line) because that framebuffer's depth and +// stencil "attachments" are placeholder texture objects backing no image - the real one is the +// swapchain's depth/stencil twin. Declining meant the call raised no GL error and wrote NOTHING, +// so the caller kept whatever its buffer already held. +// +// That silence is what the framebuffer_blit family trips over. Every one of its cases begins by +// clearing the default framebuffer's depth and stencil and reading them straight back as a +// sanity check, into a local pre-initialised to 0.2 (depth) and 50 (stencil); an untouched +// buffer therefore reports "expected DEPTH[0.25] but got DEPTH[0.2]" and "expected STENCIL[1] but +// got STENCIL[50]" - the exact strings in the 15 Magma failures - long before any blit happens. +// A test that only checked "no GL error" would pass against the broken path, so every case here +// poisons its destination with a value the correct answer cannot be. +// +// The orientation case is the second half. This renderer stores the default framebuffer +// display-side-up and converts GL rects on their way in, so the depth copy needs the same rect +// mapping and row re-ordering the colour readback got in the M-1 fix; without them a +// vertically-varying depth buffer reads back mirrored, which no full-extent uniform-value test +// can see. +// +// Depth/stencil readback through a USER framebuffer already worked and is asserted here too, as +// the built-in control: it shares ReadDepthStencilImageToClient with the default-framebuffer +// path, so it is what says a failure is about the default framebuffer specifically. + +#include +#include +#include + +#include "../Harness/HeadlessGL.h" +#include "../Harness/ScenarioFixture.h" + +#ifdef GLAPI +#undef GLAPI +#endif +#define GL_GLEXT_PROTOTYPES +#include +#include +#undef GL_GLEXT_PROTOTYPES + +namespace MGITest { + namespace { + + // Values no correct read can produce, so "the backend wrote nothing" fails loudly instead + // of passing on whatever happened to be in the variable. These are the CTS's own poison + // values, which is why its logs report exactly them. + constexpr float kDepthPoison = 0.2f; + constexpr int kStencilPoison = 50; + + class DepthStencilReadbackScenario : public ScenarioTest { + protected: + // DirectGLES reads depth and stencil back through the ES driver, which has no + // guaranteed path for either (GL_NV_read_depth / GL_NV_read_stencil are optional and + // absent on both the Adreno device and Mesa's ES). That gap is tracked separately as + // the packed_depth_stencil cluster and needs a shader-sampling emulation, not this + // change; asserting it here would only pin a known-missing feature. + bool BackendReadsDepthStencil() const { return Gl().BackendName() == "DirectVulkan"; } + + float ReadDepthAt(int x, int y) const { + float depth = kDepthPoison; + glReadPixels(x, y, 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT, &depth); + return depth; + } + + int ReadStencilAt(int x, int y) const { + int stencil = kStencilPoison; + glReadPixels(x, y, 1, 1, GL_STENCIL_INDEX, GL_INT, &stencil); + return stencil; + } + }; + + // A depth buffer whose value depends on the row: bottom half `bottom`, top half `top`. + // Built with a scissored clear rather than a draw so the test stays independent of + // depth-test and shader behaviour. + void ClearDepthInBands(int width, int height, float bottom, float top) { + glEnable(GL_SCISSOR_TEST); + glScissor(0, 0, width, height / 2); + glClearDepth(bottom); + glClear(GL_DEPTH_BUFFER_BIT); + glScissor(0, height / 2, width, height - height / 2); + glClearDepth(top); + glClear(GL_DEPTH_BUFFER_BIT); + glDisable(GL_SCISSOR_TEST); + } + + } // namespace + + TEST_F(DepthStencilReadbackScenario, DefaultFramebufferDepthClearIsVisibleToReadPixels) { + if (!Ready()) return; + if (!BackendReadsDepthStencil()) { + GTEST_SKIP() << "backend " << Gl().BackendName() + << " has no depth readback path (ES lacks GL_NV_read_depth); see the packed_depth_stencil " + "cluster"; + } + HeadlessGL& gl = Gl(); + const int width = gl.Width(); + const int height = gl.Height(); + + BindDefaultFramebuffer(); + glViewport(0, 0, width, height); + glDisable(GL_SCISSOR_TEST); + glDepthMask(GL_TRUE); + glClearDepth(0.25); + glClear(GL_DEPTH_BUFFER_BIT); + + const float centre = ReadDepthAt(width / 2, height / 2); + EXPECT_EQ(FirstGLError(), 0u); + EXPECT_NEAR(centre, 0.25f, 1.0f / 4096.0f) + << "glReadPixels(GL_DEPTH_COMPONENT) of the default framebuffer returned " << centre + << (std::fabs(centre - kDepthPoison) < 1e-6f ? " - the destination was never written at all" : ""); + + gl.EndFrame(); + } + + TEST_F(DepthStencilReadbackScenario, DefaultFramebufferStencilClearIsVisibleToReadPixels) { + if (!Ready()) return; + if (!BackendReadsDepthStencil()) { + GTEST_SKIP() << "backend " << Gl().BackendName() + << " has no stencil readback path (ES lacks GL_NV_read_stencil); see the " + "packed_depth_stencil cluster"; + } + HeadlessGL& gl = Gl(); + const int width = gl.Width(); + const int height = gl.Height(); + + BindDefaultFramebuffer(); + glViewport(0, 0, width, height); + glDisable(GL_SCISSOR_TEST); + glStencilMask(0xFFu); + glClearStencil(3); + glClear(GL_STENCIL_BUFFER_BIT); + + const int centre = ReadStencilAt(width / 2, height / 2); + EXPECT_EQ(FirstGLError(), 0u); + EXPECT_EQ(centre, 3) << "glReadPixels(GL_STENCIL_INDEX) of the default framebuffer returned " << centre + << (centre == kStencilPoison ? " - the destination was never written at all" : ""); + + gl.EndFrame(); + } + + // The orientation half: a depth buffer that varies with the row must read back in GL's + // bottom-up order. A full-extent uniform clear is a fixed point of the flip, so only a banded + // buffer can tell the two apart. + TEST_F(DepthStencilReadbackScenario, DefaultFramebufferDepthReadbackKeepsTheGLRowOrder) { + 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(height, 8); + + BindDefaultFramebuffer(); + glViewport(0, 0, width, height); + glDepthMask(GL_TRUE); + ClearDepthInBands(width, height, /*bottom=*/0.25f, /*top=*/0.75f); + EXPECT_EQ(FirstGLError(), 0u); + + const float bottom = ReadDepthAt(width / 2, height / 4); + const float top = ReadDepthAt(width / 2, height - 1 - height / 4); + EXPECT_EQ(FirstGLError(), 0u); + EXPECT_NEAR(bottom, 0.25f, 1.0f / 4096.0f) + << "GL row " << (height / 4) << " is in the bottom band and was cleared to 0.25, but read back " << bottom + << " (0.75 there means the readback is upside down)"; + EXPECT_NEAR(top, 0.75f, 1.0f / 4096.0f) + << "GL row " << (height - 1 - height / 4) << " is in the top band and was cleared to 0.75, but read back " + << top << " (0.25 there means the readback is upside down)"; + + 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) { + if (!Ready()) return; + if (!BackendReadsDepthStencil()) { + GTEST_SKIP() << "backend " << Gl().BackendName() << " has no depth readback path"; + } + HeadlessGL& gl = Gl(); + const int width = 64; + const int height = 48; + + 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)); + ASSERT_EQ(FirstGLError(), 0u); + + glViewport(0, 0, width, height); + glDisable(GL_SCISSOR_TEST); + glDepthMask(GL_TRUE); + glStencilMask(0xFFu); + glClearDepth(0.5); + glClearStencil(7); + glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); + + const float depth = ReadDepthAt(width / 2, height / 2); + const int stencil = ReadStencilAt(width / 2, height / 2); + EXPECT_EQ(FirstGLError(), 0u); + EXPECT_NEAR(depth, 0.5f, 1.0f / 4096.0f) << "user-framebuffer depth readback returned " << depth; + EXPECT_EQ(stencil, 7) << "user-framebuffer stencil readback returned " << stencil; + + glBindFramebuffer(GL_FRAMEBUFFER, 0); + glDeleteTextures(1, &depthTex); + glDeleteTextures(1, &colorTex); + glDeleteFramebuffers(1, &fbo); + gl.EndFrame(); + } +} // namespace MGITest