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

+54
View File
@@ -109,6 +109,44 @@ impl WideSpan {
} }
} }
impl WideSpan {
/// A box given as a part of this one: the same composition carried one
/// level further, with the part on the ordinary grid and the frame it
/// lands in already on the fine one. That is the way round a draw
/// descends -- a widget states its child's box as a part of its own --
/// so composing this way never puts an intermediate back on the grid.
pub const fn select(self, part: &UiSpan) -> Self {
Self {
start: self.end_at(part.start),
end: self.end_at(part.end),
}
}
/// How long such a part is, in half the multiplies its two ends cost:
/// where this box sits falls out of the difference.
pub const fn select_len(self, part: &UiSpan) -> WideLen {
let len = part.len();
let rel_span = (self.end.rel - self.start.rel) as i128;
let px_span = (self.end.px - self.start.px) >> PX_GAIN;
WideLen {
rel: ((rel_span * len.rel.raw() as i128) >> REL_SHIFT) as i64,
px: ((len.px.raw() as i64) << PX_GAIN)
+ ((px_span * len.rel.raw() as i64) >> (REL_SHIFT - PX_GAIN)),
}
}
const fn end_at(self, at: Len) -> WideLen {
let rel_span = (self.end.rel - self.start.rel) as i128;
let px_span = (self.end.px - self.start.px) >> PX_GAIN;
WideLen {
rel: self.start.rel + ((rel_span * at.rel.raw() as i128) >> REL_SHIFT) as i64,
px: self.start.px
+ ((at.px.raw() as i64) << PX_GAIN)
+ ((px_span * at.rel.raw() as i64) >> (REL_SHIFT - PX_GAIN)),
}
}
}
/// How long a box is on each axis, part-way through a composition. What /// How long a box is on each axis, part-way through a composition. What
/// reads a box in pixels almost always wants this and not where it sits. /// reads a box in pixels almost always wants this and not where it sits.
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -144,6 +182,22 @@ pub struct WideRegion {
} }
impl WideRegion { impl WideRegion {
/// A box given as a part of this one, on both axes.
pub const fn select(self, part: &UiRegion) -> Self {
Self {
x: self.x.select(&part.x),
y: self.y.select(&part.y),
}
}
/// How big such a part is, which is what reads a box in pixels.
pub const fn select_size(self, part: &UiRegion) -> WideSize {
WideSize {
x: self.x.select_len(&part.x),
y: self.y.select_len(&part.y),
}
}
pub const fn of(region: UiRegion) -> Self { pub const fn of(region: UiRegion) -> Self {
Self { Self {
x: WideSpan::of(region.x), x: WideSpan::of(region.x),
+16 -5
View File
@@ -3,7 +3,7 @@ use crate::layout_diagnostics::{self as diag, Counter};
use crate::{ use crate::{
Axis, Holds, LayoutLen, Len, Px, PxVec2, RegionAlign, Rel, RenderedText, Size, StrongWidget, Axis, Holds, LayoutLen, Len, Px, PxVec2, RegionAlign, Rel, RenderedText, Size, StrongWidget,
TextAttrs, TextBuffer, TextData, TextureHandle, UiRegion, UiRenderState, UiRsc, UiVec2, Weight, TextAttrs, TextBuffer, TextData, TextureHandle, UiRegion, UiRenderState, UiRsc, UiVec2, Weight,
WidgetId, Widgets, WideRegion, WidgetId, Widgets,
render::{ render::{
GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveHandle, PrimitiveInst, GlyphPrimitive, Mask, MaskIdx, MoveIdx, Primitive, PrimitiveHandle, PrimitiveInst,
PrimitiveKind, TexturePrimitive, PrimitiveKind, TexturePrimitive,
@@ -19,6 +19,9 @@ pub struct Painter<'a> {
/// This widget's box, in the coordinates of `move_idx`. /// This widget's box, in the coordinates of `move_idx`.
pub(super) region: UiRegion, 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) mask: MaskIdx,
pub(super) textures: Vec<TextureHandle>, pub(super) textures: Vec<TextureHandle>,
pub(super) primitives: Vec<PrimitiveHandle>, pub(super) primitives: Vec<PrimitiveHandle>,
@@ -196,6 +199,7 @@ impl<'a> Painter<'a> {
mask: self.mask, mask: self.mask,
offer, offer,
offered_px: self.px_within_offer(offer), offered_px: self.px_within_offer(offer),
slot_wide: self.slot_wide,
align: align_override, align: align_override,
}, },
None, None,
@@ -264,7 +268,7 @@ impl<'a> Painter<'a> {
if let Some(hint) = self.size_hint(child, axis) { if let Some(hint) = self.size_hint(child, axis) {
return Some(hint); return Some(hint);
} }
let px = self.state.px_of(self.move_idx, within); let px = self.px_of(within);
let (size, holds) = let (size, holds) =
self.state self.state
.retained_size(child.id(), px, self.move_idx, self.rsc.widgets())?; .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>) { fn depend_on<W: ?Sized>(&mut self, child: &StrongWidget<W>) {
if !self.size_deps.contains(&child.id()) { if !self.size_deps.contains(&child.id()) {
self.size_deps.push(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 /// 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. /// holds for this box only, until `holds` says how far it goes.
pub fn px_size(&mut self) -> PxVec2 { 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]) { for (own, len) in self.own.iter_mut().zip([px.x, px.y]) {
if *own == Holds::ANY { if *own == Holds::ANY {
*own = Holds::at(len); *own = Holds::at(len);
@@ -400,7 +411,7 @@ impl<'a> Painter<'a> {
/// One axis of this widget's box in pixels. Prefer this to /// One axis of this widget's box in pixels. Prefer this to
/// [`Self::px_size`] when the other axis cannot affect the drawing. /// [`Self::px_size`] when the other axis cannot affect the drawing.
pub fn px_len(&mut self, axis: Axis) -> Px { 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]; let own = &mut self.own[axis as usize];
if *own == Holds::ANY { if *own == Holds::ANY {
*own = Holds::at(len); *own = Holds::at(len);
@@ -415,7 +426,7 @@ impl<'a> Painter<'a> {
pub fn holds(&mut self, axis: Axis, holds: impl Into<Holds>) { pub fn holds(&mut self, axis: Axis, holds: impl Into<Holds>) {
let holds = holds.into(); let holds = holds.into();
debug_assert!( 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", "'{}' ({:?}) says its drawing holds for lengths that leave out its own box",
self.label(), self.label(),
self.id self.id
+22 -7
View File
@@ -4,7 +4,7 @@ use crate::ui::painter::{declared_box, declared_lens, placed_box};
use crate::{ use crate::{
ActiveData, Axis, DrawLayers, Holds, IdLike, LayoutLen, Len, MaskIdx, MoveIdx, Moves, Painter, ActiveData, Axis, DrawLayers, Holds, IdLike, LayoutLen, Len, MaskIdx, MoveIdx, Moves, Painter,
PixelRegion, Px, PxVec2, RegionAlign, Rel, Size, StrongWidget, UiRegion, UiRsc, UiSpan, Weight, PixelRegion, Px, PxVec2, RegionAlign, Rel, Size, StrongWidget, UiRegion, UiRsc, UiSpan, Weight,
WidgetId, Widgets, WideRegion, WidgetId, Widgets,
util::{HashMap, Vec2}, util::{HashMap, Vec2},
}; };
@@ -24,6 +24,9 @@ pub(super) struct DrawInfo {
/// that box in pixels. /// that box in pixels.
pub offer: UiRegion, pub offer: UiRegion,
pub offered_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,
/// A container's answer for where the widget sits. `None` uses the /// A container's answer for where the widget sits. `None` uses the
/// widget's own property. /// widget's own property.
pub align: Option<RegionAlign>, pub align: Option<RegionAlign>,
@@ -106,6 +109,7 @@ impl UiRenderState {
mask: MaskIdx::NONE, mask: MaskIdx::NONE,
offer: UiRegion::FULL, offer: UiRegion::FULL,
offered_px: self.output_size, offered_px: self.output_size,
slot_wide: self.moves.compose(self.root_move, UiRegion::FULL),
align: None, align: None,
} }
} }
@@ -276,29 +280,37 @@ impl UiRenderState {
old: Option<ActiveData>, old: Option<ActiveData>,
rsc: &mut dyn UiRsc, rsc: &mut dyn UiRsc,
) -> (Size, [Holds; 2]) { ) -> (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 // 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 => ( true => (
self.move_slot(id, info.parent_move, region), self.move_slot(id, info.parent_move, region),
UiRegion::FULL, UiRegion::FULL,
None, None,
info.slot_wide.select(&region),
), ),
// Keep the old entry alive until every descendant has migrated. // Keep the old entry alive until every descendant has migrated.
// Reusing its index sooner could make an old parent look current. // 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 { let (old_children, old_answer) = match old {
Some(old) => (old.children, Some(old.answer)), Some(old) => (old.children, Some(old.answer)),
None => (Vec::new(), None), None => (Vec::new(), None),
}; };
rsc.widgets_mut().needs_redraw.remove(&id); 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 at_offer = same_px(px, info.offered_px);
let mut painter = Painter { let mut painter = Painter {
state: self, state: self,
region: local, region: local,
slot_wide,
mask: info.mask, mask: info.mask,
layer: info.layer, layer: info.layer,
own_layer: info.layer, own_layer: info.layer,
@@ -332,6 +344,7 @@ impl UiRenderState {
state: _, state: _,
rsc: _, rsc: _,
region: _, region: _,
slot_wide: _,
mask, mask,
textures, textures,
primitives, primitives,
@@ -403,6 +416,7 @@ impl UiRenderState {
mask, mask,
offer: UiRegion::FULL, offer: UiRegion::FULL,
offered_px: px, offered_px: px,
slot_wide,
align: None, align: None,
}, },
rsc, rsc,
@@ -515,7 +529,7 @@ impl UiRenderState {
{ {
return None; 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; let (size, holds) = active.answer;
(holds[0].contains(px.x) && holds[1].contains(px.y)).then_some((size, holds)) (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 // 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 // box may be what changed -- an unchanged fraction of a box half the
// size is half the widget. // 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")] #[cfg(feature = "layout-diagnostics")]
{ {
diag::bump(Counter::ReuseOutside); diag::bump(Counter::ReuseOutside);
@@ -985,6 +999,7 @@ impl UiRenderState {
mask: active.mask, mask: active.mask,
offer: active.offer, offer: active.offer,
offered_px, offered_px,
slot_wide: self.moves.compose(active.parent_move, UiRegion::FULL),
align: active.align_override.then_some(active.align), align: active.align_override.then_some(active.align),
}; };
let (was_answer, was) = (active.answer, (active.size, active.holds)); let (was_answer, was) = (active.answer, (active.size, active.holds));