Skip to main content

renew_fixed/
wide.rs

1//! Products that have not been narrowed yet.
2
3use crate::Fixed;
4
5/// The fractional bits of a [`Fixed`].
6const FRAC_BITS: u32 = 16;
7
8/// A product of two [`Fixed`] values, kept at full width: Q95.32 in an `i128`.
9///
10/// # Why this exists
11///
12/// [`Fixed::saturating_mul`] narrows its `i128` product back to an `i64`,
13/// which is right for arithmetic that stays in the world and wrong for
14/// geometry that squares things twice. Ray-versus-sphere forms `b² − 4ac`
15/// where `a` and `c` are themselves squared lengths; at ordinary game scales
16/// `a·c` overflows a `Fixed` long before the ray does anything unusual, and
17/// the result saturates — deterministically, and to the wrong number.
18///
19/// So the narrowing is deferred. A product of two full-range `Fixed` values
20/// needs 126 bits and an `i128` holds 127, which means **a single `wide_mul`
21/// can never overflow**, for any inputs at all. Sums of a few of them cannot
22/// either at any scale a world reaches.
23///
24/// # Contract
25///
26/// - **32 fractional bits**, being the sum of its operands' sixteen. That is
27///   not an implementation detail: it is why [`Wide::sqrt`] needs no shift.
28/// - **Ordering is exact**, so comparing two products — which is most of what
29///   geometry does with them — never rounds at all.
30/// - **Narrowing is explicit**, and says whether it lost anything.
31#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
32#[repr(transparent)]
33pub struct Wide(i128);
34
35impl Wide {
36    /// Zero.
37    pub const ZERO: Self = Self(0);
38
39    /// The raw Q95.32 pattern.
40    ///
41    /// There is no `from_bits` counterpart, deliberately. A `Wide` is an
42    /// intermediate rather than state: it is produced by a multiply and
43    /// consumed by a root, a comparison or a narrowing, and nothing
44    /// serialises one. An unused constructor is a promise to keep working
45    /// that nobody asked for.
46    #[must_use]
47    pub const fn to_bits(self) -> i128 {
48        self.0
49    }
50
51    /// −1, 0 or 1.
52    #[must_use]
53    pub const fn signum(self) -> i32 {
54        self.0.signum() as i32
55    }
56
57    /// The square root, as a [`Fixed`].
58    ///
59    /// **No shift, and that is the point of 32 fractional bits.** A `Wide`
60    /// holding the real value `v` has raw pattern `v · 2³²`, whose integer
61    /// square root is `√v · 2¹⁶` — exactly a `Fixed`'s raw pattern. Squaring
62    /// and then rooting therefore loses nothing to scaling, where the
63    /// narrow path had to shift left by sixteen first and could overflow
64    /// doing it.
65    ///
66    /// Floor-exact, by `u128::isqrt`'s own contract.
67    ///
68    /// # Panics
69    ///
70    /// If negative. See [`Wide::checked_sqrt`].
71    #[must_use]
72    pub fn sqrt(self) -> Fixed {
73        assert!(self.0 >= 0, "Wide::sqrt of a negative value");
74        self.checked_sqrt().unwrap_or(Fixed::ZERO)
75    }
76
77    /// The square root, or `None` if negative — which a discriminant is,
78    /// routinely, and which is a miss rather than a mistake.
79    #[must_use]
80    pub fn checked_sqrt(self) -> Option<Fixed> {
81        if self.0 < 0 {
82            return None;
83        }
84        #[expect(clippy::cast_sign_loss, reason = "non-negative by the check above")]
85        let root = (self.0 as u128).isqrt();
86
87        // **A root below 2^64 does not fit an `i64`, and this cast was written
88        // as though it did.** Squared lengths above about 2^126 have roots
89        // above `i64::MAX`, and casting those produced a *negative* length — a
90        // distance less than nothing, returned silently by a function named
91        // `checked_`. Reachable from `Vec2::length` at coordinates beyond the
92        // documented world bounds, which is where a caller has least reason to
93        // expect a wrong answer and most reason to expect a saturated one.
94        //
95        // This crate promises that overflow saturates in every profile and is
96        // counted, never wraps. So it saturates, and it is counted.
97        if root > i64::MAX as u128 {
98            crate::saturation::record();
99            return Some(Fixed::from_bits(i64::MAX));
100        }
101        #[expect(
102            clippy::cast_possible_truncation,
103            reason = "bounded by the saturation check immediately above"
104        )]
105        let narrowed = root as i64;
106        Some(Fixed::from_bits(narrowed))
107    }
108
109    /// Back to a [`Fixed`], or `None` if it will not fit.
110    #[must_use]
111    pub const fn checked_narrow(self) -> Option<Fixed> {
112        let rounded = round_shift(self.0);
113        if rounded > i64::MAX as i128 || rounded < i64::MIN as i128 {
114            None
115        } else {
116            #[expect(
117                clippy::cast_possible_truncation,
118                reason = "the branches above establish the value is in range"
119            )]
120            let narrowed = rounded as i64;
121            Some(Fixed::from_bits(narrowed))
122        }
123    }
124}
125
126/// Shift a Q95.32 value down to Q47.16, rounding to nearest, ties away from
127/// zero — the same rule the scalar multiply uses, so a value that travels the
128/// wide path and one that does not agree.
129const fn round_shift(value: i128) -> i128 {
130    let half = 1i128 << (FRAC_BITS - 1);
131    if value >= 0 {
132        (value + half) >> FRAC_BITS
133    } else {
134        -((-value + half) >> FRAC_BITS)
135    }
136}
137
138impl Fixed {
139    /// Multiply without narrowing, so nothing can overflow.
140    ///
141    /// The form to reach for when the product is itself going to be squared,
142    /// summed with other products, or only compared — which covers most of
143    /// what collision detection does with a multiply.
144    #[must_use]
145    pub const fn wide_mul(self, other: Self) -> Wide {
146        Wide(self.to_bits() as i128 * other.to_bits() as i128)
147    }
148}
149
150/// Saturate, and count it — the same contract the narrow arithmetic has.
151///
152/// **These counted nothing until a review pointed it out.** A `Wide` sum that
153/// overflowed clamped in silence, which matters more here than anywhere else:
154/// this type exists so geometry can stop worrying about overflow, and a
155/// caller asserting the saturation count is zero across a step would have
156/// been told nothing about the path it was told to use.
157///
158/// Reachable rather than theoretical: three squared full-range products
159/// summed is 3·2¹²⁶, which an `i128` does not hold.
160fn counted(value: i128, saturated: bool) -> Wide {
161    if saturated {
162        crate::saturation::record();
163    }
164    Wide(value)
165}
166
167impl core::ops::Add for Wide {
168    type Output = Self;
169    fn add(self, other: Self) -> Self {
170        match self.0.checked_add(other.0) {
171            Some(value) => counted(value, false),
172            None => counted(if self.0 > 0 { i128::MAX } else { i128::MIN }, true),
173        }
174    }
175}
176
177impl core::ops::Sub for Wide {
178    type Output = Self;
179    fn sub(self, other: Self) -> Self {
180        match self.0.checked_sub(other.0) {
181            Some(value) => counted(value, false),
182            None => counted(if self.0 > 0 { i128::MAX } else { i128::MIN }, true),
183        }
184    }
185}
186
187impl core::ops::Neg for Wide {
188    type Output = Self;
189    fn neg(self) -> Self {
190        match self.0.checked_neg() {
191            Some(value) => counted(value, false),
192            None => counted(i128::MAX, true),
193        }
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use super::Wide;
200    use crate::Fixed;
201
202    #[test]
203    fn a_product_of_two_extremes_does_not_overflow() {
204        // The case the narrow multiply saturates on, and this one cannot.
205        let wide = Fixed::MAX.wide_mul(Fixed::MAX);
206        assert!(wide.to_bits() > 0, "the product stayed positive");
207        assert_eq!(wide.checked_narrow(), None, "and it does not fit a Fixed");
208        // Which is the whole point: it is representable here and reportable
209        // there, rather than clamped in silence.
210        assert_eq!(Fixed::MIN.wide_mul(Fixed::MAX).signum(), -1);
211    }
212
213    #[test]
214    fn squaring_then_rooting_returns_the_value() {
215        for units in [1i32, 2, 7, 100, 4096, 1_000_000] {
216            let value = Fixed::from_int(units);
217            let root = value.wide_mul(value).sqrt();
218            assert_eq!(root, value, "sqrt of {units} squared should be {units}");
219        }
220    }
221
222    /// The scaling property the type exists for: no shift between squaring
223    /// and rooting, so nothing overflows on the way.
224    #[test]
225    fn rooting_works_at_magnitudes_the_narrow_path_cannot_reach() {
226        // A value whose square does not fit a Fixed at all.
227        let big = Fixed::from_int(100_000_000);
228        assert_eq!(
229            big.wide_mul(big).checked_narrow(),
230            None,
231            "square does not fit"
232        );
233        // And yet its root comes back exactly.
234        assert_eq!(big.wide_mul(big).sqrt(), big);
235    }
236
237    #[test]
238    fn narrowing_rounds_the_way_the_scalar_multiply_does() {
239        // A value that travels the wide path and one that does not must
240        // agree, or the two multiplies are different arithmetic.
241        for (a, b) in [(3, 7), (-3, 7), (3, -7), (-3, -7), (1, 3), (-1, 3)] {
242            let x = Fixed::from_ratio(a, 4);
243            let y = Fixed::from_ratio(b, 8);
244            assert_eq!(
245                x.wide_mul(y).checked_narrow(),
246                Some(x.saturating_mul(y)),
247                "wide and narrow multiply disagreed on {a}/4 * {b}/8"
248            );
249        }
250    }
251
252    /// The counter must see a wide saturation, or a caller's assertion about
253    /// a step's saturation count is blind to the path this type provides.
254    /// It counted nothing at all until a review asked.
255    #[test]
256    fn a_wide_overflow_is_counted_like_a_narrow_one() {
257        let huge = Fixed::MAX.wide_mul(Fixed::MAX);
258        let before = crate::saturations();
259        let _ = huge + huge + huge;
260        assert!(
261            crate::saturations().0 > before.0,
262            "a wide sum that overflowed reported nothing"
263        );
264        // And an ordinary sum reports nothing, so the counter means something.
265        let small = Fixed::ONE.wide_mul(Fixed::ONE);
266        let quiet = crate::saturations();
267        let _ = small + small - small;
268        assert_eq!(crate::saturations(), quiet);
269
270        // Subtraction and negation too, since each has its own arm and a
271        // counter fitted to one of three operations reports a third of the
272        // truth.
273        let very_negative = Fixed::MIN.wide_mul(Fixed::MAX);
274        let floored = very_negative + very_negative + very_negative;
275        let before_sub = crate::saturations();
276        let _ = floored - huge;
277        assert!(
278            crate::saturations().0 > before_sub.0,
279            "a wide subtraction that overflowed reported nothing"
280        );
281        let before_neg = crate::saturations();
282        let _ = -floored;
283        assert!(
284            crate::saturations().0 > before_neg.0,
285            "negating the bottom of the range reported nothing"
286        );
287    }
288
289    #[test]
290    fn a_negative_value_has_no_root_and_says_so() {
291        let negative = Fixed::from_int(-1).wide_mul(Fixed::from_int(1));
292        assert_eq!(negative.checked_sqrt(), None);
293        assert_eq!(negative.signum(), -1);
294        assert_eq!(Wide::ZERO.signum(), 0);
295        assert_eq!(Wide::ZERO.sqrt(), Fixed::ZERO);
296    }
297
298    #[test]
299    #[should_panic(expected = "Wide::sqrt of a negative value")]
300    fn the_asserting_root_refuses_a_negative() {
301        let _ = Fixed::from_int(-1).wide_mul(Fixed::ONE).sqrt();
302    }
303
304    /// Sums and differences, which is what a discriminant is.
305    #[test]
306    fn wide_values_add_subtract_and_order() {
307        let two = Fixed::from_int(2);
308        let three = Fixed::from_int(3);
309        let six = two.wide_mul(three);
310        let four = two.wide_mul(two);
311        assert!(six > four);
312        assert_eq!((six - four).checked_narrow(), Some(Fixed::from_int(2)));
313        assert_eq!((four + four).checked_narrow(), Some(Fixed::from_int(8)));
314        assert_eq!((-six).signum(), -1);
315        // Ordering is exact, which is what makes comparing products safe.
316        assert_eq!(six.max(four), six);
317    }
318}