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}