Finish the transparent-frames handoff
This commit is contained in:
1 parent
bdddb610c0
commit
280fad7472
3 files changed
+781
-1617
No files matched your search
+315
-1595
File diff suppressed because it is too large.
Load diff
+217
-22
@@ -4,11 +4,12 @@ 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) and the notes below have been
|
||||
brought to what shipped rather than what was proposed; §4 to §6 describe the
|
||||
same design as it stands, and name types that have since been replaced where
|
||||
they were written before it. `docs/HANDOFF.md` has the invariants
|
||||
the code now rests on and what is still to do.
|
||||
§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.
|
||||
|
||||
## Design
|
||||
|
||||
@@ -143,14 +144,14 @@ Drawing that ancestor consumes the marks of every dirty descendant it
|
||||
reaches; the loop then takes whatever remains. Drawing never synchronously
|
||||
invalidates or invokes a parent, so there is no layout recursion.
|
||||
|
||||
Dirty widgets settle deepest-first, and `dirty_size_under` stops a reader
|
||||
taking a retained answer while something below that answer is still dirty --
|
||||
an optimisation against laying out twice rather than a second validity
|
||||
mechanism. An exact `size_hint` stops propagation when both axes still equal
|
||||
the retained size; otherwise propagation is deliberately conservative, since
|
||||
only a dependent ancestor can assign the final boxes. 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.
|
||||
Dirty widgets settle deepest-first. `dirty_size_under` has been deleted;
|
||||
settling consumes descendant marks bottom-up, so no clean retained answer can
|
||||
hide an unsettled size dependency. An exact `size_hint` stops propagation when
|
||||
both axes still equal the retained size; otherwise propagation is deliberately
|
||||
conservative, since only a dependent ancestor can assign the final boxes.
|
||||
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"
|
||||
|
||||
@@ -312,14 +313,208 @@ fragment stage cannot make. Rendering and hit-testing both traverse the full
|
||||
mask chain and use the same rounded-rectangle coverage; `iris/tests/mask_sdf.rs`
|
||||
checks the WGSL implementation against the CPU SDF.
|
||||
|
||||
## Offered boxes
|
||||
## Frames, decided boxes and padding
|
||||
|
||||
`Pad` must work in every container: it offers an inset region to its child and
|
||||
reports the child's used size plus padding. In a generous parent it behaves as
|
||||
an inset; in a tight parent it grows the result outward.
|
||||
Containers that only divide room are transparent to fractions. A child frame
|
||||
is narrowed by a length its parent decided: a declared `px` or `rel` length,
|
||||
or the resolved slot of a `leftover` child. A box a widget reports for itself
|
||||
does not narrow its descendants' frame.
|
||||
|
||||
When a widget does not fit its offered box, it is redrawn at the box implied by
|
||||
its reported size in the same frame. Deferring would leave ordinary
|
||||
`.background(rect(..))` surfaces one frame behind their content. The settling
|
||||
draw occurs only when the widget's own size changes. Widgets whose size varies
|
||||
with every offered box are therefore unsuitable as `LazySpan` rows.
|
||||
`Pad` is an outset: it forwards its frame less the padding, draws the child
|
||||
inside that area, and reports the child's used size plus padding. A
|
||||
`rel(1.0)` child inside padding inside a share is a fraction of the resolved
|
||||
share less that padding. The mixed "outset pixels, inset fractions and
|
||||
shares" interpretation is rejected.
|
||||
|
||||
The current experiment still redraws some widgets in boxes derived from their
|
||||
own answers. That is the open protocol defect, not a design invariant. The
|
||||
target in `docs/HANDOFF.md` evaluates container bodies only in boxes a parent
|
||||
offered or decided; placing an answer reuses or translates its drawing rather
|
||||
than running the body in an answer-derived box.
|
||||
|
||||
## Layout decisions and invariants (2026-09-15 to 2026-09-17)
|
||||
|
||||
Moved here from the handoff on 2026-09-18. These are settled unless a
|
||||
subsection explicitly says it is pending.
|
||||
|
||||
### Fixed point
|
||||
|
||||
Decided with Bryan on 2026-09-15. Layout decides on a grid rather than in
|
||||
floats.
|
||||
|
||||
- **`Fixed<SHIFT>` is an `i32` counting `1 / 2^SHIFT`.** Adding and
|
||||
subtracting are exact; `mul` drops to the step below (Bryan, 2026-09-16:
|
||||
truncation is preferable); `div`, `div_int` and `ratio` round to nearest;
|
||||
`to_scale` takes the nearest step. Two routes to one place that land on
|
||||
one number are the same place, so everything downstream compares for
|
||||
equality.
|
||||
- **`Px` is `1/1024` px, `Rel` is `1/2^24` of a box, `Weight` is `1/65536`
|
||||
of a share.** `PX_SHIFT` and `REL_SHIFT` are the only statement of the
|
||||
first two; the shader's copy is prepended from them by
|
||||
`render::module_source`. `Px` was `1/64` first, where one rounding's
|
||||
residue was 0.016 px and enough to move a box. Range is +/-2.1M px and
|
||||
conversion to `f32` is exact to 16,384 px.
|
||||
- A weight is not a fraction: a list divides its room by the total of its
|
||||
weights, and `Rel::ratio` turns two weights into a share on the finer
|
||||
grid.
|
||||
- **Arithmetic wraps** (`4febabf`, Bryan: a coordinate past the range will
|
||||
not draw reasonably anyway, so wrap and break clearly). Saturating cost a
|
||||
twelfth of layout's instructions. `MIN` and `MAX` stand in for an
|
||||
unbounded end and are only ever compared against; `from_f32` is the one
|
||||
operation that clamps, and `Holds` keeps a saturating `narrow`.
|
||||
- A pointer, a wheel notch, a shaped glyph advance and a window size arrive
|
||||
as floats and go on the grid where they arrive. `Vec2` is what the GPU
|
||||
and the platform speak; `PxVec2` is what layout decides in.
|
||||
- **Do not widen the grid to chase a residue.** Every failure seen was one
|
||||
value reached by two expressions, sitting on a boundary defined by the
|
||||
same value coming back the other way. No precision shrinks a residue that
|
||||
is the whole distance.
|
||||
- **A value that comes back as a box is rounded away from the measurement,
|
||||
not to the nearest step.** `Fixed::ceil_from_f32` exists for that and is
|
||||
the only rounding on the grid that is not to nearest. Rounding to nearest
|
||||
is right for a value being carried and wrong for a bound; a text reporting
|
||||
`ceil` of its longest line is what keeps the box it is handed back one its
|
||||
line fits in (`4bd8607`).
|
||||
- **A structural decision may not be taken on a hair's breadth.** A
|
||||
boundary that decides which children exist (a span's leftover split) is
|
||||
derived through the inverse of the expression that draws, never by a
|
||||
second expression for the same length: `mul` floors while `div` rounds,
|
||||
so a boundary derived with a division guards a drawing made with a
|
||||
multiply (`53b00c6`).
|
||||
|
||||
### A box in pixels is one multiply from its parent's
|
||||
|
||||
A draw threads pixel lengths down: the box a parent gave a widget, then the
|
||||
part of that box its own answer placed its drawing in. `Painter::px_size`
|
||||
and `px_len` read that value, and a local redraw takes the same steps back
|
||||
up the parent chain (`asked_px`). Neither chain has a coordinate frame in it,
|
||||
so a region node cannot break either, and warm and cold reach every length
|
||||
by the same expression.
|
||||
|
||||
- **`Holds::through` is the exact preimage of `px + floor(rel * box)`**:
|
||||
`floor(rel * B) >= lo - px` is `rel * B >= (lo - px) << REL` and
|
||||
`floor(rel * B) <= hi - px` is `rel * B < (hi - px + 1) << REL`, two
|
||||
`div_toward`s once the sign of `rel` has said which bound is which. The
|
||||
answer is an interval even for a single length, because a floor is not
|
||||
invertible. The range has to contain the box a drawing was made in (the
|
||||
`Holds` assertion in `draw_at`, debug only) and must not contain a box
|
||||
the drawing does not hold for (the oracle); being the preimage makes
|
||||
those one statement rather than a trade-off.
|
||||
- **Symbolic regions are for the GPU, hit testing and remaps alone.**
|
||||
`Moves::resolve` is the only walk left and it is the vertex shader's.
|
||||
Nothing layout decides is composed back up the move chain.
|
||||
- **`px` is not stored on `ActiveData`, deliberately.** A resize every
|
||||
widget's `Holds` admits redraws nothing, so a stored pixel length would
|
||||
be stale on every widget in the tree with nothing to say so. `asked_px`
|
||||
walks up only where a widget is already being redrawn; the mean chain is
|
||||
2.8 levels.
|
||||
- **The window is not a move entry** (`5b78002`). A chain bottoms out in
|
||||
`MoveIdx::NONE`; the window is applied where a fraction becomes pixels,
|
||||
`to_px(output_size)` on the CPU and the uniform in the shader. A resize
|
||||
rewrites no retained entry and re-uploads nothing but the uniform; its
|
||||
cost is whatever `Holds` redraws.
|
||||
- **A move that keeps a box's length is a translation, and exact.** A box
|
||||
that changed length re-expresses each part as a fraction of the new one,
|
||||
which rounds. `tests/cases/drift.rs` pins that the grid does not drift
|
||||
either way. A length given in pixels is that many pixels wherever it ends
|
||||
up (`Len::within` adds a part's own pixels rather than scaling them);
|
||||
equal shares come out one or two steps apart because positions, not
|
||||
lengths, are what gets rounded, so the row fills and no two children
|
||||
leave a seam.
|
||||
|
||||
### Retained-layout invariants
|
||||
|
||||
- `Holds` is the interval of box lengths for which a widget's drawing and
|
||||
reported size remain valid. Reading `Painter::px_len` or `px_size` narrows
|
||||
it; `Painter::holds` widens it. The contract is trusted rather than checked
|
||||
defensively on every use.
|
||||
- A retained drawing is reusable only when its `Holds` contains the new box
|
||||
on both axes, its parent and region-node choice match, it is on the layer it
|
||||
was drawn on, and the widget is clean. A valid ordinary subtree moves by
|
||||
recursive remap; a region node moves by one entry. A container that draws a
|
||||
child to learn its size uses `Painter::child_layer_at`, the layer the child
|
||||
will actually occupy.
|
||||
- An answer's validity and its final drawing's validity are independent. A
|
||||
parent may reuse an answer while redrawing the placed output. Translate the
|
||||
drawing contract back through its placement; do not intersect it into the
|
||||
answer contract.
|
||||
- A fraction resolves once against its frame. A report returns raw and is
|
||||
composed only where a parent narrowed that frame. A part's own pixel length
|
||||
is added rather than scaled, so a pixel length remains that many pixels at
|
||||
every nesting depth.
|
||||
- An asked-but-undrawn size dependency belongs to the widget that asked. Keep
|
||||
it recorded so a later child change reaches the parent that decided not to
|
||||
draw it. Dirty size dependencies settle deepest-first.
|
||||
- A widget that creates a mask clips to and reports its box. Its own mask and
|
||||
its inherited mask are distinct retained state: the former says which mask
|
||||
a move rewrites, while a local redraw receives the latter.
|
||||
- A span's leftover/no-leftover boundary is a strict structural decision, not
|
||||
a tolerance. Derive the boundary through the inverse of the expression that
|
||||
places children. A cap may not contain `leftover`, because feeding the
|
||||
span's own room division back into a cap admits multiple fixed points.
|
||||
- Text shaping is retained separately from line breaking. A greedy break
|
||||
holds from its longest produced line through the width at which it was
|
||||
made, expressed with `Painter::holds`.
|
||||
- A region node stores one whole `UiRegion` in its parent node's coordinates;
|
||||
`FULL` is the identity. Changing node ownership redraws the subtree once,
|
||||
and a removed node's move entry remains alive until every descendant has
|
||||
migrated.
|
||||
- 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()`.
|
||||
|
||||
### What the fuzzers tolerate
|
||||
|
||||
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.
|
||||
|
||||
### Rendering the grid (pending)
|
||||
|
||||
`snap_floor` in `prelude.wgsl` adds half a layout step before flooring,
|
||||
which absorbs float error and not a layout step, so a third of 900 px
|
||||
(299.999 on the grid) lands at 299 on screen. Bryan approved on 2026-09-17
|
||||
rounding to the nearest pixel in the shader together with round-to-nearest
|
||||
in `Fixed::mul` on the CPU, as one change with one verification; neither has
|
||||
landed. The reason for the CPU half: a `Rel` is off by at most `2^-25` of
|
||||
its box, so with round-to-nearest every product whose true value is a whole
|
||||
number of steps is exact for boxes under about 8,000 px, where truncation
|
||||
leaves half of them one step short and layout then decides "does not fit"
|
||||
on a container the user meant to fit exactly. Use the branchless
|
||||
round-half-up form, `(a * b + (1 << (BY - 1))) >> BY`; re-derive
|
||||
`Holds::through` for it; check with `nm` that `UiSpan::within` still
|
||||
inlines.
|
||||
|
||||
## Measuring layout cost on this machine
|
||||
|
||||
- **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.
|
||||
`random.rs`'s `Branch` picks a subtree by a measured pixel length, so the
|
||||
fixture's shape moves with the thing measured; `Edits::fixed_branches`
|
||||
pins it for timing and the oracle keeps measured branches on purpose. A
|
||||
3x once reported was that artifact.
|
||||
- **`perf stat` in this VM returns garbage readings** for both
|
||||
`instructions:u` and `cycles:u`, roughly a quarter of the time, off by a
|
||||
factor of five to fifteen. Take medians of nine or more and report how
|
||||
many readings a filter kept. Instruction counts hold to 0.02% within a
|
||||
binary and move 0.5% across a rebuild, so build the baseline beside the
|
||||
thing measured and quote a delta.
|
||||
- **What moves cycles is whether `UiSpan::within` inlines.** It is the
|
||||
hottest line in layout; `nm` shows it as a symbol when it does not.
|
||||
Shrinking its body until the inliner takes it won; `#[inline]` on the
|
||||
body it had lost 1.5% cycles. Shrink it, do not annotate it.
|
||||
- `Holds::through` divides twice per call and accounts for essentially all
|
||||
of a run's `i64` divisions: 2.8% of a 500-frame `many`.
|
||||
- Tried and rejected, with numbers: a float reciprocal for the remap
|
||||
division, +6% cycles; branchless `shift_round`, +6.7%; removing the
|
||||
per-child hash lookup in `remap_subtree`, 0.0%; short-circuiting
|
||||
`apply_scalar` where the fraction is nought or one, +17%. Short-circuits
|
||||
guarding a saturating multiply stopped paying once the multiply wrapped;
|
||||
re-price a short-circuit before keeping it. Rust does not contract
|
||||
`a + b * c`. Wrapping (`4febabf`) was -8.6% instructions; truncating
|
||||
(`08c9d5a`) costs a share a thousandth of a pixel of its row.
|
||||
- Threading the pixel box down the draw (2026-09-17) was free on cold
|
||||
layout and 9-13% of instructions off the retained paths, measured against
|
||||
`5b78002` at seed 1, depth 8, medians of 21.
|
||||
@@ -0,0 +1,249 @@
|
||||
# Layout findings log
|
||||
|
||||
What the sessions working on Iris's retained layout found, planner and
|
||||
worker alike, kept so that nothing here is rediscovered. Each entry says who
|
||||
found it and when. **Delete this file when transparent frames lands**; what
|
||||
must outlive it (settled design, the measurement method) is already in
|
||||
`docs/LAYOUT.md`, and the current plan is in `docs/HANDOFF.md`. Commit ids
|
||||
are in `/home/bob/repos/iris-layout-experiment` unless said otherwise.
|
||||
|
||||
## The two open shrinker seeds on `wip/transparent-frames` (planner, 2026-09-18)
|
||||
|
||||
Both are the rule in `draw_inner` that decides which answer places a widget's
|
||||
box: `measured = if info.offer() { fresh } else { retained }` with
|
||||
`offer() := place == offer_place`. That bit is meant to say "this ask is a
|
||||
measurement" and has no consistent value once a container's body runs in
|
||||
more than one box.
|
||||
|
||||
- **Seed 2, `repaint`, depth 5** shrinks to `Stack{sized by child 0}[x:
|
||||
leftover] > [OneLine, Pad{0} > Span{DOWN}[Rect, OneLine], Branch]`. The
|
||||
stack's box is one text line tall. The span measured in the stack's room
|
||||
has leftover room, draws the rect and reports `leftover` across; in the
|
||||
stack's box it has none, undraws the rect and reports the text's width.
|
||||
Cold kept the first because the stack's placing evaluation reused the
|
||||
pad's retained answer, which looked valid only because `in_parent`'s
|
||||
`Part::Of` arm drops the span's `extent_len` pin. Warm re-asked the span in
|
||||
the box and got the second. The correct layout is the second: the stack's
|
||||
non-sizing children belong in the stack's box, and the measuring pass drew
|
||||
them in a box the stack never has.
|
||||
- **Seed 108, `reorder`, depth 5** shrinks to `Span{LEFT} > [Span{LEFT} >
|
||||
[Span{DOWN} > [Branch{probe Rect, wide Rect, narrow Wrapped, 483},
|
||||
Wrapped], Rect], Rect]`. A draw trace shows the `DOWN` span evaluated at
|
||||
900, 450, 600 and 300 px across in one cold layout (room of the outer
|
||||
span, slot inside the inner span's measuring pass, slot of the inner span,
|
||||
slot inside that), the wrapping text re-shaped at each. In the last one
|
||||
the branch's narrow text is drawn in its real 300 px box and answers 286
|
||||
px; `draw_inner` discards that for the retained 438.9 px answer from the
|
||||
450 px evaluation because the branch's `below` place embeds
|
||||
`extent_len(Y)`, which moved when the sibling text's height changed, so
|
||||
`place != offer_place`; the text is redrawn 438.9 px wide in a 300 px box.
|
||||
Warm does the same with a different stale answer (286 px from the first
|
||||
frame). The same shape is `unsettled::a_widget_under_a_region_node_is_asked_in_the_box_that_node_was_offered`.
|
||||
|
||||
Two one-line experiments, both reverted, run at `49cec82` with the fast
|
||||
oracle, the debug suite and the shrinker at 400 seeds of depth 5:
|
||||
|
||||
| change | seed 2 | seed 108 | else |
|
||||
| --- | --- | --- | --- |
|
||||
| compose the pin through `Part::Of` (`len.px - part_len.px` where `part_len.rel == ONE`) | fixed | fails | seed 220 `reorder` and one suite test fail |
|
||||
| `measured = answer.0` always, and every ask records `answer`/`offer_part` | fails | fixed | seeds 184, 246, 292, 372 and two suite tests fail, all under `Scroll` |
|
||||
|
||||
The second experiment's failures are the case the gate was secretly holding
|
||||
up: a container re-drawn in a box its own answer derived. The specific one
|
||||
is `Scroll`, whose `apply_leftover` content length adds a whole viewport on
|
||||
top of the content's pixels (a row reporting `600 px + leftover` in a 900 px
|
||||
viewport gets a 1500 px content box), so the wrapping text inside is offered
|
||||
900 px more room than it was measured in and re-wraps; the layout drifts one
|
||||
iteration per evaluation, and warm and cold differ by how many they ran.
|
||||
Bryan: that content length was always wrong; a share is the room left in the
|
||||
viewport, so `content = max(viewport, px + rel*viewport)`. With it the
|
||||
content box never re-offers room and `Scroll` needs no own-answer
|
||||
evaluation.
|
||||
|
||||
The `many`/`resize` cost is the same mechanism in miniature: four
|
||||
evaluations of a ten-widget subtree in one cold layout, from `Within(cursor..
|
||||
far)` measuring rooms followed by `Fill(slot)` placing, with the span's
|
||||
`extent_len` pin forcing a redraw at every box length.
|
||||
|
||||
## What the plan of 2026-09-18 got wrong (planner, same day)
|
||||
|
||||
The plan claimed the retained model rests on the frame's length being the
|
||||
same on every ask. Answers depend on the *part* (the box offered), not the
|
||||
frame, for every widget that reads its box, so transparent frames fixed
|
||||
fraction resolution and moved the second geometry from the frame to the
|
||||
extent rather than removing it. Step 6 ("redraw without the deferral; both
|
||||
seeds must pass now because the frame no longer changes under the widget")
|
||||
followed from that claim and was false for the same reason. The plan also
|
||||
carried `offer_place` ("from the first ask of the parent's draw at the
|
||||
offer") forward from the old protocol without noticing it is undefinable
|
||||
once a container is evaluated in several boxes, and its `Inset` sketch
|
||||
assumed a narrowed frame and a placed extent could coexist, which
|
||||
`frame_and_extent` collapses ("a narrowed frame is its own extent") and which
|
||||
the extent-in-frame-coordinates representation cannot express in general
|
||||
(`(0.5*B - 20)/(B - 20)` is not `rel + px`). The plan also wrote "a frame is
|
||||
narrowed only by a declared length, an inset or the root" and the code's
|
||||
`declared_lens` filters `leftover` out; Bryan: a share was always meant to
|
||||
narrow the frame like the other two.
|
||||
|
||||
The worker executed the plan as written, found exactly this ("whether a
|
||||
given draw is a measurement cannot be recovered from the ask"), tried four
|
||||
bookkeeping rules (first-ness alone, place matching the retained offer place,
|
||||
box matching the measured box, a retained flag on the drawing), each fixing
|
||||
some seeds and breaking others, restored the deferral and stopped. That was
|
||||
the right call. The plan as handed over is at ai-app-2 `152bed7` if the
|
||||
wording is ever needed.
|
||||
|
||||
## What the worker measured on `wip/transparent-frames` (worker, 2026-09-18)
|
||||
|
||||
Widget draws / distinct widgets / update ms from `tests/layout_diagnostics.rs`
|
||||
at depth 8; draw counts are deterministic so these are single runs.
|
||||
`e44dea3` is #18's head, `34cafb6` the commit the branch starts from,
|
||||
`49cec82` its head.
|
||||
|
||||
| seed 1 | e44dea3 | 34cafb6 | 49cec82 |
|
||||
| --- | --- | --- | --- |
|
||||
| cold | 369/261/10.6 | 463/274/13.3 | 516/288/12.0 |
|
||||
| repaint | 1 | 1 | 1 |
|
||||
| many | 157/95/0.33 | 263/108/0.59 | 187/119/0.52 |
|
||||
| size | 16/12/0.018 | 3/3 | 3/3/0.010 |
|
||||
| scroll | 2/0.002 | 1 | 1/0.004 |
|
||||
| resize | 13/13/0.019 | 22/15/0.032 | 24/76/0.090 |
|
||||
|
||||
| seed 13 | e44dea3 | 49cec82 |
|
||||
| --- | --- | --- |
|
||||
| cold | 1330/707/20.3 | 2940/982/28.3 |
|
||||
| many | 524/159/1.09 | 1091/423/2.39 |
|
||||
| resize | nothing drawn | 2215/510/6.56 |
|
||||
|
||||
Three findings, each applied on the branch:
|
||||
|
||||
- **Lazy `Within` placement costs more than it saves.** Leaving a child's
|
||||
answer to be placed at the end of the parent's draw puts the drawing in the
|
||||
part first and in the answer's box after; where it does not hold for both
|
||||
that is two drawings. Seed 1's resize went from 391 widget draws to 29
|
||||
with it removed.
|
||||
- **An inset said in frame lengths makes a container read its own box.**
|
||||
"Less eleven pixels at the end" needs the length, and a container whose
|
||||
box is its own answer then depends on its own answer: `Pad` drew
|
||||
sixty-four times in one resize frame at seed 13. `Part::Of` says it as a
|
||||
part of the box and composes without a length.
|
||||
- **Pin one axis at a time.** `extent_len` pinning both axes made a span
|
||||
dividing one hold for one length of the other.
|
||||
|
||||
`Part::Of`'s `in_parent` arm drops a child's `extent_len` pin (only `All`
|
||||
composes one), which is what let seed 2's pad reuse an answer for a box its
|
||||
span had never been measured in.
|
||||
|
||||
Reference renders at `49cec82` against `34cafb6`: `view` and `minimal`
|
||||
byte-identical; `tabs` 2,332 pixels; `text` shows `wtext(..).width(rel(1.0))`
|
||||
inside `.pad(16)` at the window's edges. Under the decided share rule that
|
||||
example is correct as written: once the share narrows the frame, `rel(1.0)`
|
||||
is the padded share and fits. The clipped render records the current bug.
|
||||
`random` moves where nested spans do.
|
||||
|
||||
## Defects landed before transparent frames, and their lessons (2026-09-17 review)
|
||||
|
||||
- **A report is a fraction of the containing widget** (`ffd79f3`). A
|
||||
report used to come back composed through the box it was offered, and a
|
||||
span offers each child the room from its cursor, so a nested span taking
|
||||
half of what it was given took a quarter of a row whose first half was
|
||||
spoken for. Consequence: a span can overflow itself without bound, so its
|
||||
`fixed <= 0` branches are ordinary and boxes of negative length reach
|
||||
children; nothing yet states what a widget may assume about one.
|
||||
- **An answer is not an answer while anything under it is dirty**
|
||||
(`0e0d4af`, superseded by `a0693ac`). `dirty_size_under` was what made an
|
||||
answer an answer until the settling walk made the state it guarded against
|
||||
unreachable. Found at seed 564, depth 6, `shuffle-every-other`.
|
||||
- **A frame settles strictly bottom-up** (`a92c6ac`, `a0693ac`). A widget
|
||||
that cannot settle defers to its parent rather than drawing the parent
|
||||
from inside itself; `update` marks the root for a resize instead of
|
||||
drawing it top-down, and only where the new output falls outside what the
|
||||
root's answer holds for. Sound by induction on depth. Bryan: "then that
|
||||
entire category of issue can't even occur."
|
||||
- **A text is handed back a box its own line fits in** (`4bd8607`): report
|
||||
`ceil` of the shaped size; the two tolerances that were holding it
|
||||
together (`BREAK_EPSILON_PX`, a nearest-step `Holds` start) both went.
|
||||
- **A subtree that changes hands is recorded on both sides** (`e44dea3`):
|
||||
the old parent's child list is repaired and the subtree's depths re-walked
|
||||
where its top moved. The fuzzer never re-parents, which is why nothing
|
||||
generated reached it.
|
||||
- **A span's leftover boundary is its own inverse** (`53b00c6`): forty
|
||||
lines became twelve and one `div` left layout.
|
||||
- **`wip/stack-fraction-twice`** (a stack sized by a child reporting
|
||||
`rel(0.5)` applied it twice) is closed by transparent frames:
|
||||
`placed_extent` takes the reported length *from* the part rather than
|
||||
composing it into the part. No oracle could see it; the branch's test pins
|
||||
it. **`wip/padding-outset-and-inset`** is superseded: padding goes outside
|
||||
what it pads, and its `rel(0.5)`-under-inset failure was the same second
|
||||
application.
|
||||
- Four findings from the frame/extent prototype still shape the code: an
|
||||
answer and a drawing each retain their dependencies (`answer_under` and
|
||||
`under`); a wider contract does not invalidate an existing guarantee; no
|
||||
measurement is different from a measured zero (`ActiveData::answer` is
|
||||
optional); the settling walk takes the deepest mark from a `BTreeSet`
|
||||
keyed by depth and what ends it is the mark set.
|
||||
|
||||
## Failed hypotheses worth not repeating
|
||||
|
||||
- **The placement pin was blamed for the `many` gap and is not the cause**
|
||||
(2026-09-17). Disabling it still redrew 487 distinct widgets a frame at
|
||||
seed 13 against 159; the per-widget trace showed local redraws deferring
|
||||
to their parents in chains to the root because a span handed its children
|
||||
its own placement as their frame. Transparent frames fixed that part.
|
||||
- **`wip/local-reask`** re-asked a dirty widget at its offer instead of
|
||||
deferring, under the old protocol, and diverged at seeds 532 and 398 of
|
||||
depth 6. Superseded; the branch can be deleted.
|
||||
- **An offer composed back up the move chain** fell back to `FULL` under a
|
||||
region node and was resolved against that node's *placed* box, so
|
||||
everything under a `Scroll` was re-asked at the content's width. Pinned by
|
||||
`unsettled::a_widget_under_a_region_node_is_asked_in_the_box_that_node_was_offered`.
|
||||
- **Four bookkeeping rules for "which draw is a measurement"** (worker) and
|
||||
**the two experiments above** (planner): each fixes some seeds and breaks
|
||||
others. The bit is undefinable; stop trying to define it.
|
||||
- **Choosing between a fixed and a relative child in pixels at the span's
|
||||
current width** admits multiple self-sizing fixed points; seed 13 settled
|
||||
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** retained zero-height
|
||||
children at seed 16. Pinned by
|
||||
`unsettled::a_box_that_only_rounds_past_its_fixed_children_leaves_nothing_over`.
|
||||
- **`Scroll` returning the final placed answer** rather than the first
|
||||
box's advanced one fixed-point iteration (seed 86). Moot once the content
|
||||
box never re-offers room, but keep the test until that is measured.
|
||||
- **A `git bisect`** once named a commit that could not be the cause; read
|
||||
the tree rather than the bisect when that happens.
|
||||
|
||||
## Fuzzer coverage
|
||||
|
||||
- Seeds 1121 and 1839 at depth 4 failed on every commit before `4bd8607`
|
||||
and nothing routine reached them: the fast oracle takes ten seeds, the
|
||||
shrinker 400 at depth 5, the long oracle 1000 at depth 6. The scan that
|
||||
found them (2000 seeds at depth 4 over all fifteen cases, 261 s) should be
|
||||
run 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; a seed listed as new after
|
||||
a change may have been hidden behind another in the same chunk. Check a
|
||||
single seed against the unpatched code before calling it new (seed 220
|
||||
was checked this way and is new under the pin experiment).
|
||||
- Depth finds things and so does breadth; widen one axis at a time and
|
||||
record which.
|
||||
|
||||
## Smaller open items (carried from the old handoff)
|
||||
|
||||
- An undrawn `leftover` child still contributes its gap, so a vanished
|
||||
child leaves a double gap.
|
||||
- Nested spans pass `leftover` weight up, so three leftover children in one
|
||||
inner span beside one in another get three quarters to one quarter. No
|
||||
other layout system does that; confirm it is wanted.
|
||||
- `Fixed::div` by zero answers `MIN`/`MAX` while `ratio` answers `ZERO`;
|
||||
both are caller bugs under `debug_assert`, but the fallbacks differ.
|
||||
- The comment on the `local == UiRegion::FULL` shortcut in `widget_at` says
|
||||
composing through `FULL` "is not quite the identity in f32"; on the grid
|
||||
it is exact and the shortcut is performance only.
|
||||
- `docs/LAYOUT.md` §4, §5 and the density section name `Painter::place`,
|
||||
`SetSize`, `desired_width`, `apply_rest`, `Len::dp`, `Aligned` and
|
||||
`MaxSize`, none of which exist. Do not restore `OnResize::Translate` or
|
||||
`OrthoSize`.
|
||||
- `tabs` changed twice across `d3b0ebf` with nobody looking; take the oracle
|
||||
as the reference and the five renders as a spot check.
|
||||
Reference in new issue
Block a user