mirror of
https://github.com/MrLowPr/overdrive.git
synced 2026-10-19 01:01:21 +02:00
183 lines
9.4 KiB
Diff
183 lines
9.4 KiB
Diff
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;
|
||
}
|
||
|