Document coalesced resize layout
This commit is contained in:
1 parent
986f98380d
commit
974d6c7a52
2 files changed
+59
-39
No files matched your search
@@ -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 `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
|
||||
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
|
||||
```
|
||||
|
||||
The first depth-8 resize run makes the amplification concrete. A 260-widget
|
||||
tree has 215 active widgets, but a resize averages 1,395 widget draws, 913
|
||||
`82fa6c1` adds targeted tracing to the same feature. Call
|
||||
`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
|
||||
12 distinct text widgets render, yet they render and reshape 282 times per
|
||||
frame with no shape-cache hits. Text rendering accounts for 11.6 of 13.2 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
|
||||
frames, below nested `Span`, `Aligned`, `Scroll`, `Pad`, and `SetSize`
|
||||
ancestors. This identifies repeated constraint discovery, rather than resize
|
||||
marking (0.001 ms), as the next subject; it does not yet choose between
|
||||
per-axis retained size validity and coalescing the resize dependency frontier.
|
||||
12 distinct text widgets rendered, yet they rendered and reshaped 282 times
|
||||
per frame with no shape-cache hits. Text rendering accounted for 11.6 of 13.2
|
||||
ms, including 9.9 ms shaping and 1.7 ms placing glyphs. The hottest two text
|
||||
widgets each drew 96 times below nested `Span`, `Aligned`, `Scroll`, `Pad`, and
|
||||
`SetSize` ancestors.
|
||||
|
||||
Tracing one of them showed that 96 was two multipliers, not 96 necessary
|
||||
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
|
||||
alignment. That exposed two retained-layout ordering bugs which `84f589e`
|
||||
fixes. The regular cold-layout equivalence suite passes. The ignored 100-seed
|
||||
sweep passed every transition through seed 59 and now gets past the former
|
||||
scroll failure at seed 52; at seed 60 it reaches the already-documented
|
||||
iterative wrapping-text defect, differing by 0.00003 px after `SwapForThree`.
|
||||
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.
|
||||
|
||||
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.
|
||||
- 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.
|
||||
During an output resize, the resize condition stays live until these updates
|
||||
finish, so an `Aligned` ancestor chooses the new child box before a
|
||||
pixel-dependent descendant draws in it.
|
||||
Output resize is the distinct invariant: all pixel-dependent leaves and
|
||||
their reader chains are marked together, then shared reader roots settle
|
||||
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,
|
||||
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
|
||||
|
||||
+13
-21
@@ -102,27 +102,19 @@ narrowings apply, and both are real, measured properties of the code as it
|
||||
stands rather than new machinery:
|
||||
|
||||
**(a) A window resize does not, by itself, require touching most widgets.**
|
||||
`shader.wgsl:105-106` recomputes every primitive's pixel position from
|
||||
`window.dim` and the primitive's stored `rel`/`abs` pair *every frame,
|
||||
already, on the GPU*. A widget laid out purely in `rel`/`abs` terms (no
|
||||
call to `px_size()`, `output_size()`, or anything else that reads a
|
||||
concrete pixel count) is therefore already correct after a resize with zero
|
||||
CPU work — the shader did it. `UiRenderState::needs_redraw_all`
|
||||
(`render_state.rs:229-231`) currently ignores this and redraws the entire
|
||||
tree on every `resized`, which was the safe default while sizing and
|
||||
drawing were two passes; it should be narrowed to only the widgets that
|
||||
*do* read a concrete pixel value. Track this the same way `needs_redraw`
|
||||
already tracks per-widget dirtiness (`Widgets::needs_redraw`,
|
||||
`core/src/widget/widgets.rs:9`): a widget's `draw` call marks itself
|
||||
pixel-dependent by calling through `Painter` methods that read
|
||||
`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.
|
||||
`shader.wgsl` recomputes every primitive's pixel position from `window.dim`
|
||||
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.
|
||||
|
||||
All pixel-dependent leaves are marked before layout begins, along with every
|
||||
chain of parents that read their sizes. Resize then settles the shallowest
|
||||
shared readers first, under the new output, so overlapping dependency paths
|
||||
are drawn once. Ordinary content changes use the opposite order: deepest
|
||||
dirty widgets first, with a changed returned size propagated one reader edge
|
||||
at a time. Re-reporting the current output size is a no-op.
|
||||
|
||||
**(b) A widget's `available` (its parent's offered region) can change
|
||||
without the widget's *content* changing — this is what
|
||||
|
||||
Reference in new issue
Block a user