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. in.
**#18 `split/18-position-chain`** is open and finished apart from one decision: **#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 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. 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% 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, 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 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 is within one constraint traversal. Re-sending the existing output size also
the useful next question. Re-sending the existing output size also no longer no longer starts a resize.
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 The generated tree now includes `Aligned` with every meaningful per-axis
alignment. That exposed two retained-layout ordering bugs which `84f589e` 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 sweep pass. The latter previously stopped at seed 60 on `140.0` versus
`139.99996948242188`: exactly two `f32` ULPs from an equivalent composition `139.99996948242188`: exactly two `f32` ULPs from an equivalent composition
order, not a visible layout difference. The oracle now keeps draw presence 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: 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 1, but remains two orders of magnitude below the old 1,313 and follows the
actual dependency invariant. 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 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 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 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. 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 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 after a resize with zero CPU work. Calls to `Painter::px_size` and
`Painter::output_size` mark the active widget as reading concrete pixels; `Painter::output_size` mark both concrete-pixel axes; `px_len(axis)` and
only those widgets become dirty when the output actually changes. `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 All pixel-dependent leaves are marked before layout begins, along with every
chain of parents that read their sizes. Resize then settles the shallowest 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 size. Otherwise propagation is deliberately conservative: the child may have
changed size, and only its dependent ancestors can assign the final boxes. changed size, and only its dependent ancestors can assign the final boxes.
Unchanged descendants still take `draw_inner`'s retained skip-or-move path. 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 An active widget also retains which offered-box and output axes flowed into
width remains reusable while the parent changes width, and its height remains the size it reported, directly or through a child size it read. A container
reusable while the parent changes height. A change on the orthogonal axis does may use that answer for the same prospective box when every observed input is
invalidate it in both directions. This is a generic constraint rule, not a still within 0.05 physical pixels; content dirtiness anywhere in its size
text exception; wrapped text is merely the common example of height depending dependency subtree rejects the answer. This is a generic constraint rule, not
on width. 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" ### 4. Wrapped text, and "needs child height before choosing width"
**Wrapped text is not a special case any more; it already reads as one **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 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 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 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 the same width is a hash-map-style cache hit, not a re-shape. Under the new