Document coalesced resize layout

This commit is contained in:
iris-ai committed 2026-09-14 17:06:41 -04:00
1 parent 986f98380d
commit 974d6c7a52
2 files changed
+59 -39

No files matched your search

+46 -18
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 `480f0bc`, sixteen commits, worktree `/home/bob/repos/iris-pr18`, head `82fa6c1`, seventeen 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.
@@ -183,24 +183,51 @@ IRIS_PHASE=resize IRIS_DEPTH=8 IRIS_FRAMES=1000 \
--test layout_diagnostics -- --ignored --nocapture --test layout_diagnostics -- --ignored --nocapture
``` ```
The first depth-8 resize run makes the amplification concrete. A 260-widget `82fa6c1` adds targeted tracing to the same feature. Call
tree has 215 active widgets, but a resize averages 1,395 widget draws, 913 `layout_diagnostics::trace_widget(id)` before a frame; `take().traces()` then
returns the selected widgets' ordered draw requests and pixel boxes, reuse
outcomes, placements, hints, size reads and reported sizes, and text widths.
The selection is a set, survives `take()`, and is removed with
`untrace_widget` or `clear_traced_widgets`. This replaces temporary
text-specific logging without imposing anything on normal builds.
The first depth-8 resize run made the amplification concrete. A 260-widget
tree has 215 active widgets, but a resize averaged 1,395 widget draws, 913
placement calls, 1,313 reads of drawn sizes and 34,844 primitive writes. Only placement calls, 1,313 reads of drawn sizes and 34,844 primitive writes. Only
12 distinct text widgets render, yet they render and reshape 282 times per 12 distinct text widgets rendered, yet they rendered and reshaped 282 times
frame with no shape-cache hits. Text rendering accounts for 11.6 of 13.2 ms, per frame with no shape-cache hits. Text rendering accounted for 11.6 of 13.2
including 9.9 ms shaping and 1.7 ms placing glyphs. The hottest two text ms, including 9.9 ms shaping and 1.7 ms placing glyphs. The hottest two text
widgets each draw 96 times per frame at 22 distinct widths across two resize widgets each drew 96 times below nested `Span`, `Aligned`, `Scroll`, `Pad`, and
frames, below nested `Span`, `Aligned`, `Scroll`, `Pad`, and `SetSize` `SetSize` ancestors.
ancestors. This identifies repeated constraint discovery, rather than resize
marking (0.001 ms), as the next subject; it does not yet choose between Tracing one of them showed that 96 was two multipliers, not 96 necessary
per-axis retained size validity and coalescing the resize dependency frontier. layout iterations. One traversal of the nested positioning chain shaped it 32
times at 12 widths; three resize-dependent descendants then caused that whole
traversal to run three times through the same highest size reader. Resize
marking had queued each pixel reader independently, and each leaf discovered
and redrew the same reader path in turn.
`82fa6c1` coalesces that frontier by marking every resize-dependent leaf and
its size-reader chain first, then settling the shallowest shared reader once.
Ordinary content and size changes remain deepest-first. On the same depth-8
load, an actual resize now averages about 694 widget draws, 435 placement
calls, 649 drawn-size reads, 15,343 primitive writes, and 116 text shapes; the
hottest texts draw 32 times. Instrumented layout is about 5.5 ms instead of
13.2 ms. An uninstrumented 1,000-frame resize run measured a 5.61 ms median,
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.
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`
fixes. The regular cold-layout equivalence suite passes. The ignored 100-seed fixes. The regular cold-layout equivalence suite and the ignored 100-seed
sweep passed every transition through seed 59 and now gets past the former sweep pass. The latter previously stopped at seed 60 on `140.0` versus
scroll failure at seed 52; at seed 60 it reaches the already-documented `139.99996948242188`: exactly two `f32` ULPs from an equivalent composition
iterative wrapping-text defect, differing by 0.00003 px after `SwapForThree`. order, not a visible layout difference. The oracle now keeps draw presence
exact and allows at most four ULPs per pixel coordinate.
The branches are invariants rather than widget exceptions: The branches are invariants rather than widget exceptions:
@@ -211,9 +238,10 @@ The branches are invariants rather than widget exceptions:
condition the discarded `/tmp/escalate.patch` missed on resize. condition the discarded `/tmp/escalate.patch` missed on resize.
- Dirty widgets settle deepest-first. A changed size queues only its immediate - Dirty widgets settle deepest-first. A changed size queues only its immediate
reader; propagation stops as soon as a reader's own answer stays unchanged. reader; propagation stops as soon as a reader's own answer stays unchanged.
During an output resize, the resize condition stays live until these updates Output resize is the distinct invariant: all pixel-dependent leaves and
finish, so an `Aligned` ancestor chooses the new child box before a their reader chains are marked together, then shared reader roots settle
pixel-dependent descendant draws in it. shallowest-first under the new output. This both chooses the parent box
before drawing its descendant and coalesces overlapping resize paths.
- A span measures an unknown child in the part of its axis still available, - A span measures an unknown child in the part of its axis still available,
rather than giving every child the whole container and immediately taking rather than giving every child the whole container and immediately taking
most of it away. This is the archive's faster shape, recreated on the current most of it away. This is the archive's faster shape, recreated on the current
+13 -21
View File
@@ -102,27 +102,19 @@ narrowings apply, and both are real, measured properties of the code as it
stands rather than new machinery: stands rather than new machinery:
**(a) A window resize does not, by itself, require touching most widgets.** **(a) A window resize does not, by itself, require touching most widgets.**
`shader.wgsl:105-106` recomputes every primitive's pixel position from `shader.wgsl` recomputes every primitive's pixel position from `window.dim`
`window.dim` and the primitive's stored `rel`/`abs` pair *every frame, and the primitive's stored `rel`/`abs` pair every frame, already, on the GPU.
already, on the GPU*. A widget laid out purely in `rel`/`abs` terms (no A widget laid out purely in `rel`/`abs` terms is therefore already correct
call to `px_size()`, `output_size()`, or anything else that reads a after a resize with zero CPU work. Calls to `Painter::px_size` and
concrete pixel count) is therefore already correct after a resize with zero `Painter::output_size` mark the active widget as reading concrete pixels;
CPU work — the shader did it. `UiRenderState::needs_redraw_all` only those widgets become dirty when the output actually changes.
(`render_state.rs:229-231`) currently ignores this and redraws the entire
tree on every `resized`, which was the safe default while sizing and All pixel-dependent leaves are marked before layout begins, along with every
drawing were two passes; it should be narrowed to only the widgets that chain of parents that read their sizes. Resize then settles the shallowest
*do* read a concrete pixel value. Track this the same way `needs_redraw` shared readers first, under the new output, so overlapping dependency paths
already tracks per-widget dirtiness (`Widgets::needs_redraw`, are drawn once. Ordinary content changes use the opposite order: deepest
`core/src/widget/widgets.rs:9`): a widget's `draw` call marks itself dirty widgets first, with a changed returned size propagated one reader edge
pixel-dependent by calling through `Painter` methods that read at a time. Re-reporting the current output size is a no-op.
`output_size`/`px_size` (both already funnel through `Painter`, so the
marking is one line at each), and `resize()` (`render_state.rs:32-35`)
walks only that set instead of unconditionally setting `resized = true`
for a full `redraw_all`. This turns "every resize redraws everything" into
"every resize redraws what depends on pixels" — a real behavior change
beyond what was asked, so verify it against the I0b `pre_present_notify`
resize regression (that fix depended on `redraw_all`'s completeness)
before narrowing this.
**(b) A widget's `available` (its parent's offered region) can change **(b) A widget's `available` (its parent's offered region) can change
without the widget's *content* changing — this is what without the widget's *content* changing — this is what