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
/// divide a whole number: a power of two also converts to `f32` without
/// 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)]
#[derive(
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
/// numbers can be and still mean the same place.
pub const STEP: Self = Self(1);
/// Also what stands in for an unbounded end, since arithmetic saturates
/// here rather than wrapping past it.
/// Also what stands in for an unbounded end: compared against, never
/// added to, since arithmetic wraps past it.
pub const MIN: Self = Self(i32::MIN);
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 {
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
/// has no nearest step and becomes zero, which is a caller's mistake
/// rather than a value worth carrying.
/// Rounds to the nearest step, and clamps to the ends of the grid rather
/// than wrapping: this is where a number from outside arrives, and a float
/// 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`,
/// 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.
pub const fn to_scale<const TO: u32>(self) -> Fixed<TO> {
Fixed(match TO >= SHIFT {
true => narrow((self.0 as i64) << (TO - SHIFT)),
false => narrow(shift_round(self.0 as i64, SHIFT - TO)),
true => self.0 << (TO - SHIFT),
false => shift_round(self.0 as i64, SHIFT - TO) as i32,
})
}
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 {
Self(self.0.saturating_sub(rhs.0))
Self(self.0.wrapping_sub(rhs.0))
}
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
/// of itself and keeps being a length: the product is measured in the
/// receiver's steps.
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
@@ -150,7 +158,7 @@ impl<const SHIFT: u32> Fixed<SHIFT> {
/// Repeated a whole number of times, which no grid rounds.
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.
@@ -159,12 +167,13 @@ impl<const SHIFT: u32> Fixed<SHIFT> {
if by == 0 {
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
/// box of no length has no fraction of itself -- and saturates so that a
/// release build lays out something absurd rather than dying.
/// box of no length has no fraction of itself -- and answers with the end
/// 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 {
debug_assert!(by.0 != 0, "dividing by a length of zero");
if by.0 == 0 {
@@ -173,7 +182,7 @@ impl<const SHIFT: u32> Fixed<SHIFT> {
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
@@ -183,7 +192,7 @@ impl<const SHIFT: u32> Fixed<SHIFT> {
if den.0 == 0 {
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
@@ -207,7 +216,7 @@ impl<const SHIFT: u32> Fixed<SHIFT> {
}
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 {
@@ -219,11 +228,11 @@ impl<const SHIFT: u32> Fixed<SHIFT> {
/// boundary. The step is the whole gap, so there is nothing to exclude
/// between this and the boundary itself.
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 {
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 {
if v > i32::MAX as i64 {
return i32::MAX;
@@ -471,12 +482,9 @@ mod tests {
}
#[test]
fn arithmetic_saturates_rather_than_wrapping() {
assert_eq!(Px::MAX + Px::ONE, Px::MAX);
assert_eq!(Px::MIN - Px::ONE, Px::MIN);
fn a_number_from_outside_is_clamped_to_the_grid() {
assert_eq!(Px::from_f32(1e12), Px::MAX);
assert_eq!(Px::from_f32(-1e12), Px::MIN);
assert_eq!(Px::from_int(i32::MAX), Px::MAX);
}
#[test]