Record that the shader, not the grid, decides screen pixels

`d21a215` answers the API question: one widget keeps one length per axis, and
`Wrapper` -- what `WidgetPtr` already was -- is the second widget the second
length needs.

The `i64` composition item is rewritten around a measurement that changes
what it is for. `prelude.wgsl` decodes the raw counts into `f32`, walks the
move chain in floats, and lands every edge on a whole pixel with
`snap_floor`. Checked in the render rather than read off the source: there is
no partially covered column anywhere along `tabs`'s band of rounded rects, so
every box edge is hard, and the two `pad(10)` gaps are exactly ten pixels on
both sides of the truncating multiply. The screen invariant is therefore
already as good as integers allow, and exact composition cannot improve it.

What it can still buy is layout's own decisions -- the `Span` leftover
boundary, the queued clamp crossover, `Holds` validity, warm-against-cold
agreement -- with `Holds::through`'s two-route allowance as the test of
whether it worked.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
iris-aiandClaude Opus 5 committed 2026-09-16 20:50:50 -04:00
1 parent 82c006cd59
commit b0c13b85a9
1 file changed
+48 -23
+48 -23
View File
@@ -8,7 +8,7 @@ log.
Canonical Iris `main` is **`ca2b4b2`** (#17, the headless rig). **#18
`split/18-position-chain`** is open in `/home/bob/repos/iris-pr18`; its local
head is **`e166e00`**, seventy-nine commits, pushed. Built-in alignment is
head is **`d21a215`**, eighty commits, pushed. Built-in alignment is
complete there; see "Built-in alignment" below for the retained-layout
details. No PR reviews were present when checked on 2026-09-15.
@@ -216,7 +216,8 @@ against `394d514`. The measurements are under "Performance".
`1d397c57b9914a2e596fa907029bc6aab4629e4d74deb0715e81325c703bcdb3`.
The run used the Venus adapter backed by the host RX 7900 XT.
- `minimal`, `text` and `view` render byte-identical at 1920x1200 across
`8220a78`. `tabs` differs only in the widget count it prints about itself,
`8220a78`. `tabs` differed only in the widget count it printed about
itself,
which is two wrapper types smaller -- so it is no longer a byte-identical
reference and the generated oracle is the check that matters.
**That last claim was then left to cover `d3b0ebf` as well, and it does
@@ -661,14 +662,13 @@ Chaining them therefore means something different now, and two of it showed:
to sit at the top of its row and filled it. Fixed in `2bc6bdf`; both match
`main` again.
- **`.width()` overwrites what `.sized()` set on the same axis**, so
`tabs`'s `rrect.sized((100, 100)).center().width(leftover(2))` is a
`tabs`'s `rrect.sized((100, 100)).center().width(leftover(2))` was a
100-tall bar across two shares where `main` draws a 100x100 square centred
in them. **Open, and an API question rather than a bug**: one widget
carries one length per axis, so "a small thing centred in a big box" now
needs a container to hold the big box -- `(thing.sized(..).center(),)
.stack().width(leftover(2))` is the idiom, and it is what the old builders
were inserting silently. The alternatives are to update the example, or to
give a widget an outer length as well as its own. Awaiting Bryan.
in them. **Answered by `d21a215`** (Bryan, 2026-09-16): one widget keeps
one length per axis, and the second length needs a second widget that does
as little as possible. `WidgetPtr` already was that widget and is now
`Wrapper`, with `.wrapper()` to make one -- `Wrapper` and not `Wrap`, and
`.wrapper()` and not `.wrapped()`, so neither reads as the text setting.
`text` still differs from `main` in its `aligned` panel, and that one is a
decision rather than a defect: `9d8415d` deleted `OrthoSize`, so a Y span
@@ -954,26 +954,51 @@ Queued from this work, in order:
since `a8898aa`. Still awaiting Bryan: whether a `Max` narrows the box the
child draws in, or only what the parent reports for it.
- **Compose in `i64` and narrow only when storing** (Bryan, 2026-09-16). The
range inside a single operation is already protected -- `Fixed::mul`
- **Compose in `i64` and narrow only when storing** (Bryan, 2026-09-16).
Chosen for the share spread; **what it buys is not that**, found on
2026-09-16 and worth reading before starting.
The range inside a single operation is already protected -- `Fixed::mul`
widens to `i64` for the product, `Holds::through` does all of its
arithmetic in `i64` and narrows once at the end, and `div_toward` takes
`i64`. What is not protected is *precision across a chain*:
`UiSpan::within` composes a box through its parent with four multiplies,
and every one of them comes back to the `i32` grid before the next starts,
which is exactly the "step per level of nesting" in "Fixed point" and what
forces `Holds::through`'s allowance for two routes. A `Wide<SHIFT>` held
through a composition and narrowed when it is written to `ActiveData` or a
primitive would round once instead of once a level, at no cost in stored
size, since storage stays `i32`.
`UiSpan::within` composes a box through its parent with four multiplies and
every one comes back to the `i32` grid before the next starts, which is the
"step per level of nesting" in "Fixed point" and what forces
`Holds::through`'s two-route allowance. A `Wide<SHIFT>` held through a
composition and narrowed when it is written to `ActiveData` or a primitive
would round once instead of once a level, at no cost in stored size.
**But it cannot make equal shares the same number of screen pixels,
because the CPU's grid is not what puts them on the screen.**
`prelude.wgsl` decodes the raw `rel`/`px` counts into `f32`, walks the move
chain in floats -- with the comment saying so, "what has to hold is that
this agrees with itself frame to frame, not that it matches the CPU to the
last bit" -- and then lands each edge with
`snap_floor(rel * dim) + snap_floor(px)`, which is a whole pixel.
**Measured: every box edge in the `tabs` render is a hard edge**, no
partially covered column anywhere along the band, and the two `pad(10)`
gaps are exactly ten pixels on both sides of the truncation change. So the
screen invariant is already as good as integers allow: a length in pixels
is that many pixels, and equal shares differ by at most one whole pixel
because three equal integers cannot sum to 1000. Layout's own one-or-two
step spread is below what the shader can express, and shows only where it
pushes a value across the `floor` -- which is what moved `tabs`'s corner
arcs by a pixel at `08c9d5a`.
What is left to buy is **layout's own decisions**, which is not nothing:
the `Span` leftover boundary and the clamp crossover are structural
decisions taken on a pixel comparison, `Holds` validity is a pixel
interval, and warm-against-cold agreement is the thing every rig here
measures. The success test is `Holds::through`'s two-route allowance
shrinking from three half steps toward zero, in debug where the assertion
is live.
Two things to decide before starting: how many fractional bits the
intermediate keeps, since chained multiplies accumulate them and an `i64`
runs out too; and whether it helps `UiSpan::within` stay inlined, which is
the thing that actually moves cycles there -- a `Wide` that makes the body
bigger loses on the axis the truncation change won on. The success test is
narrowing `Holds::through`'s two-route allowance back to a step, which is a
check the generated oracle can answer in debug.
runs out too; and whether it keeps `UiSpan::within` inside the inliner,
which is what actually moves cycles there -- a `Wide` that makes the body
bigger loses on the axis the truncating multiply won on.
Other queued work, in dependency order: