Commit Graph
6 Commits
Author SHA1 Message Date
iris-aiandClaude Opus 5 2bc6bdfc77 Let a child with room to move use its own alignment
`Stack` and `Pad` forced the near edge on every child. That override exists
so a container that reports a child's size and then hands it the box derived
from that report does not place its content twice -- and it is owed only
where the box really is the child's own answer.

`Stack` gives every child the box its sizing child defines. That box is
`box_of(child.size())`, so the sizing child has no room in it and needs the
override; every other child is handed a box that owes nothing to it, and
where it sits in one bigger than itself is its own business. With the
override it could not be aligned at all.

`Pad` reports its inner's size plus the padding, so where its box is that
answer the inset box is exactly the inner and alignment has nowhere to move
it. Where the box is bigger -- a share of a row, a rule over the pad -- the
slack belongs to the inner, and the override pinned it to a corner.

The `tabs` example is the visible case both ways: its counters asked for
`Align::RIGHT` inside a stack and sat at the left, and `text`'s narrow panel
filled a row it had asked to sit at the top of. Both match canonical `main`
again. Neither was noticed when `d3b0ebf` made alignment a property, and the
handoff's claim that `tabs` then "differs only in the widget count it prints
about itself" was wrong -- it was checked at `8220a78` and not re-checked
after the next commit.

Checked: fmt, clippy, 81 suite tests, 17 core unit tests, the release oracle
at 100 seeds and at 1000 seeds of depth 6, and all fifteen shrinker cases at
400 seeds of depth 5. `tabs`, `text` and `random` change exactly where a
child now honours its own alignment; `view` and `minimal` are unchanged.

`tabs` is still not `main`'s render: `.sized((100, 100)).center().width(
leftover(2))` on one widget no longer means a square centred in a two-share
box, because one widget carries one length per axis and `.width` overwrites
what `.sized` set. That one is an API question, not a bug, and is open.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 18:03:09 -04:00
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 Opus 5 a8898aaa54 Give a length with no share in it its own type again
`UiScalar` was `Len` without the `leftover` weight, which is the separation
canonical `main` already had as `Len` beside `LayoutLen` and this branch
collapsed. It is needed back for the queued clamp: a cap may not contain a
share, because a cap has to read the report a rule otherwise makes moot, and
a share puts the container's division into the same equation -- two
self-consistent assignments, which is the multiple-fixed-point failure
generated seed 13 punished for orthogonal sizing. `min(report, cap)` is not
a `LayoutLen` either: it is a sum of parts, and the smaller of two of them
is not one.

So `UiScalar` is `Len`, what was `Len` is `LayoutLen`, and the two say in
their docs which is which: a `Len` is pixels plus a fraction of a box -- a
position being the length from the box's start, which is why a span is two
of them -- and a `LayoutLen` is a `Len` plus a claim only a container
dividing its room can answer. `From<Len> for LayoutLen` is the one-way step
between them.

Names only; the shader's `UiScalar` is renamed with them. Checked: fmt,
clippy, 105 tests, and `tabs`, `minimal`, `view`, `text` and `random`
byte-identical at 1920x1200.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 13:40:15 -04:00
iris-aiandClaude Opus 5 bdab55824f Take two roundings out of where a box comes from
Traced what was left of the warm-against-cold difference after fixed point.
It is not accumulation and not one place: it is the same box reached two
ways, and each way rounds where the other does not.

`Scroll` was writing a box it had been given back out as its own length in
pixels. That is the same box in another form, and centring a part in `rel 1`
lands a step from centring it in `px 900`, because halving a difference is
not halving each part of it. Content that fills the viewport and has not been
scrolled is now handed back as it came, which makes the shrinker's `repaint`
and `resize-repaint` cases agree exactly rather than within a step.

`Span` placed each child a step from where the last one ended, so the
rounding of every share was carried along the row. A position is now the
fixed parts before it -- a sum, exact -- plus one rounded share of the room.
Measured: two hundred equal shares of a 1000 px row ended at 999.999 and now
end at 1000, and `tests/cases/layout.rs` pins it at 2, 3, 7, 64 and 200.

What is left is a step per level of nesting between the two ways, which is
what the fuzzers now allow: four of the five shrinker cases pass at one step
and the fifth is five spans deep. Closing it needs one way of asking where a
box is, which is a bigger change than this.

Checked: fmt, clippy, 103 tests, all five shrinker cases at 300 seeds, 100
generated seeds, five examples byte-identical at 1920x1200.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 03:42:09 -04:00
iris-aiandClaude Opus 5 9d8415d65f Delete OrthoSize, and run the seeds in parallel
A span is as long across itself as its longest child, unless a rule beside it
already says how long it is -- and then reading the children answers nothing
and only makes its size depend on theirs. `OrthoSize::Full` was that second
case written twice, once as an enum on the span and once as the rule that
actually decides; `Painter::ruled` lets the span ask which it is in. The
widget under a rule still does not learn what the rule says, only that its
answer for that axis is not wanted.

The fuzzers grow, lay out and drop a tree within one seed, so the seeds share
nothing and take a thread each, one short of every core. Measured here: the
generated oracle's hundred seeds went from 68 s to 10 s, and a shrinker case
at 300 seeds from 18 s to 3.5 s. A seed that fails still shrinks and panics
on its own thread, and `std::thread::scope` carries that out.

The shrinker now allows the two steps the oracle already did -- the deeper
trees these grow reach a second composition, and a step is a thousandth of a
pixel.

Checked: fmt, clippy, 102 tests, all five shrinker cases at 300 seeds, 100
generated seeds, and five examples byte-identical at 1920x1200.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 03:18:49 -04:00
iris-aiandClaude Opus 5 cb955f1023 Link the ordinary tests once, and keep their debug info to line tables
Eleven `tests/*.rs` were eleven binaries, each linking the whole graph --
`wgpu` and all -- to run a handful of cases. They are modules of one target
now, under `tests/cases/`, and `cargo test --test suite layout::` still picks
one out. The fuzzers and the `*_cost` measurements stay their own targets:
they are run on their own and want to be selectable without building the
rest.

`profile.test` takes `debug = "line-tables-only"`, which is what a backtrace
here actually reads; the type and variable information was the bulk of what
the linker was writing.

Measured on this machine, rebuilding `iris`'s test targets after a change to
the crate: 14.3 s before, 9.8 s with one target, 7.7 s with both. `target/`
went from 45 GB to 13 GB. The suite still passes 102 tests, and the binary
still carries `.debug_line`.

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