mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 04:08:32 +09:00
[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:
@@ -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<Uint32>(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<Int>(defaultFboExtent.width),
|
||||
mapping.mirrorX);
|
||||
copyOffsetY = MapDefaultFramebufferRectAxis(y, height, static_cast<Int>(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<Uint32>(width), static_cast<Uint32>(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<Uint32>(width), static_cast<Uint32>(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<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 {
|
||||
switch (vkFormat) {
|
||||
case VK_FORMAT_D16_UNORM: {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -58,6 +58,7 @@ add_executable(MobileGLIntegrationTest
|
||||
Scenarios/PixelStoreSweepScenario.cpp
|
||||
Scenarios/FragCoordOriginScenario.cpp
|
||||
Scenarios/ClearThenReadPixelsScenario.cpp
|
||||
Scenarios/DepthStencilReadbackScenario.cpp
|
||||
)
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user