Commit Graph
7 Commits
Author SHA1 Message Date
iris-aiandClaude Opus 5 08c9d5aa32 Drop a multiply to the step below rather than rounding it
Bryan's call, 2026-09-16, taken for the cycles: a share now lands a
thousandth of a pixel short of its row instead of on it, which is less than
an even number of pixels draws.

`Fixed::mul` is a widening multiply and a shift, with the sign branch and the
half-step add gone. The two short-circuits priced against the old multiply go
with it: `UiSpan::within`'s test for a span that is the whole of its parent,
and `Fixed::scaled`'s test for nothing scaled by something, which was the
whole of `scaled` -- both cases come out of the truncating multiply unchanged,
and the bodies the comparisons cost were what kept the inliner from taking
`within` at all. `nm` is the check: `<UiSpan>::within` is a symbol in the
rounding head and in neither the float head nor this one.

`Holds::through` inverts the multiply, so its widening is re-derived: each
rounding now drops a whole step where it dropped half of one, which doubles
the allowance for the two routes to a length, and the multiply on the way in
drops only downward, so its own step goes at the top of the range alone. The
derived allowance for one truncation either side is measurably too narrow --
it excludes boxes drawings were made in, in eleven generated cases -- because
each route is a chain of multiplies rather than one.

Measured on the fixed-shape fixture (`Edits::fixed_branches`), seed 1 depth 8,
500 frames of `many`, medians of 25 runs of uninstrumented release binaries
with this VM's garbage `perf` readings dropped:

| | instructions | cycles | IPC |
| --- | ---: | ---: | ---: |
| `5ed9e87`, the float head | 1,761M | 688M | 2.561 |
| `60367d8`, rounding | 1,915M | 777M | 2.465 |
| this | 1,800M | 715M | 2.516 |

-6.0% instructions and -8.0% cycles against `60367d8`, whose twenty-five work
counters are identical to this one's, so that pair is the same work at a
different speed. It leaves +2.2% and +3.9% against the float head, from
+8.7% and +12.9% -- but the float head draws 100 widgets to this one's 97 and
writes 4,272 primitives to 3,951, so that pair is not, and the remainder is
not all arithmetic.

Checked: fmt, clippy, 80 suite tests and 18 core unit tests, the release
oracle at 100 seeds, all fifteen shrinker cases at 400 seeds of depth 5 (seed
288 on `region-node` still failing, unchanged), and depth-6 oracle seeds 18
and 190 passing with 326 still failing. `view`, `minimal`, `text`, `random`
and the tab replay render byte-identical at 1920x1200; `tabs` differs on
4,664 of 2,304,000 pixels, single-pixel-wide runs along 80 columns of one
band of rounded rects, which is an antialiased edge moved less than a pixel.

Three tests say what changed rather than being relaxed: a multiply drops on
both sides of zero, a division cannot put back what it dropped, and an
unevenly nested row's shares stay contiguous and end at its edge with each
edge on the even division or one step below.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 17:03:45 -04:00
iris-aiandClaude Fable 5.1 4febabfd2e Wrap rather than saturate: nothing draws two million pixels out
A saturating add is five instructions where a wrapping one is one, and
it has no i32 vector form. Measured on the fixed-shape fixture, seed 1,
depth 8, 500 frames of `many`: 2,098M instructions and ~826M cycles down
to 1,918M and ~771M, with `random`, `tabs` and `text` byte-identical at
1920x1200 and the 100-seed oracle passing.

What saturating bought was ordering past the end of the range, where a
layout is already a defect; wrapping makes that defect obvious instead
of plausible. `from_f32` still clamps, since a float has the range to
come from anywhere, and `narrow` stays for `Holds`, whose range past
i32 really does mean unbounded. MIN and MAX remain unbounded ends only
where they are compared and never added to, which is every use.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 14:22:48 -04:00
iris-aiandClaude Opus 5 4cbb242a5d Do not multiply by a part of nothing
`lerp` is `a + (b - a) * f`, and `b - a` is nothing often enough to be worth
asking: a box with the same pixels at both ends of an axis, a span with no
fraction of one, a part of a subtree whose box did not move on that axis.
`Fixed::scaled` is `mul` that answers a zero receiver without widening to
`i64`, rounding and narrowing back, and `lerp` uses it -- so every lerp in
layout gets it rather than the two places that were about to grow their own
comparison.

