Skip to main content

box3d_rust/recording/
registry.rs

1//! Geometry registry for snapshot/recording interning.
2//! Port of `b3GeometryRegistry` / `b3InternGeometry` / `b3RecIntern*` from recording.c.
3//!
4//! SPDX-FileCopyrightText: 2026 Erin Catto
5//! SPDX-License-Identifier: MIT
6
7use crate::compound::{convert_bytes_to_compound, CompoundData};
8use crate::height_field::HeightFieldData;
9use crate::hull::HullData;
10use crate::mesh::MeshData;
11use std::collections::HashMap;
12
13/// Geometry kinds for the trailing registry section. (b3GeometryKind)
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15#[repr(u32)]
16pub enum GeometryKind {
17    Hull = 0,
18    Mesh = 1,
19    HeightField = 2,
20    Compound = 3,
21}
22
23/// One entry per unique geometry blob. (b3GeometryEntry)
24#[derive(Debug, Clone)]
25pub struct GeometryEntry {
26    pub content_hash: u64,
27    pub id: u32,
28    pub kind: GeometryKind,
29    pub bytes: Vec<u8>,
30    /// Next entry sharing the same content hash; `NULL_INDEX` ends the chain.
31    pub hash_next: i32,
32}
33
34/// Growable array of geometry entries with content-hash dedup. (b3GeometryRegistry)
35#[derive(Debug, Default)]
36pub struct GeometryRegistry {
37    pub entries: Vec<GeometryEntry>,
38    /// Maps content hash → first entry index for O(1) chain start.
39    dedup: HashMap<u64, i32>,
40}
41
42impl GeometryRegistry {
43    pub fn new() -> Self {
44        Self::default()
45    }
46
47    /// Append without dedup (id == slot index). (b3AppendGeometry)
48    pub fn append(&mut self, kind: GeometryKind, content_hash: u64, bytes: Vec<u8>) -> u32 {
49        let id = self.entries.len() as u32;
50        let hash_next = self.dedup.get(&content_hash).copied().unwrap_or(-1);
51        self.entries.push(GeometryEntry {
52            content_hash,
53            id,
54            kind,
55            bytes,
56            hash_next,
57        });
58        self.dedup.insert(content_hash, id as i32);
59        id
60    }
61
62    /// (b3InternGeometry)
63    pub fn intern(&mut self, kind: GeometryKind, content_hash: u64, bytes: Vec<u8>) -> u32 {
64        if let Some(&head) = self.dedup.get(&content_hash) {
65            let mut idx = head;
66            while idx >= 0 {
67                let e = &self.entries[idx as usize];
68                if e.kind == kind && e.bytes == bytes {
69                    return e.id;
70                }
71                idx = e.hash_next;
72            }
73        }
74
75        let id = self.entries.len() as u32;
76        let hash_next = self.dedup.get(&content_hash).copied().unwrap_or(-1);
77        self.entries.push(GeometryEntry {
78            content_hash,
79            id,
80            kind,
81            bytes,
82            hash_next,
83        });
84        self.dedup.insert(content_hash, id as i32);
85        id
86    }
87
88    /// (b3RecInternHull)
89    pub fn intern_hull(&mut self, hull: &HullData) -> u32 {
90        let bytes = hull.to_bytes();
91        let h = hash64_blob(&bytes);
92        self.intern(GeometryKind::Hull, h, bytes)
93    }
94
95    /// (b3RecInternMesh)
96    pub fn intern_mesh(&mut self, mesh: &MeshData) -> u32 {
97        let bytes = mesh.to_bytes();
98        let h = hash64_blob(&bytes);
99        self.intern(GeometryKind::Mesh, h, bytes)
100    }
101
102    /// (b3RecInternHeightField)
103    pub fn intern_height_field(&mut self, hf: &HeightFieldData) -> u32 {
104        let bytes = hf.to_bytes();
105        let h = hash64_blob(&bytes);
106        self.intern(GeometryKind::HeightField, h, bytes)
107    }
108
109    /// (b3RecInternCompound)
110    pub fn intern_compound(&mut self, compound: &CompoundData) -> u32 {
111        let bytes = compound.to_bytes();
112        let h = hash64_blob(&bytes);
113        self.intern(GeometryKind::Compound, h, bytes)
114    }
115
116    /// Build reader slots from registry entries (id == index).
117    pub fn to_slots(&self) -> Vec<RegistrySlot> {
118        self.entries
119            .iter()
120            .map(|e| RegistrySlot {
121                kind: e.kind,
122                bytes: e.bytes.clone(),
123                live_compound: None,
124            })
125            .collect()
126    }
127}
128
129/// Resolved geometry slot for deserialization. (b3RegistrySlot)
130#[derive(Debug, Clone)]
131pub struct RegistrySlot {
132    pub kind: GeometryKind,
133    pub bytes: Vec<u8>,
134    /// Lazily converted compound (C: slot->live).
135    pub live_compound: Option<CompoundData>,
136}
137
138impl RegistrySlot {
139    pub fn ensure_compound(&mut self) -> Option<&CompoundData> {
140        if self.live_compound.is_none() {
141            self.live_compound = convert_bytes_to_compound(&self.bytes);
142        }
143        self.live_compound.as_ref()
144    }
145}
146
147/// Content hash for geometry blobs. (b3Hash64Blob)
148///
149/// Word-folded FNV-1a salted by length, then a splitmix64 finalizer.
150pub fn hash64_blob(bytes: &[u8]) -> u64 {
151    let n = bytes.len();
152    let mut h = 0xcbf2_9ce4_8422_2325u64 ^ (n as u32 as u64);
153    const PRIME: u64 = 0x1000_0000_01b3;
154    let mut i = 0;
155    while i + 8 <= n {
156        let mut word_bytes = [0u8; 8];
157        word_bytes.copy_from_slice(&bytes[i..i + 8]);
158        let word = u64::from_le_bytes(word_bytes);
159        h = (h ^ word).wrapping_mul(PRIME);
160        i += 8;
161    }
162    while i < n {
163        h = (h ^ bytes[i] as u64).wrapping_mul(PRIME);
164        i += 1;
165    }
166    h ^= h >> 30;
167    h = h.wrapping_mul(0xbf58_476d_1ce4_e5b9);
168    h ^= h >> 27;
169    h = h.wrapping_mul(0x94d0_49bb_1331_11eb);
170    h ^= h >> 31;
171    h
172}