Carry the composed box down the draw, rather than walking back up for it

Every widget that reads its box in pixels was making `Moves` compose its
slot's chain again, a mean of 2.8 levels, about eight hundred times a frame.
A draw already descends past every one of those entries on its way in, so
`DrawInfo` carries what the slot composes to and `draw_at` steps it one box
further -- which is a select where it was a walk. `Moves::size_of` and
`compose` are left for `redraw`, which starts mid-tree with nothing above it
in flight.

Measured on the fixed-shape fixture, seed 1 depth 8, 500 frames of `many`,
medians of 25 runs, twenty-five work counters identical throughout:

| | instructions | cycles |
| --- | ---: | ---: |
| `d21a215`, before exact composition | 1,908M | 760M |
| `45a7176`, composing on the fine grid | 1,880M | 755M |
| this | **1,840M** | **735M** |

So exact composition ends up 3.6% fewer instructions and 3.3% fewer cycles
than the rounding-per-level walk it replaced, and the widening it needed was
paid for twice over by not doing the walk.

`Holds::through`'s allowance does not move: two half steps is where shrinker
seed 220 pins it, not where the arithmetic does. `Painter` still composes a
child's region into its own on the grid before asking for it in pixels, which
is the last narrow step in that path; taking it out needs the child's region
as its parent stated it, which `draw_inner` is not handed.

Checked: fmt, clippy, 83 suite tests, 17 core unit tests, the release oracle
at 100 seeds and at 1000 seeds of depth 6, all fifteen shrinker cases at 400
seeds of depth 5, and `tabs`, `view`, `minimal`, `text`, `random` and the tab
replay byte-identical at 1920x1200 against `45a7176`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
iris-aiandClaude Opus 5 committed 2026-09-16 21:49:25 -04:00
1 parent 45a717695b
commit 5f16617511
3 files changed
+92 -12

No files matched your search

