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 Canonical Iris `main` is **`ca2b4b2`** (#17, the headless rig). **#18
`split/18-position-chain`** is open in `/home/bob/repos/iris-pr18`; its local `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 complete there; see "Built-in alignment" below for the retained-layout
details. No PR reviews were present when checked on 2026-09-15. details. No PR reviews were present when checked on 2026-09-15.
@@ -216,7 +216,8 @@ against `394d514`. The measurements are under "Performance".
`1d397c57b9914a2e596fa907029bc6aab4629e4d74deb0715e81325c703bcdb3`. `1d397c57b9914a2e596fa907029bc6aab4629e4d74deb0715e81325c703bcdb3`.
The run used the Venus adapter backed by the host RX 7900 XT. The run used the Venus adapter backed by the host RX 7900 XT.
- `minimal`, `text` and `view` render byte-identical at 1920x1200 across - `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 which is two wrapper types smaller -- so it is no longer a byte-identical
reference and the generated oracle is the check that matters. reference and the generated oracle is the check that matters.
**That last claim was then left to cover `d3b0ebf` as well, and it does **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 to sit at the top of its row and filled it. Fixed in `2bc6bdf`; both match
`main` again. `main` again.
- **`.width()` overwrites what `.sized()` set on the same axis**, so - **`.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 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 in them. **Answered by `d21a215`** (Bryan, 2026-09-16): one widget keeps
carries one length per axis, so "a small thing centred in a big box" now one length per axis, and the second length needs a second widget that does
needs a container to hold the big box -- `(thing.sized(..).center(),) as little as possible. `WidgetPtr` already was that widget and is now
.stack().width(leftover(2))` is the idiom, and it is what the old builders `Wrapper`, with `.wrapper()` to make one -- `Wrapper` and not `Wrap`, and
were inserting silently. The alternatives are to update the example, or to `.wrapper()` and not `.wrapped()`, so neither reads as the text setting.
give a widget an outer length as well as its own. Awaiting Bryan.
`text` still differs from `main` in its `aligned` panel, and that one is a `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 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 since `a8898aa`. Still awaiting Bryan: whether a `Max` narrows the box the
child draws in, or only what the parent reports for it. child draws in, or only what the parent reports for it.
- **Compose in `i64` and narrow only when storing** (Bryan, 2026-09-16). The - **Compose in `i64` and narrow only when storing** (Bryan, 2026-09-16).
range inside a single operation is already protected -- `Fixed::mul` 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 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 arithmetic in `i64` and narrows once at the end, and `div_toward` takes
`i64`. What is not protected is *precision across a chain*: `i64`. What is not protected is *precision across a chain*:
`UiSpan::within` composes a box through its parent with four multiplies, `UiSpan::within` composes a box through its parent with four multiplies and
and every one of them comes back to the `i32` grid before the next starts, every one comes back to the `i32` grid before the next starts, which is the
which is exactly the "step per level of nesting" in "Fixed point" and what "step per level of nesting" in "Fixed point" and what forces
forces `Holds::through`'s allowance for two routes. A `Wide<SHIFT>` held `Holds::through`'s two-route allowance. A `Wide<SHIFT>` held through a
through a composition and narrowed when it is written to `ActiveData` or a composition and narrowed when it is written to `ActiveData` or a primitive
primitive would round once instead of once a level, at no cost in stored would round once instead of once a level, at no cost in stored size.
size, since storage stays `i32`.
**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 Two things to decide before starting: how many fractional bits the
intermediate keeps, since chained multiplies accumulate them and an `i64` intermediate keeps, since chained multiplies accumulate them and an `i64`
runs out too; and whether it helps `UiSpan::within` stay inlined, which is runs out too; and whether it keeps `UiSpan::within` inside the inliner,
the thing that actually moves cycles there -- a `Wide` that makes the body which is what actually moves cycles there -- a `Wide` that makes the body
bigger loses on the axis the truncation change won on. The success test is bigger loses on the axis the truncating multiply won on.
narrowing `Holds::through`'s two-route allowance back to a step, which is a
check the generated oracle can answer in debug.
Other queued work, in dependency order: Other queued work, in dependency order: