Files
MrLowPr--overdrive/ps5-react/patches/embeddedReact-damage-split.patch
2026-10-14 04:20:24 +02:00

183 lines
9.4 KiB
Diff
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
Embedded React: keep a scroll strip and a card apart in the damage set.
Modified from Embedded React (Apache-2.0, Copyright 2026 Cory Lamming) at the
revision pinned in dependencies.lock.json, after the patches listed before this
one there. Changed files: engine/scene/damage.c,
engine/scene/er_damage_internal.h and engine/tests/scene/test_damage_set.c.
Proposed upstream as pull request #309.
The damage set merged any two rects that overlapped or abutted into their
bounding box. During a scroll, the exposed full-width strip and a card or menu
overlapping it (a focus animation, a row entering) became the full width times
both heights: on the PS5 a 62 px scroll step repainted a 287 px band. A rect
now merges with a stored one only when their bounding box is at most a quarter
clean pixels (or a few thousand); otherwise only the input's parts outside the
stored rect are added, so the set stays disjoint without repainting the clean
span. The invariant is relaxed from disjoint and non-abutting to disjoint,
which is what the per-rect paint passes need. test_damage_set covers the split.
diff --git a/engine/scene/damage.c b/engine/scene/damage.c
index 5e341e7..20e6c5f 100644
--- a/engine/scene/damage.c
+++ b/engine/scene/damage.c
@@ -50,6 +50,30 @@ static uint32_t rect_area(const ERRect* r)
return (uint32_t)r->w * (uint32_t)r->h;
}
+/** @brief True when the two rects share at least one pixel (abutting does not count). */
+static bool overlaps(const ERRect* a, const ERRect* b)
+{
+ return a->x < b->x + b->w && b->x < a->x + a->w && a->y < b->y + b->h && b->y < a->y + a->h;
+}
+
+/**
+ * @brief Whether the bounding box of a ∪ b is almost all their own pixels: at most a quarter clean, or a
+ * few thousand clean pixels, which an extra paint pass would cost more than.
+ *
+ * A full-width scroll strip and a card or menu overlapping it fail this: their box is the full width
+ * times both heights, mostly pixels neither changed, so they stay apart.
+ */
+static bool cheap_union(const ERRect* a, const ERRect* b)
+{
+ ERRect u = *a;
+ bbox_union(&u, b);
+ int ix = (a->x + a->w < b->x + b->w ? a->x + a->w : b->x + b->w) - (a->x > b->x ? a->x : b->x);
+ int iy = (a->y + a->h < b->y + b->h ? a->y + a->h : b->y + b->h) - (a->y > b->y ? a->y : b->y);
+ const uint32_t shared = (ix > 0 && iy > 0) ? (uint32_t)ix * (uint32_t)iy : 0U;
+ const uint32_t waste = rect_area(&u) - rect_area(a) - rect_area(b) + shared;
+ return waste * 4U <= rect_area(&u) || waste <= 4096U;
+}
+
/**
* @brief Absorbs into @p acc every stored rect that overlaps or abuts it, cascading until stable.
*
@@ -65,7 +89,9 @@ static void absorb_touching(ERDamageSet* s, ERRect* acc)
removed_any = false;
for (uint8_t i = 0; i < s->count;)
{
- if (touch_or_overlap(acc, &s->r[i]))
+ /* An abutting rect stays apart when the box would be mostly clean; an overlapping one is always
+ * absorbed, which keeps the set disjoint. */
+ if (touch_or_overlap(acc, &s->r[i]) && (overlaps(acc, &s->r[i]) || cheap_union(acc, &s->r[i])))
{
bbox_union(acc, &s->r[i]);
s->r[i] = s->r[--s->count]; /* swap-remove; re-test the swapped-in rect at i */
@@ -127,7 +153,25 @@ void er_damage_set_add(ERDamageSet* s, int x, int y, int w, int h)
acc.w = w;
acc.h = h;
- /* Merge with everything the input touches (disjointness invariant), cascading. */
+ /* Overlapping a stored rect whose bounding box with the input would be mostly clean: add only the
+ * parts of the input outside it (up to four bands), each as an input of its own. */
+ for (uint8_t i = 0; i < s->count; i++)
+ {
+ const ERRect r = s->r[i];
+ if (!overlaps(&acc, &r) || cheap_union(&acc, &r))
+ continue;
+ const int ix0 = acc.x > r.x ? acc.x : r.x;
+ const int iy0 = acc.y > r.y ? acc.y : r.y;
+ const int ix1 = (acc.x + acc.w < r.x + r.w) ? acc.x + acc.w : r.x + r.w;
+ const int iy1 = (acc.y + acc.h < r.y + r.h) ? acc.y + acc.h : r.y + r.h;
+ er_damage_set_add(s, acc.x, acc.y, acc.w, iy0 - acc.y); /* above */
+ er_damage_set_add(s, acc.x, iy1, acc.w, acc.y + acc.h - iy1); /* below */
+ er_damage_set_add(s, acc.x, iy0, ix0 - acc.x, iy1 - iy0); /* left */
+ er_damage_set_add(s, ix1, iy0, acc.x + acc.w - ix1, iy1 - iy0); /* right */
+ return;
+ }
+
+ /* Merge with what the input touches (disjointness invariant), cascading. */
absorb_touching(s, &acc);
if (s->count < (uint8_t)ER_DAMAGE_RECTS_MAX)
diff --git a/engine/scene/er_damage_internal.h b/engine/scene/er_damage_internal.h
index 744b545..62e7398 100644
--- a/engine/scene/er_damage_internal.h
+++ b/engine/scene/er_damage_internal.h
@@ -26,11 +26,13 @@
* ER_DAMAGE_RECTS_MAX rects instead, letting the compositor repaint (and a backend flush) each
* changed area on its own.
*
- * Invariant: the stored rects are PAIRWISE DISJOINT and non-abutting. This is load-bearing, not an
- * optimisation — the compositor paints each rect in a separate clipped pass, and a pixel covered by
- * two passes would have translucent content blended into it twice (visibly darker). er_damage_set_add
- * therefore merges any overlapping-or-touching input into the existing rects (cascading, since a
- * merge can grow a rect into contact with another) rather than storing it alongside them.
+ * Invariant: the stored rects are PAIRWISE DISJOINT. This is load-bearing, not an optimisation — the
+ * compositor paints each rect in a separate clipped pass, and a pixel covered by two passes would
+ * have translucent content blended into it twice (visibly darker). er_damage_set_add merges an input
+ * into a rect it overlaps or abuts when their bounding box is mostly their own pixels (cascading,
+ * since a merge can grow a rect into contact with another). When the box would be mostly clean — a
+ * full-width scroll strip and a card overlapping one end of it — it adds only the input's parts
+ * outside that rect instead, so neither drags the other's empty span into the repaint.
*
* When the set is full, the incoming rect is merged with whichever stored rect wastes the least area
* (union area minus the two parts) — so pathological scattered damage degrades gracefully toward the
@@ -50,7 +52,7 @@
* er_get_dirty_rects(), whose contract it caps. */
/**
- * @brief A small set of pairwise-disjoint, non-abutting dirty rects.
+ * @brief A small set of pairwise-disjoint dirty rects.
*
* Zero-initialisation is a valid empty set.
*/
diff --git a/engine/tests/scene/test_damage_set.c b/engine/tests/scene/test_damage_set.c
index 57dc2fb..df066fe 100644
--- a/engine/tests/scene/test_damage_set.c
+++ b/engine/tests/scene/test_damage_set.c
@@ -18,8 +18,8 @@
* ERDamageSet (er_damage_internal.h): the disjoint dirty-rect set behind the compositor's damage
* tracking. The properties that matter:
*
- * - disjointness: no two stored rects overlap or abut (a pixel covered twice would double-blend
- * translucent content when the compositor paints each rect in its own pass),
+ * - disjointness: no two stored rects overlap (a pixel covered twice would double-blend translucent
+ * content when the compositor paints each rect in its own pass),
* - coverage: every added rect is covered by the stored rects, including through cascade merges
* and pool saturation (coverage may grow — never shrink),
* - graceful saturation: MAX+1 scattered rects still fit by merging the least-wasteful pair.
@@ -49,7 +49,13 @@ static bool rects_touch(const ERRect* a, const ERRect* b)
return !(a->x + a->w < b->x || b->x + b->w < a->x || a->y + a->h < b->y || b->y + b->h < a->y);
}
-/** @brief Asserts the set's core invariant: pairwise disjoint AND non-abutting. */
+/** @brief True when the two rects share a pixel. */
+static bool rects_overlap(const ERRect* a, const ERRect* b)
+{
+ return a->x < b->x + b->w && b->x < a->x + a->w && a->y < b->y + b->h && b->y < a->y + a->h;
+}
+
+/** @brief Asserts the set's core invariant: pairwise disjoint. */
static bool check_disjoint(const ERDamageSet* s)
{
for (uint8_t i = 0; i < s->count; i++)
@@ -60,7 +66,7 @@ static bool check_disjoint(const ERDamageSet* s)
}
for (uint8_t j = (uint8_t)(i + 1U); j < s->count; j++)
{
- if (rects_touch(&s->r[i], &s->r[j]))
+ if (rects_overlap(&s->r[i], &s->r[j]))
{
return false;
}
@@ -192,7 +198,18 @@ static int check_basics(void)
if (s.count != 0U)
return fail("clear did not empty the set");
- printf("PASS: basics — empty ignored, disjoint kept apart, overlap/abut merged\n");
+ /* A full-width strip and a narrow rect overlapping its top stay apart: the strip keeps its rect and
+ * only the part of the narrow one above it is added, so no clean span is repainted. */
+ history_reset();
+ if (!add_checked(&s, 0, 150, 200, 20))
+ return fail("invariant after strip add");
+ if (!add_checked(&s, 10, 60, 20, 100))
+ return fail("invariant after card overlapping the strip");
+ if (s.count != 2U || er_damage_set_area(&s) != 200U * 20U + 20U * 90U)
+ return fail("strip and overlapping card were merged into their bounding box");
+ er_damage_set_clear(&s);
+
+ printf("PASS: basics — empty ignored, disjoint kept apart, cheap overlap/abut merged, costly split\n");
return EXIT_SUCCESS;
}