5fcace1bfaa9ded96ec49e4302cd0b5da30cd4c4
18
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
5fcace1bfa |
WIP: a widget's region stays put and its placement moves in it
The protocol split: `region` is the box a parent gives a widget -- what a fraction it declares or reports is a fraction of, and the coordinates every region it writes composes within -- and it is the same box on the ask that measures and the ask that places. `placement` is what of that region the drawing takes, chosen by the parent per axis or by the widget's own answer and alignment. That is what stops a fraction being resolved twice: the placing ask no longer hands the widget its own answer as its box, so nothing under it re-resolves against a box that came from its own report. `reports_of` and `decided` are gone, folded into the two regions; `box_of` is gone; `declared_box` becomes `ask_box`, which gives a rule the region's length and takes the position from the placement. 84 of 92 suite tests pass. Five text and region-node cases still diverge warm against cold, and three count a second widget draw where a span's measuring ask and its placing ask give different placements. |
||
|
|
e44dea34b4 |
Record which widget is drawing a subtree that changed hands
A subtree can be reused whole under a different parent -- same box, same layer, same region node, clean -- and nothing in the drawing says it moved. Two things read who its parent is, and both were wrong after one of these. The old parent still listed it as a child, and a parent's next draw undraws whatever is missing from that list: two spans under one root, with the root swapping which of them it holds, drew the subtree under the new span and then erased it when the old one drew. The move is recorded on both sides where `draw_inner` already writes what the ask decided, rather than guarded at each reader. Its depth was also the one it had under the old parent, which is what the settling walk orders by, so a change made under it afterwards settled at the wrong point in the frame. `try_reuse` re-walks the subtree's depths, and only where the top of it moved, which is what makes that free in the ordinary case. Two tests: one shape where the subtree's box does not move and the span it left erases it, one where it changes depth and the change made under it has to reach the span it moved to. Each fails without one half. |
||
|
|
4bd8607968 |
Report the step at or above a text's longest line
A wrapping text reported the width it used rounded to the nearest step, which is under the line it measured half the time. A parent that sizes itself from that report then hands the text back a box its own longest line does not fit in, and breaking there is a different break -- one line more. Two tolerances were hiding it and both go. `TextBuffer::shape` answered a width up to 0.05 px under the longest line from the break in hand, which is a structural decision taken on a hair's breadth: it kept a warm tree self-consistent while a cold tree at the same width broke differently, and 0.05 px is fifty steps of the grid. The `Holds` range the text declares started at the nearest step to its longest line for the same reason, so it admitted boxes the line does not fit in. Both are the line itself now, exactly, because the report no longer lands under it. Found by seeds 1121 and 1839 at depth 4, which fail on `ea6dbae` and every commit before it: a defect older than anything on this branch, reached by running 2000 seeds at a depth the long runs do not use. Shrunk to the eight widgets `a_text_is_given_back_a_box_the_line_it_measured_fits_in` builds. 2000 seeds at depth 4 over all fifteen cases are clean now, as are the three long runs. `text` is the one reference render that moves: its lower paragraph shifts a pixel, the box being a step wider and its left edge crossing a snap boundary. Same words, same lines, same breaks; `tabs`, `view`, `minimal` and `random` are byte-identical. |
||
|
|
ffd79f32d3 |
Read a child's report as a fraction of the containing widget
`rel(0.5)` is half the span whatever else is in it and wherever the child sits among them (Bryan, 2026-09-17). It was half of what the span had left at the point it asked, because a report came back composed through the box it was offered and a span offers each child the room from its cursor -- so a nested span taking half of what it was given took a quarter of a row whose first half was already spoken for, where the same half written as a rule on the child took half the row. The offer stays the remainder: a text has to wrap at the width actually there, and `a_text_in_a_span_wraps_at_the_room_left_rather_than_the_whole_row` pins that. What separates from it is the base a report's fractions are of, which the ask now carries. It is the box the child was given wherever that box is the child's whole area -- a pad's inset, a stack child, a scroll's content -- and a span passes its own extent along the row. `widget_decided` becomes `widget_at`, which says both things about an ask rather than one of them; `widget_within` is still the sugar for neither. Two spans asking for half each now take the whole row between them and a third overflows, which the rewritten `a_span_reads_a_child_report_as_a_fraction_of_the_row` states outright. The five reference renders are byte-identical at 1920x1200 and `random` live-resized still matches a cold render, so nothing that exists reports a fraction to a span today. |
||
|
|
ea6dbae0dc |
Hand a redrawn widget the mask it inherited, not its own
`ActiveData::mask` is the mask a widget's drawing is clipped to, which is either one it set itself or the one it inherited. `redraw` passed it back as the *inherited* mask, so a `Masked` widget settled on its own was handed its own mask and `set_mask` asserted -- a panic on any local redraw of one, for as long as there has been a local-redraw path. The two are separate facts, so `parent_mask` keeps the second. That also states the question `remap_subtree` was asking. It compared a widget's mask with the one threaded down from its parent to find out whether the widget owned it; the comparison is now between the two fields on the widget, which is the same question asked where the answer lives, and the parameter goes. Checked: fmt, clippy, 87 suite tests including the new one, which panics without this; 18 core unit tests; the release oracle at 100 seeds; the fifteen shrinker cases at 400 seeds of depth 5; and `tabs` renders byte-identical at 1920x1200. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
32542d0c0b |
Thread a box in pixels down the draw, one multiply from its parent's
A box in pixels was composed back up the move chain, on a grid fine enough that the walk rounded once, while a widget's offer was threaded down through its ancestors' offers. Two routes to one length, which is what `Holds::through` allowed for -- and the offer's route broke at a region node. `offered_region` fell back to `UiRegion::FULL` there, and `redraw` resolved that against the node's slot entry, which holds the box its parent *placed* the node in. Under a `Scroll` that is as long as the content rather than the viewport, so everything below was re-asked at a width its own answer had produced and the old answer confirmed itself: shrinker seed 220 on `reorder` left a widget 290px out. `ActiveData` now keeps a widget's box as lengths of its parent's box -- `given_len`, and `offer_len` for the box it was first asked about -- and `DrawInfo` carries the pixel lengths, threaded down one `Len::to_px` at a time: the box its parent gave it, then the part of that box its own answer placed the drawing in, which `placed_lens` states once for both `placed_box` and the walk. `Painter::px_size` and `px_len` read that value, and `UiRenderState::asked_px` takes the same steps back up the parent chain where a local redraw starts part-way down the tree. Neither chain has a coordinate frame in it, so neither can break at a region node, and warm and cold reach every length by the same expression. Three things follow. `Holds::through` is the exact preimage of `px + floor(rel * box)` -- two divisions, no allowance, the whole of a box mapping back to itself. A local redraw asks in the box its parent gave it and only where that box is as long as the offer, which retires `redraw`'s third ask and the region-node exception beside it; `draw_inner` places the answer inside that box itself. And symbolic regions are left to the GPU, hit testing and remaps, where `Moves::resolve` is the only walk: `wide.rs`, `Moves::compose`, `Moves::size_of`, `px_of`, `px_region`, `offered_region` and `slot_wide` are gone, 252 lines of `core/` net. `px` is deliberately not stored beside those lengths. A resize every widget's `Holds` admits redraws nothing, so a stored pixel length would be stale on every widget in the tree with nothing on it to say so, and refreshing it costs a walk down every reused subtree on the resize path. Instructions:u, medians of 21 runs, seed 1 at depth 8: | phase | before | after | | | --- | ---: | ---: | ---: | | `cold`, 200 frames | 313.1M | 312.9M | -0.04% | | `resize` | 408.1M | 405.6M | -0.61% | | `many` | 1,924M | 1,756M | -8.75% | | `scroll` | 357.3M | 323.4M | -9.49% | | `repaint` | 363.3M | 315.4M | -13.18% | `cold` and `resize` have all twenty-five work counters identical, so those two rows say the draw path costs the same threaded as composed. The other three do less work: `repaint` goes from 23 draw requests and 13 widget draws a frame to 1 and 1, `scroll` from 20 and 11 to 8 and 2, `many` from 273 and 186 to 207 and 157. Primitive writes are unmoved in every phase. Verified: `view`, `minimal`, `random`, `tabs` and `text` render byte-identical at 1920x1200 against `5b78002`, as does the `tabs` touch replay before and after the gesture, and a live resize of `random` to 1280x800 is identical both to the old head's and to a cold render at that size. The oracle passes 100 seeds in release and 120 in debug -- the debug run is the one that exercises the `Holds` assertion -- and the fifteen shrinker cases pass at 400 seeds of depth 5 and 1000 of depth 6. Seed 220 is `unsettled::a_widget_under_a_region_node_is_asked_in_the_box_that_node_was_offered`, which needs both halves of this to fail: the old chain with the old allowance passes it, and the old chain with the exact preimage does not. `AGREE_STEPS` stays 2. One step passes the 100-seed oracle and fails the 400-seed shrinker on `resize-size` by 0.002 px, so what is left there is the resize path re-expressing a part as a fraction of a box that changed length, not a length reached two ways. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
5b7800264d |
Read a child's answer in the asker's frame, and drop the root move entry
A widget reports a fraction of the box it was given. Span added that
fraction straight into a cursor that counts fractions of the row, and Pad
summed its padding onto it, both right only while the offer had the
parent's whole extent -- which a span's does not after a relative child.
DrawResult::size and known_len now compose the answer through the offer's
length, so a container reads lengths of its own box.
That exposed placed_box scaling a fractional answer against a box the
parent had already chosen from it, halving a nested span twice. The
near-edge alignment override becomes per-axis `decided` flags: a box the
parent chose from the answer is the answer, and is not placed again.
Span decides the row axis; Scroll and Stack's sizing child decide both.
Alignment is always the widget's own property now.
The window is no longer a move entry. Chains bottom out in MoveIdx::NONE
and the window is applied where a fraction becomes pixels, in to_px on the
CPU and by the uniform in the shader, which now snaps the summed coordinate
since a floor does not distribute over a sum. A resize rewrites no entry.
Verified: view, minimal, random, tabs and text render byte-identical at
1920x1200 against
|
||
|
|
e166e005dc |
Pin that a length in pixels is that many pixels
Asked of the `tabs` render: does a gap come out the same number of pixels wherever it appears? For a length in pixels it does, and structurally rather than by luck -- `Len::within` adds a part's own pixels rather than scaling them, and both ends of a gap carry the same fraction, so the multiply that rounds is the same on each and cancels. The test buries a row of five under three containers that are each a fraction of their parent, so nothing reaches the window without being composed and rounded, and checks every gap and every declared width at five box widths. Swept over 2,100 widths when it was written and exact at every one. For a share it does not, and the second test pins by how much rather than pretending otherwise: one or two steps between children that asked for the same fraction, 0.001 to 0.002 px. A position is the quantity that gets rounded so the row fills exactly and no two children leave a seam, and that is what costs it. Exact composition would shrink the spread, not remove it: five equal lengths cannot fill a row whose step count is not a multiple of five. Checked: fmt, clippy, 83 suite tests, 17 core unit tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
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> |
||
|
|
aea878d141 |
Place a locally redrawn widget in its box, not just at its length
A dirty widget is asked again in the box its parent asked it in, and then again in the box its parent chose from that answer. The second ask was skipped whenever the two boxes were the same *length*, which is not the same question: an offer as long as the final box but somewhere else is a different box. `d3b0ebf` already compared whole boxes for `parent_must_place` and left this one a length comparison, so the two halves of one decision disagreed. It shows on a region node, which draws the box it drew in into its own move entry. A scroll inside a scrolled span is offered the outer scroll's whole viewport and placed 24px above it, the height of the sized child the outer scroll snaps to the end of; redrawing only its text left it at the offer and 24px too low. `tests/cases/unsettled.rs` had that five-widget tree ignored as a known defect and now runs it. `px_region` names the walk both comparisons and `window_region` were writing out. Checked: fmt, clippy, 80 tests, the release oracle at 100 seeds, all fifteen shrinker cases at 400 seeds of depth 5, and `tabs`, `view`, `minimal`, `text`, `random` plus the tab replay byte-identical at 1920x1200 against `98d4e98`. The `many` fixture's twenty-five counters are unchanged. Fixed with it, from the handoff's unreduced leads: shrinker seeds 174 and 175 on `repaint-some` and seed 2 on `region-node`, and oracle seeds 18 and 190 at depth 6. Still failing: shrinker seed 288 on `region-node`, and oracle seed 326 at depth 6, which reduces to 43 widgets around two `Branch`es and is not this. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
98d4e98a29 |
Describe a tree before building it, so a failing seed can be reduced
The oracle grew its trees from a seed and the shrinker grew its own, with every scenario written out on each side. So a failure the oracle found could not be handed to the shrinker: there was no tree to pass it, only a seed, and a seed cannot be made smaller. The shrinker could only grow its own trees and hope to meet the same shape, which it does not -- 20,000 of its trees never reproduced what the oracle's seed 18 shows at depth 6. `iris::random` now answers with a `Plan`: `plan(seed, depth, &edits)` draws one out of the random stream and `build(rsc, &plan)` makes the widgets, where `grow` did both at once. Every draw happens in the order it always has, so a seed still means the tree it meant -- checked by running the oracle at 1000 seeds of depth 6 before and after and getting the same three failures with the same boxes. `Plan::smaller` reduces one, `Plan::edited` applies an `Edits` to a tree that already exists, and `tests/scenario/` holds the fifteen cases both rigs now run over the same trees. A span keeps the order it holds its children in apart from the children themselves, so detaching, attaching and reordering leave the widgets made in the same order and two builds still line up index for index. `Tree::detached` is gone: `Spanned::spares` is everything made for a span that it does not hold, which is what both of those were. `tests/cases/plan.rs` pins the three properties the rest rests on: editing a plan is growing one with those edits, every simplification is smaller than what it came from, and reducing ends. The second caught this change's own defect, where dropping a side of a `Branch` duplicated another and grew the tree by four widgets. What it found, on its first run: `SHRINK_SEED=18 SHRINK_DEPTH=6 SHRINK_CASE=repaint-some` reduces 277 widgets to 5. A scroll inside a scroll, the inner one owning a movable region, and only the text at the bottom marked for redraw -- and the span lands 24px out, which is exactly the sized child's height. `git bisect` names `95fb4f9`, where `Masked` began reporting its box rather than its inner's size, so what the outer scroll is told its content measures now depends on whether the inner subtree was redrawn this frame. `tests/cases/unsettled.rs` has it written out, ignored until it is fixed. Checked: fmt, clippy over all targets with -D warnings, the workspace tests (79 + 11 + 15, one ignored for the defect above), and the 100-seed oracle over all fifteen cases at depth 4. The shrinker at 400 seeds of depth 5 now fails, which it did not before running the oracle's trees and cases: seeds 2 and 288 on region-node and 174 and 175 on repaint-some are unreduced leads. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
97cc8b32ed |
Measure a child on the layer it draws on, not twice on two
Fixed point cost 3x in layout: `many` went from 0.179 ms a frame to 0.544, and `scroll` from 0.011 to 0.030. The counters said why -- eight more "placed by redrawing" a frame -- and the reason was mine rather than the grid's. A retained drawing belongs to the layer it was made on, which `4e28f10` started enforcing, and `Stack` measures the child that sizes it by drawing it on its own layer and then draws it again on the child layer. So every stacked child redrew twice a frame, forever. `Painter::child_layer_at` addresses a child's layer rather than walking to it, and `Stack` measures on the layer that child ends up on. The second ask is then a reuse. Its glyphs are written once rather than once under the background and once over it. Measured on the same fixture: `scroll` 0.031 ms to 0.020, `many` 0.570 to 0.283, and the scroll phase's counters are back to what they were before fixed point -- 4 widget draws and 12 draw requests a frame, exactly. What is left above that baseline is not this. `ReuseOutcome` could not say "another layer" or "the region-node choice changed"; both returned without a counter, which is why the first look at this said nothing. They have counters now. Checked: fmt, clippy, 105 tests, five shrinker cases at 300 seeds, 100 generated seeds, and the examples byte-identical but for 36 pixels of `random` at one level -- edges that were being drawn twice. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
95fb4f962c |
Hold a clipping widget to its box, and check that it is
`Scroll` reports `LEFTOVER` on both axes because it clips its content to its box: it can neither take less of one nor honestly ask for more. `Masked` is the other widget that clips and was passing its inner's size up, so a mask over something taller than its box asked to be placed at the length it had just cut off. It reports its box now, for the same reason. The `debug_assert` the handoff has been asking for is the one that would have caught both, narrowed to what is actually true: a widget that set a mask this draw has to report inside the box it drew in. Reported as "does not exceed the box" it fires on ordinary overflow instead -- measured, a hundred fuzzer trees produce thousands of them, every one a text too tall for the box it was offered, which is what a text is meant to say. `tests/cases/scroll.rs` has a clipping widget that reports its content, to show the assertion catches it. Checked: fmt, clippy, 104 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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |