Land the layout repair's findings and retire the working log
`docs/LAYOUT_LOG.md` existed until the one-ask protocol landed. It has, as Iris PR #19 replacing #18, so what has to outlive the log moves into `docs/LAYOUT.md`: the hint rule and the one ask a span makes, the reported shares that are left and the two-answers design that would remove them, what the fuzzers reach and what they cannot, the hypotheses not to repeat, and the counter and dump commands with the numbers they print now. The handoff becomes what it is for -- where the work stands, which is waiting on a review.
This commit is contained in:
1 parent
e4fada298d
commit
3ecd676bcf
3 files changed
+191
-1183
No files matched your search
+124
-8
@@ -4,12 +4,13 @@ A widget draws once and records its size on the `Painter`. Reading a child
|
||||
`DrawResult::size()` records a retained size dependency; drawing the child
|
||||
without reading that result does not make the parent's size depend on it.
|
||||
|
||||
§1, §2 and §3 have landed in Iris (#16 and #18). §4 to §6 and the density
|
||||
section retain the rationale of the design but still name types that have
|
||||
since been replaced; they are not an API reference. `docs/HANDOFF.md` is the
|
||||
current transparent-frames work and its checks; `docs/LAYOUT_LOG.md` is what
|
||||
the sessions doing that work found, kept until it lands. The sections at the
|
||||
end of this file are durable design moved out of the handoff on 2026-09-18.
|
||||
§1 landed in Iris as #16. §2 and §3 were #18, which is closed: Iris PR #19
|
||||
carries those commits whole plus the retained-layout repair built on them. §4 to §6 and the density section
|
||||
retain the rationale of the design but still name types that have since been
|
||||
replaced; they are not an API reference. `docs/HANDOFF.md` is where the work
|
||||
in flight stands. The sections from "Frames, decided boxes and padding"
|
||||
onwards are the settled design, the findings that outlived the working log,
|
||||
and the measurement method.
|
||||
|
||||
## Design
|
||||
|
||||
@@ -333,7 +334,38 @@ that box by re-expressing the drawing, and nothing is drawn again in a box an
|
||||
answer chose. `Holds` is a contract about the ask box alone, read only to
|
||||
decide whether a re-ask can be skipped. A container that puts an answer
|
||||
somewhere other than where it asked says so with `Painter::place_at`, which
|
||||
never runs the body. This is `wip/one-ask` in the experiment checkout.
|
||||
never runs the body.
|
||||
|
||||
The answer box is never a question, so no contract can be demanded of a
|
||||
widget there: a `Text` asked at 45 px whose longest word is 89.5 px cannot
|
||||
promise its drawing holds for the box its own answer chose. An earlier plan
|
||||
asserted exactly that and was unsatisfiable.
|
||||
|
||||
A container that can learn a child's length without drawing it does.
|
||||
`Painter::size_hint` answers from the child's rule, or from
|
||||
`Widget::size_hint` where it implements one, resolved against the asking
|
||||
widget's frame; `Scroll`, `Masked` and a `Stack` without a sizing child hint
|
||||
`LEFTOVER`, since each always reports the whole of its box. `Span` takes each
|
||||
child's length from its hint where there is one and asks that child exactly
|
||||
once, in the slot it decided; a child with no hint is drawn in the room left
|
||||
from the cursor, because a text has to wrap at the width actually there, and
|
||||
its drawing is moved to its slot with `place_at`. Reading a hint records a
|
||||
size dependency and pins the frame where the hint declared a fraction, the
|
||||
same pin a rule that is a fraction takes.
|
||||
|
||||
**What that leaves, and the design that would remove it.** A widget that
|
||||
reports a share its children gave it -- a span whose children report
|
||||
`leftover`, a stack sized by such a child, a wrapper round either -- can have
|
||||
no hint, so its parent asks it in the room and again in its slot, and both
|
||||
asks draw, since the room drawing divided the room. Under a resize that
|
||||
multiplies down the tree: at seed 1, depth 8, the hottest widget in the
|
||||
resize frame is a `Span` drawn 8 times. Removing it means two answers on one
|
||||
record -- a room answer from an explicit measuring ask (`Painter::measure`,
|
||||
so which ask is the measurement is stated rather than inferred) and a slot
|
||||
answer from the placing ask, with the drawing belonging to the placing one --
|
||||
plus a local redraw that re-asks both questions and marks the parent if
|
||||
either answer moved, which also retires `re_asked`'s deferral. Do it only
|
||||
once an app screen shows the cost, and measure that screen first.
|
||||
|
||||
**There is one coordinate unit, the window** (`1512d84`, `23523ee`). Every box in the
|
||||
tree is a region in window units; a widget's frame is a *length* in the same
|
||||
@@ -489,14 +521,68 @@ of its parent's frame and a local redraw walked back up the parent chain in
|
||||
- Alignment is one value per axis and defaults to the middle because neither
|
||||
edge is neutral without a direction. One widget has one length per axis; a
|
||||
second length requires a second widget through `.wrapper()`.
|
||||
- No measurement is a different state from a measured zero, so
|
||||
`ActiveData::answer` is an `Option` rather than a zero size.
|
||||
|
||||
### What the fuzzers tolerate
|
||||
### What the fuzzers tolerate, and what they reach
|
||||
|
||||
Warm and cold pixel regions must compare exactly; there is no step
|
||||
allowance. When a row's grid-step count is not divisible by the number of
|
||||
children, individual share widths differ, but every rerun of that layout
|
||||
must still agree exactly.
|
||||
|
||||
- The routine runs are ten seeds (fast oracle), 400 at depth 5 through the
|
||||
shrinker, and 1000 at depth 6. The 2000-seed depth-4 scan over every case
|
||||
is what found seeds that had failed on every commit for a week, so run it
|
||||
after any layout change. `Rng::new` is `seed | 1`, so an even seed and the
|
||||
odd one above it are one tree.
|
||||
- The shrinker panics per thread at the first failing seed, so a run reports
|
||||
at most one seed per chunk of about 58. Check a single seed against the
|
||||
unpatched code before calling it new.
|
||||
- Depth finds things and so does breadth; widen one axis at a time and
|
||||
record which.
|
||||
- If a seed fails once and never again, suspect the rig's determinism before
|
||||
believing an edit fixed it: seed 30 at depth 5 did exactly that on
|
||||
2026-09-19 and reverting each part of the change did not bring it back.
|
||||
- The cases are what a change is *then* compared at. Until the `size-resize`
|
||||
case was added, every one of them compared at the window the change was
|
||||
made in, and a length kept as a fraction of the wrong box agrees there and
|
||||
parts from cold at every other window. There is still no case that changes
|
||||
a tree twice with a resize between, and none that resizes twice.
|
||||
- The fuzzer grows no `rel` rules and never re-parents a widget, so neither
|
||||
a fraction resolved against a frame nor a subtree changing hands is
|
||||
covered by any generated tree. Both have cost a defect that only a
|
||||
hand-written expectation or a render caught.
|
||||
|
||||
### Failed hypotheses worth not repeating
|
||||
|
||||
- **"Which draw is the measurement" cannot be defined.** Four bookkeeping
|
||||
rules and two experiments each fixed some seeds and broke others. The bit
|
||||
existed only because a widget was drawn twice; one draw leaves it nothing
|
||||
to name. A plan that proposes bookkeeping for a distinction should first
|
||||
ask whether the distinction has to exist.
|
||||
- **A frame narrowed by a region does not move when its part moves.** A
|
||||
narrowed frame must be a *length*, put back into the part on every
|
||||
placement, exactly as a declared rule already is.
|
||||
- **Choosing between a fixed and a relative child in pixels at the span's
|
||||
current width** admits multiple self-sizing fixed points; seed 13 settles
|
||||
differently warm and cold under it. The same circularity is what a cap
|
||||
containing `leftover` would put into `SizeRule::Max`.
|
||||
- **A tolerant endpoint on the span's leftover split** retains zero-height
|
||||
children (seed 16). The boundary is derived through the inverse of the
|
||||
expression that places children, and is pinned by
|
||||
`unsettled::a_box_that_only_rounds_past_its_fixed_children_leaves_nothing_over`.
|
||||
- **Not reading a span's own length where no slot depends on it** saves
|
||||
nothing (`wip/inset`, measured 2026-09-19): counters identical at every
|
||||
phase. A symbolic pin survives a resize because every ask box is
|
||||
symbolically stable across one, so the pin only fails where an answer
|
||||
changed, which is a real relayout.
|
||||
- **Resolving a rule that is a fraction of the frame from the record instead
|
||||
of redrawing** recovers nothing: `reuse outside: a frame length` is 3 of
|
||||
264 cold draws at seed 1 and 1 of 758 at seed 13.
|
||||
- **A `git bisect`** once named a commit that could not be the cause; read
|
||||
the tree rather than the bisect when that happens.
|
||||
|
||||
### Rendering the grid (pending)
|
||||
|
||||
`snap_floor` in `prelude.wgsl` adds half a layout step before flooring,
|
||||
@@ -515,6 +601,36 @@ inlines.
|
||||
|
||||
## Measuring layout cost on this machine
|
||||
|
||||
- **Counters, at two seeds, before anything else.**
|
||||
|
||||
IRIS_SEED=1 IRIS_DEPTH=8 cargo test --release --features layout-diagnostics \
|
||||
--test layout_diagnostics -- --ignored --nocapture layout_cost
|
||||
|
||||
reports each phase's `widget draws` and `distinct widgets`, and
|
||||
`IRIS_PHASE=resize IRIS_FRAMES=2` names who is drawn how often. Where the
|
||||
one-ask protocol stands, draws / distinct at depth 8, beside #18:
|
||||
|
||||
| seed 1 | #18 | now | | seed 13 | #18 | now |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| cold | 369/261 | 264/232 | | cold | 1330/707 | 758/627 |
|
||||
| many | 157/95 | 41/41 | | many | 524/159 | 16/16 |
|
||||
| size | 16/12 | 3/3 | | resize | nothing | nothing |
|
||||
| scroll | 2 | 1 | | | | |
|
||||
| resize | 13/13 | 36/13 | | | | |
|
||||
|
||||
`many`, `size` and `scroll` are at their floor: every draw is a marked
|
||||
widget, or the parent a marked widget deferred to. `resize` at seed 1 and
|
||||
cold's draws over its distinct count are the reported-share cost above.
|
||||
|
||||
- **A change that could move cold layout gets the dump diff**, which the
|
||||
warm/cold oracle cannot replace -- both of its sides move together:
|
||||
|
||||
IRIS_DUMP_SEEDS=400 IRIS_DUMP_DEPTH=5 cargo test --release \
|
||||
--test layout_dump -- --ignored --nocapture | grep -E '^[0-9]+ [0-9]+ '
|
||||
|
||||
once at each commit, then `diff`. Zero differing lines of 34,488 is the
|
||||
expectation for a cost change.
|
||||
|
||||
- **Check the work counters before comparing two commits' times.**
|
||||
`tests/layout_diagnostics.rs` prints drawn widgets, widget draws and
|
||||
primitive writes; a comparison is only worth reading when they match.
|
||||
|
||||
Reference in new issue
Block a user