From c0b40ff5087faa29a31f7f2e18304f1d395c6045 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Sun, 20 Sep 2026 11:50:47 -0400 Subject: [PATCH] [MG_Remote] chunk texture replies and cover GPU readback packing --- .../Scenarios/F1WireScenario.cpp | 60 +++++++++++++++++++ .../MG_Remote/Client/TextureReadbackEmit.inc | 19 +++--- 2 files changed, 72 insertions(+), 7 deletions(-) diff --git a/MobileGL/MG_IntegrationTest/Scenarios/F1WireScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/F1WireScenario.cpp index 891efb35..aa68d7fc 100644 --- a/MobileGL/MG_IntegrationTest/Scenarios/F1WireScenario.cpp +++ b/MobileGL/MG_IntegrationTest/Scenarios/F1WireScenario.cpp @@ -120,6 +120,66 @@ protected: }; } +TEST_F(F1WireScenario, TextureReadbackLargerThanAReplySlotContainsGpuWrites) { + if (!Ready()) return; + constexpr int width = 1024, height = 600; + glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, width, height); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0); + ASSERT_EQ(glCheckFramebufferStatus(GL_FRAMEBUFFER), GLenum(GL_FRAMEBUFFER_COMPLETE)); + glClearColor(1, 0, 0, 1); + glClear(GL_COLOR_BUFFER_BIT); + glEnable(GL_SCISSOR_TEST); + glScissor(0, height / 2, width, height / 2); + glClearColor(0, 0, 1, 1); + glClear(GL_COLOR_BUFFER_BIT); + glDisable(GL_SCISSOR_TEST); + std::vector pixels(width * height * 4, 0x5a); + glGetTextureImage(texture, 0, GL_RGBA, GL_UNSIGNED_BYTE, static_cast(pixels.size()), pixels.data()); + ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)); + for (int y = 0; y < height; ++y) { + for (int x : {0, width / 2, width - 1}) { + const size_t offset = (size_t(y) * width + x) * 4; + const std::array expected = y < height / 2 + ? std::array{255, 0, 0, 255} : std::array{0, 0, 255, 255}; + EXPECT_TRUE(std::equal(expected.begin(), expected.end(), pixels.begin() + offset)) << x << "," << y; + } + } +} + +TEST_F(F1WireScenario, TextureAndFramebufferReadsPreservePackBufferPadding) { + if (!Ready()) return; + Attach(GL_RGBA8); + glClearColor(0, 1, 0, 1); + glClear(GL_COLOR_BUFFER_BIT); + constexpr size_t size = 512, offset = 12, stride = 40; + std::vector expected(size, 0x5a), actual(size); + GLuint pbo = 0; + glGenBuffers(1, &pbo); + glBindBuffer(GL_PIXEL_PACK_BUFFER, pbo); + glBufferData(GL_PIXEL_PACK_BUFFER, size, expected.data(), GL_DYNAMIC_READ); + glPixelStorei(GL_PACK_ROW_LENGTH, 10); + glPixelStorei(GL_PACK_SKIP_ROWS, 1); + glPixelStorei(GL_PACK_SKIP_PIXELS, 1); + for (int mode = 0; mode < 2; ++mode) { + glBufferSubData(GL_PIXEL_PACK_BUFFER, 0, size, expected.data()); + if (mode == 0) glGetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_BYTE, reinterpret_cast(offset)); + else glReadPixels(0, 0, 8, 8, GL_RGBA, GL_UNSIGNED_BYTE, reinterpret_cast(offset)); + ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)); + glGetBufferSubData(GL_PIXEL_PACK_BUFFER, 0, size, actual.data()); + auto wanted = expected; + for (size_t y = 0; y < 8; ++y) for (size_t x = 0; x < 8; ++x) { + const size_t at = offset + stride + 4 + y * stride + x * 4; + wanted[at] = 0; wanted[at + 1] = 255; wanted[at + 2] = 0; wanted[at + 3] = 255; + } + EXPECT_EQ(actual, wanted) << "read API " << mode; + } + glPixelStorei(GL_PACK_ROW_LENGTH, 0); + glPixelStorei(GL_PACK_SKIP_ROWS, 0); + glPixelStorei(GL_PACK_SKIP_PIXELS, 0); + glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); + glDeleteBuffers(1, &pbo); +} + // P5f exit: each published frame must have a real draw and server stamp, and // zero residual reads. Missing instrumentation is a failure rather than a false zero. TEST_F(F1WireScenario, EachWireFrameHasZeroResidualPulls) { diff --git a/MobileGL/MG_Remote/Client/TextureReadbackEmit.inc b/MobileGL/MG_Remote/Client/TextureReadbackEmit.inc index 7fc55736..f3062d14 100644 --- a/MobileGL/MG_Remote/Client/TextureReadbackEmit.inc +++ b/MobileGL/MG_Remote/Client/TextureReadbackEmit.inc @@ -27,14 +27,19 @@ info.Type = type; info.Target = wholeCube ? GL_TEXTURE_CUBE_MAP : MG_Util::ConvertTextureUploadTargetToGLEnum(uploadTarget); info.Level = static_cast(level); - info.DstSize = tight; - session.RequireReadPixelsReplyFits(info.Box.W, info.Box.H, format, type, tight); Vector bytes(static_cast(tight)); - Int32 status = Wire::ReplySink::kStatusError; - Uint64 received = 0; - session.EmitAndWait(MG_Pipe::MGPWireOp::GetTextureImage, &info, sizeof(info), nullptr, 0, - bytes.data(), tight, &status, &received); - RequireReadbackReplyComplete(status, received, tight); + const Uint64 capacity = session.MaxReplyBytes(); + if (!capacity) Wire::WireProtocolFatal("GetTextureImage.reply", "zero reply capacity"); + for (Uint64 offset = 0; offset < tight; ) { + info.DstOffset = offset; + info.DstSize = std::min(tight - offset, capacity); + Int32 status = Wire::ReplySink::kStatusError; + Uint64 received = 0; + session.EmitAndWait(MG_Pipe::MGPWireOp::GetTextureImage, &info, sizeof(info), nullptr, 0, + bytes.data() + offset, info.DstSize, &status, &received); + RequireReadbackReplyComplete(status, received, info.DstSize); + offset += info.DstSize; + } const auto pack = MG_State::pGLContext->GetPixelStoreParameters(false); ApplyReadbackByteSwap(bytes.data(), tight, type, pack); const Uint64 rowBytes = static_cast(size.x()) * bpp;