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}