`many` over 500 frames: 1,705,786,553 instructions to 1,657,571,216, and
638.9M cycles against 657.9M, averaged over four runs each.

Checked: fmt, clippy, 105 tests, all five shrinker cases at 300 seeds, and
`tabs`, `text`, `random`, `minimal` and `view` byte-identical at 1920x1200.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 14:01:52 -04:00
iris-aiandClaude Opus 5 39e4ca20e6 Decide layout on the grid end to end, and delete the tolerance
`Px` and `PxVec2` reach the last places a pixel was a float: the window, the
box a widget reads, the box it is compared against, and `PixelRegion`. A
pointer, a wheel notch and a shaped glyph advance still arrive as floats,
and each is put on the grid where it arrives.

`Holds` is an interval of `Px`. `HOLDS_EPSILON_PX` is gone with the
`exact`/tolerant split it existed for: `at` is the length a widget read, an
open end is the next step along, and `same_px` is equality. `Span`'s margin
from `5ed9e87` goes too -- the box a parent hands back and the sum of what
its children asked for are counts of the same step, so the boundary decides
the same way from either side.

Three things had to be true for that, and were not:

`Holds::through` inverts `px + rel * box`, which rounds -- so a part of a
given length came from a range of boxes, and inverting the length alone gave
a point that need not contain the box the part was drawn in. It now maps the
half step either side, and one more for a length composed down the chain
against the same length measured against the window.

`RegionRemap` translates when a box only moved, rather than dividing to find
each part's fraction and multiplying to place it again. Two roundings landed
a step from where growing the tree that way does; a move is exact on a grid,
which is the whole reason `tests/drift.rs` was written.

A pixel is `1/1024` rather than `1/64`. At `1/64` the residue of a length
reached two ways was one step, and one step was 0.016 px -- enough to move
a box. `PX_SHIFT` and `REL_SHIFT` are the only statement of the grid now,
and the shader's copy is prepended from them rather than written twice.

Checked: fmt, clippy, 102 tests, 100 generated seeds in 75 s, all five
shrinker cases at 300 seeds, and `tabs`, `view`, `minimal`, `text` and
`random` byte-identical at 1920x1200.

What the fuzzers ask for is now a step, not a twentieth of a pixel: the
shrinker's five cases agree within one (`resize` exactly), and the oracle's
two-operation cases within two. The residue is a single rounding either way
-- it scales with the grid rather than accumulating, which is why it is a
thousandth of a pixel now. Closing it means one way of asking how long a box
is, rather than a chain composed down and a length measured against the
window; that is a bigger change than this one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 02:56:48 -04:00
iris-aiandClaude Opus 5 bd6de71a55 Put lengths, padding, gaps and alignment on the grid too
`Len` is `Px` beside `Rel` beside `Weight`, so the seam `4e28f10` left in
`Span` -- a float length added to a fixed-point cursor -- is gone, and the
sum a span compares against its box is exact.

`Weight` is its own scale, `Fixed<16>`, because a share of what is left over
is not a fraction of anything: a list divides its room by the total of them,
so the range has to hold a whole list's worth while the precision only has to
tell two weights apart. `Rel::ratio` turns two weights into a share on the
finer grid, which is what a span needs and what dividing them on their own
grid would round away.

`AxisAlign` holds a `Rel` rather than a float, which is what the layout was
reading out of it anyway. `Padding` and `Span::gap` hold `Px`, converted
where they are built instead of on every frame. `RegionAlign::rel` is gone;
its one caller wanted a position, and now builds one.

`Fixed` gains `from_num` for a number as it is written in source, `mul_int`
for a length repeated a whole number of times, and `ratio`.

Checked: fmt, clippy, 101 tests, 100 generated seeds in 86 s, all five
shrinker cases at 300 seeds, and all five examples byte-identical at
1920x1200 against `4e28f10`.

