Skip to main content

renew_fixed/
scalar.rs

1//! The scalar type.
2
3use core::ops::{Add, Div, Mul, Neg, Sub};
4
5use crate::saturation;
6
7/// Fractional bits. Q47.16.
8const FRAC_BITS: u32 = 16;
9
10/// One whole unit, as a raw pattern.
11const ONE_RAW: i64 = 1 << FRAC_BITS;
12
13/// A fixed-point number: Q47.16 in an `i64`.
14///
15/// # Contract
16///
17/// - **Resolution 2⁻¹⁶ ≈ 0.0000153; range ±2⁴⁷ ≈ ±1.4 × 10¹⁴.**
18/// - **Every operation saturates on overflow**, in every build profile, and
19///   increments the thread's [`crate::saturations`] counter when it does.
20///   Never wraps. Never differs between debug and release.
21/// - **Multiplication and division round to nearest, ties away from zero.**
22///   Symmetric under negation, which the obvious implementation is not — see
23///   [`Fixed::saturating_mul`].
24/// - **Total ordering.** `Ord`, `Eq` and `Hash` are derived from the `i64`,
25///   so this sorts, deduplicates and hashes the way an integer does and
26///   floats cannot.
27///
28/// # Why Q47.16 and not Q32.32
29///
30/// Because physics squares things. A squared value has to fit the type it is
31/// stored in, so the range that matters is not what is representable but what
32/// is **squarable** — the square root of the representable range:
33///
34/// | | representable | squarable |
35/// |---|---|---|
36/// | Q47.16 | ±1.4 × 10¹⁴ | **±1.2 × 10⁷** |
37/// | Q32.32 | ±2.1 × 10⁹ | **±4.6 × 10⁴** |
38///
39/// Two hundred and fifty-six times the working room, for a resolution that is
40/// already finer than anything a game perceives.
41#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
42#[repr(transparent)]
43pub struct Fixed(i64);
44
45impl Fixed {
46    /// Zero.
47    pub const ZERO: Self = Self(0);
48    /// One whole unit.
49    pub const ONE: Self = Self(ONE_RAW);
50    /// The smallest representable value.
51    pub const MIN: Self = Self(i64::MIN);
52    /// The largest representable value.
53    pub const MAX: Self = Self(i64::MAX);
54    /// The smallest step between two values: 2⁻¹⁶.
55    pub const EPSILON: Self = Self(1);
56
57    /// The raw Q47.16 pattern, for serialisation and tests.
58    #[must_use]
59    pub const fn from_bits(raw: i64) -> Self {
60        Self(raw)
61    }
62
63    /// The raw pattern back out.
64    #[must_use]
65    pub const fn to_bits(self) -> i64 {
66        self.0
67    }
68
69    /// A whole number, exactly.
70    ///
71    /// `i32` rather than `i64` so the shift cannot overflow: every `i32`
72    /// shifted left by 16 fits an `i64` with room to spare, which makes this
73    /// total and lets it be `const`.
74    #[must_use]
75    pub const fn from_int(value: i32) -> Self {
76        Self((value as i64) << FRAC_BITS)
77    }
78
79    /// A ratio of two integers — how a value like 9.81 is written without a
80    /// float ever existing: `Fixed::from_ratio(981, 100)`.
81    ///
82    /// Rounds to nearest, ties away from zero, like [`Fixed::saturating_mul`].
83    ///
84    /// # Panics
85    ///
86    /// If `denominator` is zero. A contract violation rather than a runtime
87    /// condition (D5): the arguments are almost always literals, so this
88    /// fails at the call site that wrote it, and in a `const` context it
89    /// fails at compile time.
90    #[must_use]
91    pub const fn from_ratio(numerator: i32, denominator: i32) -> Self {
92        assert!(
93            denominator != 0,
94            "Fixed::from_ratio needs a nonzero denominator"
95        );
96        let scaled = (numerator as i64) << FRAC_BITS;
97        let den = denominator as i64;
98        Self(round_div(scaled, den))
99    }
100
101    /// The whole part, truncated toward zero.
102    #[must_use]
103    pub const fn trunc_int(self) -> i64 {
104        self.0 / ONE_RAW
105    }
106
107    /// The fractional part, with the sign of the whole.
108    #[must_use]
109    pub const fn fract(self) -> Self {
110        Self(self.0 % ONE_RAW)
111    }
112
113    /// Absolute value, saturating at [`Fixed::MAX`] for [`Fixed::MIN`].
114    #[must_use]
115    pub fn abs(self) -> Self {
116        let Some(value) = self.0.checked_abs() else {
117            saturation::record();
118            return Self::MAX;
119        };
120        Self(value)
121    }
122
123    /// -1, 0 or 1, as whole units.
124    #[must_use]
125    pub const fn signum(self) -> Self {
126        Self(ONE_RAW * self.0.signum())
127    }
128
129    /// The smaller of two values.
130    #[must_use]
131    pub const fn min(self, other: Self) -> Self {
132        if self.0 < other.0 { self } else { other }
133    }
134
135    /// The larger of two values.
136    #[must_use]
137    pub const fn max(self, other: Self) -> Self {
138        if self.0 > other.0 { self } else { other }
139    }
140
141    /// Constrained to `[low, high]`.
142    ///
143    /// # Panics
144    ///
145    /// If `low > high`, which is a contract violation rather than a value to
146    /// interpret — the caller has said something they cannot mean.
147    #[must_use]
148    pub const fn clamp(self, low: Self, high: Self) -> Self {
149        assert!(low.0 <= high.0, "Fixed::clamp needs low <= high");
150        self.max(low).min(high)
151    }
152
153    /// Multiply, rounding to nearest with ties away from zero.
154    ///
155    /// **The rounding rule is load-bearing.** The obvious implementation —
156    /// `(a as i128 * b as i128) >> 16` — is an arithmetic shift, which rounds
157    /// toward negative infinity and is therefore *asymmetric under negation*:
158    /// `(-a) * b` and `-(a * b)` differ for some inputs. That is deterministic
159    /// and still wrong for physics, because a body moving left and the same
160    /// body moving right would accumulate different error. Rounding to nearest
161    /// with ties away from zero is symmetric, and halves the worst-case error
162    /// besides.
163    ///
164    /// Saturates rather than wrapping, and counts when it does.
165    #[must_use]
166    pub fn saturating_mul(self, other: Self) -> Self {
167        let product = i128::from(self.0) * i128::from(other.0);
168        Self(narrow(round_shift(product)))
169    }
170
171    /// Divide, rounding to nearest with ties away from zero.
172    ///
173    /// # Panics
174    ///
175    /// If `other` is zero. Division by zero is a contract violation (D5), and
176    /// returning a sentinel would put a NaN-shaped value into a type whose
177    /// whole contract is that it has none.
178    #[must_use]
179    pub fn saturating_div(self, other: Self) -> Self {
180        assert!(other.0 != 0, "Fixed division by zero");
181        let numerator = i128::from(self.0) << FRAC_BITS;
182        Self(narrow(round_div_i128(numerator, i128::from(other.0))))
183    }
184
185    /// The square root, floored to the representable value below the exact
186    /// result.
187    ///
188    /// Uses `u128::isqrt`, which is exact by its own contract, on a `u128`
189    /// intermediate — the shifted value needs 79 bits, so a 64-bit one would
190    /// be wrong rather than merely slower. Not a hand-rolled iteration: the
191    /// standard library's is boring and already correct, and this is the one
192    /// kernel here with a non-trivial correctness argument.
193    ///
194    /// # Panics
195    ///
196    /// If `self` is negative. See [`Fixed::checked_sqrt`] for the form that
197    /// answers instead of refusing.
198    #[must_use]
199    pub fn sqrt(self) -> Self {
200        assert!(self.0 >= 0, "Fixed::sqrt of a negative value");
201        // The assertion above is the whole precondition, so the only `None`
202        // this can produce is one the assertion already refused.
203        self.checked_sqrt().unwrap_or(Self::ZERO)
204    }
205
206    /// The square root, or `None` for a negative value.
207    #[must_use]
208    pub fn checked_sqrt(self) -> Option<Self> {
209        if self.0 < 0 {
210            return None;
211        }
212        // Both casts are guarded by the sign check above: the widening is
213        // value-preserving on a non-negative input, and the root of a value
214        // below 2^63 shifted left by 16 is below 2^40, so narrowing it back
215        // cannot reach the sign bit.
216        #[expect(
217            clippy::cast_sign_loss,
218            clippy::cast_possible_truncation,
219            reason = "guarded by the sign check above and by the root's own magnitude"
220        )]
221        let root = ((self.0 as u128) << FRAC_BITS).isqrt() as i64;
222        Some(Self(root))
223    }
224
225    /// Add, or `None` if the result would not fit.
226    #[must_use]
227    pub const fn checked_add(self, other: Self) -> Option<Self> {
228        match self.0.checked_add(other.0) {
229            Some(sum) => Some(Self(sum)),
230            None => None,
231        }
232    }
233
234    /// Subtract, or `None` if the result would not fit.
235    #[must_use]
236    pub const fn checked_sub(self, other: Self) -> Option<Self> {
237        match self.0.checked_sub(other.0) {
238            Some(difference) => Some(Self(difference)),
239            None => None,
240        }
241    }
242
243    /// Divide, or `None` for a zero divisor or a result that would not fit.
244    ///
245    /// The form to reach for wherever a divisor comes from data rather than
246    /// from a literal — a ray direction, a difference of two positions, a
247    /// time of impact. [`Fixed::saturating_div`] asserts on zero because a
248    /// literal zero divisor is a programming error; a *computed* zero is an
249    /// ordinary value that geometry produces constantly, and asserting on it
250    /// would put a panic on a path that runs every frame.
251    #[must_use]
252    pub const fn checked_div(self, other: Self) -> Option<Self> {
253        if other.0 == 0 {
254            return None;
255        }
256        let rounded = round_div_i128((self.0 as i128) << FRAC_BITS, other.0 as i128);
257        if rounded > i64::MAX as i128 || rounded < i64::MIN as i128 {
258            None
259        } else {
260            #[expect(
261                clippy::cast_possible_truncation,
262                reason = "the branches above establish the value is in range"
263            )]
264            let narrowed = rounded as i64;
265            Some(Self(narrowed))
266        }
267    }
268
269    /// Multiply, or `None` if the result would not fit.
270    ///
271    /// `const`, and therefore not counted: a compile-time context has no
272    /// thread to count on, and a caller asking this question wants the answer
273    /// rather than a diagnostic.
274    #[must_use]
275    pub const fn checked_mul(self, other: Self) -> Option<Self> {
276        let rounded = round_shift(self.0 as i128 * other.0 as i128);
277        if rounded > i64::MAX as i128 || rounded < i64::MIN as i128 {
278            None
279        } else {
280            #[expect(
281                clippy::cast_possible_truncation,
282                reason = "the branches above establish the value is in range"
283            )]
284            let narrowed = rounded as i64;
285            Some(Self(narrowed))
286        }
287    }
288}
289
290/// Shift a 128-bit product right by the fractional bits, rounding to nearest
291/// with ties away from zero.
292const fn round_shift(product: i128) -> i128 {
293    let half = 1i128 << (FRAC_BITS - 1);
294    if product >= 0 {
295        (product + half) >> FRAC_BITS
296    } else {
297        // Symmetric: round the magnitude, then restore the sign. Using the
298        // shift directly here is what makes the operation asymmetric.
299        -((-product + half) >> FRAC_BITS)
300    }
301}
302
303/// Divide, rounding to nearest with ties away from zero.
304const fn round_div(numerator: i64, denominator: i64) -> i64 {
305    let (magnitude, negative) = match (numerator < 0, denominator < 0) {
306        (false, false) => (numerator / denominator, false),
307        (true, true) => ((-numerator) / (-denominator), false),
308        (true, false) => ((-numerator) / denominator, true),
309        (false, true) => (numerator / (-denominator), true),
310    };
311    let remainder = (numerator % denominator).abs();
312    let half = denominator.abs() / 2;
313    let rounded = if remainder * 2 >= denominator.abs() && half >= 0 {
314        magnitude + 1
315    } else {
316        magnitude
317    };
318    if negative { -rounded } else { rounded }
319}
320
321/// The 128-bit form of [`round_div`].
322const fn round_div_i128(numerator: i128, denominator: i128) -> i128 {
323    let negative = (numerator < 0) != (denominator < 0);
324    let num = if numerator < 0 { -numerator } else { numerator };
325    let den = if denominator < 0 {
326        -denominator
327    } else {
328        denominator
329    };
330    let quotient = num / den;
331    let rounded = if (num % den) * 2 >= den {
332        quotient + 1
333    } else {
334        quotient
335    };
336    if negative { -rounded } else { rounded }
337}
338
339/// Bring a 128-bit result back to an `i64`, saturating and counting.
340fn narrow(value: i128) -> i64 {
341    if value > i128::from(i64::MAX) {
342        saturation::record();
343        i64::MAX
344    } else if value < i128::from(i64::MIN) {
345        saturation::record();
346        i64::MIN
347    } else {
348        #[expect(
349            clippy::cast_possible_truncation,
350            reason = "the branches above establish the value is in range"
351        )]
352        let narrowed = value as i64;
353        narrowed
354    }
355}
356
357impl Add for Fixed {
358    type Output = Self;
359    fn add(self, other: Self) -> Self {
360        let Some(sum) = self.0.checked_add(other.0) else {
361            saturation::record();
362            return if self.0 > 0 { Self::MAX } else { Self::MIN };
363        };
364        Self(sum)
365    }
366}
367
368impl Sub for Fixed {
369    type Output = Self;
370    fn sub(self, other: Self) -> Self {
371        let Some(difference) = self.0.checked_sub(other.0) else {
372            saturation::record();
373            return if self.0 > 0 { Self::MAX } else { Self::MIN };
374        };
375        Self(difference)
376    }
377}
378
379impl Neg for Fixed {
380    type Output = Self;
381    fn neg(self) -> Self {
382        let Some(negated) = self.0.checked_neg() else {
383            saturation::record();
384            return Self::MAX;
385        };
386        Self(negated)
387    }
388}
389
390impl Mul for Fixed {
391    type Output = Self;
392    fn mul(self, other: Self) -> Self {
393        self.saturating_mul(other)
394    }
395}
396
397impl Div for Fixed {
398    type Output = Self;
399    fn div(self, other: Self) -> Self {
400        self.saturating_div(other)
401    }
402}
403
404#[cfg(test)]
405mod tests {
406    use super::{Fixed, round_div};
407    use crate::saturations;
408
409    #[test]
410    fn absolute_value_saturates_at_the_bottom_of_the_range() {
411        assert_eq!(Fixed::from_int(-3).abs(), Fixed::from_int(3));
412        assert_eq!(Fixed::from_int(3).abs(), Fixed::from_int(3));
413        assert_eq!(Fixed::ZERO.abs(), Fixed::ZERO);
414        // MIN has no positive counterpart, which is the one input where
415        // this cannot answer exactly.
416        let before = saturations();
417        assert_eq!(Fixed::MIN.abs(), Fixed::MAX);
418        assert_eq!(saturations().0, before.0 + 1, "the clamp must be counted");
419    }
420
421    #[test]
422    fn signum_reports_whole_units() {
423        assert_eq!(Fixed::from_int(-9).signum(), Fixed::from_int(-1));
424        assert_eq!(Fixed::ZERO.signum(), Fixed::ZERO);
425        assert_eq!(Fixed::from_ratio(1, 1000).signum(), Fixed::ONE);
426    }
427
428    #[test]
429    fn min_max_and_clamp_agree_with_the_ordering() {
430        let low = Fixed::from_int(-2);
431        let high = Fixed::from_int(5);
432        assert_eq!(low.min(high), low);
433        assert_eq!(low.max(high), high);
434        assert_eq!(Fixed::from_int(9).clamp(low, high), high);
435        assert_eq!(Fixed::from_int(-9).clamp(low, high), low);
436        assert_eq!(Fixed::from_int(1).clamp(low, high), Fixed::from_int(1));
437    }
438
439    #[test]
440    #[should_panic(expected = "Fixed::clamp needs low <= high")]
441    fn clamp_refuses_an_inverted_range() {
442        let _ = Fixed::ZERO.clamp(Fixed::ONE, Fixed::ZERO);
443    }
444
445    #[test]
446    #[should_panic(expected = "Fixed::from_ratio needs a nonzero denominator")]
447    fn a_ratio_over_zero_is_refused() {
448        let _ = Fixed::from_ratio(1, 0);
449    }
450
451    #[test]
452    #[should_panic(expected = "Fixed division by zero")]
453    fn division_by_zero_is_refused() {
454        let _ = Fixed::ONE.saturating_div(Fixed::ZERO);
455    }
456
457    #[test]
458    #[should_panic(expected = "Fixed::sqrt of a negative value")]
459    fn the_square_root_of_a_negative_is_refused() {
460        let _ = Fixed::from_int(-1).sqrt();
461    }
462
463    /// The checked forms answer instead of clamping, which is what a caller
464    /// wanting to handle overflow rather than be told about it asks for.
465    #[test]
466    fn the_checked_forms_report_rather_than_saturate() {
467        assert_eq!(Fixed::ONE.checked_add(Fixed::ONE), Some(Fixed::from_int(2)));
468        assert_eq!(Fixed::MAX.checked_add(Fixed::ONE), None);
469        assert_eq!(Fixed::ONE.checked_sub(Fixed::ONE), Some(Fixed::ZERO));
470        assert_eq!(Fixed::MIN.checked_sub(Fixed::ONE), None);
471        assert_eq!(
472            Fixed::from_int(3).checked_mul(Fixed::from_int(4)),
473            Some(Fixed::from_int(12))
474        );
475        assert_eq!(Fixed::MAX.checked_mul(Fixed::MAX), None);
476
477        // And they do not touch the counter: a caller asking the question
478        // wants the answer, not a diagnostic about having asked.
479        let before = saturations();
480        let _ = Fixed::MAX.checked_add(Fixed::ONE);
481        let _ = Fixed::MAX.checked_mul(Fixed::MAX);
482        assert_eq!(saturations(), before);
483    }
484
485    /// The divisor a ray direction or a position difference produces is
486    /// often zero, and that is data rather than a mistake — so there is a
487    /// form that answers instead of asserting.
488    #[test]
489    fn checked_division_answers_where_the_asserting_form_refuses() {
490        assert_eq!(
491            Fixed::from_int(6).checked_div(Fixed::from_int(3)),
492            Some(Fixed::from_int(2))
493        );
494        assert_eq!(Fixed::ONE.checked_div(Fixed::ZERO), None);
495        assert_eq!(Fixed::ZERO.checked_div(Fixed::ZERO), None);
496        // A quotient too large to represent is reported, not clamped.
497        assert_eq!(Fixed::MAX.checked_div(Fixed::EPSILON), None);
498        // Rounds the same way the asserting form does, so swapping between
499        // them never changes a value.
500        assert_eq!(
501            Fixed::from_int(7).checked_div(Fixed::from_int(2)),
502            Some(Fixed::from_int(7).saturating_div(Fixed::from_int(2)))
503        );
504        // And touches no counter: asking is not saturating.
505        let before = saturations();
506        let _ = Fixed::MAX.checked_div(Fixed::EPSILON);
507        let _ = Fixed::ONE.checked_div(Fixed::ZERO);
508        assert_eq!(saturations(), before);
509    }
510
511    #[test]
512    fn subtraction_and_negation_saturate_at_both_ends() {
513        assert_eq!(Fixed::from_int(5) - Fixed::from_int(3), Fixed::from_int(2));
514        assert_eq!(-Fixed::from_int(3), Fixed::from_int(-3));
515        let before = saturations();
516        assert_eq!(Fixed::MAX - Fixed::MIN, Fixed::MAX);
517        assert_eq!(-Fixed::MIN, Fixed::MAX);
518        assert_eq!(saturations().0, before.0 + 2);
519    }
520
521    /// The whole and fractional parts of a negative value both carry the
522    /// sign, which is the convention `i64` division already has and the one
523    /// a reader will assume.
524    #[test]
525    fn the_parts_of_a_negative_value_carry_its_sign() {
526        let value = Fixed::from_ratio(-7, 2);
527        assert_eq!(value.trunc_int(), -3);
528        assert_eq!(value.fract(), Fixed::from_ratio(-1, 2));
529    }
530
531    /// Ratios round to nearest with ties away from zero, symmetrically, so
532    /// a negative constant is the negation of its positive twin.
533    #[test]
534    fn ratios_round_symmetrically() {
535        assert_eq!(Fixed::from_ratio(-981, 100), -Fixed::from_ratio(981, 100));
536        assert_eq!(Fixed::from_ratio(981, -100), -Fixed::from_ratio(981, 100));
537        assert_eq!(Fixed::from_ratio(1, 2), Fixed::from_bits(1 << 15));
538    }
539
540    /// The rounding helper on its own, over the sign quadrants and the tie,
541    /// because every constructor and both of the rounded operators go
542    /// through one of these.
543    #[test]
544    fn the_rounding_helper_is_symmetric_and_rounds_ties_away_from_zero() {
545        assert_eq!(round_div(7, 2), 4);
546        assert_eq!(round_div(-7, 2), -4);
547        assert_eq!(round_div(7, -2), -4);
548        assert_eq!(round_div(-7, -2), 4);
549        assert_eq!(round_div(5, 2), 3, "a tie rounds away from zero");
550        assert_eq!(round_div(-5, 2), -3, "and symmetrically");
551        assert_eq!(round_div(4, 2), 2, "an exact quotient is untouched");
552    }
553
554    /// The operators are the ergonomic surface and the named forms are the
555    /// documented ones; nothing but this asserts they are the same
556    /// arithmetic. A `Mul` that reached for the shift while
557    /// `saturating_mul` rounded would pass every other test in this file.
558    #[test]
559    fn the_operators_delegate_to_the_named_forms() {
560        let a = Fixed::from_ratio(7, 3);
561        let b = Fixed::from_ratio(-11, 5);
562        assert_eq!(a * b, a.saturating_mul(b));
563        assert_eq!(a / b, a.saturating_div(b));
564        assert_eq!(a + b, Fixed::from_bits(a.to_bits() + b.to_bits()));
565        assert_eq!(a - b, Fixed::from_bits(a.to_bits() - b.to_bits()));
566        // And the operators saturate, since they are the same code path.
567        assert_eq!(Fixed::MAX * Fixed::MAX, Fixed::MAX);
568    }
569
570    /// Division saturates like everything else rather than wrapping.
571    #[test]
572    fn division_saturates_when_the_quotient_does_not_fit() {
573        let before = saturations();
574        assert_eq!(Fixed::MAX.saturating_div(Fixed::EPSILON), Fixed::MAX);
575        assert_eq!(saturations().0, before.0 + 1);
576    }
577}