Skip to main content

copc_streaming/
hierarchy.rs

1//! Incremental COPC hierarchy loading.
2//!
3//! Loads hierarchy pages on demand rather than all at once.
4
5use std::collections::HashMap;
6use std::io::Cursor;
7
8use crate::types::{Aabb, VoxelKey};
9use byteorder::{LittleEndian, ReadBytesExt};
10
11use crate::byte_source::ByteSource;
12use crate::error::CopcError;
13use crate::header::CopcInfo;
14
15/// A single hierarchy entry: metadata for one octree node.
16///
17/// The `key` is duplicated from the hierarchy map for convenience when
18/// passing entries around without the map key.
19#[derive(Debug, Clone)]
20#[non_exhaustive]
21pub struct HierarchyEntry {
22    /// The octree node this entry describes.
23    pub key: VoxelKey,
24    /// Absolute file offset to the compressed point data.
25    pub offset: u64,
26    /// Size of compressed data in bytes.
27    pub byte_size: u32,
28    /// Number of points in this node.
29    pub point_count: u32,
30}
31
32/// Reference to a hierarchy page that hasn't been loaded yet.
33#[derive(Debug, Clone)]
34struct PendingPage {
35    /// The voxel key of the node that points to this page.
36    key: VoxelKey,
37    offset: u64,
38    size: u64,
39}
40
41/// Incrementally-loaded hierarchy cache.
42pub struct HierarchyCache {
43    /// All loaded node entries (point_count >= 0).
44    entries: HashMap<VoxelKey, HierarchyEntry>,
45    /// Pages we know about but haven't fetched yet.
46    pending_pages: Vec<PendingPage>,
47    /// Whether the root page has been loaded.
48    root_loaded: bool,
49}
50
51impl Default for HierarchyCache {
52    fn default() -> Self {
53        Self::new()
54    }
55}
56
57impl HierarchyCache {
58    /// Create an empty hierarchy cache.
59    pub fn new() -> Self {
60        Self {
61            entries: HashMap::new(),
62            pending_pages: Vec::new(),
63            root_loaded: false,
64        }
65    }
66
67    /// Load the root hierarchy page.
68    pub async fn load_root(
69        &mut self,
70        source: &impl ByteSource,
71        info: &CopcInfo,
72    ) -> Result<(), CopcError> {
73        if self.root_loaded {
74            return Ok(());
75        }
76        let data = source
77            .read_range(info.root_hier_offset, info.root_hier_size)
78            .await?;
79        self.parse_page(&data, info.root_hier_offset)?;
80        self.root_loaded = true;
81        Ok(())
82    }
83
84    /// Load the next batch of pending hierarchy pages.
85    pub async fn load_pending_pages(&mut self, source: &impl ByteSource) -> Result<(), CopcError> {
86        if self.pending_pages.is_empty() {
87            return Ok(());
88        }
89        let pages: Vec<_> = self.pending_pages.drain(..).collect();
90        let ranges: Vec<_> = pages.iter().map(|p| (p.offset, p.size)).collect();
91        let results = source.read_ranges(&ranges).await?;
92
93        for (page, data) in pages.iter().zip(results) {
94            self.parse_page(&data, page.offset)?;
95        }
96
97        Ok(())
98    }
99
100    /// Load all pending pages (breadth-first).
101    ///
102    /// Each depth level is fetched in a single [`ByteSource::read_ranges`]
103    /// call, so HTTP backends that override `read_ranges` with parallel
104    /// fetches will issue one round-trip per depth level.
105    pub async fn load_all(
106        &mut self,
107        source: &impl ByteSource,
108        info: &CopcInfo,
109    ) -> Result<(), CopcError> {
110        self.load_root(source, info).await?;
111
112        while !self.pending_pages.is_empty() {
113            self.load_pending_pages(source).await?;
114        }
115
116        Ok(())
117    }
118
119    /// Load only pending pages whose subtree intersects `bounds`.
120    ///
121    /// Pages whose voxel key falls outside the query region are left pending
122    /// for future calls. New child pages discovered during loading are
123    /// evaluated in subsequent iterations, so the full relevant subtree is
124    /// loaded by the time this returns.
125    pub async fn load_pages_for_bounds(
126        &mut self,
127        source: &impl ByteSource,
128        bounds: &Aabb,
129        root_bounds: &Aabb,
130    ) -> Result<(), CopcError> {
131        loop {
132            let matching: Vec<PendingPage> = self
133                .pending_pages
134                .iter()
135                .filter(|p| p.key.bounds(root_bounds).intersects(bounds))
136                .cloned()
137                .collect();
138
139            if matching.is_empty() {
140                break;
141            }
142
143            self.pending_pages
144                .retain(|p| !p.key.bounds(root_bounds).intersects(bounds));
145
146            let ranges: Vec<_> = matching.iter().map(|p| (p.offset, p.size)).collect();
147            let results = source.read_ranges(&ranges).await?;
148
149            for (page, data) in matching.iter().zip(results) {
150                self.parse_page(&data, page.offset)?;
151            }
152        }
153
154        Ok(())
155    }
156
157    /// Like [`load_pages_for_bounds`](Self::load_pages_for_bounds) but stops
158    /// at `max_level` — pages whose key is deeper than `max_level` are left
159    /// pending even if they intersect the bounds.
160    pub async fn load_pages_for_bounds_to_level(
161        &mut self,
162        source: &impl ByteSource,
163        bounds: &Aabb,
164        root_bounds: &Aabb,
165        max_level: i32,
166    ) -> Result<(), CopcError> {
167        loop {
168            let matching: Vec<PendingPage> = self
169                .pending_pages
170                .iter()
171                .filter(|p| {
172                    p.key.level <= max_level && p.key.bounds(root_bounds).intersects(bounds)
173                })
174                .cloned()
175                .collect();
176
177            if matching.is_empty() {
178                break;
179            }
180
181            self.pending_pages.retain(|p| {
182                !(p.key.level <= max_level && p.key.bounds(root_bounds).intersects(bounds))
183            });
184
185            let ranges: Vec<_> = matching.iter().map(|p| (p.offset, p.size)).collect();
186            let results = source.read_ranges(&ranges).await?;
187
188            for (page, data) in matching.iter().zip(results) {
189                self.parse_page(&data, page.offset)?;
190            }
191        }
192
193        Ok(())
194    }
195
196    /// Whether there are unloaded hierarchy pages.
197    pub fn has_pending_pages(&self) -> bool {
198        !self.pending_pages.is_empty()
199    }
200
201    /// Iterate the [`VoxelKey`]s anchoring the currently-pending hierarchy
202    /// pages. A pending page is a sub-tree whose entries live in a separate
203    /// page in the file that hasn't been fetched yet. The anchor key is the
204    /// node that owns the page pointer; resolving the page populates that
205    /// node's subtree.
206    ///
207    /// Useful to consumers that want to surface unloaded subtrees as
208    /// "proxy" nodes in their own data structure, then trigger
209    /// [`Self::load_pages_for_bounds`] / equivalent on demand.
210    pub fn pending_page_keys(&self) -> impl Iterator<Item = VoxelKey> + '_ {
211        self.pending_pages.iter().map(|p| p.key)
212    }
213
214    /// Look up a hierarchy entry by key.
215    pub fn get(&self, key: &VoxelKey) -> Option<&HierarchyEntry> {
216        self.entries.get(key)
217    }
218
219    /// Iterate all loaded entries.
220    pub fn iter(&self) -> impl Iterator<Item = (&VoxelKey, &HierarchyEntry)> {
221        self.entries.iter()
222    }
223
224    /// Number of loaded entries.
225    pub fn len(&self) -> usize {
226        self.entries.len()
227    }
228
229    /// Whether no entries have been loaded.
230    pub fn is_empty(&self) -> bool {
231        self.entries.is_empty()
232    }
233
234    /// Parse a hierarchy page and add entries / page pointers.
235    fn parse_page(&mut self, data: &[u8], _base_offset: u64) -> Result<(), CopcError> {
236        let entry_size = 32; // VoxelKey (16) + offset (8) + byte_size (4) + point_count (4)
237        let mut r = Cursor::new(data);
238
239        while (r.position() as usize + entry_size) <= data.len() {
240            let level = r.read_i32::<LittleEndian>()?;
241            let x = r.read_i32::<LittleEndian>()?;
242            let y = r.read_i32::<LittleEndian>()?;
243            let z = r.read_i32::<LittleEndian>()?;
244            let offset = r.read_u64::<LittleEndian>()?;
245            let byte_size = r.read_i32::<LittleEndian>()?;
246            let point_count = r.read_i32::<LittleEndian>()?;
247
248            let key = VoxelKey { level, x, y, z };
249
250            if point_count == -1 {
251                // Page pointer — register for later loading
252                self.pending_pages.push(PendingPage {
253                    key,
254                    offset,
255                    size: byte_size as u64,
256                });
257            } else if point_count >= 0 && byte_size >= 0 {
258                self.entries.insert(
259                    key,
260                    HierarchyEntry {
261                        key,
262                        offset,
263                        byte_size: byte_size as u32,
264                        point_count: point_count as u32,
265                    },
266                );
267            }
268            // Silently skip entries with invalid negative values (corrupt file).
269        }
270
271        Ok(())
272    }
273}
274
275#[cfg(test)]
276mod tests {
277    use super::*;
278    use byteorder::WriteBytesExt;
279
280    #[allow(clippy::too_many_arguments)]
281    fn write_hierarchy_entry(
282        buf: &mut Vec<u8>,
283        level: i32,
284        x: i32,
285        y: i32,
286        z: i32,
287        offset: u64,
288        byte_size: i32,
289        point_count: i32,
290    ) {
291        buf.write_i32::<LittleEndian>(level).unwrap();
292        buf.write_i32::<LittleEndian>(x).unwrap();
293        buf.write_i32::<LittleEndian>(y).unwrap();
294        buf.write_i32::<LittleEndian>(z).unwrap();
295        buf.write_u64::<LittleEndian>(offset).unwrap();
296        buf.write_i32::<LittleEndian>(byte_size).unwrap();
297        buf.write_i32::<LittleEndian>(point_count).unwrap();
298    }
299
300    /// Build a Vec<u8> source containing:
301    /// - Root page at offset 0: root node + two page pointers (left child, right child)
302    /// - Left child page at some offset: a single node entry
303    /// - Right child page at some offset: a single node entry
304    ///
305    /// Root bounds: center [50, 50, 50], halfsize 50 → [0..100] on each axis.
306    /// Level-1 child (1,0,0,0) covers [0..50] on x → "left"
307    /// Level-1 child (1,1,0,0) covers [50..100] on x → "right"
308    fn build_two_child_source() -> (Vec<u8>, Aabb) {
309        let root_bounds = Aabb {
310            min: [0.0, 0.0, 0.0],
311            max: [100.0, 100.0, 100.0],
312        };
313
314        // Build child pages first so we know their offsets
315        let mut left_page = Vec::new();
316        // Node in left subtree: level 2, (0,0,0)
317        write_hierarchy_entry(&mut left_page, 2, 0, 0, 0, 9000, 64, 10);
318
319        let mut right_page = Vec::new();
320        // Node in right subtree: level 2, (2,0,0)
321        write_hierarchy_entry(&mut right_page, 2, 2, 0, 0, 9500, 64, 20);
322
323        // Root page: root node + two page pointers
324        let mut root_page = Vec::new();
325        write_hierarchy_entry(&mut root_page, 0, 0, 0, 0, 1000, 256, 100);
326
327        // root_page will have 3 entries (root node + 2 page pointers) = 96 bytes
328        let root_page_size = 3 * 32;
329        let left_page_offset = root_page_size as u64;
330        let right_page_offset = left_page_offset + left_page.len() as u64;
331
332        // Page pointer for left child (1,0,0,0) → covers [0..50] on x
333        write_hierarchy_entry(
334            &mut root_page,
335            1,
336            0,
337            0,
338            0,
339            left_page_offset,
340            left_page.len() as i32,
341            -1,
342        );
343        // Page pointer for right child (1,1,0,0) → covers [50..100] on x
344        write_hierarchy_entry(
345            &mut root_page,
346            1,
347            1,
348            0,
349            0,
350            right_page_offset,
351            right_page.len() as i32,
352            -1,
353        );
354
355        let mut source = root_page;
356        source.extend_from_slice(&left_page);
357        source.extend_from_slice(&right_page);
358
359        (source, root_bounds)
360    }
361
362    #[tokio::test]
363    async fn test_load_pages_for_bounds_filters_spatially() {
364        let (source, root_bounds) = build_two_child_source();
365
366        let mut cache = HierarchyCache::new();
367        // Parse root page (offset 0, 96 bytes = 3 entries)
368        cache.parse_page(&source[..96], 0).unwrap();
369
370        assert_eq!(cache.len(), 1); // root node
371        assert_eq!(cache.pending_pages.len(), 2); // left + right page pointers
372
373        // Query only the left side: x in [0..30]
374        let left_query = Aabb {
375            min: [0.0, 0.0, 0.0],
376            max: [30.0, 100.0, 100.0],
377        };
378        cache
379            .load_pages_for_bounds(&source, &left_query, &root_bounds)
380            .await
381            .unwrap();
382
383        // Should have loaded left child page (level 2, node (2,0,0,0))
384        assert_eq!(cache.len(), 2); // root + left level-2 node
385        assert!(
386            cache
387                .get(&VoxelKey {
388                    level: 2,
389                    x: 0,
390                    y: 0,
391                    z: 0,
392                })
393                .is_some()
394        );
395
396        // Right page should still be pending
397        assert_eq!(cache.pending_pages.len(), 1);
398        assert_eq!(
399            cache.pending_pages[0].key,
400            VoxelKey {
401                level: 1,
402                x: 1,
403                y: 0,
404                z: 0
405            }
406        );
407
408        // Now load with a right-side query
409        let right_query = Aabb {
410            min: [60.0, 0.0, 0.0],
411            max: [100.0, 100.0, 100.0],
412        };
413        cache
414            .load_pages_for_bounds(&source, &right_query, &root_bounds)
415            .await
416            .unwrap();
417
418        assert_eq!(cache.len(), 3); // root + left + right level-2 nodes
419        assert!(
420            cache
421                .get(&VoxelKey {
422                    level: 2,
423                    x: 2,
424                    y: 0,
425                    z: 0,
426                })
427                .is_some()
428        );
429        assert!(cache.pending_pages.is_empty());
430    }
431
432    #[tokio::test]
433    async fn test_load_pages_for_bounds_to_level_stops_at_max_level() {
434        let (source, root_bounds) = build_two_child_source();
435
436        let mut cache = HierarchyCache::new();
437        cache.parse_page(&source[..96], 0).unwrap();
438
439        assert_eq!(cache.len(), 1); // root node
440        assert_eq!(cache.pending_pages.len(), 2); // level-1 page pointers
441
442        // Query the entire bounds but limit to level 0 — no pages should load
443        // because the pending pages are level-1 pointers.
444        let full_query = Aabb {
445            min: [0.0, 0.0, 0.0],
446            max: [100.0, 100.0, 100.0],
447        };
448        cache
449            .load_pages_for_bounds_to_level(&source, &full_query, &root_bounds, 0)
450            .await
451            .unwrap();
452
453        assert_eq!(cache.len(), 1); // still just root
454        assert_eq!(cache.pending_pages.len(), 2); // both pages still pending
455
456        // Now allow level 1 — both pages should load
457        cache
458            .load_pages_for_bounds_to_level(&source, &full_query, &root_bounds, 1)
459            .await
460            .unwrap();
461
462        assert_eq!(cache.len(), 3); // root + two level-2 nodes
463        assert!(cache.pending_pages.is_empty());
464    }
465
466    #[tokio::test]
467    async fn test_parse_hierarchy_page() {
468        let mut page_data = Vec::new();
469        write_hierarchy_entry(&mut page_data, 0, 0, 0, 0, 1000, 256, 100);
470        write_hierarchy_entry(&mut page_data, 1, 0, 0, 0, 2000, 512, 50);
471        write_hierarchy_entry(&mut page_data, 1, 1, 0, 0, 5000, 128, -1);
472
473        let mut cache = HierarchyCache::new();
474        cache.parse_page(&page_data, 0).unwrap();
475
476        assert_eq!(cache.len(), 2); // 2 node entries
477        assert_eq!(cache.pending_pages.len(), 1); // 1 page pointer
478
479        let root = cache
480            .get(&VoxelKey {
481                level: 0,
482                x: 0,
483                y: 0,
484                z: 0,
485            })
486            .unwrap();
487        assert_eq!(root.point_count, 100);
488        assert_eq!(root.offset, 1000);
489    }
490}