Compare commits

..
5 Commits
Author SHA1 Message Date
iris-aiandClaude Opus 5 394d5149a5 Measure a cost on a tree that does not move when layout does
`Branch` picks which of two subtrees to draw by comparing a measured pixel
length with a threshold. That is exactly what the oracle wants -- it is how a
widget believing a measurement a cold start would not have given it becomes a
different tree -- and exactly what a rig measuring cost must not have: the
fixture's shape moves with the thing being measured.

It has been moving. Seed 1 at depth 8 draws 88 widgets and writes 2,298
primitives a frame at `5ed9e87`, and 115 and 8,209 at `bd6de71` -- three and
a half times the work -- so the handoff's "fixed point cost 3x" compared two
different workloads and is withdrawn. Measured on one tree instead, with
`Edits::fixed_branches`, `5ed9e87` is 1,761M instructions and ~699M cycles
against this head's 2,093M and ~819M, while drawing 100 widgets against 97
and writing 4,272 primitives against 3,951. Fixed point costs something like
a fifth to a quarter, not three times.

The oracle keeps measured branches: `fixed_branches` is false by default and
only the rig sets it. A branch consumes its randomness either way, so both
grow the same ids.

**Check the work counters before comparing two commits' times.** The rig
prints drawn widgets, widget draws and primitive writes for this reason;
an undrawn `leftover` child still moves them, which no flag can remove.

Checked: fmt, clippy, 105 tests, the 100-seed generated oracle.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 14:06:44 -04:00
iris-aiandClaude Opus 5 4cbb242a5d Do not multiply by a part of nothing
`lerp` is `a + (b - a) * f`, and `b - a` is nothing often enough to be worth
asking: a box with the same pixels at both ends of an axis, a span with no
fraction of one, a part of a subtree whose box did not move on that axis.
`Fixed::scaled` is `mul` that answers a zero receiver without widening to
`i64`, rounding and narrowing back, and `lerp` uses it -- so every lerp in
layout gets it rather than the two places that were about to grow their own
comparison.

`many` over 500 frames: 1,705,786,553 instructions to 1,657,571,216, and
638.9M cycles against 657.9M, averaged over four runs each.

Checked: fmt, clippy, 105 tests, all five shrinker cases at 300 seeds, and
`tabs`, `text`, `random`, `minimal` and `view` byte-identical at 1920x1200.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 14:01:52 -04:00
iris-aiandClaude Opus 5 d75a1e2129 Do not multiply a box through the whole of its parent
Composing a box within another is four multiplies an axis, and two of the
shapes it is asked for compose to nothing: a part that is the whole box is
the box, and a box composed through the whole of its parent is itself. Both
are exact -- multiplying by one on the grid rounds to what it started as --
so four comparisons answer what four multiplies would have.

`many` over 500 frames: 1,742,553,104 instructions to 1,705,786,553, 2.1%
fewer, and 660M cycles to 658M. The cycles are the honest number and they
say this is worth little here; it is kept because instructions are what a
phone pays for and the check is four comparisons.

Checked: fmt, clippy, 105 tests, all five shrinker cases at 300 seeds, the
100-seed generated oracle, and `tabs`, `text` and `random` byte-identical at
1920x1200.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 13:55:30 -04:00
iris-aiandClaude Opus 5 1940e85c70 Move a whole box at once, since that is what a move does
Profiling a move by cycles rather than by instructions says the cost is not
where the last session recorded it. In `apply_scalar` the `i64` division is
**0.00%** of cycles and the multiply 1.5%: the time is in `saturating_add`,
which is five instructions and no vector form for an `i32`, and a box that
only moved does eight of them. Asking for them one scalar at a time, each
behind a match on which kind of move this is, gives the compiler four short
sequences where it had four adds in a row to pair up.

So a translation is now asked for once for the whole region -- which is what
a translation is -- and the match happens once above it rather than per
scalar. `many` over 500 frames: 684M cycles to 660M, and 1,815,666,327
instructions to 1,742,553,104.

