Skip to content

Commit 5ab4e6d

Browse files
committed
Rework the writer with caller metadata, atomic output, single-pass validation, direct encoding, and optional parallel compression
1 parent 76b0ab0 commit 5ab4e6d

17 files changed

Lines changed: 2958 additions & 2084 deletions

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,5 @@ las = { version = "0.8", features = ["laz"] }
1616
laz = "0.8"
1717
memmap2 = "0.9"
1818
rayon = "1"
19+
tempfile = { version = "3", default-features = false }
1920
thiserror = "2"

copc-writer/Cargo.toml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,19 @@ documentation = "https://docs.rs/copc-writer"
1212
keywords = ["copc", "las", "laz", "point-cloud", "gis"]
1313
categories = ["science", "encoding"]
1414

15+
[features]
16+
## Parallel LAZ chunk compression with rayon; each octree node is still one
17+
## COPC chunk, compressed on a worker thread and written in order.
18+
parallel = ["dep:rayon"]
19+
1520
[dependencies]
1621
byteorder = { workspace = true }
1722
copc-core = { version = "0.4.2", path = "../copc-core" }
1823
las = { workspace = true }
1924
laz = { workspace = true }
2025
memmap2 = { workspace = true }
21-
tempfile = { version = "3", default-features = false }
26+
rayon = { workspace = true, optional = true }
27+
tempfile = { workspace = true }
2228

2329
[dev-dependencies]
2430
copc-reader = { version = "0.4.2", path = "../copc-reader" }

