iris: ScrollArea measures its content instead of drawing it twice

A container that probes a child's size was still doing it with a real
draw, so `Painter::measure` existed and almost nothing used it. Tracing
every draw of one streamed frame: 1,083 `Widget::draw` calls over 113
distinct widgets, the worst drawn 11 times at nesting depth 7-8, every
one of them mode `Draw` and none of them dirty. The cache was working --
each of the 11 was offered a genuinely different region, alternating
between an oversized probe box and a real one.

ScrollArea::draw was one of the two sources. It drew its whole content
at a box built from a stale hint, read the length back, corrected the
scroll position, and drew the content again where it belonged. The first
of those is now a measurement.

The other half is the measure fast path. A widget's reported size is a
function of its own state and the size it was offered, not of where it
was offered -- so an undirtied widget already drawn at a region of this
size has already answered, and `active.size` is the answer. This is the
same assumption `mov` makes one branch further down (same offered size,
therefore identical output, therefore a translation); it is only stated
as a size here rather than acted on as a move. Without it a measurement
costs a full recursive walk, which is what made the nesting compound.

A measurement now also peeks at the redraw mark instead of consuming it
-- it is not the redraw the mark asked for, and swallowing it would
leave the widget stale until something marked it again.

453 draws from 1,083, and the streamed frame is p50 1.18ms (from 1.22ms,
and 2.20ms before this run of work). The headless phone render is
byte-identical to the previous commit's on the real GPU.

What is deliberately NOT here: the same change to `Span::draw`'s phase 1,
which is the remaining 2x and which moves the layout by a few pixels.
The layout stays intact -- it is a position difference, not a broken
frame -- but which of the two is correct was not established, and the
suspicion (that phase 2 now takes `mov`, and `mov` accumulates deltas
where a redraw recomputes) points at a bug in `mov` rather than in
`Span`. Written up in docs/IRIS_TODO.md with Iris's target shape for
`Span`: no probe phase at all for `abs` children, and a `rest` child
forcing a reposition pass rather than a redraw.
This commit is contained in:
iris committed 2026-09-09 12:08:20 -04:00
1 parent 2540f6517c
commit 4b69f3cc6b
3 files changed
+55 -21

No files matched your search

+5 -1
View File
@@ -107,7 +107,11 @@ impl Widget for ScrollArea {
// has been measured: a zero-length region on the first frame would
// place the child's primitives against a box of no size.
let hint = self.content_len.unwrap_or(container_len);
let used = painter.widget_within(&self.inner, self.child_region(hint));
// A **measurement**: this asks how long the content is, and the
// box it asks about is built from a hint that the answer below is
// about to correct. Drawing it here painted the whole content at a
// provisional offset and then painted it again at the real one.
let used = painter.measure(&self.inner, self.child_region(hint));
// A child reporting `rel` means "this fraction of what I was
// offered", and what it was offered is this scroll area -- so the