+16 -5
View File
@@ -3,7 +3,7 @@ use crate::layout_diagnostics::{self as diag, Counter};
use crate::{
Axis, Holds, LayoutLen, Len, Px, PxVec2, RegionAlign, Rel, RenderedText, Size, StrongWidget,
TextAttrs, TextBuffer, TextData, TextureHandle, UiRegion, UiRenderState, UiRsc, UiVec2, Weight,
WidgetId, Widgets,
WideRegion, WidgetId, Widgets,
render::{
GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveHandle, PrimitiveInst,
PrimitiveKind, TexturePrimitive,
@@ -19,6 +19,9 @@ pub struct Painter<'a> {
/// This widget's box, in the coordinates of `move_idx`.
pub(super) region: UiRegion,
/// What this widget's slot composes to, so its own box and its children's
/// are a step further rather than a walk back up the chain.
pub(super) slot_wide: WideRegion,
pub(super) mask: MaskIdx,
pub(super) textures: Vec<TextureHandle>,
pub(super) primitives: Vec<PrimitiveHandle>,
@@ -196,6 +199,7 @@ impl<'a> Painter<'a> {
mask: self.mask,
offer,
offered_px: self.px_within_offer(offer),
slot_wide: self.slot_wide,
align: align_override,
},
None,
@@ -264,7 +268,7 @@ impl<'a> Painter<'a> {
if let Some(hint) = self.size_hint(child, axis) {
return Some(hint);
}
let px = self.state.px_of(self.move_idx, within);
let px = self.px_of(within);
let (size, holds) =
self.state
.retained_size(child.id(), px, self.move_idx, self.rsc.widgets())?;
@@ -301,6 +305,13 @@ impl<'a> Painter<'a> {
)
}
/// A box stated as a part of this widget's slot, in pixels.
fn px_of(&self, region: UiRegion) -> PxVec2 {
self.slot_wide
.select_size(&region)
.to_px(self.state.output_size())
}
fn depend_on<W: ?Sized>(&mut self, child: &StrongWidget<W>) {
if !self.size_deps.contains(&child.id()) {
self.size_deps.push(child.id());
@@ -388,7 +399,7 @@ impl<'a> Painter<'a> {
/// This widget's box in pixels. Reading it makes the drawing one that
/// holds for this box only, until `holds` says how far it goes.
pub fn px_size(&mut self) -> PxVec2 {
let px = self.state.px_of(self.move_idx, self.region);
let px = self.px_of(self.region);
for (own, len) in self.own.iter_mut().zip([px.x, px.y]) {
if *own == Holds::ANY {
*own = Holds::at(len);
@@ -400,7 +411,7 @@ impl<'a> Painter<'a> {
/// One axis of this widget's box in pixels. Prefer this to
/// [`Self::px_size`] when the other axis cannot affect the drawing.
pub fn px_len(&mut self, axis: Axis) -> Px {
let len = self.state.px_of(self.move_idx, self.region).axis(axis);
let len = self.px_of(self.region).axis(axis);
let own = &mut self.own[axis as usize];
if *own == Holds::ANY {
*own = Holds::at(len);
@@ -415,7 +426,7 @@ impl<'a> Painter<'a> {
pub fn holds(&mut self, axis: Axis, holds: impl Into<Holds>) {
let holds = holds.into();
debug_assert!(
holds.contains(self.state.px_of(self.move_idx, self.region).axis(axis)),
holds.contains(self.px_of(self.region).axis(axis)),
"'{}' ({:?}) says its drawing holds for lengths that leave out its own box",
self.label(),
self.id
+22 -7
View File
@@ -4,7 +4,7 @@ use crate::ui::painter::{declared_box, declared_lens, placed_box};
use crate::{
ActiveData, Axis, DrawLayers, Holds, IdLike, LayoutLen, Len, MaskIdx, MoveIdx, Moves, Painter,
PixelRegion, Px, PxVec2, RegionAlign, Rel, Size, StrongWidget, UiRegion, UiRsc, UiSpan, Weight,
WidgetId, Widgets,
WideRegion, WidgetId, Widgets,
util::{HashMap, Vec2},
};
@@ -24,6 +24,9 @@ pub(super) struct DrawInfo {
/// that box in pixels.
pub offer: UiRegion,
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,
/// A container's answer for where the widget sits. `None` uses the
/// widget's own property.
pub align: Option<RegionAlign>,
@@ -106,6 +109,7 @@ impl UiRenderState {
mask: MaskIdx::NONE,
offer: UiRegion::FULL,
offered_px: self.output_size,
slot_wide: self.moves.compose(self.root_move, UiRegion::FULL),
align: None,
}
}
@@ -276,29 +280,37 @@ impl UiRenderState {
old: Option<ActiveData>,
rsc: &mut dyn UiRsc,
) -> (Size, [Holds; 2]) {
let (move_idx, local, retired_move) = match info.region_node {
let (move_idx, local, retired_move, slot_wide) = 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.
// region's coordinates and its box is one entry to rewrite --
// and that box is what its contents compose through.
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)),
false => (
info.parent_move,
region,
self.slots.remove(&id),
info.slot_wide,
),
};
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 = self.px_of(move_idx, local);
let px = slot_wide.select_size(&local).to_px(self.output_size);
let at_offer = same_px(px, info.offered_px);
let mut painter = Painter {
state: self,
region: local,
slot_wide,
mask: info.mask,
layer: info.layer,
own_layer: info.layer,
@@ -332,6 +344,7 @@ impl UiRenderState {
state: _,
rsc: _,
region: _,
slot_wide: _,
mask,
textures,
primitives,
@@ -403,6 +416,7 @@ impl UiRenderState {
mask,
offer: UiRegion::FULL,
offered_px: px,
slot_wide,
align: None,
},
rsc,
@@ -515,7 +529,7 @@ impl UiRenderState {
{
return None;
}
let px = self.px_of(info.parent_move, region);
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))
}
@@ -616,7 +630,7 @@ impl UiRenderState {
// 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(self.px_of(info.parent_move, region)) {
if !active.holds_at(info.slot_wide.select_size(&region).to_px(self.output_size)) {
#[cfg(feature = "layout-diagnostics")]
{
diag::bump(Counter::ReuseOutside);
@@ -985,6 +999,7 @@ impl UiRenderState {
mask: active.mask,
offer: active.offer,
offered_px,
slot_wide: self.moves.compose(active.parent_move, UiRegion::FULL),
align: active.align_override.then_some(active.align),
};
let (was_answer, was) = (active.answer, (active.size, active.holds));