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:
1 parent
45a717695b
commit
5f16617511
3 files changed
+92
-12
No files matched your search
@@ -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
@@ -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(®ion)
|
||||||
|
.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
|
||||||
|
|||||||
@@ -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(®ion),
|
||||||
),
|
),
|
||||||
// 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(®ion).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(®ion).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));
|
||||||
|
|||||||
Reference in new issue
Block a user