[Fix, Test] (MG_Backend/DirectVulkan): read the default framebuffer's depth and stencil back instead of leaving the caller's buffer untouched

This commit is contained in:
2026-08-11 10:11:22 -04:00
parent 18c17ae5ca
commit b1fdffd767
4 changed files with 433 additions and 17 deletions
@@ -7194,6 +7194,96 @@ void main() {
return true; 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<Uint32>(clearAspects));
return true;
}
// A glClear on the DEFAULT framebuffer is parked as a pending clear and folded into the next // 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 // 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 // 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)) { if (!m_clearManager->GetPendingClear(attachment, payload)) {
return true; 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 || MOBILEGL_ASSERT(VkRenderPassManager::GetActiveRenderPass() == nullptr ||
commandBuffer != m_frameContext.GetCurrent().commandBuffer, commandBuffer != m_frameContext.GetCurrent().commandBuffer,
"MaterializePendingClearForDefaultFramebuffer requires no active render pass"); "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); const VkImage swapchainImage = m_swapchainObject.GetImage(m_imageIndexAcquired);
if (swapchainImage == VK_NULL_HANDLE) { if (swapchainImage == VK_NULL_HANDLE) {
return false; return false;
@@ -8750,10 +8839,6 @@ void main() {
void VulkanRenderer::ReadDepthStencilPixels(MG_State::GLState::FramebufferObject& readFbo, GLint x, GLint y, void VulkanRenderer::ReadDepthStencilPixels(MG_State::GLState::FramebufferObject& readFbo, GLint x, GLint y,
GLsizei width, GLsizei height, GLenum format, GLenum type, GLsizei width, GLsizei height, GLenum format, GLenum type,
void* pixels) { void* pixels) {
if (readFbo.IsDefaultFramebuffer()) {
MGLOG_E("DirectVulkan::ReadDepthStencilPixels skipped: default framebuffer readback is unsupported");
return;
}
if (width <= 0 || height <= 0) { if (width <= 0 || height <= 0) {
return; return;
} }
@@ -8764,10 +8849,13 @@ void main() {
// framebuffers lacking either, so resolving via the depth attachment is enough. // framebuffers lacking either, so resolving via the depth attachment is enough.
const auto attachmentType = wantDepth ? MobileGL::FramebufferAttachmentType::Depth const auto attachmentType = wantDepth ? MobileGL::FramebufferAttachmentType::Depth
: MobileGL::FramebufferAttachmentType::Stencil; : MobileGL::FramebufferAttachmentType::Stencil;
const auto& attachment = readFbo.GetAttachment(attachmentType); const Bool readIsDefaultFbo = readFbo.IsDefaultFramebuffer();
if (!attachment.IsValid() || attachment.IsEmpty()) { if (!readIsDefaultFbo) {
MGLOG_E("DirectVulkan::ReadDepthStencilPixels skipped: no depth/stencil attachment image"); const auto& attachment = readFbo.GetAttachment(attachmentType);
return; if (!attachment.IsValid() || attachment.IsEmpty()) {
MGLOG_E("DirectVulkan::ReadDepthStencilPixels skipped: no depth/stencil attachment image");
return;
}
} }
auto& frame = m_frameContext.GetCurrent(); auto& frame = m_frameContext.GetCurrent();
@@ -8778,6 +8866,47 @@ void main() {
VkRenderPassManager::EndRenderPass(frame.commandBuffer); 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; VkImage image = VK_NULL_HANDLE;
VkFormat vkFormat = VK_FORMAT_UNDEFINED; VkFormat vkFormat = VK_FORMAT_UNDEFINED;
VkImageLayout* trackedLayout = nullptr; VkImageLayout* trackedLayout = nullptr;
@@ -8828,7 +8957,8 @@ void main() {
void VulkanRenderer::ReadDepthStencilImageToClient(VkImage image, VkFormat vkFormat, VkImageLayout* trackedLayout, void VulkanRenderer::ReadDepthStencilImageToClient(VkImage image, VkFormat vkFormat, VkImageLayout* trackedLayout,
VkImageAspectFlags imageAspect, Uint32 mipLevel, VkImageAspectFlags imageAspect, Uint32 mipLevel,
Uint32 baseArrayLayer, GLint x, GLint y, GLsizei width, 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 wantDepth = format != GL_STENCIL_INDEX;
const Bool wantStencil = format != GL_DEPTH_COMPONENT; const Bool wantStencil = format != GL_DEPTH_COMPONENT;
auto& frame = m_frameContext.GetCurrent(); 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); 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__); 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<Int>(defaultFboExtent.width),
mapping.mirrorX);
copyOffsetY = MapDefaultFramebufferRectAxis(y, height, static_cast<Int>(defaultFboExtent.height),
mapping.flipY);
}
VkBufferImageCopy regions[2]{}; VkBufferImageCopy regions[2]{};
Uint32 regionCount = 0; Uint32 regionCount = 0;
if (wantDepth) { if (wantDepth) {
@@ -8902,7 +9047,7 @@ void main() {
region.imageSubresource.mipLevel = mipLevel; region.imageSubresource.mipLevel = mipLevel;
region.imageSubresource.baseArrayLayer = baseArrayLayer; region.imageSubresource.baseArrayLayer = baseArrayLayer;
region.imageSubresource.layerCount = 1; region.imageSubresource.layerCount = 1;
region.imageOffset = {x, y, 0}; region.imageOffset = {copyOffsetX, copyOffsetY, 0};
region.imageExtent = {static_cast<Uint32>(width), static_cast<Uint32>(height), 1}; region.imageExtent = {static_cast<Uint32>(width), static_cast<Uint32>(height), 1};
} }
if (wantStencil) { if (wantStencil) {
@@ -8912,7 +9057,7 @@ void main() {
region.imageSubresource.mipLevel = mipLevel; region.imageSubresource.mipLevel = mipLevel;
region.imageSubresource.baseArrayLayer = baseArrayLayer; region.imageSubresource.baseArrayLayer = baseArrayLayer;
region.imageSubresource.layerCount = 1; region.imageSubresource.layerCount = 1;
region.imageOffset = {x, y, 0}; region.imageOffset = {copyOffsetX, copyOffsetY, 0};
region.imageExtent = {static_cast<Uint32>(width), static_cast<Uint32>(height), 1}; region.imageExtent = {static_cast<Uint32>(width), static_cast<Uint32>(height), 1};
} }
vkCmdCopyImageToBuffer(frame.commandBuffer, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, readback.GetHandle(), vkCmdCopyImageToBuffer(frame.commandBuffer, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, readback.GetHandle(),
@@ -8937,6 +9082,38 @@ void main() {
const Uint8* depthSrc = mapped; const Uint8* depthSrc = mapped;
const Uint8* stencilSrc = mapped + stencilOffset; 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<Uint8> remappedDepth;
Vector<Uint8> 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<Uint32>(width),
static_cast<Uint32>(height), preTransform,
depthCopyBytes, remappedDepth.data());
}
if (remapped && wantStencil) {
remappedStencil.resize(pixelCount);
remapped = RemapDefaultFboReadbackToGLOrientation(stencilSrc, static_cast<Uint32>(width),
static_cast<Uint32>(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<Int>(preTransform));
}
}
const auto depthValueAt = [&](SizeT i) -> Float { const auto depthValueAt = [&](SizeT i) -> Float {
switch (vkFormat) { switch (vkFormat) {
case VK_FORMAT_D16_UNORM: { case VK_FORMAT_D16_UNORM: {
@@ -211,10 +211,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
GLsizei height, GLenum format, GLenum type, void* pixels); GLsizei height, GLenum format, GLenum type, void* pixels);
// Copy-and-repack core shared by depth-stencil ReadPixels and GetTexImage; // Copy-and-repack core shared by depth-stencil ReadPixels and GetTexImage;
// expects command recording to be active and any render pass already ended. // 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, void ReadDepthStencilImageToClient(VkImage image, VkFormat vkFormat, VkImageLayout* trackedLayout,
VkImageAspectFlags imageAspect, Uint32 mipLevel, Uint32 baseArrayLayer, VkImageAspectFlags imageAspect, Uint32 mipLevel, Uint32 baseArrayLayer,
GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, 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 // Same-extent depth blit between images of different depth formats: host
// round-trip with a per-texel re-encode (see BlitNamedFramebuffer). // round-trip with a per-texel re-encode (see BlitNamedFramebuffer).
Bool BlitDepthAcrossFormats(FrameContext::FrameData& frame, VkImage srcImage, VkFormat srcFormat, Bool BlitDepthAcrossFormats(FrameContext::FrameData& frame, VkImage srcImage, VkFormat srcFormat,
@@ -1125,6 +1130,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool MaterializePendingClearForDefaultFramebuffer(VkCommandBuffer commandBuffer, Bool MaterializePendingClearForDefaultFramebuffer(VkCommandBuffer commandBuffer,
MG_State::GLState::FramebufferObject& fbo, MG_State::GLState::FramebufferObject& fbo,
FramebufferAttachmentType attachmentType); 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); VkPipeline GetOrCreateBlitPipeline(const RenderPassEntry& renderPassEntry);
Bool GenerateDepthMipmapWithShader(FrameContext::FrameData& frame, Bool GenerateDepthMipmapWithShader(FrameContext::FrameData& frame,
MG_State::GLState::ITextureObject& texture, MG_State::GLState::ITextureObject& texture,
@@ -58,6 +58,7 @@ add_executable(MobileGLIntegrationTest
Scenarios/PixelStoreSweepScenario.cpp Scenarios/PixelStoreSweepScenario.cpp
Scenarios/FragCoordOriginScenario.cpp Scenarios/FragCoordOriginScenario.cpp
Scenarios/ClearThenReadPixelsScenario.cpp Scenarios/ClearThenReadPixelsScenario.cpp
Scenarios/DepthStencilReadbackScenario.cpp
) )
target_include_directories(MobileGLIntegrationTest PRIVATE target_include_directories(MobileGLIntegrationTest PRIVATE
@@ -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 <cmath>
#include <string>
#include <vector>
#include "../Harness/HeadlessGL.h"
#include "../Harness/ScenarioFixture.h"
#ifdef GLAPI
#undef GLAPI
#endif
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#include <GL/glcorearb.h>
#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