Wrap rather than saturate: nothing draws two million pixels out

A saturating add is five instructions where a wrapping one is one, and
it has no i32 vector form. Measured on the fixed-shape fixture, seed 1,
depth 8, 500 frames of `many`: 2,098M instructions and ~826M cycles down
to 1,918M and ~771M, with `random`, `tabs` and `text` byte-identical at
1920x1200 and the 100-seed oracle passing.

What saturating bought was ordering past the end of the range, where a
layout is already a defect; wrapping makes that defect obvious instead
of plausible. `from_f32` still clamps, since a float has the range to
come from anywhere, and `narrow` stays for `Holds`, whose range past
i32 really does mean unbounded. MIN and MAX remain unbounded ends only
where they are compared and never added to, which is every use.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
iris-aiandClaude Fable 5.1 committed 2026-09-16 14:22:48 -04:00
1 parent 394d5149a5
commit 4febabfd2e
1 file changed
+33 -25
+33 -25
View File
@@ -18,6 +18,13 @@ use std::{
/// `SHIFT` is the number of fractional bits, which is what makes the steps /// `SHIFT` is the number of fractional bits, which is what makes the steps
/// divide a whole number: a power of two also converts to `f32` without /// divide a whole number: a power of two also converts to `f32` without
/// rounding while the value fits in its mantissa. /// rounding while the value fits in its mantissa.
///
/// Arithmetic wraps at the ends of the range, the way the `i32` underneath
/// does. Saturating instead was measured at a twelfth of layout's
/// instructions -- five per add against one -- to keep the ordering of
/// coordinates two million pixels out, where nothing draws anyway. A value
/// off the end is a defect either way; wrapping makes it an obvious one.
/// Only [`Self::from_f32`] clamps, since a float has further to come from.
#[repr(transparent)] #[repr(transparent)]
#[derive( #[derive(
Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, bytemuck::Pod, bytemuck::Zeroable, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, bytemuck::Pod, bytemuck::Zeroable,
@@ -56,8 +63,8 @@ impl<const SHIFT: u32> Fixed<SHIFT> {
/// The gap between neighbouring values, which is also how far apart two /// The gap between neighbouring values, which is also how far apart two
/// numbers can be and still mean the same place. /// numbers can be and still mean the same place.
pub const STEP: Self = Self(1); pub const STEP: Self = Self(1);
/// Also what stands in for an unbounded end, since arithmetic saturates /// Also what stands in for an unbounded end: compared against, never
/// here rather than wrapping past it. /// added to, since arithmetic wraps past it.
pub const MIN: Self = Self(i32::MIN); pub const MIN: Self = Self(i32::MIN);
pub const MAX: Self = Self(i32::MAX); pub const MAX: Self = Self(i32::MAX);
@@ -77,12 +84,13 @@ impl<const SHIFT: u32> Fixed<SHIFT> {
} }
pub const fn from_int(v: i32) -> Self { pub const fn from_int(v: i32) -> Self {
Self(v.saturating_mul(Self::one().0)) Self(v.wrapping_mul(Self::one().0))
} }
/// Rounds to the nearest step, and saturates rather than wrapping. A NaN /// Rounds to the nearest step, and clamps to the ends of the grid rather
/// has no nearest step and becomes zero, which is a caller's mistake /// than wrapping: this is where a number from outside arrives, and a float
/// rather than a value worth carrying. /// has the range to be anywhere. A NaN has no nearest step and becomes
/// zero, which is a caller's mistake rather than a value worth carrying.
/// ///
/// Half-away is written out rather than called through `f32::round`, /// Half-away is written out rather than called through `f32::round`,
/// which is not `const`: a layout constant has to stay a constant. /// which is not `const`: a layout constant has to stay a constant.
@@ -114,28 +122,28 @@ impl<const SHIFT: u32> Fixed<SHIFT> {
/// The same value on another grid, rounded where the new one is coarser. /// The same value on another grid, rounded where the new one is coarser.
pub const fn to_scale<const TO: u32>(self) -> Fixed<TO> { pub const fn to_scale<const TO: u32>(self) -> Fixed<TO> {
Fixed(match TO >= SHIFT { Fixed(match TO >= SHIFT {
true => narrow((self.0 as i64) << (TO - SHIFT)), true => self.0 << (TO - SHIFT),
false => narrow(shift_round(self.0 as i64, SHIFT - TO)), false => shift_round(self.0 as i64, SHIFT - TO) as i32,
}) })
} }
pub const fn add(self, rhs: Self) -> Self { pub const fn add(self, rhs: Self) -> Self {
Self(self.0.saturating_add(rhs.0)) Self(self.0.wrapping_add(rhs.0))
} }
pub const fn sub(self, rhs: Self) -> Self { pub const fn sub(self, rhs: Self) -> Self {
Self(self.0.saturating_sub(rhs.0)) Self(self.0.wrapping_sub(rhs.0))
} }
pub const fn neg(self) -> Self { pub const fn neg(self) -> Self {
Self(self.0.saturating_neg()) Self(self.0.wrapping_neg())
} }
/// Scaled by a number on any grid, which is how a length takes a fraction /// Scaled by a number on any grid, which is how a length takes a fraction
/// of itself and keeps being a length: the product is measured in the /// of itself and keeps being a length: the product is measured in the
/// receiver's steps. /// receiver's steps.
pub const fn mul<const BY: u32>(self, by: Fixed<BY>) -> Self { pub const fn mul<const BY: u32>(self, by: Fixed<BY>) -> Self {
Self(narrow(shift_round(self.0 as i64 * by.0 as i64, BY))) Self(shift_round(self.0 as i64 * by.0 as i64, BY) as i32)
} }
/// A part of a span that is often nothing: no part of nothing is /// A part of a span that is often nothing: no part of nothing is
@@ -150,7 +158,7 @@ impl<const SHIFT: u32> Fixed<SHIFT> {
/// 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(self.0.wrapping_mul(by))
} }
/// Divided into a whole number of parts, rounded to the nearest step. /// Divided into a whole number of parts, rounded to the nearest step.
@@ -159,12 +167,13 @@ impl<const SHIFT: u32> Fixed<SHIFT> {
if by == 0 { if by == 0 {
return Self::ZERO; return Self::ZERO;
} }
Self(narrow(div_round(self.0 as i64, by as i64))) Self(div_round(self.0 as i64, by as i64) as i32)
} }
/// Divided by a number on any grid. A zero divisor is a caller bug -- a /// Divided by a number on any grid. A zero divisor is a caller bug -- a
/// box of no length has no fraction of itself -- and saturates so that a /// box of no length has no fraction of itself -- and answers with the end
/// release build lays out something absurd rather than dying. /// of the range so that a release build lays out something absurd rather
/// than dying.
pub const fn div<const BY: u32>(self, by: Fixed<BY>) -> Self { pub const fn div<const BY: u32>(self, by: Fixed<BY>) -> Self {
debug_assert!(by.0 != 0, "dividing by a length of zero"); debug_assert!(by.0 != 0, "dividing by a length of zero");
if by.0 == 0 { if by.0 == 0 {
@@ -173,7 +182,7 @@ impl<const SHIFT: u32> Fixed<SHIFT> {
false => Self::MAX, false => Self::MAX,
}; };
} }
Self(narrow(div_round((self.0 as i64) << BY, by.0 as i64))) Self(div_round((self.0 as i64) << BY, by.0 as i64) as i32)
} }
/// `num / den` on *this* grid rather than on theirs, for weights coarser /// `num / den` on *this* grid rather than on theirs, for weights coarser
@@ -183,7 +192,7 @@ impl<const SHIFT: u32> Fixed<SHIFT> {
if den.0 == 0 { if den.0 == 0 {
return Self::ZERO; return Self::ZERO;
} }
Self(narrow(div_round((num.0 as i64) << SHIFT, den.0 as i64))) Self(div_round((num.0 as i64) << SHIFT, den.0 as i64) as i32)
} }
/// `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
@@ -207,7 +216,7 @@ impl<const SHIFT: u32> Fixed<SHIFT> {
} }
pub const fn abs(self) -> Self { pub const fn abs(self) -> Self {
Self(self.0.saturating_abs()) Self(self.0.wrapping_abs())
} }
pub const fn clamp(self, lo: Self, hi: Self) -> Self { pub const fn clamp(self, lo: Self, hi: Self) -> Self {
@@ -219,11 +228,11 @@ impl<const SHIFT: u32> Fixed<SHIFT> {
/// boundary. The step is the whole gap, so there is nothing to exclude /// boundary. The step is the whole gap, so there is nothing to exclude
/// between this and the boundary itself. /// between this and the boundary itself.
pub const fn next_up(self) -> Self { pub const fn next_up(self) -> Self {
Self(self.0.saturating_add(1)) Self(self.0.wrapping_add(1))
} }
pub const fn next_down(self) -> Self { pub const fn next_down(self) -> Self {
Self(self.0.saturating_sub(1)) Self(self.0.wrapping_sub(1))
} }
} }
@@ -260,6 +269,8 @@ pub(crate) const fn div_toward(num: i64, den: i64, up: bool) -> i64 {
} }
} }
/// Clamped to the ends, unlike a [`Fixed`]'s own arithmetic: a range of box
/// lengths that runs past `i32` really is unbounded.
pub(crate) const fn narrow(v: i64) -> i32 { pub(crate) const fn narrow(v: i64) -> i32 {
if v > i32::MAX as i64 { if v > i32::MAX as i64 {
return i32::MAX; return i32::MAX;
@@ -471,12 +482,9 @@ mod tests {
} }
#[test] #[test]
fn arithmetic_saturates_rather_than_wrapping() { fn a_number_from_outside_is_clamped_to_the_grid() {
assert_eq!(Px::MAX + Px::ONE, Px::MAX);
assert_eq!(Px::MIN - Px::ONE, Px::MIN);
assert_eq!(Px::from_f32(1e12), Px::MAX); assert_eq!(Px::from_f32(1e12), Px::MAX);
assert_eq!(Px::from_f32(-1e12), Px::MIN); assert_eq!(Px::from_f32(-1e12), Px::MIN);
assert_eq!(Px::from_int(i32::MAX), Px::MAX);
} }
#[test] #[test]