From 5f16617511c812861e22c398317282b1018c6ada Mon Sep 17 00:00:00 2001 From: iris-ai <4+iris-ai@noreply.localhost> Date: Wed, 16 Sep 2026 21:49:25 -0400 Subject: [PATCH] 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 --- core/src/orientation/wide.rs | 54 ++++++++++++++++++++++++++++++++++++ core/src/ui/painter.rs | 21 ++++++++++---- core/src/ui/render_state.rs | 29 ++++++++++++++----- 3 files changed, 92 insertions(+), 12 deletions(-) diff --git a/core/src/orientation/wide.rs b/core/src/orientation/wide.rs index 288c064..7d92344 100644 --- a/core/src/orientation/wide.rs +++ b/core/src/orientation/wide.rs @@ -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 /// reads a box in pixels almost always wants this and not where it sits. #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -144,6 +182,22 @@ pub struct 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 { Self { x: WideSpan::of(region.x), diff --git a/core/src/ui/painter.rs b/core/src/ui/painter.rs index 535d826..4b8050e 100644 --- a/core/src/ui/painter.rs +++ b/core/src/ui/painter.rs @@ -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, pub(super) primitives: Vec, @@ -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(®ion) + .to_px(self.state.output_size()) + } + fn depend_on(&mut self, child: &StrongWidget) { 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) { 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 diff --git a/core/src/ui/render_state.rs b/core/src/ui/render_state.rs index 8b08ff9..b968ab0 100644 --- a/core/src/ui/render_state.rs +++ b/core/src/ui/render_state.rs @@ -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, @@ -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, 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(®ion), ), // 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(®ion).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(®ion).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));