Document tested frame and extent prototype and remaining costs

This commit is contained in:
iris-ai committed 2026-09-17 14:50:22 -04:00
1 parent 3084491b9b
commit 0080bcbcba
2 files changed
+86 -131

No files matched your search

+75 -131
View File
@@ -424,147 +424,91 @@ back.
- `Fixed::div` by zero answers `MIN`/`MAX` while `ratio` answers `ZERO`; - `Fixed::div` by zero answers `MIN`/`MAX` while `ratio` answers `ZERO`;
both are caller bugs under `debug_assert`, but the fallbacks differ. both are caller bugs under `debug_assert`, but the fallbacks differ.
### Frame and extent ### Frame and extent: retained prototype
Read on 2026-09-17, against Bryan's question of whether the approach is The original proposal was implemented by Claude as `5fcace1` on
fundamentally wrong or needs adjusting. The pieces the earlier review `wip/region-and-placement` in `/home/bob/repos/iris-pr18`. It kept the fraction
approved are sound and the fuzzers agree: fixed point, the pixel chain reference stable but failed five layout cases and three draw-count cases.
threaded down, `Holds::through` as the exact preimage, and the bottom-up The correction is saved as `efb416b`, pushed to origin's
settle, with every residual they leave one or two steps in a position. One `wip/region-and-placement`, in the isolated checkout
thing is wrong, and it is the protocol rather than any widget: **the `/home/bob/repos/iris-layout-experiment`; **it is not on PR #18**. The original
placing ask replaces the box a widget's children resolve fractions against checkout is unchanged. Keep the experiment's build directory separate: Cargo
with the widget's own answer.** Every "fraction twice" item is that defect, can accept a different worktree's artifacts as fresh when a target directory
and it has lasted because it was fixed per widget four times (`decided`, is shared. The comparison checkout's source timestamps can precede its build.
`reports_of`, `box_of`, the stack branch's `resolve`, `Inset` against
`Pad`), each of which stops the re-resolution at one level while it happens
again inside the child's second draw, which no flag on the parent's ask
can reach.
The repository asserts both answers. `tests/cases/layout.rs` pins a nested **The distinction stays.** `Painter::region` is the frame, the parent's chosen
span's `rel(0.5)` child at a quarter of the row and calls it correct ("half reference for fractions. `placement` is the extent in that frame. A sizing
of that final box is what its own child takes"); the parked stack branch's report must not replace the reference against which that report was obtained.
test pins the same shape as a half. Both are consequences of the one line `px_len` reads the extent's pixel length; `region_px_len` reads the frame's.
where a child's region composes within the widget's current box, which the Text wraps at the available extent, not the whole fraction reference.
placing ask has made the answer:
```rust **The offer includes availability.** A span can keep the same reference frame
// painter.rs, widget_at while offering a text less room after an earlier sibling. `known_len` therefore
let within = match local == UiRegion::FULL { accepts the placement as well as the frame. The first measurement's
true => self.region, `offer_placement` survives later placing evaluations, and local redraw asks
false => local.within(&self.region), that original question before restoring the assigned slot. Frame-length
}; equality alone no longer identifies the measurement question.
```
The reuse path gives the same result, because `AxisRemap::Scale` **An answer and a drawing each retain their dependencies.** `LayoutHolds`
re-expresses the child as a fraction of the new box. `Len::within` itself keeps frame ranges, extent ranges, and an optional raw placement snapshot.
is correct geometry and was never the problem. `px_len` narrows one extent axis without making position or the other axis a
dependency; `holds` widens that extent range without erasing a frame read.
`Holds::through` still inverts one fixed mapping exactly. A measured answer
and the final drawing can only substitute for one another when those
independent inputs satisfy the retained contract. The old fallback checked
only frame ranges, so text placed at a fixed pixel width answered a narrower
window's measurement with its previous line breaks.
**The protocol is one draw body evaluated twice, and that is right.** The **Geometry retains its reference.** `DrawRegion::{Frame, Extent}` records local
first evaluation measures and the second places, and the retained primitive and mask coordinates before composition. Ordinary `primitive()`
machinery skips the second wherever it can. Only what the second is told follows the extent; explicit `UiRegion` arguments remain frame-relative.
its box is has to change. The widget stays in its **frame**, the box it was Text glyphs use extent-relative origins. `Painter::widget()` records a child
offered, and is handed its **extent** as a region in the frame's that inherits placement, so a pass-through wrapper and its valid descendants
coordinates: can be repositioned without rerunning their draw bodies. `placement()` is
still a conservative raw read for arbitrary widget computations.
```rust **No measurement is different from a measured zero.** The first wider scan
pub struct Painter<'a> { found seeds 560 (`shuffle-swap-for-three`) and 1690 (`shuffle-add-three`) at
/// This widget's frame: the box it was offered, in `move_idx` depth 4. A previously undrawn pure share had only supplied a hint; its
/// coordinates. Every region this draw writes is in its coordinates placeholder zero answer became reusable once it was drawn during placement.
/// and every fraction in one is of it. It does not change between the `ActiveData::answer` is now optional. Both seeds pass after that correction,
/// ask that measures and the ask that places. and `adding_text_to_a_reverse_row_keeps_its_shared_height` reproduces the
pub(super) frame: UiRegion, zero-height failure with a small tree.
/// What of the frame this widget's answer took, in the frame's own
/// coordinates: `FULL` while the answer is not yet known, and the
/// placed answer once its parent has chosen where it sits.
extent: UiRegion,
/// The frame in pixels; the extent is one `Len` of it.
pub(super) px: PxVec2,
/// Whether this draw read its extent, which makes the drawing one that
/// holds only for that extent, the way `px_len` does for a length.
reads_extent: bool,
}
impl Painter<'_> { Ordinary verification currently passes: formatting, workspace clippy with
pub fn extent(&mut self) -> UiRegion { `layout-diagnostics`, all workspace tests (96 suite tests), and the fast
self.reads_extent = true; generated cases. New regressions also inspect primitive coordinates for mixed
self.extent frame/extent drawings, including a region node, and a content change after
} moving an inherited extent. A `tabs` resize from 1920x1200 to 900x1200 matches
} a cold 900x1200 screenshot with zero differing pixels. The release oracle
``` passes 2000 seeds at depth 4 and 1000 seeds at depth 6 over all fifteen cases.
The shrinker passes 400 seeds at depth 5, and the debug oracle passes 120
seeds at depth 4 with assertions enabled.
`px_len` stays the frame's length, which is what a text wraps at and what **Do not land this as a performance improvement.** The old stretching and
`Span`'s leftover decision reads. `widget_at` composes the child's region wrapper reuse tests pass, and one numeric-size test improves from three draws
within `self.frame` and resolves the child's fractions, declared or to two. Cold work also improves against the broken `5fcace1` prototype: 604 to
reported, against the child's own offer, which `ActiveData::offer_len` 535 widget draws and 15,296 to 10,384 primitive writes on seed 1, depth 8.
already records. `draw_inner` places the answer as a length of the offer But against the last working `e44dea3`, the same rig does substantially more
rather than of the region passed this time: nested layout work:
```rust | phase | widget draws before | prototype | primitive writes before | prototype |
let lens = placed_lens(answer.0, declared, info.decided); | --- | ---: | ---: | ---: | ---: |
let extent = lens.within_len(info.offer_len); | cold | 369 | 535 | 9635 | 10384 |
let placed = placed_box(region, extent, align); | repaint | 1 | 1 | 1 | 1 |
``` | many | 157 | 310 | 4721 | 6970 |
| size | 16 | 275 | 106 | 5980 |
| scroll | 2 | 2 | 0 | 0 |
| resize | 13 | 328.5 | 0 | 6721.5 |
Where the parent hands back exactly the answer the slack is zero and the These are instrumented work counts, not elapsed-time speedups (50 frames per
multiply exact, so `decided` stops being load-bearing. `try_reuse` compares phase except cold, which is one; 578 generated widgets, 191 finally active).
frames, not placed boxes: a nested span's frame is the row on both asks, so Raw placement reads in containers and repeated offer/placement evaluations
nothing remaps and its rect stays at half the row. Remapping is for a frame remain conservative. Retaining local coordinates also costs memory per
that moved, a translation in the common case and a scale on a resize, the primitive. The experiment demonstrates selective reuse with one `Widget::draw`
two cases `AxisRemap` has. body, not a finished performance replacement. Optimize the container protocol
under the same correctness tests before adopting it. The parked `Inset`/`Outset`
**For a widget author the rule is one sentence: regions are written in the API changes are not included in this experiment.
frame, and `painter.extent()` is "my box".**
```rust
// span.rs: children sit across the row inside the span's extent.
let across = painter.extent().axis(!axis);
let region = UiRegion::from_axis(axis, span, across);
// pad.rs: the inner sits in the pad's extent less the padding.
painter.widget_within(&self.inner, self.padding.region_of(painter.extent()))
// stack.rs: every child gets the stack's extent; `box_of` is deleted.
let region = painter.extent();
```
`Span`'s cursor arithmetic does not change: it is already a sum of the
children's answers, which are fractions of the row.
Checked by hand against the failing shapes:
- Nested span: the inner's frame is the row on both asks; its rect is 200
px. `a_span_reads_a_child_report_as_a_fraction_of_the_row` flips to
assert 200..400 for the inner.
- Stack sized by a `rel(0.5)` child: the child's extent and the stack's are
the same half of the frame, zero slack, exact. Seed 1091's three steps
were `placed_box` scaling a nonzero slack per level.
- Pad and Inset: the inner's fraction is of its offer and its extent sits in
`extent().inset(padding)`. The 47.5 px was the offer changing between
asks.
- Pad around a 40 px rect, bottom aligned: on the second ask the pad's
extent is 60 px, the inner region 40 px, slack zero, rect at 20..60. This
is the case that needs the second evaluation at all, and why a frame
alone is not enough.
- A rule: a stack declared `width(rel(0.5))` holding a `rel(0.5)` rect gives
a quarter of the row, correctly. A rule sets the frame; a report does not.
Reading the extent narrows reuse the way reading a pixel length does. A
widget that never calls `extent()` has a first drawing that holds for any
extent and keeps it: `Stack` without a sizing child, `Scroll`, every leaf,
`Span` along its axis. One that reads it is redrawn on the second ask only
where the extent differs from the frame, and its children reuse through
their own `Holds` since their frames did not move. Suppressing primitives
on the first evaluation would be an optimization over this, not a
requirement. The one place it costs more than today is a stack whose
sizing child reports less than the frame: `box_of` narrowed the other
children's first draw so they landed right at once, and with `extent()`
they draw at `FULL` and again at the extent. They are usually a background
rect; let the rig's counters say whether it matters.
Fixed point, the pixel chain and `Holds::through` are untouched. The chain
already threads `offer_len` beside `given_len` and `offered_px` beside
`px`; the change is that the offer chain becomes the coordinate base and
the placed chain is derived from it, rather than the other way round.
## How layout is decided ## How layout is decided
+11
View File
@@ -864,6 +864,17 @@ such as corner radius and text's position within horizontal overflow use
`Len`. Flexible padding participates in the same proportional allocation as a `Len`. Flexible padding participates in the same proportional allocation as a
span rather than silently discarding its `rest` component. span rather than silently discarding its `rest` component.
**The frame/extent experiment keeps measurement and geometry references separate**
(2026-09-17, prototype only; verification and costs in `HANDOFF.md`). The parent
chooses the reference for fractions; placing a widget's reported size does not
replace that reference. A measurement also records the available extent, so
reusing it checks both inputs. Retained primitive and mask coordinates name
whether they follow the frame or the extent. Numeric extent reads constrain
layout validity; merely drawing in an extent does not require another widget
evaluation when it moves. `Widget::draw` remains the only layout body. The
prototype has significant container-update costs and is not yet adopted into
the pinned framework.
**A `Span` leaves its children in the offered orthogonal region unless **A `Span` leaves its children in the offered orthogonal region unless
explicitly compacted** (2026-09-12). It still reports the widest orthogonal explicitly compacted** (2026-09-12). It still reports the widest orthogonal
child as its intrinsic size, so a row nested in a column keeps its content child as its intrinsic size, so a row nested in a column keeps its content