From c822a9fef79396dcdf847bec2f808ff6b623ab65 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Fri, 2 Oct 2026 21:00:10 -0400 Subject: [PATCH] [Fix] (Espryt): copy only the damaged rectangles into shared images again --- MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp | 39 +++++++++---- MobileGL/MG_Test/Util/DamageTest.cpp | 58 +++++++++++++++++++ MobileGL/MG_Util/Damage/Damage.h | 24 ++++++++ 3 files changed, 110 insertions(+), 11 deletions(-) diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index f374b4e0..a6e1922d 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -18655,6 +18655,8 @@ namespace MobileGL::MG_Backend::DirectGLES { EglImageRef Image; GLuint Renderbuffer = 0; Uint64 LastUse = 0; + // A present has written the image through this target (its content is a frame). + Bool Written = false; }; struct PresentTargets { std::mutex Mutex; @@ -18926,6 +18928,10 @@ namespace MobileGL::MG_Backend::DirectGLES { it = targets.ById.emplace(view.Id, std::move(target)).first; } it->second.LastUse = ++targets.UseClock; + // A target written before keeps what it was given; one new to this process is + // copied whole whatever the client asked (it cannot know the image's history here). + const Bool wholeTarget = !it->second.Written; + it->second.Written = true; GLuint& framebuffer = g_presentFramebuffers.Current(); if (framebuffer == 0) g_GLESFuncs.glGenFramebuffers(1, &framebuffer); @@ -18956,17 +18962,28 @@ namespace MobileGL::MG_Backend::DirectGLES { // TOP ROW FIRST: GL's top row (y = height - 1) lands in the image's first memory // row, which an AHardwareBuffer-backed renderbuffer addresses as y = 0. // - // ALWAYS THE WHOLE FRAME, whatever `region` says. A blit of only the damaged - // rectangles into the image's renderbuffer was measured to lose texels around - // them on a tiler: the driver does not load the image's other content into the - // pass that blits a part of it, and what it stores back there is stale tile memory - // (old frames' pixels around the copied rectangles). GL has no way to ask for the - // load, so the copy stays whole here; the region still narrows what the client - // damages on its surface, which is what the compositor repaints. - (void)region; - g_GLESFuncs.glBlitFramebuffer(0, 0, sourceWidth, sourceHeight, 0, static_cast(view.Height), - static_cast(view.Width), 0, GL_COLOR_BUFFER_BIT, - sameExtent ? GL_NEAREST : GL_LINEAR); + // Only where the image differs from the frame: the client keeps, per image, the + // damage it missed (BufferDamageTracker), and a partial blit keeps the rest of + // the renderbuffer as GL promises (checked texel for texel against the frame on + // Adreno). What once left old texels around the rectangles was the scissor + // guard above trusting another session's shadow: the copy was clipped, and + // the missed-damage bookkeeping then never brought those texels over again. + if (sameExtent && !wholeTarget && !region.IsFull()) { + // Each rectangle flipped the same way (an empty region copies nothing: the + // image already is the frame). + for (const MG_Util::Damage::FlippedCopy& copy : + MG_Util::Damage::FlippedCopies(region, sourceWidth, sourceHeight)) { + const MG_Util::Damage::Rect& from = copy.Source; + const MG_Util::Damage::Rect& to = copy.Destination; + g_GLESFuncs.glBlitFramebuffer(from.X, from.Y, from.X + from.Width, from.Y + from.Height, + to.X, to.Y + to.Height, to.X + to.Width, to.Y, + GL_COLOR_BUFFER_BIT, GL_NEAREST); + } + } else { + g_GLESFuncs.glBlitFramebuffer(0, 0, sourceWidth, sourceHeight, 0, + static_cast(view.Height), static_cast(view.Width), + 0, GL_COLOR_BUFFER_BIT, sameExtent ? GL_NEAREST : GL_LINEAR); + } const GLenum error = g_GLESFuncs.glGetError(); copied = error == GL_NO_ERROR; if (readBuffer != GL_BACK) g_GLESFuncs.glReadBuffer(static_cast(readBuffer)); diff --git a/MobileGL/MG_Test/Util/DamageTest.cpp b/MobileGL/MG_Test/Util/DamageTest.cpp index f23a82d5..de74f221 100644 --- a/MobileGL/MG_Test/Util/DamageTest.cpp +++ b/MobileGL/MG_Test/Util/DamageTest.cpp @@ -269,3 +269,61 @@ TEST(Damage, SwapchainAgeIsUnknownWhenContentWasNotKeptOrTheChainChanged) { EXPECT_EQ(ages.AgeOf(1), 0); EXPECT_EQ(ages.AgeOf(7), 0) << "an index the chain does not have"; } + +// A partial copy into a shared image: the frame's rows run bottom-up, the image's top-down. Run +// the blits the way glBlitFramebuffer does (a destination named with its Y bounds swapped takes +// the source rows in reverse) over a frame of known texels: every pixel of the region, and only +// those, must land where a whole flipped copy would have put it - edges included. +TEST(Damage, FlippedCopiesLandEachDamagedTexelWhereAWholeFlipWouldAndNothingElse) { + constexpr Int32 W = 37, H = 23; + const auto texel = [](Int32 x, Int32 y) { return y * 1000 + x; }; + const auto run = [&](const Region& region) { + Vector image(static_cast(W) * H, -1); + for (const FlippedCopy& copy : FlippedCopies(region, W, H)) { + const Rect& s = copy.Source; + const Rect& d = copy.Destination; + EXPECT_EQ(s.Width, d.Width); + EXPECT_EQ(s.Height, d.Height); + EXPECT_TRUE(Contains({0, 0, W, H}, s)); + EXPECT_TRUE(Contains({0, 0, W, H}, d)); + for (Int32 i = 0; i < d.Height; ++i) { + for (Int32 x = 0; x < d.Width; ++x) { + image[static_cast(d.Y + i) * W + d.X + x] = texel(s.X + x, s.Y + s.Height - 1 - i); + } + } + } + return image; + }; + const auto inRegion = [](const Region& region, Int32 x, Int32 y) { + if (region.IsFull()) return true; + for (const Rect& r : region.Rects()) { + if (x >= r.X && x < r.X + r.Width && y >= r.Y && y < r.Y + r.Height) return true; + } + return false; + }; + const Region cases[] = { + Of({{0, 0, 5, 1}}), // the bottom row + Of({{30, H - 1, 7, 1}}), // the top row, the right edge + Of({{3, 4, 6, 5}, {5, 6, 10, 10}}), // overlapping + Of({{-4, -4, 9, 9}, {W - 2, H - 3, 9, 9}}), // past the edges + Of({{0, 0, W, H}}), // all of it + Region::Full(), + }; + for (const Region& region : cases) { + Region clipped = region; + clipped.Normalize(W, H); + const Vector image = run(region); + for (Int32 row = 0; row < H; ++row) { + for (Int32 x = 0; x < W; ++x) { + const Int32 y = H - 1 - row; // the frame row that belongs in image row `row` + const Int32 got = image[static_cast(row) * W + x]; + if (inRegion(clipped, x, y)) { + EXPECT_EQ(got, texel(x, y)) << "frame texel " << x << "," << y << " misplaced"; + } else { + EXPECT_EQ(got, -1) << "image texel " << x << "," << row << " written outside the region"; + } + } + } + } + EXPECT_TRUE(FlippedCopies(Region{}, W, H).empty()) << "an empty region copies nothing"; +} diff --git a/MobileGL/MG_Util/Damage/Damage.h b/MobileGL/MG_Util/Damage/Damage.h index b49eb9cd..085051cc 100644 --- a/MobileGL/MG_Util/Damage/Damage.h +++ b/MobileGL/MG_Util/Damage/Damage.h @@ -283,6 +283,30 @@ namespace MobileGL::MG_Util::Damage { return extBufferAge || (partialUpdate && damageRegionFollows); } + // One blit of a copy from a frame whose rows run bottom-up (a GL default framebuffer) into an + // image of the same size addressed top row first (a shared image's renderbuffer): `Source` in + // the frame's coordinates, `Destination` the same pixels in the image's. The blit names the + // destination's Y bounds swapped (Y + Height, then Y), which is what turns the rows over. + struct FlippedCopy { + Rect Source; + Rect Destination; + }; + + // The blits that bring `region` (the frame's coordinates) of a `width` x `height` frame into + // the image: clipped to the frame, none for an empty region (the image already is the frame), + // and one whole-frame blit for a region that covers it. + inline Vector FlippedCopies(const Region& region, Int32 width, Int32 height) { + if (width <= 0 || height <= 0) return {}; + Region clipped = region; + clipped.Normalize(width, height); + const Rect whole{0, 0, width, height}; + if (clipped.IsFull()) return {FlippedCopy{whole, whole}}; + Vector copies; + copies.reserve(clipped.Rects().size()); + for (const Rect& rect : clipped.Rects()) copies.push_back({rect, FlipY(rect, height)}); + return copies; + } + // Per buffer of a rotating set: the damage each has missed since it was last written. class BufferDamageTracker { public: