Skip to main content

crcbl_core/
world.rs

1//! Sector-tiled world positions.
2//!
3//! Every absolute position in the engine — simulation, physics, streaming,
4//! networking — is a [`WorldPos`]: an integer sector index plus an `f64` offset
5//! inside that sector. Nothing anywhere holds an absolute `Vec3`. The only
6//! sanctioned way to reach `Vec3` is [`WorldPos::relative_to`], which produces
7//! a *camera-relative* render-space vector; see its docs.
8//!
9//! This lands in stage 1, long before physics, because retrofitting galaxy-
10//! scale coordinates into an engine that assumed `Vec3` is a rewrite of every
11//! system that touches a position.
12
13use core::fmt;
14use core::ops::{Add, AddAssign, Sub, SubAssign};
15
16use glam::{DVec3, I64Vec3, Vec3};
17
18/// Base-2 logarithm of [`SECTOR_SIZE`].
19///
20/// The sector edge is a power of two so that rebasing is *exact*: dividing an
21/// `f64` by a power of two only changes the exponent, `floor` of the result is
22/// exact, and `local - k * SECTOR_SIZE` is representable, so no bits are lost
23/// when a body crosses a sector boundary. A decimal sector size (say 1e6 m)
24/// would round on every crossing and the error would accumulate over a session.
25pub const SECTOR_SIZE_LOG2: u32 = 20;
26
27/// Edge length of a sector, in metres: `2^20 m` = 1 048 576 m ≈ 1048.6 km.
28///
29/// The sector grid is one structure with three consumers — streaming unit,
30/// broadphase partition and interest-management key (see
31/// `docs/notes/simulation.md`) — so the cell size is chosen to be a *useful
32/// spatial cell first*, and the index widened until the extent follows:
33///
34/// * **As a cell**: an FPS map or a town sits inside one sector; Earth's
35///   diameter spans ~12 of them; a planet's surface is a few hundred; a solar
36///   system is a sparse hash of the populated ones. At `2^24 m` (16 777 km) the
37///   whole Earth fits in **0.76** of a single cell, which is why this is not
38///   that: a cell that cannot subdivide a planet cannot stream one, and the
39///   per-sector BVH would degenerate into one global tree.
40/// * **Extent**: `I64Vec3` gives `2^64` indices per axis, so the addressable
41///   span is `2^64 * 2^20 = 2^84 m` ≈ 1.93e25 m ≈ **2.04 billion light-years**
42///   per axis. The Milky Way (~100 000 ly) is a rounding error inside that; the
43///   galaxy-scale requirement stopped being the binding constraint the moment
44///   the index went 64-bit.
45/// * **Precision**: local offsets stay in `[0, 2^20)`, where the worst-case
46///   `f64` ULP (at the far corner of a sector, exponent 19) is `2^-33 m` =
47///   **0.116 nanometres** — finer than an atomic radius, and ~524 000× finer
48///   than the `f32` render space that consumes it 1 km from the camera (61 µm
49///   ULP). `WorldPos` is nowhere near being the precision bottleneck.
50///
51/// Every number above is asserted by tests in this module, so this comment
52/// cannot quietly become a lie.
53pub const SECTOR_SIZE: f64 = (1u64 << SECTOR_SIZE_LOG2) as f64;
54
55/// Largest `f64` strictly below [`SECTOR_SIZE`] — the canonical local offset at
56/// the far edge of a sector, used when a position saturates.
57///
58/// Decrementing the bit pattern of a positive finite float steps exactly one
59/// ULP down, giving `2^20 - 2^-33`. Asserted against `f64::next_down` in the
60/// tests.
61const LOCAL_MAX: f64 = f64::from_bits(SECTOR_SIZE.to_bits() - 1);
62
63/// Largest sector-index difference whose metre conversion is exact: `2^53`, the
64/// point where `i64 -> f64` stops being injective.
65///
66/// Scaling by [`SECTOR_SIZE`] is exact at *any* magnitude (it only shifts the
67/// exponent of a power of two), so the sector term of a delta rounds only when
68/// the index difference itself leaves `f64`'s integer range. Public because it
69/// is the honest boundary on [`WorldPos::delta`], and code that cares (an
70/// interest-management cull, a debug assert) should be able to test against it
71/// rather than trust prose. In metres that is `2^73` ≈ 998 000 light-years.
72pub const EXACT_SECTOR_DELTA: i64 = 1 << 53;
73
74/// An absolute position: sector index plus a local offset in metres.
75///
76/// # Canonical form
77///
78/// A normalized `WorldPos` keeps `local` in `[0, SECTOR_SIZE)` on every axis —
79/// a half-open, **non-negative** range rather than a range centred on zero.
80/// That makes `sector` literally `floor(absolute / SECTOR_SIZE)`, i.e. the
81/// index of the grid cell the position is in, which is exactly what the
82/// streaming loader, the broadphase partition and interest management want as a
83/// key (one spatial structure, three consumers — see `docs/notes/simulation.md`).
84/// A centred range would buy one bit of precision and cost that identity, plus
85/// an off-by-half in every consumer that hashes a cell.
86///
87/// The fields are public because this is a plain data type, but a hand-built
88/// value may be denormalized. Arithmetic through the operators below always
89/// returns normalized values; [`WorldPos::new`] normalizes on construction, and
90/// [`WorldPos::normalize`] canonicalizes anything.
91///
92/// # Out-of-range positions
93///
94/// A position that would leave the addressable volume **saturates** at the
95/// boundary sector (`i64::MIN` / `i64::MAX`); it never wraps. Wrapping would
96/// teleport a ship from one edge of the universe to the other and every
97/// downstream distance test would silently agree with it. Saturation is lossy
98/// but monotone, obvious in a debugger, and idempotent under repeated
99/// normalization.
100#[derive(Clone, Copy, PartialEq, Default)]
101pub struct WorldPos {
102    /// Sector index on each axis.
103    pub sector: I64Vec3,
104    /// Offset within the sector, in metres. Normalized to `[0, SECTOR_SIZE)`.
105    pub local: DVec3,
106}
107
108impl WorldPos {
109    /// The origin of sector `(0, 0, 0)`.
110    pub const ORIGIN: Self = Self {
111        sector: I64Vec3::ZERO,
112        local: DVec3::ZERO,
113    };
114
115    /// Builds a normalized position from its parts.
116    #[inline]
117    #[must_use]
118    pub fn new(sector: I64Vec3, local: DVec3) -> Self {
119        Self { sector, local }.normalize()
120    }
121
122    /// Builds a position from an offset relative to the origin of sector
123    /// `(0, 0, 0)`.
124    ///
125    /// Convenient for small worlds and tests. Note that the argument is a plain
126    /// `DVec3` *offset*, not a render-space `Vec3`.
127    #[inline]
128    #[must_use]
129    pub fn from_offset(offset: DVec3) -> Self {
130        Self::new(I64Vec3::ZERO, offset)
131    }
132
133    /// Builds a position at the origin of `sector`.
134    #[inline]
135    #[must_use]
136    pub const fn from_sector(sector: I64Vec3) -> Self {
137        Self {
138            sector,
139            local: DVec3::ZERO,
140        }
141    }
142
143    /// Returns the canonical form of this position: `local` inside
144    /// `[0, SECTOR_SIZE)` with the overflow carried into `sector`.
145    ///
146    /// Exact — no bits are lost — because [`SECTOR_SIZE`] is a power of two.
147    /// Idempotent, including for saturated and non-finite inputs.
148    ///
149    /// A non-finite `local` is a bug upstream: debug builds assert, release
150    /// builds propagate the NaN/infinity rather than inventing a plausible
151    /// position.
152    #[inline]
153    #[must_use]
154    pub fn normalize(self) -> Self {
155        debug_assert!(
156            self.local.is_finite(),
157            "WorldPos::local must be finite, got {}",
158            self.local
159        );
160        let (sx, lx) = normalize_axis(self.sector.x, self.local.x);
161        let (sy, ly) = normalize_axis(self.sector.y, self.local.y);
162        let (sz, lz) = normalize_axis(self.sector.z, self.local.z);
163        Self {
164            sector: I64Vec3::new(sx, sy, sz),
165            local: DVec3::new(lx, ly, lz),
166        }
167    }
168
169    /// Canonicalizes in place. Same operation as [`WorldPos::normalize`], named
170    /// for the physics-side vocabulary ("rebase on sector crossing").
171    #[inline]
172    pub fn rebase(&mut self) {
173        *self = self.normalize();
174    }
175
176    /// Whether `local` is already inside `[0, SECTOR_SIZE)` on every axis.
177    #[inline]
178    #[must_use]
179    pub fn is_normalized(&self) -> bool {
180        let inside = |v: f64| (0.0..SECTOR_SIZE).contains(&v);
181        inside(self.local.x) && inside(self.local.y) && inside(self.local.z)
182    }
183
184    /// The vector from `origin` to `self`, in metres.
185    ///
186    /// This — not any absolute coordinate — is what physics, queries and
187    /// rendering actually consume.
188    ///
189    /// # Precision
190    ///
191    /// The sector difference is taken in `i128`, so it cannot wrap even between
192    /// opposite corners of the addressable volume, and the largest possible
193    /// delta (`2^84 m`) is nowhere near `f64` overflow. The result is therefore
194    /// always finite and never wrong by wraparound.
195    ///
196    /// Scaling the index difference by [`SECTOR_SIZE`] is *always* exact — it
197    /// only shifts the exponent of a power of two. What is not always exact is
198    /// the `i128 -> f64` conversion of the difference itself, which is
199    /// injective only below `2^53`. So:
200    ///
201    /// * **|Δsector| ≤ 2^53** (a separation up to `2^73 m` ≈ **998 000
202    ///   light-years** — the Milky Way and its satellites, ten times over): the
203    ///   sector term is exact, and the only rounding is the `local` difference
204    ///   and the final addition, i.e. sub-nanometre.
205    /// * **Beyond that**: the delta degrades to `f64`'s ordinary relative
206    ///   precision, about `d * 2^-53` for a separation of `d` metres — `2^31 m`
207    ///   ≈ 2.1 million km ≈ 0.014 AU at the full 2.04-billion-light-year span.
208    ///   Honest, finite and monotone, but a question nobody should be asking:
209    ///   anything that far away should have been culled on sector distance long
210    ///   before a metre-valued delta was wanted.
211    ///
212    /// The boundary is pinned by a test.
213    #[inline]
214    #[must_use]
215    pub fn delta(self, origin: Self) -> DVec3 {
216        DVec3::new(
217            delta_axis(self.sector.x, self.local.x, origin.sector.x, origin.local.x),
218            delta_axis(self.sector.y, self.local.y, origin.sector.y, origin.local.y),
219            delta_axis(self.sector.z, self.local.z, origin.sector.z, origin.local.z),
220        )
221    }
222
223    /// Distance between two positions, in metres.
224    #[inline]
225    #[must_use]
226    pub fn distance(self, other: Self) -> f64 {
227        self.delta(other).length()
228    }
229
230    /// Squared distance between two positions, in metres².
231    ///
232    /// Beware the dynamic range: squaring an intergalactic delta stays finite
233    /// in `f64` (`(2^84)^2 = 2^168`) but is only useful for comparisons.
234    #[inline]
235    #[must_use]
236    pub fn distance_squared(self, other: Self) -> f64 {
237        self.delta(other).length_squared()
238    }
239
240    /// Converts to camera-relative **render space**.
241    ///
242    /// This is the *only* sanctioned path from a `WorldPos` to a `Vec3`
243    /// anywhere in the engine. A plain `Vec3` never holds an absolute position:
244    /// it is always an offset from the render origin (typically the camera's
245    /// `WorldPos`), which is what keeps the GPU's `f32` transforms free of
246    /// jitter no matter where in the galaxy the camera is.
247    ///
248    /// The delta is computed in `f64` and narrowed to `f32` exactly once, here.
249    /// Anything far enough away for that narrowing to matter is far enough away
250    /// to have been culled.
251    #[inline]
252    #[must_use]
253    pub fn relative_to(self, origin: Self) -> Vec3 {
254        self.delta(origin).as_vec3()
255    }
256
257    /// Moves by `offset` metres, renormalizing.
258    #[inline]
259    #[must_use]
260    pub fn translated(self, offset: DVec3) -> Self {
261        Self {
262            sector: self.sector,
263            local: self.local + offset,
264        }
265        .normalize()
266    }
267}
268
269/// Canonicalizes one axis. See [`WorldPos::normalize`].
270#[inline]
271fn normalize_axis(sector: i64, local: f64) -> (i64, f64) {
272    // Exact for every finite `local`: the divisor is a power of two, so the
273    // quotient is exact and `floor` of it is exact. NaN yields NaN, whose
274    // `as i128` cast is 0, leaving the position untouched (and idempotent).
275    let mut carry = (local / SECTOR_SIZE).floor();
276    let mut rest = local - carry * SECTOR_SIZE;
277    // `rest` is exact whenever `carry * SECTOR_SIZE` is, which holds across the
278    // whole addressable range. The one exception is a `local` so small and
279    // negative that `local + SECTOR_SIZE` rounds up to exactly `SECTOR_SIZE`;
280    // carry one more sector in that case so the half-open range still holds.
281    if rest >= SECTOR_SIZE {
282        carry += 1.0;
283        rest = 0.0;
284    } else if rest < 0.0 {
285        // Unreachable with exact arithmetic; clamp rather than emit a
286        // denormalized value if a platform ever disagrees.
287        rest = 0.0;
288    }
289
290    // `i128` so the carry cannot wrap the index: `f64 -> i128` saturates (and
291    // maps NaN to 0), and the sum of two `i64`-range values always fits. An
292    // out-of-range position then clamps to the edge of the addressable volume
293    // instead of wrapping to the far side of it.
294    let carried = sector as i128 + carry as i128;
295    if carried < i64::MIN as i128 {
296        (i64::MIN, 0.0)
297    } else if carried > i64::MAX as i128 {
298        (i64::MAX, LOCAL_MAX)
299    } else {
300        (carried as i64, rest)
301    }
302}
303
304/// One axis of [`WorldPos::delta`].
305#[inline]
306fn delta_axis(sector: i64, local: f64, origin_sector: i64, origin_local: f64) -> f64 {
307    // `i128` keeps `i64::MAX - i64::MIN` from wrapping. The `i128 -> f64` cast
308    // is the only lossy step, and only past `2^53` sectors — see
309    // [`WorldPos::delta`].
310    let sectors = sector as i128 - origin_sector as i128;
311    sectors as f64 * SECTOR_SIZE + (local - origin_local)
312}
313
314impl Add<DVec3> for WorldPos {
315    type Output = Self;
316
317    /// Translation by a metre offset, renormalizing.
318    #[inline]
319    fn add(self, offset: DVec3) -> Self {
320        self.translated(offset)
321    }
322}
323
324impl AddAssign<DVec3> for WorldPos {
325    #[inline]
326    fn add_assign(&mut self, offset: DVec3) {
327        *self = self.translated(offset);
328    }
329}
330
331impl Sub<DVec3> for WorldPos {
332    type Output = Self;
333
334    #[inline]
335    fn sub(self, offset: DVec3) -> Self {
336        self.translated(-offset)
337    }
338}
339
340impl SubAssign<DVec3> for WorldPos {
341    #[inline]
342    fn sub_assign(&mut self, offset: DVec3) {
343        *self = self.translated(-offset);
344    }
345}
346
347impl Sub<WorldPos> for WorldPos {
348    type Output = DVec3;
349
350    /// The relative vector between two positions — see [`WorldPos::delta`].
351    #[inline]
352    fn sub(self, origin: Self) -> DVec3 {
353        self.delta(origin)
354    }
355}
356
357impl fmt::Debug for WorldPos {
358    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
359        write!(
360            f,
361            "WorldPos(sector [{}, {}, {}], local [{}, {}, {}] m)",
362            self.sector.x, self.sector.y, self.sector.z, self.local.x, self.local.y, self.local.z
363        )
364    }
365}
366
367#[cfg(test)]
368mod tests {
369    use super::*;
370    use proptest::prelude::*;
371
372    /// Metres per light-year (IAU: Julian year x speed of light).
373    const METRES_PER_LIGHT_YEAR: f64 = 9_460_730_472_580_800.0;
374
375    #[test]
376    fn constructors_agree_and_rebase_matches_normalize() {
377        assert_eq!(WorldPos::ORIGIN, WorldPos::default());
378        assert_eq!(WorldPos::ORIGIN.sector, I64Vec3::ZERO);
379        assert!(WorldPos::ORIGIN.is_normalized());
380
381        let offset = DVec3::new(SECTOR_SIZE * 1.5, -1.0, 42.0);
382        let from_offset = WorldPos::from_offset(offset);
383        assert_eq!(from_offset.sector, I64Vec3::new(1, -1, 0));
384        assert_eq!(from_offset - WorldPos::ORIGIN, offset);
385        assert_eq!(WorldPos::ORIGIN.translated(offset), from_offset);
386        assert_eq!(WorldPos::ORIGIN + offset, from_offset);
387
388        let sector = I64Vec3::new(-4, 5, 6);
389        assert_eq!(WorldPos::from_sector(sector).sector, sector);
390        assert_eq!(WorldPos::from_sector(sector).local, DVec3::ZERO);
391        assert_eq!(
392            WorldPos::new(sector, offset),
393            WorldPos::from_sector(sector) + offset
394        );
395
396        // `rebase` is `normalize` in place.
397        let mut raw = WorldPos {
398            sector,
399            local: offset,
400        };
401        let normalized = raw.normalize();
402        raw.rebase();
403        assert_eq!(raw, normalized);
404        assert!(
405            !WorldPos {
406                sector,
407                local: offset
408            }
409            .is_normalized()
410        );
411
412        assert_eq!(
413            from_offset.distance_squared(WorldPos::ORIGIN),
414            offset.length_squared()
415        );
416        assert!(format!("{from_offset:?}").starts_with("WorldPos(sector [1, -1, 0]"));
417    }
418
419    /// The power-of-two claim in the [`SECTOR_SIZE_LOG2`] docs, and the
420    /// consequence that matters: scaling a sector index by `SECTOR_SIZE` is
421    /// exact at *every* magnitude, because it only shifts the exponent. With an
422    /// odd sector size it would not be, and [`WorldPos::delta`] would round at
423    /// sector scale (kilometres) rather than at local scale (nanometres).
424    #[test]
425    fn sector_size_is_a_power_of_two() {
426        // Spelled out rather than computed with `powi`, which is a library
427        // function whose last bit is not contractually exact.
428        assert_eq!(SECTOR_SIZE, 1_048_576.0);
429        assert_eq!(SECTOR_SIZE, (1u64 << SECTOR_SIZE_LOG2) as f64);
430        assert_eq!(
431            SECTOR_SIZE.to_bits() & ((1 << 52) - 1),
432            0,
433            "mantissa must be all zeroes"
434        );
435        assert_eq!(LOCAL_MAX, SECTOR_SIZE.next_down());
436
437        // Exact scaling all the way out to the edge of the addressable volume,
438        // where the product needs 84 bits — none of which the multiply can
439        // disturb. Every input here is itself exactly representable, which is
440        // the point: this isolates the *scaling* from the `i128 -> f64`
441        // conversion that the next test pins.
442        for sectors in [
443            1i128,
444            -1,
445            12_345_678,
446            EXACT_SECTOR_DELTA as i128,
447            i64::MIN as i128,
448        ] {
449            let scaled = sectors as f64 * SECTOR_SIZE;
450            assert_eq!(
451                scaled as i128,
452                sectors << SECTOR_SIZE_LOG2,
453                "{sectors} sectors did not scale exactly"
454            );
455        }
456    }
457
458    /// The doc comment on [`SECTOR_SIZE`] claims 0.116 nm at the sector edge,
459    /// ~2.04 billion ly of extent, and ~12 sectors across Earth. Assert all of
460    /// them, so the claims cannot rot.
461    #[test]
462    fn documented_precision_and_extent_hold() {
463        // Worst-case ULP inside a sector: just below the far edge.
464        let edge = SECTOR_SIZE.next_down();
465        let ulp = edge - edge.next_down();
466        assert_eq!(
467            ulp,
468            1.0 / 8_589_934_592.0,
469            "worst-case local ULP should be 2^-33 m"
470        );
471        assert!(
472            (ulp - 0.116e-9).abs() < 0.001e-9,
473            "sector-edge precision should be ~0.116 nm, got {ulp} m"
474        );
475        // ... and it is far finer than f32 render space 1 km from the camera.
476        let f32_ulp_at_1km = (1000.0f32.next_up() - 1000.0f32) as f64;
477        assert!(
478            (f32_ulp_at_1km / ulp - 524_288.0).abs() < 1.0,
479            "should be ~524 000x finer than render space, got {}x",
480            f32_ulp_at_1km / ulp
481        );
482
483        // Sector size as a *cell*: a town fits in one, Earth spans about a
484        // dozen. A cell that cannot subdivide a planet cannot stream one.
485        const EARTH_DIAMETER_M: f64 = 12_742_000.0;
486        let earth_cells = EARTH_DIAMETER_M / SECTOR_SIZE;
487        assert!(
488            (12.0..13.0).contains(&earth_cells),
489            "Earth should span ~12 sectors, got {earth_cells}"
490        );
491        const {
492            assert!(
493                4_000.0 < SECTOR_SIZE,
494                "an FPS map should fit inside one sector"
495            )
496        };
497
498        // Addressable extent per axis: the full i64 sector span.
499        let sectors = (i64::MAX as f64 - i64::MIN as f64) + 1.0;
500        let extent_ly = sectors * SECTOR_SIZE / METRES_PER_LIGHT_YEAR;
501        assert!(
502            (2.03e9..2.06e9).contains(&extent_ly),
503            "extent should be ~2.04 billion ly, got {extent_ly}"
504        );
505        assert!(extent_ly > 1e5, "the Milky Way must fit many times over");
506
507        // The largest possible delta stays far from f64 overflow.
508        let corner = WorldPos::from_sector(I64Vec3::splat(i64::MAX));
509        let opposite = WorldPos::from_sector(I64Vec3::splat(i64::MIN));
510        assert!(corner.distance(opposite).is_finite());
511        assert!(corner.distance_squared(opposite).is_finite());
512    }
513
514    /// Where the delta stops being exact, pinned so the doc comment on
515    /// [`WorldPos::delta`] stays true. The sector *scaling* is exact forever
516    /// (asserted above); the `i128 -> f64` conversion of the index difference
517    /// is what gives out, at `2^53`.
518    #[test]
519    fn delta_is_exact_out_to_two_to_the_fifty_three_sectors() {
520        assert_eq!(EXACT_SECTOR_DELTA as f64 as i64, EXACT_SECTOR_DELTA);
521        assert_ne!(
522            (EXACT_SECTOR_DELTA + 1) as f64 as i64,
523            EXACT_SECTOR_DELTA + 1,
524            "2^53 + 1 is the first integer f64 cannot hold — the boundary is here"
525        );
526
527        // A separation of exactly 2^53 sectors is still metre-exact.
528        let a = WorldPos::from_sector(I64Vec3::new(EXACT_SECTOR_DELTA, 0, 0));
529        let b = WorldPos::ORIGIN;
530        let delta = a - b;
531        assert_eq!(
532            delta.x as i128,
533            (EXACT_SECTOR_DELTA as i128) << SECTOR_SIZE_LOG2
534        );
535        // ... and a one-metre nudge at that separation still registers.
536        assert_eq!((a + DVec3::X) - b, delta + DVec3::X);
537
538        // The documented reach of that exactness, in light-years.
539        let reach_ly = delta.x / METRES_PER_LIGHT_YEAR;
540        assert!(
541            (998_000.0..999_000.0).contains(&reach_ly),
542            "exact-delta reach should be ~998 000 ly, got {reach_ly}"
543        );
544
545        // Past it, the answer is lossy but still finite, monotone and of the
546        // documented relative magnitude (~d * 2^-52).
547        let corner = WorldPos::from_sector(I64Vec3::new(i64::MAX, 0, 0));
548        let opposite = WorldPos::from_sector(I64Vec3::new(i64::MIN, 0, 0));
549        let full = (corner - opposite).x;
550        assert!(full.is_finite() && full > 0.0);
551        let quantum = full - full.next_down();
552        assert_eq!(
553            quantum, 2_147_483_648.0,
554            "quantization at the full addressable span should be 2^31 m"
555        );
556    }
557
558    #[test]
559    fn normalize_pulls_local_into_range() {
560        let pos = WorldPos {
561            sector: I64Vec3::new(1, 1, 1),
562            local: DVec3::splat(SECTOR_SIZE * 2.5),
563        }
564        .normalize();
565        assert_eq!(pos.sector, I64Vec3::new(3, 3, 3));
566        assert_eq!(pos.local, DVec3::splat(SECTOR_SIZE * 0.5));
567        assert!(pos.is_normalized());
568    }
569
570    #[test]
571    fn normalize_handles_negative_offsets() {
572        let pos = WorldPos {
573            sector: I64Vec3::ZERO,
574            local: DVec3::new(-1.0, -SECTOR_SIZE, 0.0),
575        }
576        .normalize();
577        assert_eq!(pos.sector, I64Vec3::new(-1, -1, 0));
578        assert_eq!(pos.local, DVec3::new(SECTOR_SIZE - 1.0, 0.0, 0.0));
579        assert!(pos.is_normalized());
580    }
581
582    /// A tiny negative offset is the classic rebase edge case: `local +
583    /// SECTOR_SIZE` rounds to exactly `SECTOR_SIZE`, which is outside the
584    /// half-open range.
585    #[test]
586    fn normalize_keeps_the_half_open_range_at_the_boundary() {
587        let pos = WorldPos {
588            sector: I64Vec3::ZERO,
589            local: DVec3::new(-1e-300, -1.0, 0.0),
590        }
591        .normalize();
592        assert!(pos.is_normalized(), "{pos:?}");
593        assert_eq!(pos.sector, I64Vec3::new(0, -1, 0));
594        assert_eq!(pos.local.x, 0.0);
595    }
596
597    #[test]
598    fn out_of_range_saturates_instead_of_wrapping() {
599        let far = WorldPos {
600            sector: I64Vec3::splat(i64::MAX),
601            local: DVec3::splat(SECTOR_SIZE * 4.0),
602        }
603        .normalize();
604        assert_eq!(far.sector, I64Vec3::splat(i64::MAX));
605        assert!(far.is_normalized());
606        assert_eq!(far, far.normalize(), "saturation must be idempotent");
607
608        let near = WorldPos {
609            sector: I64Vec3::splat(i64::MIN),
610            local: DVec3::splat(-SECTOR_SIZE),
611        }
612        .normalize();
613        assert_eq!(near.sector, I64Vec3::splat(i64::MIN));
614        assert_eq!(near.local, DVec3::ZERO);
615        assert_eq!(near, near.normalize());
616    }
617
618    #[test]
619    fn non_finite_local_survives_normalization_without_inventing_a_position() {
620        // The debug assert fires in debug builds; check the release behaviour
621        // by driving the axis helper directly.
622        let (sector, local) = normalize_axis(7, f64::NAN);
623        assert_eq!(sector, 7);
624        assert!(local.is_nan());
625        let (sector, local) = normalize_axis(0, f64::INFINITY);
626        assert_eq!(sector, i64::MAX, "infinity saturates rather than wraps");
627        assert_eq!(local, LOCAL_MAX);
628    }
629
630    #[test]
631    fn delta_is_exact_across_a_sector_boundary() {
632        let a = WorldPos::new(I64Vec3::ZERO, DVec3::new(SECTOR_SIZE - 1.0, 0.0, 0.0));
633        let b = a + DVec3::new(2.0, 0.0, 0.0);
634        assert_eq!(b.sector, I64Vec3::new(1, 0, 0));
635        assert_eq!(b.local.x, 1.0);
636        assert_eq!(b - a, DVec3::new(2.0, 0.0, 0.0));
637        assert_eq!(a.distance(b), 2.0);
638    }
639
640    #[test]
641    fn delta_does_not_wrap_between_opposite_corners() {
642        let a = WorldPos::from_sector(I64Vec3::new(i64::MAX, 0, 0));
643        let b = WorldPos::from_sector(I64Vec3::new(i64::MIN, 0, 0));
644        let delta = a - b;
645        assert!(delta.x > 0.0, "i64 subtraction would have wrapped negative");
646        assert_eq!(delta.x, (i64::MAX as f64 - i64::MIN as f64) * SECTOR_SIZE);
647    }
648
649    #[test]
650    fn relative_to_is_the_render_space_conversion() {
651        let camera = WorldPos::new(I64Vec3::new(12, -3, 7), DVec3::new(1000.0, 2000.0, 3000.0));
652        let object = camera + DVec3::new(1.5, -2.5, 0.25);
653        assert_eq!(object.relative_to(camera), Vec3::new(1.5, -2.5, 0.25));
654        // Same answer regardless of how far from the origin the pair sits.
655        let far = WorldPos::new(I64Vec3::splat(1_000_000), DVec3::splat(SECTOR_SIZE - 10.0));
656        let far_object = far + DVec3::new(1.5, -2.5, 0.25);
657        assert_eq!(far_object.relative_to(far), Vec3::new(1.5, -2.5, 0.25));
658    }
659
660    #[test]
661    fn assign_operators_match_the_value_forms() {
662        let base = WorldPos::new(I64Vec3::new(2, 2, 2), DVec3::splat(10.0));
663        let offset = DVec3::new(3.0, -4.0, 5.0);
664        let mut moved = base;
665        moved += offset;
666        assert_eq!(moved, base + offset);
667        moved -= offset;
668        assert_eq!(moved, base);
669        assert_eq!(base - offset, base + -offset);
670    }
671
672    // --- property tests -------------------------------------------------
673
674    /// Sector indices well inside the i64 range so nothing saturates, and local
675    /// offsets spanning the whole sector.
676    fn any_pos() -> impl Strategy<Value = WorldPos> {
677        (
678            -1_000_000i64..1_000_000,
679            -1_000_000i64..1_000_000,
680            -1_000_000i64..1_000_000,
681            0.0..SECTOR_SIZE,
682            0.0..SECTOR_SIZE,
683            0.0..SECTOR_SIZE,
684        )
685            .prop_map(|(sx, sy, sz, lx, ly, lz)| {
686                WorldPos::new(I64Vec3::new(sx, sy, sz), DVec3::new(lx, ly, lz))
687            })
688    }
689
690    /// Worst-case rounding error of one `local + offset` addition.
691    ///
692    /// The addition happens at the magnitude of the *result*, which is at most
693    /// one sector plus the offset, so it rounds to that magnitude's ULP — not
694    /// to the ULP of the offset. That is inherent to storing a position as a
695    /// float, and it is exactly why the sector is bounded: a translation is
696    /// accurate to a fraction of a nanometre anywhere in the addressable
697    /// volume, whereas an absolute f64 universe coordinate would round to
698    /// kilometres.
699    /// `f64::EPSILON` is `2^-52`, twice the half-ULP bound, which covers the
700    /// rebase subtraction too.
701    fn step_bound(offset: DVec3) -> f64 {
702        (SECTOR_SIZE + offset.abs().max_element()) * f64::EPSILON
703    }
704
705    /// Offsets across the magnitudes a simulation actually produces: sub-
706    /// millimetre nudges up to multi-sector jumps.
707    fn any_offset() -> impl Strategy<Value = DVec3> {
708        let axis = prop_oneof![
709            -1e-3..1e-3f64,
710            -1e3..1e3f64,
711            -1e9..1e9f64,
712            -4.0 * SECTOR_SIZE..4.0 * SECTOR_SIZE,
713        ];
714        (axis.clone(), axis.clone(), axis).prop_map(|(x, y, z)| DVec3::new(x, y, z))
715    }
716
717    proptest! {
718        #[test]
719        fn prop_normalize_is_idempotent(pos in any_pos(), offset in any_offset()) {
720            let raw = WorldPos { sector: pos.sector, local: pos.local + offset };
721            let once = raw.normalize();
722            prop_assert!(once.is_normalized(), "{once:?}");
723            prop_assert_eq!(once, once.normalize());
724        }
725
726        /// Rebasing must not move the point — *exactly*. This is the property
727        /// that justifies the power-of-two sector size: the delta between a
728        /// denormalized position and its canonical form is bit-for-bit zero.
729        #[test]
730        fn prop_normalize_does_not_move_the_point(pos in any_pos(), offset in any_offset()) {
731            let raw = WorldPos { sector: pos.sector, local: pos.local + offset };
732            prop_assert_eq!(raw.normalize().delta(raw), DVec3::ZERO);
733        }
734
735        /// The same exactness claim, but with a sector carry of up to `10^15`
736        /// instead of single digits — offsets out to 1e21 m (~100 000 ly) in a
737        /// single un-rebased jump, which is what an on-rails body handed a
738        /// season's worth of accumulated motion looks like. `carry *
739        /// SECTOR_SIZE` needs ~70 significant bits there, and only stays exact
740        /// because the low 20 are zero. Exercises the `floor`, the saturating
741        /// `f64 -> i128` cast and the large-carry subtraction together.
742        #[test]
743        fn prop_normalize_is_exact_at_interstellar_offsets(
744            sector in -1_000_000_000i64..1_000_000_000,
745            local in -1e21..1e21f64,
746        ) {
747            let raw = WorldPos { sector: I64Vec3::splat(sector), local: DVec3::splat(local) };
748            let normalized = raw.normalize();
749            prop_assert!(normalized.is_normalized(), "{normalized:?}");
750            prop_assert_eq!(normalized.delta(raw), DVec3::ZERO);
751        }
752
753        /// The round-trip that every physics step relies on.
754        #[test]
755        fn prop_add_then_sub_returns_the_offset(pos in any_pos(), offset in any_offset()) {
756            let recovered = (pos + offset) - pos;
757            // Not bit-exact, and honestly so: `local + offset` rounds at the
758            // magnitude of the sum before any rebasing happens. The rebase
759            // itself contributes nothing.
760            let tolerance = step_bound(offset);
761            prop_assert!(
762                (recovered - offset).abs().max_element() <= tolerance,
763                "recovered {recovered} vs {offset}, tolerance {tolerance}"
764            );
765        }
766
767        #[test]
768        fn prop_delta_is_antisymmetric(a in any_pos(), b in any_pos()) {
769            prop_assert_eq!(a - b, -(b - a));
770            prop_assert_eq!(a.distance(b), b.distance(a));
771        }
772
773        #[test]
774        fn prop_distance_matches_delta_length(a in any_pos(), b in any_pos()) {
775            prop_assert_eq!(a.distance(b), (a - b).length());
776            let d2 = a.distance_squared(b);
777            prop_assert!((d2 - a.distance(b).powi(2)).abs() <= d2 * 1e-12);
778        }
779
780        /// The accumulation test: a random walk of N steps must land exactly
781        /// where the single summed step lands. Any rounding introduced by a
782        /// sector crossing would show up here as drift.
783        #[test]
784        fn prop_random_walk_matches_single_step(
785            start in any_pos(),
786            steps in proptest::collection::vec(-1e6..1e6f64, 3..90),
787        ) {
788            let steps: Vec<DVec3> = steps
789                .as_chunks::<3>().0.iter()
790                .map(|c| DVec3::new(c[0], c[1], c[2]))
791                .collect();
792
793            let mut walked = start;
794            for step in &steps {
795                walked += *step;
796            }
797
798            // Sum the steps in f64 first, then apply once. The two paths agree
799            // exactly only because rebasing is exact; the sum itself rounds
800            // like any f64 addition chain, so compare against the same order.
801            let mut total = DVec3::ZERO;
802            for step in &steps {
803                total += *step;
804            }
805            let jumped = start + total;
806
807            // Bounded by one rounding per step — and *not* by anything the
808            // rebase does. A rebase that lost bits would blow this up by orders
809            // of magnitude the moment the walk crossed a sector boundary.
810            let drift = (walked - jumped).abs().max_element();
811            let bound: f64 = steps.iter().copied().map(step_bound).sum::<f64>()
812                + step_bound(total);
813            prop_assert!(drift <= bound, "drift {drift} m over {} steps", steps.len());
814        }
815
816        /// A walk that retraces its steps returns to where it started, to
817        /// within the same per-step rounding bound.
818        #[test]
819        fn prop_walk_and_back_returns_home(
820            start in any_pos(),
821            steps in proptest::collection::vec(any_offset(), 1..24),
822        ) {
823            let mut pos = start;
824            for step in &steps {
825                pos += *step;
826            }
827            for step in steps.iter().rev() {
828                pos -= *step;
829            }
830            let drift = (pos - start).abs().max_element();
831            // Every step is applied twice, out and back.
832            let bound: f64 = 2.0 * steps.iter().copied().map(step_bound).sum::<f64>();
833            prop_assert!(drift <= bound, "drift {drift} m, bound {bound}");
834        }
835
836        #[test]
837        fn prop_relative_to_agrees_with_delta(a in any_pos(), offset in any_offset()) {
838            let b = a + offset;
839            prop_assert_eq!(b.relative_to(a), (b - a).as_vec3());
840        }
841    }
842}