From 8af60919ffb957f1a4ed39dc320c8c7792c0a4c0 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Wed, 23 Sep 2026 10:53:36 -0400 Subject: [PATCH] [MG_IntegrationTest] (P7): banded readback scenarios on every split arm and Magma --- MobileGL/MG_IntegrationTest/CMakeLists.txt | 42 ++ .../Scenarios/BandedReadbackScenario.cpp | 466 ++++++++++++++++++ 2 files changed, 508 insertions(+) create mode 100644 MobileGL/MG_IntegrationTest/Scenarios/BandedReadbackScenario.cpp diff --git a/MobileGL/MG_IntegrationTest/CMakeLists.txt b/MobileGL/MG_IntegrationTest/CMakeLists.txt index 173a86f7..bdf1c54c 100644 --- a/MobileGL/MG_IntegrationTest/CMakeLists.txt +++ b/MobileGL/MG_IntegrationTest/CMakeLists.txt @@ -81,6 +81,9 @@ add_executable(MobileGLIntegrationTest Scenarios/PipelineFailureScenario.cpp Scenarios/AdvertisedLimitsScenario.cpp Scenarios/PixelStoreSweepScenario.cpp + # P7 gate 5 (g5-readback): readbacks larger than one reply slot. Ordinary GL cases, so the + # monolith arms run them too - that is the parity half of the split registrations below. + Scenarios/BandedReadbackScenario.cpp Scenarios/PrimitiveRestartScenario.cpp Scenarios/FragCoordOriginScenario.cpp Scenarios/ClientVertexArrayScenario.cpp @@ -2436,6 +2439,12 @@ if (MOBILEGL_BUILD_DISAGGREGATED) # a transport every one of those pairs is also a create/destroy record round trip, so this is # the only entry in the readback block that says anything about the OBJECT lifetime half. mgl_itest_register_split_arms("FramebufferChurnScenario.*" "") + # P7 gate 5 (g5-readback): a glReadPixels larger than one SEG_REPLY slot is banded by the + # client instead of Fatal{ReplyTooLarge}. Exactly 2 MiB (the CTS renderbuffers_storage shape), + # 2 MiB + one row, more than the whole 16 MiB segment, a non-neutral pack state, a pack + # buffer, depth and packed depth/stencil, and glGetTexImage's windowed twin. The Magma arms of + # the same cases are the `Band.` block after the Fm lane below. + mgl_itest_register_split_arms("BandedReadbackScenario.*" "") # DepthStencilReadbackMatrixScenario GETS ITS OWN BLOCK, because it needs a variable the # registration macro cannot carry: MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION=1. @@ -3375,6 +3384,39 @@ if (MOBILEGL_BUILD_DISAGGREGATED) endforeach() endif() +# P7 gate 5 (g5-readback): BANDED READBACK ON MAGMA, all three arms, GATED. The flavour is the +# one the device CTS window ran KHR-GL46 under (role-split state, strict errors, run-ahead), so the +# host lane measures the configuration that aborted on the Redmi with +# Fatal{ReplyTooLarge, "ReadPixels 256x512 0x1908/0x1406 2097152 > 2097136"}. One entry per case +# for a private log each - two processes append to it on the spawn arm. +if (MOBILEGL_BUILD_DISAGGREGATED) + set(MGL_ITEST_MAGMA_BAND_FLAVOUR_ENV + "MOBILEGL_BACKEND_TYPE=DirectVulkan" + "MOBILEGL_IPC_ROLE_SPLIT_STATE=1" "MOBILEGL_IPC_STRICT_ERRORS=1" "MOBILEGL_IPC_RUN_AHEAD=1" + "MOBILEGL_ITEST_REQUIRE_GPU=1" "MGITEST_SPLIT_LANE=1" + ${MGL_ITEST_CAPABILITY_ENV} ${MGL_ITEST_VULKAN_ENV}) # the ICD pin, see the Fm lane above + foreach(mglItestArm IN LISTS MGL_ITEST_MAGMA_ARMS) + mgl_itest_magma_arm_shape(${mglItestArm}) + mgl_itest_join_environment(mglItestMagmaBandEnvironment + ${MGL_ITEST_MAGMA_BAND_FLAVOUR_ENV} ${mglItestArmTransportEnv}) + foreach(bandCase RenderbufferReadOfExactlyTwoMiBIsBandedNotRefused + TwoMiBPlusOneRowIsBanded + AReadLargerThanTheWholeReplySegmentIsBanded + BandedReadsHonourPackAlignmentRowLengthAndSkips + BandedReadIntoAPixelPackBufferKeepsItsPadding + BandedDepthAndDepthStencilReads + GetTexImageLargerThanAReplySlotIsWindowed) + gtest_discover_tests(MobileGLIntegrationTest + TEST_PREFIX "DirectVulkan.${mglItestArm}.Band." + TEST_FILTER "BandedReadbackScenario.${bandCase}" + DISCOVERY_TIMEOUT 30 + PROPERTIES LABELS "${mglItestMagmaLabels}" TIMEOUT ${MGL_ITEST_TIMEOUT} + ${mglItestArmProperties} + ENVIRONMENT "${mglItestMagmaBandEnvironment}\;MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/g5-band${mglItestArmLogTag}-${bandCase}.log") + endforeach() + endforeach() +endif() + # Magma application buffers: public GL pixels/bytes, with the two role blocks and # strict field checks forced on. Existing public scenarios supply offset, mutation, # persistent-map, growth and indirect coverage; no copied backend implementation. diff --git a/MobileGL/MG_IntegrationTest/Scenarios/BandedReadbackScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/BandedReadbackScenario.cpp new file mode 100644 index 00000000..39f9fc4a --- /dev/null +++ b/MobileGL/MG_IntegrationTest/Scenarios/BandedReadbackScenario.cpp @@ -0,0 +1,466 @@ +// MobileGL - MobileGL/MG_IntegrationTest/Scenarios/BandedReadbackScenario.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 +// +// P7 gate 5, package g5-readback: readbacks LARGER THAN ONE REPLY SLOT. +// +// On every split arm a readback's answer travels back in one SEG_REPLY slot, and a slot carries +// at most MaxReplyBytes = 2 MiB - 16 = 2,097,136 bytes (ID-47). Until this package a +// glReadPixels whose tight answer was larger than that was Fatal{ReplyTooLarge} at the client - +// KHR-GL46.direct_state_access.renderbuffers_storage reads 256x512 RGBA/FLOAT = 2,097,152 bytes, +// sixteen over, and aborted on the inproc arm while the monolith passed. The client now splits +// such a read into row bands that each fit a slot (EmitTables.h PlanReadbackBands) and scatters +// every band under the application's GL_PACK_* state. +// +// THESE ARE ORDINARY GL CASES and run on the monolith arms too, which is the parity half: the +// same pixels must come back whichever side of the wire answers. Every shape below straddles +// the slot boundary on purpose - exactly 2 MiB, 2 MiB plus one row, more than the WHOLE 16 MiB +// SEG_REPLY - and every image is POSITION-DEPENDENT, so a band that landed at the wrong row, +// was dropped, or was read from the wrong box cannot look right. +// +// A row wider than a whole reply (the column-piece arm of the plan) is not reachable here: it +// needs a >131071-pixel RGBA/FLOAT row, wider than any attachment, and GL leaves the pixels +// outside the framebuffer undefined - so that arm is covered by the unit controls over the +// plan and the band scatter (MG_Test/Wire/RemoteClientTest.cpp, RemoteReadback.*). + +#include +#include +#include +#include +#include +#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 { + + // The shared-memory links' reply payload cap (ID-47). Named here only to state the + // arithmetic each shape straddles; the client reads the live number from its link. + constexpr std::size_t kReplySlotPayload = 2u * 1024u * 1024u - 16u; + // The whole SEG_REPLY segment: eight slots of 2 MiB. + constexpr std::size_t kReplySegmentBytes = 16u * 1024u * 1024u; + + // A texel that names its own position, so a misplaced band cannot match. + std::array PatternRgba8(int x, int y) { + return {static_cast(x & 0xFF), + static_cast(((x >> 8) & 0x0F) | (((y >> 8) & 0x0F) << 4)), + static_cast(y & 0xFF), static_cast((x * 3 + y * 5) & 0xFF)}; + } + + // Every component an integer (or a half) below 2^24, so float storage is exact. + std::array PatternRgba32f(int x, int y) { + return {static_cast(x), static_cast(y), + static_cast(x) * 0.5f + static_cast(y) * 1024.0f, + static_cast(1 + (x ^ y))}; + } + + struct PatternFormat { + const char* name; + GLenum internalFormat; + GLenum type; + std::size_t bytesPerPixel; + }; + constexpr PatternFormat kRgba8{"RGBA8 / UNSIGNED_BYTE", GL_RGBA8, GL_UNSIGNED_BYTE, 4}; + constexpr PatternFormat kRgba32f{"RGBA32F / FLOAT", GL_RGBA32F, GL_FLOAT, 16}; + + // The pattern's bytes for one texel, in the format's client layout. + void PatternBytes(const PatternFormat& format, int x, int y, GLubyte* out) { + if (format.type == GL_FLOAT) { + const auto texel = PatternRgba32f(x, y); + std::memcpy(out, texel.data(), sizeof(texel)); + } else { + const auto texel = PatternRgba8(x, y); + std::memcpy(out, texel.data(), sizeof(texel)); + } + } + + class BandedReadbackScenario : public ScenarioTest { + protected: + void SetUp() override { + ScenarioTest::SetUp(); + if (!Ready()) return; + DrainErrors(); + glGenFramebuffers(1, &m_fbo); + glBindFramebuffer(GL_FRAMEBUFFER, m_fbo); + glDisable(GL_SCISSOR_TEST); + glDisable(GL_DEPTH_TEST); + glDisable(GL_STENCIL_TEST); + glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); + glDepthMask(GL_TRUE); + glStencilMask(~0u); + ResetPack(); + } + + void TearDown() override { + if (!Ready()) return; + ResetPack(); + glDisable(GL_SCISSOR_TEST); + glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); + if (m_pbo != 0) glDeleteBuffers(1, &m_pbo); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + if (m_fbo != 0) glDeleteFramebuffers(1, &m_fbo); + for (GLuint texture : m_textures) glDeleteTextures(1, &texture); + for (GLuint renderbuffer : m_renderbuffers) glDeleteRenderbuffers(1, &renderbuffer); + m_pbo = 0; + m_fbo = 0; + m_textures.clear(); + m_renderbuffers.clear(); + DrainErrors(); + ScenarioTest::TearDown(); + } + + static void ResetPack() { + glPixelStorei(GL_PACK_ALIGNMENT, 4); + glPixelStorei(GL_PACK_ROW_LENGTH, 0); + glPixelStorei(GL_PACK_SKIP_ROWS, 0); + glPixelStorei(GL_PACK_SKIP_PIXELS, 0); + glPixelStorei(GL_UNPACK_ALIGNMENT, 4); + glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); + } + + static void DrainErrors() { + for (int i = 0; i < 16 && glGetError() != GL_NO_ERROR; ++i) { + } + } + + // A texture holding the position pattern, attached as COLOR_ATTACHMENT0 and read. + void AttachPatternedTexture(const PatternFormat& format, int width, int height) { + std::vector texels(static_cast(width) * height * format.bytesPerPixel); + for (int y = 0; y < height; ++y) { + for (int x = 0; x < width; ++x) { + PatternBytes(format, x, y, + texels.data() + (static_cast(y) * width + x) * format.bytesPerPixel); + } + } + GLuint texture = 0; + glGenTextures(1, &texture); + m_textures.push_back(texture); + glBindTexture(GL_TEXTURE_2D, texture); + glTexStorage2D(GL_TEXTURE_2D, 1, format.internalFormat, width, height); + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_RGBA, format.type, texels.data()); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0); + glReadBuffer(GL_COLOR_ATTACHMENT0); + ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast(GL_FRAMEBUFFER_COMPLETE)) + << format.name << " " << width << "x" << height; + ASSERT_EQ(FirstGLError(), static_cast(GL_NO_ERROR)) << "pattern upload " << format.name; + } + + // Every texel of a tight w*h read at (x0, y0), against the pattern. + static void ExpectTightPattern(const PatternFormat& format, const std::vector& actual, int x0, + int y0, int width, int height) { + std::array expected{}; + std::size_t bad = 0; + std::string first; + for (int y = 0; y < height; ++y) { + for (int x = 0; x < width; ++x) { + PatternBytes(format, x0 + x, y0 + y, expected.data()); + const GLubyte* got = + actual.data() + (static_cast(y) * width + x) * format.bytesPerPixel; + if (std::memcmp(got, expected.data(), format.bytesPerPixel) != 0) { + if (bad++ == 0) first = "(" + std::to_string(x) + "," + std::to_string(y) + ")"; + } + } + } + EXPECT_EQ(bad, 0u) << format.name << " " << width << "x" << height << ": " << bad + << " texels differ from the pattern, first at " << first; + } + + void ReadWholeAndExpect(const PatternFormat& format, int width, int height) { + AttachPatternedTexture(format, width, height); + if (HasFatalFailure()) return; + std::vector actual(static_cast(width) * height * format.bytesPerPixel, 0xC3); + glReadPixels(0, 0, width, height, GL_RGBA, format.type, actual.data()); + ASSERT_EQ(FirstGLError(), static_cast(GL_NO_ERROR)) << "glReadPixels " << format.name; + ExpectTightPattern(format, actual, 0, 0, width, height); + } + + GLuint m_fbo = 0; + GLuint m_pbo = 0; + std::vector m_textures; + std::vector m_renderbuffers; + }; + + } // namespace + + // THE CTS SHAPE, byte for byte: KHR-GL46.direct_state_access.renderbuffers_storage reads a + // 256x512 RGBA32F renderbuffer as GL_RGBA/GL_FLOAT - 2,097,152 bytes, sixteen more than a slot. + // A renderbuffer cannot be uploaded, so it is painted with scissored clears: 16 row stripes x 2 + // column halves, each its own colour (every value a multiple of 1/32, exact in float and inside + // [0, 1] so no backend's clamping can move it). The last band is the single top row. + TEST_F(BandedReadbackScenario, RenderbufferReadOfExactlyTwoMiBIsBandedNotRefused) { + if (!Ready()) return; + constexpr int kWidth = 256; + constexpr int kHeight = 512; + static_assert(static_cast(kWidth) * kHeight * 16 == kReplySlotPayload + 16, + "the CTS read is exactly 2 MiB, sixteen bytes over one reply slot"); + GLuint renderbuffer = 0; + glGenRenderbuffers(1, &renderbuffer); + m_renderbuffers.push_back(renderbuffer); + glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer); + glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA32F, kWidth, kHeight); + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderbuffer); + glDrawBuffer(GL_COLOR_ATTACHMENT0); + glReadBuffer(GL_COLOR_ATTACHMENT0); + ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast(GL_FRAMEBUFFER_COMPLETE)); + + auto stripeColour = [](int x, int y) { + const int stripe = y / 32; + const int half = x / 128; + return std::array{static_cast(stripe) / 16.0f, static_cast(half), + static_cast(stripe * 2 + half) / 32.0f, + 0.25f + 0.5f * static_cast(half)}; + }; + glEnable(GL_SCISSOR_TEST); + for (int stripe = 0; stripe < kHeight / 32; ++stripe) { + for (int half = 0; half < 2; ++half) { + const auto colour = stripeColour(half * 128, stripe * 32); + glScissor(half * 128, stripe * 32, 128, 32); + glClearColor(colour[0], colour[1], colour[2], colour[3]); + glClear(GL_COLOR_BUFFER_BIT); + } + } + glDisable(GL_SCISSOR_TEST); + ASSERT_EQ(FirstGLError(), static_cast(GL_NO_ERROR)) << "stripe clears"; + + std::vector pixels(static_cast(kWidth) * kHeight * 4, -7.0f); + glReadPixels(0, 0, kWidth, kHeight, GL_RGBA, GL_FLOAT, pixels.data()); + ASSERT_EQ(FirstGLError(), static_cast(GL_NO_ERROR)) << "glReadPixels RGBA/FLOAT 256x512"; + std::size_t bad = 0; + std::string first; + for (int y = 0; y < kHeight; ++y) { + for (int x = 0; x < kWidth; ++x) { + const auto expected = stripeColour(x, y); + const GLfloat* got = pixels.data() + (static_cast(y) * kWidth + x) * 4; + if (std::memcmp(got, expected.data(), sizeof(expected)) != 0 && bad++ == 0) { + first = "(" + std::to_string(x) + "," + std::to_string(y) + ") = " + std::to_string(got[0]) + + "," + std::to_string(got[1]) + "," + std::to_string(got[2]) + "," + + std::to_string(got[3]); + } + } + } + EXPECT_EQ(bad, 0u) << bad << " of " << kWidth * kHeight << " texels are wrong, first " << first; + } + + // One row over the slot: 1024x513 RGBA8 = 2 MiB + 4 KiB. Two bands, the second a single row. + TEST_F(BandedReadbackScenario, TwoMiBPlusOneRowIsBanded) { + if (!Ready()) return; + static_assert(1024u * 512u * 4u > kReplySlotPayload, "512 rows alone are already over a slot"); + ReadWholeAndExpect(kRgba8, 1024, 513); + } + + // Larger than the WHOLE reply segment, not just one slot: 2048x2100 RGBA8 = 17,203,200 bytes + // against a 16 MiB SEG_REPLY. A reply pool that merely grew could never answer this; nine bands + // through the same slot ring can. + TEST_F(BandedReadbackScenario, AReadLargerThanTheWholeReplySegmentIsBanded) { + if (!Ready()) return; + static_assert(2048u * 2100u * 4u > kReplySegmentBytes, "the read must outgrow SEG_REPLY itself"); + ReadWholeAndExpect(kRgba8, 2048, 2100); + } + + // The bounce path: a non-neutral pack state, for both a 4- and a 16-byte pixel, read from an + // offset box so every band's own box origin is exercised as well. PACK_ALIGNMENT 8 with an odd + // ROW_LENGTH pads every row, SKIP_ROWS / SKIP_PIXELS move the first byte - and the bytes GL does + // NOT name must keep their sentinel, which is what a band scattered on the wrong stride breaks. + TEST_F(BandedReadbackScenario, BandedReadsHonourPackAlignmentRowLengthAndSkips) { + if (!Ready()) return; + struct PackCase { + PatternFormat format; + int textureWidth, textureHeight; + int x0, y0, width, height; + int alignment, rowLength, skipRows, skipPixels; + }; + const PackCase cases[] = { + {kRgba8, 1100, 700, 3, 5, 1023, 600, 8, 1025, 2, 5}, + {kRgba32f, 320, 640, 1, 2, 300, 600, 8, 303, 1, 2}, + }; + for (const PackCase& c : cases) { + SCOPED_TRACE(c.format.name); + ASSERT_GT(static_cast(c.width) * c.height * c.format.bytesPerPixel, kReplySlotPayload); + AttachPatternedTexture(c.format, c.textureWidth, c.textureHeight); + if (HasFatalFailure()) return; + const std::size_t bpp = c.format.bytesPerPixel; + const std::size_t stride = + (static_cast(c.rowLength) * bpp + c.alignment - 1) / c.alignment * c.alignment; + const std::size_t first = static_cast(c.skipRows) * stride + c.skipPixels * bpp; + const std::size_t size = first + static_cast(c.height) * stride + 64; + constexpr GLubyte kSentinel = 0xA5; + std::vector destination(size, kSentinel); + glPixelStorei(GL_PACK_ALIGNMENT, c.alignment); + glPixelStorei(GL_PACK_ROW_LENGTH, c.rowLength); + glPixelStorei(GL_PACK_SKIP_ROWS, c.skipRows); + glPixelStorei(GL_PACK_SKIP_PIXELS, c.skipPixels); + glReadPixels(c.x0, c.y0, c.width, c.height, GL_RGBA, c.format.type, destination.data()); + ResetPack(); + ASSERT_EQ(FirstGLError(), static_cast(GL_NO_ERROR)); + + std::vector wanted(size, kSentinel); + for (int y = 0; y < c.height; ++y) { + for (int x = 0; x < c.width; ++x) { + PatternBytes(c.format, c.x0 + x, c.y0 + y, wanted.data() + first + y * stride + x * bpp); + } + } + std::size_t bad = 0, firstBad = 0; + for (std::size_t i = 0; i < size; ++i) { + if (destination[i] != wanted[i] && bad++ == 0) firstBad = i; + } + EXPECT_EQ(bad, 0u) << bad << " of " << size << " destination bytes differ, first at byte " << firstBad + << " (row " << (firstBad >= first ? (firstBad - first) / stride : 0) << ")"; + } + } + + // A PIXEL_PACK_BUFFER destination at a non-zero offset: the bands are uploaded row by row into + // the buffer under the pack state, and the bytes around them are the application's. + TEST_F(BandedReadbackScenario, BandedReadIntoAPixelPackBufferKeepsItsPadding) { + if (!Ready()) return; + constexpr int kWidth = 1024; + constexpr int kHeight = 600; + constexpr int kRowLength = 1030; + constexpr int kSkipRows = 1; + constexpr std::size_t kOffset = 16; + static_assert(static_cast(kWidth) * kHeight * 4 > kReplySlotPayload, "must band"); + AttachPatternedTexture(kRgba8, kWidth, kHeight); + if (HasFatalFailure()) return; + const std::size_t stride = static_cast(kRowLength) * 4; + const std::size_t first = kOffset + kSkipRows * stride; + const std::size_t size = first + static_cast(kHeight) * stride + 32; + constexpr GLubyte kSentinel = 0x3C; + const std::vector initial(size, kSentinel); + glGenBuffers(1, &m_pbo); + glBindBuffer(GL_PIXEL_PACK_BUFFER, m_pbo); + glBufferData(GL_PIXEL_PACK_BUFFER, static_cast(size), initial.data(), GL_DYNAMIC_READ); + glPixelStorei(GL_PACK_ROW_LENGTH, kRowLength); + glPixelStorei(GL_PACK_SKIP_ROWS, kSkipRows); + glReadPixels(0, 0, kWidth, kHeight, GL_RGBA, GL_UNSIGNED_BYTE, reinterpret_cast(kOffset)); + ResetPack(); + ASSERT_EQ(FirstGLError(), static_cast(GL_NO_ERROR)); + std::vector actual(size, 0); + glGetBufferSubData(GL_PIXEL_PACK_BUFFER, 0, static_cast(size), actual.data()); + glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); + ASSERT_EQ(FirstGLError(), static_cast(GL_NO_ERROR)); + + std::vector wanted = initial; + for (int y = 0; y < kHeight; ++y) { + for (int x = 0; x < kWidth; ++x) { + const auto texel = PatternRgba8(x, y); + std::memcpy(wanted.data() + first + y * stride + x * 4, texel.data(), 4); + } + } + std::size_t bad = 0, firstBad = 0; + for (std::size_t i = 0; i < size; ++i) { + if (actual[i] != wanted[i] && bad++ == 0) firstBad = i; + } + EXPECT_EQ(bad, 0u) << bad << " of " << size << " buffer bytes differ, first at byte " << firstBad; + } + + // Depth and packed depth/stencil take the SAME banded path - the plan is about the layout, + // not the component. DEPTH_COMPONENT/FLOAT is 4 bytes a pixel (2 bands here), and + // DEPTH_STENCIL/FLOAT_32_UNSIGNED_INT_24_8_REV is 8 (3 bands). Depth is painted in row + // stripes and stencil in column quarters, so both axes of a band's placement are checked. + TEST_F(BandedReadbackScenario, BandedDepthAndDepthStencilReads) { + if (!Ready()) return; + constexpr int kWidth = 1024; + constexpr int kHeight = 640; + constexpr int kStripeRows = 40; + static_assert(static_cast(kWidth) * kHeight * 4 > kReplySlotPayload, "depth must band"); + GLuint texture = 0; + glGenTextures(1, &texture); + m_textures.push_back(texture); + glBindTexture(GL_TEXTURE_2D, texture); + glTexStorage2D(GL_TEXTURE_2D, 1, GL_DEPTH32F_STENCIL8, kWidth, kHeight); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, texture, 0); + glDrawBuffer(GL_NONE); + glReadBuffer(GL_NONE); + ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), static_cast(GL_FRAMEBUFFER_COMPLETE)); + + auto depthAt = [](int y) { return static_cast(y / kStripeRows + 1) / 32.0f; }; + auto stencilAt = [](int x) { return static_cast(10 + (x / (kWidth / 4)) * 20); }; + glEnable(GL_SCISSOR_TEST); + for (int stripe = 0; stripe < kHeight / kStripeRows; ++stripe) { + glScissor(0, stripe * kStripeRows, kWidth, kStripeRows); + glClearDepth(depthAt(stripe * kStripeRows)); + glClear(GL_DEPTH_BUFFER_BIT); + } + for (int quarter = 0; quarter < 4; ++quarter) { + glScissor(quarter * (kWidth / 4), 0, kWidth / 4, kHeight); + glClearStencil(static_cast(stencilAt(quarter * (kWidth / 4)))); + glClear(GL_STENCIL_BUFFER_BIT); + } + glDisable(GL_SCISSOR_TEST); + glClearDepth(1.0); + glClearStencil(0); + ASSERT_EQ(FirstGLError(), static_cast(GL_NO_ERROR)) << "depth/stencil stripe clears"; + + const std::size_t pixels = static_cast(kWidth) * kHeight; + { + std::vector depth(pixels, -3.0f); + glReadPixels(0, 0, kWidth, kHeight, GL_DEPTH_COMPONENT, GL_FLOAT, depth.data()); + ASSERT_EQ(FirstGLError(), static_cast(GL_NO_ERROR)) << "DEPTH_COMPONENT/FLOAT"; + std::size_t bad = 0, firstBad = 0; + for (std::size_t i = 0; i < pixels; ++i) { + const int y = static_cast(i / kWidth); + if (std::fabs(depth[i] - depthAt(y)) > 1.0f / 4096.0f && bad++ == 0) firstBad = i; + } + EXPECT_EQ(bad, 0u) << bad << " depth values wrong, first at pixel " << firstBad << " = " + << depth[firstBad]; + } + { + struct D32fS8 { + GLfloat depth; + GLuint stencil; + }; + static_assert(sizeof(D32fS8) == 8, "FLOAT_32_UNSIGNED_INT_24_8_REV is two 32-bit words"); + std::vector packed(pixels, D32fS8{-3.0f, 0xFFFFFFFFu}); + glReadPixels(0, 0, kWidth, kHeight, GL_DEPTH_STENCIL, GL_FLOAT_32_UNSIGNED_INT_24_8_REV, packed.data()); + ASSERT_EQ(FirstGLError(), static_cast(GL_NO_ERROR)) << "DEPTH_STENCIL/FLOAT_32_24_8_REV"; + std::size_t bad = 0, firstBad = 0; + for (std::size_t i = 0; i < pixels; ++i) { + const int x = static_cast(i % kWidth); + const int y = static_cast(i / kWidth); + if ((std::fabs(packed[i].depth - depthAt(y)) > 1.0f / 4096.0f || + (packed[i].stencil & 0xFFu) != stencilAt(x)) && + bad++ == 0) { + firstBad = i; + } + } + EXPECT_EQ(bad, 0u) << bad << " packed depth/stencil pairs wrong, first at pixel " << firstBad + << " = depth " << packed[firstBad].depth << " stencil " + << (packed[firstBad].stencil & 0xFFu); + } + } + + // THE OTHER READBACK THAT CROSSES THE REPLY SLOT. glGetTexImage was already windowed + // (TextureReadbackEmit.inc: DstOffset/DstSize windows of at most MaxReplyBytes), so this is + // the parity row that keeps it so beside the banded ReadPixels: 1024x1024 RGBA8 = 4 MiB. + TEST_F(BandedReadbackScenario, GetTexImageLargerThanAReplySlotIsWindowed) { + if (!Ready()) return; + constexpr int kWidth = 1024; + constexpr int kHeight = 1024; + AttachPatternedTexture(kRgba8, kWidth, kHeight); + if (HasFatalFailure()) return; + std::vector actual(static_cast(kWidth) * kHeight * 4, 0x77); + glGetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_BYTE, actual.data()); + ASSERT_EQ(FirstGLError(), static_cast(GL_NO_ERROR)) << "glGetTexImage"; + ExpectTightPattern(kRgba8, actual, 0, 0, kWidth, kHeight); + } + +} // namespace MGITest