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>
This commit is contained in:
iris-aiandClaude Opus 5 committed 2026-09-17 00:12:56 -04:00
1 parent 5b7800264d
commit 32542d0c0b
9 files changed
+399 -557

No files matched your search

+153 -165
View File
@@ -1,9 +1,9 @@
#[cfg(feature = "layout-diagnostics")]
use crate::layout_diagnostics::{self as diag, Counter, ReuseOutcome, TimerKind};
use crate::ui::painter::{declared_box, declared_lens, fills, placed_box};
use crate::ui::painter::{declared_box, declared_lens, placed_box, placed_lens};
use crate::{
ActiveData, Axis, DrawLayers, Holds, IdLike, LayoutLen, Len, MaskIdx, MoveIdx, Moves, Painter,
PixelRegion, Px, PxVec2, Rel, Size, StrongWidget, UiRegion, UiRsc, UiSpan, Weight, WideRegion,
PixelRegion, Px, PxVec2, Rel, Size, StrongWidget, UiRegion, UiRsc, UiSpan, UiVec2, Weight,
WidgetId, Widgets,
util::{HashMap, Vec2},
};
@@ -20,13 +20,16 @@ pub(super) struct DrawInfo {
pub parent_move: MoveIdx,
pub region_node: bool,
pub mask: MaskIdx,
/// The box it was first asked about in, as a part of its parent's, and
/// that box in pixels.
pub offer: UiRegion,
/// The box its parent gave it, as lengths of the parent's own box, and
/// the lengths of the box it was first asked about in the same form.
/// Both describe the box the *parent* stated, so the second, placing ask
/// carries them unchanged while its own region is the placement inside.
pub given_len: UiVec2,
pub offer_len: UiVec2,
/// This ask's box in pixels, and the offer's: one multiply from the
/// parent's own, which is where every pixel length in layout comes from.
pub px: PxVec2,
pub offered_px: PxVec2,
/// The box `parent_move` composes to, on the fine grid, so a widget's own
/// box is one step further and not a walk back up the chain.
pub slot_wide: WideRegion,
/// The axes along which the parent chose this box from the widget's own
/// answer, so the answer is not placed inside it again. See
/// [`Painter::widget_decided`].
@@ -85,7 +88,13 @@ impl UiRenderState {
self.resized = true;
}
fn root_info(&self) -> DrawInfo {
/// The root is asked about in the output: the window is where a fraction
/// becomes pixels rather than a box of its own, so the root's box is the
/// first length threaded down. Its own rules narrow that box, and where
/// they do the narrowed box is also the offer -- nothing above it chose
/// anything else.
fn root_info(&self, region: UiRegion) -> DrawInfo {
let px = region.size().to_px(self.output_size);
DrawInfo {
layer: 0,
parent: None,
@@ -93,9 +102,10 @@ impl UiRenderState {
parent_move: MoveIdx::NONE,
region_node: false,
mask: MaskIdx::NONE,
offer: UiRegion::FULL,
offered_px: self.output_size,
slot_wide: WideRegion::of(UiRegion::FULL),
given_len: region.size(),
offer_len: UiVec2::FULL_SIZE,
px,
offered_px: px,
decided: [false; 2],
}
}
@@ -136,8 +146,8 @@ impl UiRenderState {
// length, found the way every other box change is found. Before
// anything dirty settles, so that whatever a new output draws
// again is drawn once, in the box it will have.
let info = self.root_info();
let region = Self::root_region(root.id(), rsc.widgets());
let info = self.root_info(region);
let answer = self.draw_inner(root.id(), region, info, None, rsc);
self.active.get_mut(&root.id()).unwrap().answer = answer;
}
@@ -154,8 +164,8 @@ impl UiRenderState {
let _layout = diag::timer(TimerKind::FullLayout);
self.clear(rsc);
if let Some(id) = root {
let info = self.root_info();
let region = Self::root_region(id.id(), rsc.widgets());
let info = self.root_info(region);
self.draw_inner(id.id(), region, info, None, rsc);
}
}
@@ -179,13 +189,7 @@ impl UiRenderState {
#[cfg(feature = "layout-diagnostics")]
{
diag::bump(Counter::DrawRequests);
diag::draw_request(
id,
info.parent,
region,
self.px_of(info.parent_move, region),
info.region_node,
);
diag::draw_request(id, info.parent, region, info.px, info.region_node);
}
let align = rsc.widgets().alignment(id);
let replace_answer = self.answer_invalid.remove(&id)
@@ -195,7 +199,7 @@ impl UiRenderState {
let retained = match replace_answer {
true => None,
false => self
.retained_answer(id, region, info, rsc.widgets())
.retained_answer(id, info, rsc.widgets())
.or_else(|| self.try_reuse(id, region, info, rsc)),
};
let answer = retained.unwrap_or_else(|| {
@@ -208,42 +212,33 @@ impl UiRenderState {
let declared = declared_lens(rsc.widgets(), id);
// The second, final ask is in a box chosen from the answer on both
// axes, which is also what makes it terminate.
let placed = placed_box(region, answer.0, align, declared, info.decided);
let lens = placed_lens(answer.0, declared, info.decided);
let placed = placed_box(region, lens, align);
let placed_info = DrawInfo {
px: lens.to_px(info.px),
decided: [true; 2],
..info
};
// The symbolic box can be unchanged while its parent slot changed
// pixel size. Reuse checks the resolved box even in that case.
if self.try_reuse(id, placed, placed_info, rsc).is_none() {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::PlaceRedraws);
let old = self.remove(id, false, rsc);
self.draw_at(id, placed, placed_info, old, rsc);
}
self.place(id, placed, placed_info, rsc);
// The answer is only reusable while both parts of the operation are:
// what the widget reported in the offered box, and what it drew in
// the box its report selected. Express the latter's contract back in
// terms of the offered box before handing it to the parent.
// what the widget reported in the box it was asked in, and what it
// drew in the box its report selected. Express the latter's contract
// back in terms of the box asked in before handing it to the parent.
let drawing_holds = self.active[&id].holds;
let mut settled = answer;
for axis in AXES {
let reported = answer.0.axis(axis);
let placed_len = match fills(
reported,
declared[axis as usize],
info.decided[axis as usize],
) {
true => Len::FULL,
false => Len::from_parts(reported.rel, reported.px),
};
settled.1[axis as usize] =
settled.1[axis as usize].and(drawing_holds[axis as usize].through(placed_len));
settled.1[axis as usize].and(drawing_holds[axis as usize].through(lens.axis(axis)));
}
let active = self.active.get_mut(&id).unwrap();
active.offer = info.offer;
// Whoever asked owns how the box was reached: the box it stated, and
// what of that box the answer then took. A local redraw asks the
// same question again from these.
active.given = region;
active.given_len = info.given_len;
active.offer_len = info.offer_len;
active.answer = settled;
active.decided = info.decided;
active.own_align = align;
@@ -251,6 +246,19 @@ impl UiRenderState {
settled
}
/// Draws a widget in the final box its answer chose, reusing the drawing
/// already there where its retained contract holds for that box. The
/// symbolic box can be unchanged while the box it sits in changed pixel
/// length, so what reuse checks is the box in pixels.
fn place(&mut self, id: WidgetId, placed: UiRegion, info: DrawInfo, rsc: &mut dyn UiRsc) {
if self.try_reuse(id, placed, info, rsc).is_none() {
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::PlaceRedraws);
let old = self.remove(id, false, rsc);
self.draw_at(id, placed, info, old, rsc);
}
}
/// Calls a widget's `draw` and keeps what it drew in `region`.
fn draw_at(
&mut self,
@@ -260,37 +268,34 @@ impl UiRenderState {
old: Option<ActiveData>,
rsc: &mut dyn UiRsc,
) -> (Size, [Holds; 2]) {
let (move_idx, local, retired_move, slot_wide) = match info.region_node {
let (move_idx, local, retired_move) = match info.region_node {
// Its box becomes its movable region, so it draws in that
// region's coordinates and its box is one entry to rewrite --
// and that box is what its contents compose through.
// region's coordinates and its box is one entry to rewrite.
true => (
self.move_slot(id, info.parent_move, region),
UiRegion::FULL,
None,
info.slot_wide.select(&region),
),
// Keep the old entry alive until every descendant has migrated.
// Reusing its index sooner could make an old parent look current.
false => (
info.parent_move,
region,
self.slots.remove(&id),
info.slot_wide,
),
false => (info.parent_move, region, self.slots.remove(&id)),
};
let (old_children, old_answer) = match old {
Some(old) => (old.children, Some(old.answer)),
None => (Vec::new(), None),
};
rsc.widgets_mut().needs_redraw.remove(&id);
let px = slot_wide.select_size(&local).to_px(self.output_size);
let at_offer = same_px(px, info.offered_px);
// A box of the offered lengths asks the offer's question wherever it
// sits, since what a drawing depends on is its lengths -- and
// equality is the comparison, these being counts of a step rather
// than floats to be compared for nearness.
let px = info.px;
let at_offer = px == info.offered_px;
let mut painter = Painter {
state: self,
region: local,
slot_wide,
px,
mask: info.mask,
layer: info.layer,
own_layer: info.layer,
@@ -324,7 +329,7 @@ impl UiRenderState {
state: _,
rsc: _,
region: _,
slot_wide: _,
px: _,
mask,
textures,
primitives,
@@ -394,9 +399,10 @@ impl UiRenderState {
parent_move: move_idx,
region_node: false,
mask,
offer: UiRegion::FULL,
given_len: UiVec2::FULL_SIZE,
offer_len: UiVec2::FULL_SIZE,
px,
offered_px: px,
slot_wide,
decided: [false; 2],
},
rsc,
@@ -408,7 +414,12 @@ impl UiRenderState {
let active = ActiveData {
id,
region,
offer: info.offer,
// The box a placing ask draws in is a part of the one its parent
// gave, which `draw_inner` writes back over these once the
// placement is done.
given: region,
given_len: info.given_len,
offer_len: info.offer_len,
// Whoever asked writes the answer, if this was the asking.
answer: old_answer.unwrap_or((size, holds)),
size,
@@ -453,18 +464,6 @@ impl UiRenderState {
}
}
/// The pixel size of a region held in `slot`'s coordinates.
pub(super) fn px_of(&self, slot: MoveIdx, region: UiRegion) -> PxVec2 {
self.moves.size_of(slot, region).to_px(self.output_size)
}
/// Where a region held in `slot`'s coordinates lands on screen, to
/// compare one box against another. Both ends of it, where
/// [`Self::px_of`] wants only the length between them.
fn px_region(&self, slot: MoveIdx, region: UiRegion) -> PixelRegion {
self.moves.compose(slot, region).to_px(self.output_size)
}
/// A clean widget's retained answer, if that answer holds for a box of
/// `px`. This does not move its drawing, which may already be in the box
/// that answer placed it in.
@@ -493,7 +492,6 @@ impl UiRenderState {
fn retained_answer(
&self,
id: WidgetId,
region: UiRegion,
info: DrawInfo,
widgets: &Widgets,
) -> Option<(Size, [Holds; 2])> {
@@ -508,9 +506,8 @@ impl UiRenderState {
{
return None;
}
let px = info.slot_wide.select_size(&region).to_px(self.output_size);
let (size, holds) = active.answer;
(holds[0].contains(px.x) && holds[1].contains(px.y)).then_some((size, holds))
(holds[0].contains(info.px.x) && holds[1].contains(info.px.y)).then_some((size, holds))
}
/// Whether anything whose size this widget's own size was read from is
@@ -525,31 +522,39 @@ impl UiRenderState {
})
}
/// The first box a widget was asked about, re-expressed in the coordinate
/// space its drawing uses. Keeping the relative box and composing it
/// again avoids rebuilding a shifted box from rounded pixel lengths.
fn offered_region(&self, id: WidgetId) -> UiRegion {
/// The pixel lengths of the box a widget was given and of the box it was
/// first asked about, which is what a local redraw needs to ask the
/// question its parent asked.
///
/// Both are threaded down from the window a length of a box at a time,
/// and this takes the same steps back up: a widget's box is a length of
/// the box its parent drew in, and its offer a length of the box its
/// parent was itself offered. Neither chain has a coordinate frame in it,
/// so neither breaks at a region node -- and both land on the numbers a
/// cold layout computes, rather than near them.
fn asked_px(&self, id: WidgetId) -> (PxVec2, PxVec2) {
let active = &self.active[&id];
let parent_region = match active.parent.and_then(|id| self.active.get(&id)) {
Some(parent) if parent.move_idx == active.parent_move => {
if parent.move_idx == parent.parent_move {
self.offered_region(parent.id)
} else {
UiRegion::FULL
}
// Nothing above the root: the window is where a fraction becomes
// pixels, which is also the whole of the box the root is given.
let (parent_px, parent_offer) = match active.parent.and_then(|p| self.active.get(&p)) {
Some(parent) => {
let (given, offer) = self.asked_px(parent.id);
let lens = placed_lens(parent.answer.0, parent.declared, parent.decided);
(lens.to_px(given), offer)
}
_ => UiRegion::FULL,
};
let mut offered = match active.offer == UiRegion::FULL {
true => parent_region,
false => active.offer.within(&parent_region),
None => (self.output_size, self.output_size),
};
let px = active.given_len.to_px(parent_px);
let mut offered = active.offer_len.to_px(parent_offer);
for axis in AXES {
// A declared length is resolved by whoever drew the widget, in
// the box that widget drew in, so the box it has is the box it
// was asked about however the offer above it moved.
if active.declared[axis as usize].is_some() {
*offered.axis_mut(axis) = *active.region.axis(axis);
*offered.axis_mut(axis) = px.axis(axis);
}
}
offered
(px, offered)
}
/// Reuses the actual drawing in a new box if its retained contract holds
@@ -606,10 +611,10 @@ impl UiRenderState {
}
return None;
}
// In pixels, because `region` is a fraction of a slot's box and that
// box may be what changed -- an unchanged fraction of a box half the
// size is half the widget.
if !active.holds_at(info.slot_wide.select_size(&region).to_px(self.output_size)) {
// In pixels, because `region` is a fraction of the box its parent
// drew in and that box may be what changed -- an unchanged fraction
// of a box half the size is half the widget.
if !active.holds_at(info.px) {
#[cfg(feature = "layout-diagnostics")]
{
diag::bump(Counter::ReuseOutside);
@@ -634,7 +639,9 @@ impl UiRenderState {
}
let active = self.active.get_mut(&id).unwrap();
active.region = region;
active.offer = info.offer;
active.given = region;
active.given_len = info.given_len;
active.offer_len = info.offer_len;
active.depth = info.depth;
#[cfg(feature = "layout-diagnostics")]
{
@@ -671,6 +678,7 @@ impl UiRenderState {
rsc: &mut dyn UiRsc,
) {
let active = self.active.get_mut(&id).unwrap();
active.given = remap.apply(active.given);
if active.move_idx != parent_move {
let region = remap.apply(active.region);
active.region = region;
@@ -766,7 +774,9 @@ impl UiRenderState {
ActiveData {
id,
region: UiRegion::FULL,
offer: UiRegion::FULL,
given: UiRegion::FULL,
given_len: UiVec2::FULL_SIZE,
offer_len: UiVec2::FULL_SIZE,
answer: (size, [Holds::ANY; 2]),
size,
holds: [Holds::ANY; 2],
@@ -898,12 +908,19 @@ impl UiRenderState {
}
/// Where a widget is on screen: its box composed through the boxes it
/// sits within. `None` for one that is not drawn.
/// sits within, the same walk the vertex shader does. `None` for one that
/// is not drawn.
///
/// This is for asking where a drawing landed: hit testing, and a test
/// reading a box back. Layout decides on the lengths threaded down the
/// draw instead, and a position is not one of its inputs.
pub fn window_region(&self, id: &impl IdLike) -> Option<PixelRegion> {
let active = self.active.get(&id.id())?;
active
.drawn
.then(|| self.px_region(active.parent_move, active.region))
active.drawn.then(|| {
self.moves
.resolve(active.parent_move, active.region)
.to_px(self.output_size)
})
}
/// Settles a dirty widget: asks it again where its parent asked, and
@@ -940,28 +957,28 @@ impl UiRenderState {
if !active.drawn {
return;
}
let region = active.region;
let region_node = active.parent.is_some() && rsc.widgets().is_region_node(id);
let asked_in = match active.parent {
Some(_) => self.offered_region(id),
None => Self::root_region(id, rsc.widgets()),
// Nothing above the root resolved its rules or its alignment, so its
// box is its own to work out again against the output. Every other
// widget was given one.
let Some(parent) = active.parent else {
let region = Self::root_region(id, rsc.widgets());
let info = DrawInfo {
mask: active.mask,
..self.root_info(region)
};
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::LocalRedraws);
let old = self.remove(id, false, rsc);
self.draw_inner(id, region, info, old, rsc);
return;
};
let offered_px = self.px_of(active.parent_move, asked_in);
// Whole boxes rather than lengths: an offer as long as the final box
// but somewhere else is a different box, and a region node drawing at
// its offer writes the box it drew in into its own entry.
let at_offer = same_pixel_region(
self.px_region(active.parent_move, region),
self.px_region(active.parent_move, asked_in),
);
let decided = active.decided.contains(&true);
let parent_must_place = active.parent.is_some() && (!region_node || decided) && !at_offer;
// An independently positioned region node can redraw at its offer
// and move its slot to its own placement. Every other widget needs
// its parent to reproduce a different final position.
if let Some(parent) = active.parent
&& parent_must_place
{
let (given_px, offered_px) = self.asked_px(id);
// Asked again in the box its parent gave it, which is the question
// its parent asked only while that box is as long as the offer. Any
// other box is a different question, so the parent asks it, with the
// mark left on. Lengths and not whole boxes: what a drawing depends
// on is its lengths, so the same lengths elsewhere is one question.
if given_px != offered_px {
rsc.widgets_mut().needs_redraw.insert(id);
self.redraw(parent, rsc);
rsc.widgets_mut().needs_redraw.remove(&id);
@@ -972,50 +989,31 @@ impl UiRenderState {
parent: active.parent,
depth: active.depth,
parent_move: active.parent_move,
region_node,
region_node: rsc.widgets().is_region_node(id),
mask: active.mask,
offer: active.offer,
given_len: active.given_len,
offer_len: active.offer_len,
px: given_px,
offered_px,
slot_wide: self.moves.compose(active.parent_move, UiRegion::FULL),
decided: active.decided,
};
let (was_answer, was) = (active.answer, (active.size, active.holds));
let (given, was_answer) = (active.given, active.answer);
#[cfg(feature = "layout-diagnostics")]
diag::bump(Counter::LocalRedraws);
let old = self.remove(id, false, rsc);
let answer = self.draw_inner(id, asked_in, info, old, rsc);
self.active.get_mut(&id).unwrap().answer = answer;
let Some(parent) = info.parent else {
return;
};
// `draw_inner` places the answer inside that box itself, which is the
// ask that leaves the widget where its parent put it.
let answer = self.draw_inner(id, given, info, old, rsc);
if answer != was_answer {
// Left where it was asked: the parent lays out again and chooses
// its final box.
// Its parent chose its box knowing the old answer, so it lays out
// again and chooses the box the new one asks for.
#[cfg(feature = "layout-diagnostics")]
{
diag::bump(Counter::SizeChanges);
diag::bump(Counter::ReaderEdges);
}
rsc.widgets_mut().needs_redraw.insert(parent);
return;
}
if at_offer {
return;
}
// Then in the final box its parent chose from that answer. That box
// is already placed, so the near edge goes with it: applying the
// widget's own alignment to it again would place its content twice,
// the way it did for a region node under a `Stack` once the stack
// stopped overriding every child's alignment.
let placed_info = DrawInfo {
decided: [true; 2],
..info
};
self.draw_inner(id, region, placed_info, None, rsc);
let active = &self.active[&id];
if (active.size, active.holds) != was {
rsc.widgets_mut().needs_redraw.insert(parent);
}
}
}
@@ -1029,16 +1027,6 @@ fn within_box(size: Size, px: PxVec2, axis: Axis) -> bool {
len.leftover != Weight::ZERO || box_len.mul(len.rel) + len.px <= box_len
}
/// The same box is the same number of steps, both of these being lengths on
/// the grid rather than floats to be compared for nearness.
fn same_px(a: PxVec2, b: PxVec2) -> bool {
a == b
}
fn same_pixel_region(a: PixelRegion, b: PixelRegion) -> bool {
same_px(a.top_left, b.top_left) && same_px(a.bot_right, b.bot_right)
}
/// A retained region rewritten from one parent box into another. A fixed
/// source extent can be translated but cannot recover fractions for a resize.
#[derive(Clone, Copy)]