Document tested frame and extent prototype and remaining costs
This commit is contained in:
1 parent
3084491b9b
commit
0080bcbcba
2 files changed
+86
-131
No files matched your search
+75
-131
@@ -424,147 +424,91 @@ back.
|
||||
- `Fixed::div` by zero answers `MIN`/`MAX` while `ratio` answers `ZERO`;
|
||||
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
|
||||
fundamentally wrong or needs adjusting. The pieces the earlier review
|
||||
approved are sound and the fuzzers agree: fixed point, the pixel chain
|
||||
threaded down, `Holds::through` as the exact preimage, and the bottom-up
|
||||
settle, with every residual they leave one or two steps in a position. One
|
||||
thing is wrong, and it is the protocol rather than any widget: **the
|
||||
placing ask replaces the box a widget's children resolve fractions against
|
||||
with the widget's own answer.** Every "fraction twice" item is that defect,
|
||||
and it has lasted because it was fixed per widget four times (`decided`,
|
||||
`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 original proposal was implemented by Claude as `5fcace1` on
|
||||
`wip/region-and-placement` in `/home/bob/repos/iris-pr18`. It kept the fraction
|
||||
reference stable but failed five layout cases and three draw-count cases.
|
||||
The correction is saved as `efb416b`, pushed to origin's
|
||||
`wip/region-and-placement`, in the isolated checkout
|
||||
`/home/bob/repos/iris-layout-experiment`; **it is not on PR #18**. The original
|
||||
checkout is unchanged. Keep the experiment's build directory separate: Cargo
|
||||
can accept a different worktree's artifacts as fresh when a target directory
|
||||
is shared. The comparison checkout's source timestamps can precede its build.
|
||||
|
||||
The repository asserts both answers. `tests/cases/layout.rs` pins a nested
|
||||
span's `rel(0.5)` child at a quarter of the row and calls it correct ("half
|
||||
of that final box is what its own child takes"); the parked stack branch's
|
||||
test pins the same shape as a half. Both are consequences of the one line
|
||||
where a child's region composes within the widget's current box, which the
|
||||
placing ask has made the answer:
|
||||
**The distinction stays.** `Painter::region` is the frame, the parent's chosen
|
||||
reference for fractions. `placement` is the extent in that frame. A sizing
|
||||
report must not replace the reference against which that report was obtained.
|
||||
`px_len` reads the extent's pixel length; `region_px_len` reads the frame's.
|
||||
Text wraps at the available extent, not the whole fraction reference.
|
||||
|
||||
```rust
|
||||
// painter.rs, widget_at
|
||||
let within = match local == UiRegion::FULL {
|
||||
true => self.region,
|
||||
false => local.within(&self.region),
|
||||
};
|
||||
```
|
||||
**The offer includes availability.** A span can keep the same reference frame
|
||||
while offering a text less room after an earlier sibling. `known_len` therefore
|
||||
accepts the placement as well as the frame. The first measurement's
|
||||
`offer_placement` survives later placing evaluations, and local redraw asks
|
||||
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`
|
||||
re-expresses the child as a fraction of the new box. `Len::within` itself
|
||||
is correct geometry and was never the problem.
|
||||
**An answer and a drawing each retain their dependencies.** `LayoutHolds`
|
||||
keeps frame ranges, extent ranges, and an optional raw placement snapshot.
|
||||
`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
|
||||
first evaluation measures and the second places, and the retained
|
||||
machinery skips the second wherever it can. Only what the second is told
|
||||
its box is has to change. The widget stays in its **frame**, the box it was
|
||||
offered, and is handed its **extent** as a region in the frame's
|
||||
coordinates:
|
||||
**Geometry retains its reference.** `DrawRegion::{Frame, Extent}` records local
|
||||
primitive and mask coordinates before composition. Ordinary `primitive()`
|
||||
follows the extent; explicit `UiRegion` arguments remain frame-relative.
|
||||
Text glyphs use extent-relative origins. `Painter::widget()` records a child
|
||||
that inherits placement, so a pass-through wrapper and its valid descendants
|
||||
can be repositioned without rerunning their draw bodies. `placement()` is
|
||||
still a conservative raw read for arbitrary widget computations.
|
||||
|
||||
```rust
|
||||
pub struct Painter<'a> {
|
||||
/// This widget's frame: the box it was offered, in `move_idx`
|
||||
/// coordinates. Every region this draw writes is in its coordinates
|
||||
/// and every fraction in one is of it. It does not change between the
|
||||
/// ask that measures and the ask that places.
|
||||
pub(super) frame: UiRegion,
|
||||
/// 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,
|
||||
}
|
||||
**No measurement is different from a measured zero.** The first wider scan
|
||||
found seeds 560 (`shuffle-swap-for-three`) and 1690 (`shuffle-add-three`) at
|
||||
depth 4. A previously undrawn pure share had only supplied a hint; its
|
||||
placeholder zero answer became reusable once it was drawn during placement.
|
||||
`ActiveData::answer` is now optional. Both seeds pass after that correction,
|
||||
and `adding_text_to_a_reverse_row_keeps_its_shared_height` reproduces the
|
||||
zero-height failure with a small tree.
|
||||
|
||||
impl Painter<'_> {
|
||||
pub fn extent(&mut self) -> UiRegion {
|
||||
self.reads_extent = true;
|
||||
self.extent
|
||||
}
|
||||
}
|
||||
```
|
||||
Ordinary verification currently passes: formatting, workspace clippy with
|
||||
`layout-diagnostics`, all workspace tests (96 suite tests), and the fast
|
||||
generated cases. New regressions also inspect primitive coordinates for mixed
|
||||
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
|
||||
`Span`'s leftover decision reads. `widget_at` composes the child's region
|
||||
within `self.frame` and resolves the child's fractions, declared or
|
||||
reported, against the child's own offer, which `ActiveData::offer_len`
|
||||
already records. `draw_inner` places the answer as a length of the offer
|
||||
rather than of the region passed this time:
|
||||
**Do not land this as a performance improvement.** The old stretching and
|
||||
wrapper reuse tests pass, and one numeric-size test improves from three draws
|
||||
to two. Cold work also improves against the broken `5fcace1` prototype: 604 to
|
||||
535 widget draws and 15,296 to 10,384 primitive writes on seed 1, depth 8.
|
||||
But against the last working `e44dea3`, the same rig does substantially more
|
||||
nested layout work:
|
||||
|
||||
```rust
|
||||
let lens = placed_lens(answer.0, declared, info.decided);
|
||||
let extent = lens.within_len(info.offer_len);
|
||||
let placed = placed_box(region, extent, align);
|
||||
```
|
||||
| phase | widget draws before | prototype | primitive writes before | prototype |
|
||||
| --- | ---: | ---: | ---: | ---: |
|
||||
| cold | 369 | 535 | 9635 | 10384 |
|
||||
| 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
|
||||
multiply exact, so `decided` stops being load-bearing. `try_reuse` compares
|
||||
frames, not placed boxes: a nested span's frame is the row on both asks, so
|
||||
nothing remaps and its rect stays at half the row. Remapping is for a frame
|
||||
that moved, a translation in the common case and a scale on a resize, the
|
||||
two cases `AxisRemap` has.
|
||||
|
||||
**For a widget author the rule is one sentence: regions are written in the
|
||||
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.
|
||||
These are instrumented work counts, not elapsed-time speedups (50 frames per
|
||||
phase except cold, which is one; 578 generated widgets, 191 finally active).
|
||||
Raw placement reads in containers and repeated offer/placement evaluations
|
||||
remain conservative. Retaining local coordinates also costs memory per
|
||||
primitive. The experiment demonstrates selective reuse with one `Widget::draw`
|
||||
body, not a finished performance replacement. Optimize the container protocol
|
||||
under the same correctness tests before adopting it. The parked `Inset`/`Outset`
|
||||
API changes are not included in this experiment.
|
||||
|
||||
## How layout is decided
|
||||
|
||||
|
||||
@@ -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
|
||||
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
|
||||
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
|
||||
|
||||
Reference in new issue
Block a user