With the fuzzer comparing for equality rather than within 0.05 px, four of
the five cases now pass 100 seeds -- `resize-repaint` joins the other three.
`reorder` still fails one seed by one step, so the last of it is in what a
box is measured *in*: `px_len` and the window are still floats.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 01:40:42 -04:00
iris-aiandClaude Opus 5 4e28f1047e Put positions on the grid, and decode them in the shader
`UiScalar` is `Rel` beside `Px` rather than two floats, so composing a
position down a chain of boxes adds exactly and rounds only at the two
multiplies `within` makes. `UiSpan`, `UiRegion` and `UiVec2` follow it, the
hand-written `Hash` goes away with the bits it hashed, and `impl_op!` grows a
`same` form for a type whose fields are not the same kind of number.

`Len` is still floats, so the seam converts: `Px::from_f32` where a span adds
a child's length to its cursor, and `to_f32` where something outside layout
wants pixels. Those go when `Len` follows.

The GPU reads what the CPU wrote: the instance attributes are `Sint32x2` and
the shader decodes by `1/64` and `1/2^24`, both exact in `f32`, then composes
the move chain in floats as before. It has to agree with itself frame to
frame rather than with the CPU to the last bit.

Two things fell out of making the numbers exact.

`floor` at the rasteriser was picking the pixel below wherever a fraction
divided a window exactly. A fifth of 1920 is 383.99998 through a rounded
`Rel` -- and was 384.0 through an `f32` that happened to round up -- so five
tabs each lost their last column. `snap_floor` takes a coordinate within half
a step of a boundary to be on it, which is the same rule as everywhere else
here: decide where values do not land.

A widget measured on one layer and drawn again on another kept the first
layer, because `try_reuse` compared everything about a retained drawing
except which list it sits in. `Stack` does exactly that for its background,
so every panel's text went under its own background. It only worked before
because the two asks differed by a rounding and forced a redraw;
`tests/retained.rs` pins it now, and `ReuseOutcome` can say `WrongLayer`.

Checked: fmt, clippy, 100 tests, 100 generated seeds in 70 s, all five
shrinker cases at 300 seeds. `tabs`, `view` and `minimal` render
byte-identical at 1920x1200; `random` differs in 36 pixels by one level;
`text` differs where glyph origins moved onto the grid -- same positions,
same spacing, different subpixel coverage, checked at 6x against the old
render.

Measured on the way: with the fuzzer comparing for *equality* rather than
within 0.05 px, `resize`, `repaint` and `size-change` already pass 100 seeds.
`reorder` fails one seed by exactly one step, which is the `Len` seam above.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 01:22:12 -04:00
iris-aiandClaude Opus 5 7548139861 Add a fixed-point number for layout to decide on
Layout reaches one place by more than one route -- a box composed down the
chain, and the same box summed from what its children asked for -- and the
two land a few bits apart in floats. Where that decides something structural
rather than something positional, a warm tree disagrees with a cold one:
`5ed9e87` is the instance, and its margin is a patch over the representation
rather than a fix to it.

`Fixed<SHIFT>` is a count of `1 / 2^SHIFT`s in an `i32`. Adding and
subtracting are exact, a multiply rounds once back onto the same steps, and
two routes that come within half a step land on the same number -- so the
comparisons downstream can ask for equality rather than for nearness.
`Px = Fixed<6>` and `Rel = Fixed<24>`: a sixty-fourth of a pixel is finer
than a display and still exact in `f32` up to 262,144 px, and twenty-four
bits of fraction matches `f32` at a half, beats it above one where anchors
sit, and leaves +/-128 of range to sum relative children in.

Nothing uses it yet. The arithmetic saturates rather than wrapping, because
a clamped coordinate keeps the ordering a wrapped one inverts, and the ends
are what an unbounded interval will be written with.

Checked: fmt, clippy, 99 tests including ten for this type -- the round trip
through `f32`, halves rounding away from zero either side, saturation at both
ends, and 20,000 additions landing exactly where the count says.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 00:34:01 -04:00