[Fix] (Espryt): copy only the damaged rectangles into shared images again

This commit is contained in:
2026-10-02 21:00:10 -04:00
parent 815217cc07
commit c822a9fef7
3 changed files with 110 additions and 11 deletions
+28 -11
View File
@@ -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<GLint>(view.Height),
static_cast<GLint>(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<GLint>(view.Height), static_cast<GLint>(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<GLenum>(readBuffer));
+58
View File
@@ -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<Int32> image(static_cast<SizeT>(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<SizeT>(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<Int32> 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<SizeT>(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";
}
+24
View File
@@ -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<FlippedCopy> 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<FlippedCopy> 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: