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:
1 parent
394d5149a5
commit
4febabfd2e
1 file changed
+33
-25
+33
-25
@@ -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]
|
||||||
|
|||||||
Reference in new issue
Block a user