Document per-axis retained layout

This commit is contained in:
iris-ai committed 2026-09-14 17:49:42 -04:00
1 parent 974d6c7a52
commit ecce4448f0
2 files changed
+43 -14

No files matched your search

+30 -5
View File
@@ -9,7 +9,7 @@ Canonical `main` is **`ca2b4b2`** (#17, the headless rig). Sixteen slices are
in.
**#18 `split/18-position-chain`** is open and finished apart from one decision:
worktree `/home/bob/repos/iris-pr18`, head `82fa6c1`, seventeen commits,
worktree `/home/bob/repos/iris-pr18`, head `b1b3eca`, eighteen commits,
workspace tests passing, fmt and clippy clean. It is LAYOUT.md §2's position chain, generalised
to boxes. `/home/bob/repos/ai-app-2` is on `rustify`, worktree clean.
@@ -217,9 +217,24 @@ hottest texts draw 32 times. Instrumented layout is about 5.5 ms instead of
60.79B instructions total (60.8M/frame), and 23.85B cycles. This is about 58%
fewer instructions than the roughly 138M/frame #18 path before coalescing,
but still about 1.8x the old 3.14 ms resize median. The remaining multiplier
is within one constraint traversal, so per-axis retained size validity is now
the useful next question. Re-sending the existing output size also no longer
starts a resize.
is within one constraint traversal. Re-sending the existing output size also
no longer starts a resize.
`b1b3eca` records pixel reads and retained size inputs per axis. `Text` says it
reads only its offered width, while `Scroll` says it reads only its scrolling
axis. `Span`, `Aligned`, and `Scroll` ask for an exact hint or a retained child
length valid under the box they are about to offer, and draw to measure only
when neither exists. On the same depth-8 load, resize now averages 449 widget
draws, 296 placement calls, 485 drawn-size reads, 12,164 primitive writes, and
107 text shapes. Cached uninstrumented 1,000-frame runs measured 4.955.24 ms
medians, 55.4B instructions total (55.4M/frame), and 21.521.6B cycles: about
9% fewer instructions than `82fa6c1`, but still about 1.6x the old 3.14 ms
median.
Pixel comparisons now use a 0.05 physical-pixel tolerance. The comparison is
against the last actual layout, not the preceding resize event or retained
move, so repeated subpixel changes accumulate and eventually redraw. The
generated cold-layout oracle uses the same visual tolerance per coordinate.
The generated tree now includes `Aligned` with every meaningful per-axis
alignment. That exposed two retained-layout ordering bugs which `84f589e`
@@ -227,7 +242,9 @@ fixes. The regular cold-layout equivalence suite and the ignored 100-seed
sweep pass. The latter previously stopped at seed 60 on `140.0` versus
`139.99996948242188`: exactly two `f32` ULPs from an equivalent composition
order, not a visible layout difference. The oracle now keeps draw presence
exact and allows at most four ULPs per pixel coordinate.
exact and allows at most 0.05 physical pixels per coordinate. Seed 98 is also
part of the ordinary suite because it catches stale retained size beneath an
aligned size-changing chain.
The branches are invariants rather than widget exceptions:
@@ -255,6 +272,14 @@ stale-size failure. Re-measuring the scroll subtree costs 11 draws rather than
1, but remains two orders of magnitude below the old 1,313 and follows the
actual dependency invariant.
A frame-local cache of several size answers under different constraint boxes
was also discarded. It reached about 3.5 ms in the instrumented resize rig,
but seed 98 showed that a valid answer is not enough: the child's one active
drawing may still realize another constraint, and a parent can then lay out
from an answer its final child placement does not realize. A correct version
needs an answer-plus-realization or verification protocol; adding dirtiness
conditions to the cache does not fix that invariant.
Do not restore the archive's old `DrawMode::Measure`. It skipped primitive and
retained-state writes while still walking widgets and shaping text, and once
improved a streamed-frame benchmark from 1.39 to 1.22 ms p50. Retained
+13 -9
View File
@@ -106,8 +106,11 @@ stands rather than new machinery:
and the primitive's stored `rel`/`abs` pair every frame, already, on the GPU.
A widget laid out purely in `rel`/`abs` terms is therefore already correct
after a resize with zero CPU work. Calls to `Painter::px_size` and
`Painter::output_size` mark the active widget as reading concrete pixels;
only those widgets become dirty when the output actually changes.
`Painter::output_size` mark both concrete-pixel axes; `px_len(axis)` and
`output_len(axis)` mark only the axis actually read. Only widgets whose read
axes changed by more than 0.05 physical pixels become dirty. The comparison
is against each widget's last actual draw, so smaller changes accumulate
rather than disappearing event by event.
All pixel-dependent leaves are marked before layout begins, along with every
chain of parents that read their sizes. Resize then settles the shallowest
@@ -145,18 +148,19 @@ An exact `size_hint` stops propagation when both axes still equal the retained
size. Otherwise propagation is deliberately conservative: the child may have
changed size, and only its dependent ancestors can assign the final boxes.
Unchanged descendants still take `draw_inner`'s retained skip-or-move path.
For a stacking container, retained child lengths are cached per axis: a child's
width remains reusable while the parent changes width, and its height remains
reusable while the parent changes height. A change on the orthogonal axis does
invalidate it in both directions. This is a generic constraint rule, not a
text exception; wrapped text is merely the common example of height depending
on width.
An active widget also retains which offered-box and output axes flowed into
the size it reported, directly or through a child size it read. A container
may use that answer for the same prospective box when every observed input is
still within 0.05 physical pixels; content dirtiness anywhere in its size
dependency subtree rejects the answer. This is a generic constraint rule, not
a text exception. Wrapped text is merely the common example: it reads width,
so changing only height leaves its answer valid.
### 4. Wrapped text, and "needs child height before choosing width"
**Wrapped text is not a special case any more; it already reads as one
draw.** `TextView::render` (`iris/src/widget/text/mod.rs:57-76`) already
does exactly what single-draw asks for: it reads `ctx.px_size().x` as the
does exactly what single-draw asks for: it reads `ctx.px_len(Axis::X)` as the
wrap width, shapes once, and memoizes the shaped layout keyed on that width
plus a changed-flag on the buffer and attrs (`:63-69`) — a second call with
the same width is a hash-map-style cache hit, not a re-shape. Under the new