copc-writer/src/hierarchy_pages.rs

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
//! COPC hierarchy page planning and serialization.
2+
3+
use std::io::{Seek, Write};
4+
5+
use copc_core::{Entry, Error, Result, VoxelKey, HIERARCHY_ENTRY_BYTES};
6+
7+
const HIERARCHY_PAGE_MAX_ENTRIES: usize = 4_096;
8+
9+
#[derive(Debug)]
10+
pub(crate) struct HierarchyPagePlan {
11+
pub(crate) key: VoxelKey,
12+
pub(crate) items: Vec<HierarchyPageItem>,
13+
pub(crate) offset: u64,
14+
pub(crate) byte_size: u64,
15+
}
16+
17+
#[derive(Debug)]
18+
pub(crate) enum HierarchyPageItem {
19+
Point(Entry),
20+
Child(Box<HierarchyPagePlan>),
21+
}
22+
23+
pub(crate) fn plan_hierarchy_pages(entries: &[Entry], key: VoxelKey) -> Result<HierarchyPagePlan> {
24+
if entries.is_empty() {
25+
return Err(Error::InvalidInput(
26+
"cannot write empty hierarchy page".into(),
27+
));
28+
}
29+
if entries.len() <= HIERARCHY_PAGE_MAX_ENTRIES {
30+
return Ok(HierarchyPagePlan {
31+
key,
32+
items: entries
33+
.iter()
34+
.copied()
35+
.map(HierarchyPageItem::Point)
36+
.collect(),
37+
offset: 0,
38+
byte_size: 0,
39+
});
40+
}
41+
42+
let mut point_entry = None;
43+
let mut child_entries: [Vec<Entry>; 8] = std::array::from_fn(|_| Vec::new());
44+
for entry in entries.iter().copied() {
45+
if entry.key == key {
46+
point_entry = Some(entry);
47+
continue;
48+
}
49+
let mut matched = false;
50+
for (octant, child_entries) in child_entries.iter_mut().enumerate() {
51+
let child_key = key.child(octant as u8);
52+
if key_contains(child_key, entry.key) {
53+
child_entries.push(entry);
54+
matched = true;
55+
break;
56+
}
57+
}
58+
if !matched {
59+
return Err(Error::InvalidInput(format!(
60+
"hierarchy entry {:?} is not under page key {:?}",
61+
entry.key, key
62+
)));
63+
}
64+
}
65+
66+
let mut items = Vec::new();
67+
if let Some(entry) = point_entry {
68+
items.push(HierarchyPageItem::Point(entry));
69+
}
70+
for (octant, child_entries) in child_entries.into_iter().enumerate() {
71+
if child_entries.is_empty() {
72+
continue;
73+
}
74+
items.push(HierarchyPageItem::Child(Box::new(plan_hierarchy_pages(
75+
&child_entries,
76+
key.child(octant as u8),
77+
)?)));
78+
}
79+
if items.len() > HIERARCHY_PAGE_MAX_ENTRIES {
80+
return Err(Error::InvalidInput(format!(
81+
"hierarchy page for {:?} has {} entries, max is {}",
82+
key,
83+
items.len(),
84+
HIERARCHY_PAGE_MAX_ENTRIES
85+
)));
86+
}
87+
Ok(HierarchyPagePlan {
88+
key,
89+
items,
90+
offset: 0,
91+
byte_size: 0,
92+
})
93+
}
94+
95+
pub(crate) fn assign_hierarchy_page_offsets(
96+
page: &mut HierarchyPagePlan,
97+
offset: u64,
98+
) -> Result<u64> {
99+
page.offset = offset;
100+
page.byte_size = hierarchy_page_byte_size(page.items.len())?;
101+
let mut next = offset
102+
.checked_add(page.byte_size)
103+
.ok_or_else(|| Error::InvalidInput("hierarchy page offset overflow".into()))?;
104+
for item in &mut page.items {
105+
if let HierarchyPageItem::Child(child) = item {
106+
next = assign_hierarchy_page_offsets(child, next)?;
107+
}
108+
}
109+
Ok(next)
110+
}
111+
112+
fn hierarchy_page_byte_size(entry_count: usize) -> Result<u64> {
113+
let bytes = entry_count
114+
.checked_mul(HIERARCHY_ENTRY_BYTES)
115+
.ok_or_else(|| Error::InvalidInput("hierarchy page size overflow".into()))?;
116+
u64::try_from(bytes).map_err(|_| Error::InvalidInput("hierarchy page is too large".into()))
117+
}
118+
119+
pub(crate) fn write_hierarchy_page_tree<W: Write + Seek>(
120+
writer: &mut W,
121+
page: &HierarchyPagePlan,
122+
) -> Result<()> {
123+
let position = writer
124+
.stream_position()
125+
.map_err(|e| Error::io("record hierarchy page offset", e))?;
126+
if position != page.offset {
127+
return Err(Error::InvalidInput(format!(
128+
"hierarchy page offset mismatch: at {position}, expected {}",
129+
page.offset
130+
)));
131+
}
132+
let mut entry_buf = [0u8; HIERARCHY_ENTRY_BYTES];
133+
for item in &page.items {
134+
hierarchy_page_item_entry(item)?.write_le(&mut entry_buf)?;
135+
writer
136+
.write_all(&entry_buf)
137+
.map_err(|e| Error::io("write hierarchy entry", e))?;
138+
}
139+
for item in &page.items {
140+
if let HierarchyPageItem::Child(child) = item {
141+
write_hierarchy_page_tree(writer, child)?;
142+
}
143+
}
144+
Ok(())
145+
}
146+
147+
fn hierarchy_page_item_entry(item: &HierarchyPageItem) -> Result<Entry> {
148+
match item {
149+
HierarchyPageItem::Point(entry) => Ok(*entry),
150+
HierarchyPageItem::Child(child) => Ok(Entry {
151+
key: child.key,
152+
offset: child.offset,
153+
byte_size: i32::try_from(child.byte_size).map_err(|_| {
154+
Error::InvalidInput("child hierarchy page exceeds COPC i32 byte size".into())
155+
})?,
156+
point_count: -1,
157+
}),
158+
}
159+
}
160+
161+
fn key_contains(ancestor: VoxelKey, key: VoxelKey) -> bool {
162+
if key.level < ancestor.level {
163+
return false;
164+
}
165+
let shift = (key.level - ancestor.level) as u32;
166+
(key.x >> shift) == ancestor.x
167+
&& (key.y >> shift) == ancestor.y
168+
&& (key.z >> shift) == ancestor.z
169+
}
170+
171+
#[cfg(test)]
172+
mod tests {
173+
use super::*;
174+
175+
use std::io::{Cursor, SeekFrom};
176+
177+
#[test]
178+
fn hierarchy_plan_splits_large_root_page() {
179+
let mut entries = vec![Entry {
180+
key: VoxelKey::root(),
181+
offset: 1,
182+
byte_size: 1,
183+
point_count: 1,
184+
}];
185+
let mut offset = 2;
186+
for z in 0..16 {
187+
for y in 0..16 {
188+
for x in 0..16 {
189+
entries.push(Entry {
190+
key: VoxelKey { level: 4, x, y, z },
191+
offset,
192+
byte_size: 1,
193+
point_count: 1,
194+
});
195+
offset += 1;
196+
}
197+
}
198+
}
199+
entries.sort_by_key(|entry| entry.key);
200+
201+
let mut plan = plan_hierarchy_pages(&entries, VoxelKey::root()).unwrap();
202+
let start = 1024;
203+
let end = assign_hierarchy_page_offsets(&mut plan, start).unwrap();
204+
205+
assert!(plan.byte_size < hierarchy_page_byte_size(entries.len()).unwrap());
206+
assert!(plan
207+
.items
208+
.iter()
209+
.any(|item| matches!(item, HierarchyPageItem::Child(_))));
210+
211+
let mut out = Cursor::new(vec![0; start as usize]);
212+
out.seek(SeekFrom::Start(start)).unwrap();
213+
write_hierarchy_page_tree(&mut out, &plan).unwrap();
214+
assert_eq!(end, out.get_ref().len() as u64);
215+
}
216+
}

0 commit comments

Comments
 (0)