Cycle counts are worth trusting here, which is the other thing to keep: three
runs of one binary varied 0.23%. It is wall time that varies 2x on this
machine, not the counters, and instructions alone cannot see a stall.

Checked: fmt, clippy, 105 tests, all five shrinker cases at 300 seeds, and
`tabs`, `text` and `random` byte-identical at 1920x1200.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 13:50:58 -04:00
iris-aiandClaude Opus 5 cb1bba4682 Work a move out once for the subtree, not once for each part
`RegionRemap` re-derived the same things for every scalar of every part of a
moving subtree: the extent it divides by, whether the box only moved, whether
it spans the whole of its parent's, and the two ends of each `lerp`. All of
them are the same for the whole walk, because the walk is one box moving into
one other box. They are worked out once in `RegionRemap::new` now, as an
`AxisRemap` per axis that is either a translation or a scale.

Identical arithmetic in the same order, so the answers are unchanged: 500
frames of the `many` phase went from 1,886,328,855 instructions to
1,815,666,327, 3.8% fewer, and `tabs`, `text` and `random` are byte-identical
at 1920x1200.

Cycles moved 0.8%, which is the finding worth keeping: the surrounding
arithmetic was never the cost. The `i64` division is, and it is still there.

Checked: fmt, clippy, 105 tests, all five shrinker cases at 300 seeds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 13:46:23 -04:00
5 changed files with 172 additions and 41 deletions

No files matched your search

