Skip to main content

qualia_core_db/container_10d/
spatial_index_section.rs

1//! `.10d` Spatial-index section — BVH node array + kd-tree node array
2//! serialized as a self-describing section (P3.7).
3//!
4//! Layout:
5//! ```text
6//! [SpatialIndexMiniHeader: 32 bytes]
7//! [bvh_nodes:  B * 40 bytes  (BvhNode)]
8//! [bvh_prim_indices: B * 4 bytes]
9//! [kd_nodes:   K * 32 bytes  (KdNode)]
10//! [kd_point_indices: K * 4 bytes]
11//! ```
12//!
13//! All arrays are little-endian, 4-byte aligned. The section is deterministic:
14//! identical input yields byte-identical output. Per-section CRC-32C is
15//! handled by the container's section-table encoder.
16
17use bytemuck::{bytes_of, cast_slice, from_bytes, Pod, Zeroable};
18
19use crate::specialized_libs::computational_geometry::{
20    BvhNode, KdNode, BVH_NODE_SIZE, KD_NODE_SIZE,
21};
22
23/// Mini-header size in bytes.
24pub const SPATIAL_INDEX_MINI_HEADER_SIZE: usize = 32;
25
26/// Maximum node count (bounded by 42MB Sentinel).
27pub const MAX_BVH_NODE_COUNT: usize = 1_048_576; // 2^20
28pub const MAX_KD_NODE_COUNT: usize = 1_048_576; // 2^20
29
30/// The 32-byte SpatialIndex-section mini-header.
31///
32/// ```text
33/// offset  size  field
34/// 0       4     bvh_node_count:u32
35/// 4       4     kd_node_count:u32
36/// 8       4     bvh_root:u32
37/// 12      4     kd_root:u32
38/// 16      4     bvh_prim_count:u32   (number of BVH primitive indices, = number of AABBs)
39/// 20      4     kd_point_count:u32  (number of kd-tree point indices, = number of points)
40/// 24      4     reserved_u32        (must be zero)
41/// 28      4     reserved_u32_2      (must be zero)
42/// ```
43#[repr(C)]
44#[derive(Debug, Clone, Copy, PartialEq, Pod, Zeroable)]
45pub struct SpatialIndexMiniHeader {
46    pub bvh_node_count: u32,
47    pub kd_node_count: u32,
48    pub bvh_root: u32,
49    pub kd_root: u32,
50    pub bvh_prim_count: u32,
51    pub kd_point_count: u32,
52    pub reserved_u32: u32,
53    pub reserved_u32_2: u32,
54}
55
56impl Default for SpatialIndexMiniHeader {
57    fn default() -> Self {
58        Self {
59            bvh_node_count: 0,
60            kd_node_count: 0,
61            bvh_root: 0,
62            kd_root: 0,
63            bvh_prim_count: 0,
64            kd_point_count: 0,
65            reserved_u32: 0,
66            reserved_u32_2: 0,
67        }
68    }
69}
70
71/// Spatial-index-section encode/decode error.
72#[derive(Debug, Clone, PartialEq, Eq)]
73pub enum SpatialIndexSectionError {
74    /// Payload too short for the mini-header.
75    PayloadTooShort { got: usize, need: usize },
76    /// Non-zero reserved field.
77    NonZeroReserved,
78    /// Node count exceeds the max.
79    NodeCountTooLarge { got: u32, max: usize },
80    /// Payload truncated relative to declared counts.
81    PayloadTruncated { expected: usize, got: usize },
82    /// Output buffer too small.
83    OutputBufferTooSmall { needed: usize, have: usize },
84}
85
86impl std::fmt::Display for SpatialIndexSectionError {
87    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88        match self {
89            Self::PayloadTooShort { got, need } => write!(
90                f,
91                "10d SPATIAL_INDEX payload too short: got {got}, need {need}"
92            ),
93            Self::NonZeroReserved => write!(f, "10d SPATIAL_INDEX non-zero reserved field"),
94            Self::NodeCountTooLarge { got, max } => {
95                write!(f, "10d SPATIAL_INDEX node_count {got} exceeds max {max}")
96            }
97            Self::PayloadTruncated { expected, got } => write!(
98                f,
99                "10d SPATIAL_INDEX payload truncated: expected {expected}, got {got}"
100            ),
101            Self::OutputBufferTooSmall { needed, have } => write!(
102                f,
103                "10d SPATIAL_INDEX output buffer too small: need {needed}, have {have}"
104            ),
105        }
106    }
107}
108
109impl std::error::Error for SpatialIndexSectionError {}
110
111/// Compute the encoded byte length for a spatial-index section.
112#[inline]
113pub fn encoded_len(
114    bvh_node_count: u32,
115    kd_node_count: u32,
116    bvh_prim_count: u32,
117    kd_point_count: u32,
118) -> usize {
119    let bc = bvh_node_count as usize;
120    let kc = kd_node_count as usize;
121    let pc = bvh_prim_count as usize;
122    let qc = kd_point_count as usize;
123    SPATIAL_INDEX_MINI_HEADER_SIZE
124        + bc * BVH_NODE_SIZE       // bvh_nodes
125        + pc * 4                   // bvh_prim_indices
126        + kc * KD_NODE_SIZE        // kd_nodes
127        + qc * 4 // kd_point_indices
128}
129
130/// Encode a spatial-index section from BVH and kd-tree node arrays.
131///
132/// Returns the number of bytes written.
133pub fn encode_spatial_index_section(
134    bvh_nodes: &[BvhNode],
135    bvh_prim_indices: &[u32],
136    bvh_root: u32,
137    kd_nodes: &[KdNode],
138    kd_point_indices: &[u32],
139    kd_root: u32,
140    out: &mut [u8],
141) -> Result<usize, SpatialIndexSectionError> {
142    let bc = bvh_nodes.len();
143    let kc = kd_nodes.len();
144    let pc = bvh_prim_indices.len();
145    let qc = kd_point_indices.len();
146
147    if bc > MAX_BVH_NODE_COUNT {
148        return Err(SpatialIndexSectionError::NodeCountTooLarge {
149            got: bc as u32,
150            max: MAX_BVH_NODE_COUNT,
151        });
152    }
153    if kc > MAX_KD_NODE_COUNT {
154        return Err(SpatialIndexSectionError::NodeCountTooLarge {
155            got: kc as u32,
156            max: MAX_KD_NODE_COUNT,
157        });
158    }
159    if bvh_prim_indices.len() < pc {
160        return Err(SpatialIndexSectionError::PayloadTruncated {
161            expected: pc,
162            got: bvh_prim_indices.len(),
163        });
164    }
165    if kd_point_indices.len() < qc {
166        return Err(SpatialIndexSectionError::PayloadTruncated {
167            expected: qc,
168            got: kd_point_indices.len(),
169        });
170    }
171
172    let need = encoded_len(bc as u32, kc as u32, pc as u32, qc as u32);
173    if out.len() < need {
174        return Err(SpatialIndexSectionError::OutputBufferTooSmall {
175            needed: need,
176            have: out.len(),
177        });
178    }
179
180    let header = SpatialIndexMiniHeader {
181        bvh_node_count: bc as u32,
182        kd_node_count: kc as u32,
183        bvh_root,
184        kd_root,
185        bvh_prim_count: pc as u32,
186        kd_point_count: qc as u32,
187        reserved_u32: 0,
188        reserved_u32_2: 0,
189    };
190
191    let mut off = 0usize;
192    out[off..off + SPATIAL_INDEX_MINI_HEADER_SIZE].copy_from_slice(bytes_of(&header));
193    off += SPATIAL_INDEX_MINI_HEADER_SIZE;
194
195    // BVH nodes.
196    let bvh_bytes = cast_slice(bvh_nodes);
197    out[off..off + bvh_bytes.len()].copy_from_slice(bvh_bytes);
198    off += bc * BVH_NODE_SIZE;
199
200    // BVH prim indices.
201    let bvh_idx_bytes = cast_slice(&bvh_prim_indices[..pc]);
202    out[off..off + bvh_idx_bytes.len()].copy_from_slice(bvh_idx_bytes);
203    off += pc * 4;
204
205    // kd-tree nodes.
206    let kd_bytes = cast_slice(kd_nodes);
207    out[off..off + kd_bytes.len()].copy_from_slice(kd_bytes);
208    off += kc * KD_NODE_SIZE;
209
210    // kd-tree point indices.
211    let kd_idx_bytes = cast_slice(&kd_point_indices[..qc]);
212    out[off..off + kd_idx_bytes.len()].copy_from_slice(kd_idx_bytes);
213    off += qc * 4;
214
215    debug_assert_eq!(off, need);
216    Ok(off)
217}
218
219/// Decoded spatial-index section.
220#[derive(Debug)]
221pub struct DecodedSpatialIndex<'a> {
222    pub header: SpatialIndexMiniHeader,
223    pub bvh_nodes: &'a [BvhNode],
224    pub bvh_prim_indices: &'a [u32],
225    pub kd_nodes: &'a [KdNode],
226    pub kd_point_indices: &'a [u32],
227}
228
229/// Decode a spatial-index section from a raw payload.
230pub fn decode_spatial_index_section(
231    payload: &[u8],
232) -> Result<DecodedSpatialIndex<'_>, SpatialIndexSectionError> {
233    if payload.len() < SPATIAL_INDEX_MINI_HEADER_SIZE {
234        return Err(SpatialIndexSectionError::PayloadTooShort {
235            got: payload.len(),
236            need: SPATIAL_INDEX_MINI_HEADER_SIZE,
237        });
238    }
239
240    let header: SpatialIndexMiniHeader = *from_bytes(&payload[..SPATIAL_INDEX_MINI_HEADER_SIZE]);
241
242    if header.reserved_u32 != 0 || header.reserved_u32_2 != 0 {
243        return Err(SpatialIndexSectionError::NonZeroReserved);
244    }
245
246    let bc = header.bvh_node_count as usize;
247    let kc = header.kd_node_count as usize;
248    let pc = header.bvh_prim_count as usize;
249    let qc = header.kd_point_count as usize;
250
251    if bc > MAX_BVH_NODE_COUNT {
252        return Err(SpatialIndexSectionError::NodeCountTooLarge {
253            got: header.bvh_node_count,
254            max: MAX_BVH_NODE_COUNT,
255        });
256    }
257    if kc > MAX_KD_NODE_COUNT {
258        return Err(SpatialIndexSectionError::NodeCountTooLarge {
259            got: header.kd_node_count,
260            max: MAX_KD_NODE_COUNT,
261        });
262    }
263
264    let need = encoded_len(
265        header.bvh_node_count,
266        header.kd_node_count,
267        header.bvh_prim_count,
268        header.kd_point_count,
269    );
270    if payload.len() < need {
271        return Err(SpatialIndexSectionError::PayloadTruncated {
272            expected: need,
273            got: payload.len(),
274        });
275    }
276
277    let mut off = SPATIAL_INDEX_MINI_HEADER_SIZE;
278
279    let bvh_nodes: &[BvhNode] = cast_slice(&payload[off..off + bc * BVH_NODE_SIZE]);
280    off += bc * BVH_NODE_SIZE;
281
282    let bvh_prim_indices: &[u32] = cast_slice(&payload[off..off + pc * 4]);
283    off += pc * 4;
284
285    let kd_nodes: &[KdNode] = cast_slice(&payload[off..off + kc * KD_NODE_SIZE]);
286    off += kc * KD_NODE_SIZE;
287
288    let kd_point_indices: &[u32] = cast_slice(&payload[off..off + qc * 4]);
289    off += qc * 4;
290
291    debug_assert_eq!(off, need);
292
293    Ok(DecodedSpatialIndex {
294        header,
295        bvh_nodes,
296        bvh_prim_indices,
297        kd_nodes,
298        kd_point_indices,
299    })
300}
301
302// ---------------------------------------------------------------------------
303// Tests
304// ---------------------------------------------------------------------------
305
306#[cfg(test)]
307mod tests {
308    use super::*;
309    use crate::specialized_libs::computational_geometry::{
310        build_bvh_recursive, build_kd_tree_3d, Aabb, Point3,
311    };
312
313    fn test_aabbs() -> Vec<Aabb> {
314        (0..8)
315            .map(|i| {
316                let x = (i % 2) as f64;
317                let y = ((i / 2) % 2) as f64;
318                let z = (i / 4) as f64;
319                Aabb::new(Point3::new(x, y, z), Point3::new(x + 1.0, y + 1.0, z + 1.0))
320            })
321            .collect()
322    }
323
324    fn test_points() -> Vec<[f64; 3]> {
325        vec![
326            [0.0, 0.0, 0.0],
327            [1.0, 0.0, 0.0],
328            [0.0, 1.0, 0.0],
329            [0.0, 0.0, 1.0],
330            [1.0, 1.0, 1.0],
331        ]
332    }
333
334    #[test]
335    fn round_trip_encode_decode() {
336        let aabbs = test_aabbs();
337        let n = aabbs.len();
338        let mut bvh_nodes = vec![BvhNode::default(); 2 * n];
339        let mut bvh_indices = vec![0u32; n];
340        let mut bvh_codes = vec![0u64; n];
341        let mut bvh_sort = vec![0u32; n];
342        let (bvh_count, bvh_root) = build_bvh_recursive(
343            &aabbs,
344            &mut bvh_nodes,
345            &mut bvh_indices,
346            &mut bvh_codes,
347            &mut bvh_sort,
348        )
349        .unwrap();
350
351        let points = test_points();
352        let np = points.len();
353        let mut kd_nodes = vec![KdNode::default(); np];
354        let mut kd_indices = vec![0u32; np];
355        let mut kd_codes = vec![0u64; np];
356        let mut kd_sort = vec![0u32; np];
357        let (kd_count, kd_root) = build_kd_tree_3d(
358            &points,
359            &mut kd_nodes,
360            &mut kd_indices,
361            &mut kd_codes,
362            &mut kd_sort,
363        )
364        .unwrap();
365
366        let need = encoded_len(bvh_count as u32, kd_count as u32, n as u32, np as u32);
367        let mut buf = vec![0u8; need];
368
369        let written = encode_spatial_index_section(
370            &bvh_nodes[..bvh_count],
371            &bvh_indices,
372            bvh_root as u32,
373            &kd_nodes[..kd_count],
374            &kd_indices,
375            kd_root as u32,
376            &mut buf,
377        )
378        .unwrap();
379        assert_eq!(written, need);
380
381        let decoded = decode_spatial_index_section(&buf).unwrap();
382        assert_eq!(decoded.header.bvh_node_count, bvh_count as u32);
383        assert_eq!(decoded.header.kd_node_count, kd_count as u32);
384        assert_eq!(decoded.header.bvh_root, bvh_root as u32);
385        assert_eq!(decoded.header.kd_root, kd_root as u32);
386        assert_eq!(decoded.header.bvh_prim_count, n as u32);
387        assert_eq!(decoded.header.kd_point_count, np as u32);
388        assert_eq!(decoded.bvh_nodes.len(), bvh_count);
389        assert_eq!(decoded.kd_nodes.len(), kd_count);
390        assert_eq!(decoded.bvh_nodes, &bvh_nodes[..bvh_count]);
391        assert_eq!(decoded.kd_nodes, &kd_nodes[..kd_count]);
392        assert_eq!(decoded.bvh_prim_indices, &bvh_indices);
393        assert_eq!(decoded.kd_point_indices, &kd_indices);
394    }
395
396    #[test]
397    fn encode_twice_is_byte_identical() {
398        let aabbs = test_aabbs();
399        let n = aabbs.len();
400        let mut bvh_nodes = vec![BvhNode::default(); 2 * n];
401        let mut bvh_indices = vec![0u32; n];
402        let mut bvh_codes = vec![0u64; n];
403        let mut bvh_sort = vec![0u32; n];
404        let (bvh_count, bvh_root) = build_bvh_recursive(
405            &aabbs,
406            &mut bvh_nodes,
407            &mut bvh_indices,
408            &mut bvh_codes,
409            &mut bvh_sort,
410        )
411        .unwrap();
412
413        let need = encoded_len(bvh_count as u32, 0, n as u32, 0);
414        let mut buf_a = vec![0u8; need];
415        let mut buf_b = vec![0u8; need];
416
417        encode_spatial_index_section(
418            &bvh_nodes[..bvh_count],
419            &bvh_indices,
420            bvh_root as u32,
421            &[],
422            &[],
423            0,
424            &mut buf_a,
425        )
426        .unwrap();
427        encode_spatial_index_section(
428            &bvh_nodes[..bvh_count],
429            &bvh_indices,
430            bvh_root as u32,
431            &[],
432            &[],
433            0,
434            &mut buf_b,
435        )
436        .unwrap();
437
438        assert_eq!(buf_a, buf_b);
439    }
440
441    #[test]
442    fn decode_rejects_nonzero_reserved() {
443        let mut buf = vec![0u8; SPATIAL_INDEX_MINI_HEADER_SIZE];
444        // Set reserved field (offset 28) to non-zero.
445        buf[28] = 1;
446        let result = decode_spatial_index_section(&buf);
447        assert!(result.is_err());
448        assert_eq!(
449            result.unwrap_err(),
450            SpatialIndexSectionError::NonZeroReserved
451        );
452    }
453
454    #[test]
455    fn decode_rejects_truncated_payload() {
456        let buf = vec![0u8; SPATIAL_INDEX_MINI_HEADER_SIZE - 1];
457        let result = decode_spatial_index_section(&buf);
458        assert!(result.is_err());
459    }
460
461    #[test]
462    fn decode_rejects_node_count_too_large() {
463        let mut buf = vec![0u8; SPATIAL_INDEX_MINI_HEADER_SIZE];
464        // Set bvh_node_count to a huge value.
465        let header = SpatialIndexMiniHeader {
466            bvh_node_count: MAX_BVH_NODE_COUNT as u32 + 1,
467            kd_node_count: 0,
468            bvh_root: 0,
469            kd_root: 0,
470            bvh_prim_count: 0,
471            kd_point_count: 0,
472            reserved_u32: 0,
473            reserved_u32_2: 0,
474        };
475        buf.copy_from_slice(bytes_of(&header));
476        let result = decode_spatial_index_section(&buf);
477        assert!(result.is_err());
478    }
479
480    #[test]
481    fn empty_section_round_trip() {
482        let need = encoded_len(0, 0, 0, 0);
483        let mut buf = vec![0u8; need];
484        encode_spatial_index_section(&[], &[], 0, &[], &[], 0, &mut buf).unwrap();
485
486        let decoded = decode_spatial_index_section(&buf).unwrap();
487        assert_eq!(decoded.header.bvh_node_count, 0);
488        assert_eq!(decoded.header.kd_node_count, 0);
489        assert!(decoded.bvh_nodes.is_empty());
490        assert!(decoded.kd_nodes.is_empty());
491    }
492
493    #[test]
494    fn header_is_pod() {
495        assert_eq!(
496            std::mem::size_of::<SpatialIndexMiniHeader>(),
497            SPATIAL_INDEX_MINI_HEADER_SIZE
498        );
499    }
500}