+11 -1
View File
@@ -138,6 +138,16 @@ impl<const SHIFT: u32> Fixed<SHIFT> {
Self(narrow(shift_round(self.0 as i64 * by.0 as i64, BY))) Self(narrow(shift_round(self.0 as i64 * by.0 as i64, BY)))
} }
/// A part of a span that is often nothing: no part of nothing is
/// nothing, for the cost of a comparison rather than a widening
/// multiply and a rounding.
pub const fn scaled<const BY: u32>(self, by: Fixed<BY>) -> Self {
match self.0 == 0 {
true => self,
false => self.mul(by),
}
}
/// Repeated a whole number of times, which no grid rounds. /// Repeated a whole number of times, which no grid rounds.
pub const fn mul_int(self, by: i32) -> Self { pub const fn mul_int(self, by: i32) -> Self {
Self(narrow(self.0 as i64 * by as i64)) Self(narrow(self.0 as i64 * by as i64))
@@ -179,7 +189,7 @@ impl<const SHIFT: u32> Fixed<SHIFT> {
/// `from` and `to` a fraction of the way apart, the fraction being the /// `from` and `to` a fraction of the way apart, the fraction being the
/// receiver -- the argument order [`crate::util::LerpUtil`] already uses. /// receiver -- the argument order [`crate::util::LerpUtil`] already uses.
pub const fn lerp<const OF: u32>(self, from: Fixed<OF>, to: Fixed<OF>) -> Fixed<OF> { pub const fn lerp<const OF: u32>(self, from: Fixed<OF>, to: Fixed<OF>) -> Fixed<OF> {
from.add(to.sub(from).mul(self)) from.add(to.sub(from).scaled(self))
} }
pub const fn min(self, other: Self) -> Self { pub const fn min(self, other: Self) -> Self {
+39
View File
@@ -282,7 +282,26 @@ impl UiSpan {
self.end += offset; self.end += offset;
} }
/// The whole of the box it sits in: a span that composes to nothing and
/// a parent that changes nothing.
pub const fn is_full(&self) -> bool {
self.start.rel.raw() == Rel::ZERO.raw()
&& self.start.px.raw() == Px::ZERO.raw()
&& self.end.rel.raw() == Rel::ONE.raw()
&& self.end.px.raw() == Px::ZERO.raw()
}
pub const fn within(&self, parent: &Self) -> Self { pub const fn within(&self, parent: &Self) -> Self {
// A part that is the whole box is the box, and a box composed through
// the whole of its parent is itself. Both are exact -- multiplying by
// one rounds to what it started as -- and both are common enough to
// be worth four comparisons rather than four multiplies to find out.
if self.is_full() {
return *parent;
}
if parent.is_full() {
return *self;
}
Self { Self {
start: self.start.within(parent), start: self.start.within(parent),
end: self.end.within(parent), end: self.end.within(parent),
@@ -292,6 +311,15 @@ impl UiSpan {
pub const fn len(&self) -> Len { pub const fn len(&self) -> Len {
self.end - self.start self.end - self.start
} }
/// Both ends by the same amount, which is what moving a box without
/// changing its length does to every part of it.
pub const fn translated(self, by: Len) -> Self {
Self {
start: self.start + by,
end: self.end + by,
}
}
} }
#[repr(C)] #[repr(C)]
@@ -302,6 +330,17 @@ pub struct UiRegion {
} }
impl UiRegion { impl UiRegion {
/// Every part of the box by the same amount on each axis. Done to the
/// whole region rather than an end at a time, because that is what it is
/// -- and because four adds in a row are four adds, where four asked for
/// separately are four sequences.
pub const fn translated(self, x: Len, y: Len) -> Self {
Self {
x: self.x.translated(x),
y: self.y.translated(y),
}
}
pub const FULL: Self = Self { pub const FULL: Self = Self {
x: UiSpan::FULL, x: UiSpan::FULL,
y: UiSpan::FULL, y: UiSpan::FULL,
+92 -37
View File
@@ -3,7 +3,7 @@ use crate::layout_diagnostics::{self as diag, Counter, ReuseOutcome, TimerKind};
use crate::ui::painter::{declared_box, declared_lens, placed_box}; 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, PxVec2, RegionAlign, Rel, Size, StrongWidget, UiRegion, UiRsc, UiSpan, Weight, PixelRegion, Px, PxVec2, RegionAlign, Rel, Size, StrongWidget, UiRegion, UiRsc, UiSpan, Weight,
WidgetId, Widgets, WidgetId, Widgets,
util::{HashMap, Vec2}, util::{HashMap, Vec2},
}; };
@@ -632,7 +632,7 @@ impl UiRenderState {
self.moves.set(slot, region); self.moves.set(slot, region);
} else { } else {
let remap = RegionRemap::new(old_region, region)?; let remap = RegionRemap::new(old_region, region)?;
self.remap_subtree(id, remap, info.parent_move, mask, rsc); self.remap_subtree(id, &remap, info.parent_move, mask, rsc);
} }
} }
let active = self.active.get_mut(&id).unwrap(); let active = self.active.get_mut(&id).unwrap();
@@ -668,7 +668,7 @@ impl UiRenderState {
fn remap_subtree( fn remap_subtree(
&mut self, &mut self,
id: WidgetId, id: WidgetId,
remap: RegionRemap, remap: &RegionRemap,
parent_move: MoveIdx, parent_move: MoveIdx,
inherited_mask: MaskIdx, inherited_mask: MaskIdx,
rsc: &mut dyn UiRsc, rsc: &mut dyn UiRsc,
@@ -1045,55 +1045,110 @@ fn same_pixel_region(a: PixelRegion, b: PixelRegion) -> bool {
/// source extent can be translated but cannot recover fractions for a resize. /// source extent can be translated but cannot recover fractions for a resize.
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
struct RegionRemap { struct RegionRemap {
from: UiRegion, axes: [AxisRemap; 2],
to: UiRegion, }
/// Moving one axis of a box into another, worked out once for the whole
/// subtree that moves with it. Every part of that subtree is divided by the
/// same extent and placed between the same two ends, so the ends and the
/// divisor belong here rather than in each part's arithmetic.
#[derive(Clone, Copy)]
enum AxisRemap {
/// A box that kept its length carries its parts by moving them, which is
/// exact. Dividing to find the fraction each sits at and multiplying to
/// place it again are two roundings, and they land a step from where
/// growing the tree that way does.
Translate(Len),
/// A box that changed length has to re-express each part as a fraction of
/// the new one, which is what a part of a box means.
Scale(AxisScale),
}
#[derive(Clone, Copy)]
struct AxisScale {
/// What the fraction is measured from, and what divides it. `whole` is
/// the common case of a box spanning the whole of its parent's, where
/// dividing by one is the expensive way to write a subtraction.
start_rel: Rel,
extent: Rel,
whole: bool,
/// `lerp` is `a + (b - a) * fraction`, and both ends are the same for
/// every part, so each is kept as its near end and its span.
from_px: Px,
from_px_span: Px,
to_rel: Rel,
to_rel_span: Rel,
to_px: Px,
to_px_span: Px,
} }
impl RegionRemap { impl RegionRemap {
fn new(from: UiRegion, to: UiRegion) -> Option<Self> { fn new(from: UiRegion, to: UiRegion) -> Option<Self> {
AXES.into_iter() Some(Self {
.all(|axis| { axes: [AxisRemap::new(from.x, to.x)?, AxisRemap::new(from.y, to.y)?],
let from = from.axis(axis);
from.start.rel != from.end.rel || from.len() == to.axis(axis).len()
}) })
.then_some(Self { from, to })
} }
fn apply(self, region: UiRegion) -> UiRegion { fn apply(&self, region: UiRegion) -> UiRegion {
// A box that only moved carries every part of itself by the same two
// amounts, and that is the common move. Asking it once for the whole
// region is what lets it be eight adds in a row rather than four
// sequences with a branch each -- measured, it is where the time in a
// move goes.
if let [AxisRemap::Translate(x), AxisRemap::Translate(y)] = self.axes {
return region.translated(x, y);
}
UiRegion { UiRegion {
x: self.apply_span(region.x, self.from.x, self.to.x), x: self.axes[0].apply_span(region.x),
y: self.apply_span(region.y, self.from.y, self.to.y), y: self.axes[1].apply_span(region.y),
} }
} }
}
fn apply_span(self, span: UiSpan, from: UiSpan, to: UiSpan) -> UiSpan { impl AxisRemap {
UiSpan { fn new(from: UiSpan, to: UiSpan) -> Option<Self> {
start: self.apply_scalar(span.start, from, to), if from.len() == to.len() {
end: self.apply_scalar(span.end, from, to), return Some(Self::Translate(to.start - from.start));
} }
}
fn apply_scalar(self, scalar: Len, from: UiSpan, to: UiSpan) -> Len {
let extent = from.end.rel - from.start.rel; let extent = from.end.rel - from.start.rel;
// A box that only moved, or that has no relative extent to divide, // Without a relative extent there is no fraction to re-express: a box
// carries its parts by moving them, which is exact. Dividing to find // of fixed length cannot say where its parts sit in a different one.
// the fraction each sits at and multiplying to place it again are two if extent == Rel::ZERO {
// roundings, and they land a step from where growing the tree that return None;
// way does. Where the box changed length there is nothing else to do,
// and the fraction is what a part means.
if from.len() == to.len() || extent == Rel::ZERO {
return scalar + to.start - from.start;
} }
// A box that spans the whole of its parent's is the common one, and Some(Self::Scale(AxisScale {
// dividing by one is the expensive way to write a subtraction. start_rel: from.start.rel,
let offset = scalar.rel - from.start.rel; extent,
let fraction = match extent == Rel::ONE { whole: extent == Rel::ONE,
true => offset, from_px: from.start.px,
false => offset / extent, from_px_span: from.end.px - from.start.px,
to_rel: to.start.rel,
to_rel_span: to.end.rel - to.start.rel,
to_px: to.start.px,
to_px_span: to.end.px - to.start.px,
}))
}
fn apply_span(&self, span: UiSpan) -> UiSpan {
UiSpan {
start: self.apply_scalar(span.start),
end: self.apply_scalar(span.end),
}
}
fn apply_scalar(&self, scalar: Len) -> Len {
let scale = match self {
Self::Translate(by) => return scalar + *by,
Self::Scale(scale) => scale,
}; };
let from_px = fraction.lerp(from.start.px, from.end.px); let offset = scalar.rel - scale.start_rel;
let to_rel = fraction.lerp(to.start.rel, to.end.rel); let fraction = match scale.whole {
let to_px = fraction.lerp(to.start.px, to.end.px); true => offset,
false => offset / scale.extent,
};
let from_px = scale.from_px + scale.from_px_span.scaled(fraction);
let to_rel = scale.to_rel + scale.to_rel_span.scaled(fraction);
let to_px = scale.to_px + scale.to_px_span.scaled(fraction);
Len::from_parts(to_rel, scalar.px - from_px + to_px) Len::from_parts(to_rel, scalar.px - from_px + to_px)
} }
} }
+19 -1
View File
@@ -29,6 +29,17 @@ pub struct Edits {
/// one. Region nodes change what a move writes and how deep a primitive's /// one. Region nodes change what a move writes and how deep a primitive's
/// chain is, so a tree that never grows one leaves both untested. /// chain is, so a tree that never grows one leaves both untested.
pub nodes: HashMap<usize, bool>, pub nodes: HashMap<usize, bool>,
/// Whether a [`Branch`] takes the side it would take at any measurement,
/// rather than the side the one it made says. The oracle wants the
/// measured side -- that is the whole point of a branch, and how a widget
/// believing a measurement a cold start would not have given it becomes a
/// different tree. A rig measuring cost wants this instead: a fixture
/// whose shape moves with the thing being measured cannot be compared
/// with itself across a change to it, and seed 1 at depth 8 went from 88
/// drawn widgets and 2,298 primitive writes a frame to 115 and 8,209
/// across fixed point, which is three and a half times the work behind a
/// number read as three and a half times the cost.
pub fixed_branches: bool,
} }
#[derive(Default, Clone)] #[derive(Default, Clone)]
@@ -286,7 +297,14 @@ impl<Rsc: UiRsc + 'static> Grow<'_, Rsc> {
let probe = self.node(depth - 1); let probe = self.node(depth - 1);
let wide = self.node(depth - 1); let wide = self.node(depth - 1);
let narrow = self.node(depth - 1); let narrow = self.node(depth - 1);
let threshold = self.rng.below(500) as f32; // Drawn either way, so the side a fixed branch takes is still a
// side the generator chose -- and it consumes the same randomness
// as a measured one, so the two grow the same ids.
let measured = self.rng.below(500) as f32;
let threshold = match self.edits.fixed_branches {
true => f32::MIN,
false => measured,
};
let id = Branch { let id = Branch {
probe, probe,
wide, wide,
+11 -2
View File
@@ -92,9 +92,18 @@ fn trace_selected(tree: &Tree) {
#[cfg(not(feature = "layout-diagnostics"))] #[cfg(not(feature = "layout-diagnostics"))]
fn trace_selected(_: &Tree) {} fn trace_selected(_: &Tree) {}
/// The shape a cost is measured on must not depend on what layout measured,
/// or two commits are compared on two different trees. See `Edits`.
fn rig_edits() -> Edits {
Edits {
fixed_branches: true,
..Default::default()
}
}
fn warm(seed: u64, depth: usize) -> (Harness, Tree) { fn warm(seed: u64, depth: usize) -> (Harness, Tree) {
let mut harness = Harness::new(OUTPUT); let mut harness = Harness::new(OUTPUT);
let (root, tree) = grow(&mut harness.rsc, seed, depth, &Edits::default()); let (root, tree) = grow(&mut harness.rsc, seed, depth, &rig_edits());
harness.state.root = Some(root); harness.state.root = Some(root);
harness.frame(); harness.frame();
println!( println!(
@@ -173,7 +182,7 @@ fn layout_cost() {
if selected("cold") { if selected("cold") {
let mut harness = Harness::new(OUTPUT); let mut harness = Harness::new(OUTPUT);
let (root, tree) = grow(&mut harness.rsc, seed, depth, &Edits::default()); let (root, tree) = grow(&mut harness.rsc, seed, depth, &rig_edits());
harness.state.root = Some(root); harness.state.root = Some(root);
println!( println!(
"fixture: seed {seed}, depth {depth}, {} widgets", "fixture: seed {seed}, depth {depth}, {} widgets",