Skip to main content

bunny_codec/
compressed.rs

1mod error;
2mod read;
3mod view;
4
5use read::{checked_add, checked_payload_len, parse_header, take, validate_triangles};
6use view::CompressedMeshParts;
7
8pub use error::CompressedMeshError;
9pub use view::{CompressedIndexWidth, CompressedMesh, CompressedTriangle};
10
11const HEADER_LEN: usize = 76;
12const VERTEX_STRIDE: usize = 6;
13const TRIANGLE16_STRIDE: usize = 6;
14const TRIANGLE32_STRIDE: usize = 12;
15const MAX_VERTICES: usize = 1_000_000;
16const MAX_TRIANGLES: usize = 1_000_000;
17
18/// Decodes a canonical Bunny compressed mesh byte stream.
19///
20/// The accepted profile is documented in `docs/goalposts/v0.4.0-gp3.md`.
21/// Decoding borrows the vertex and triangle byte sections, validates all
22/// triangle indices, and performs no heap allocation on the accepted path.
23///
24/// # Errors
25/// Returns `CompressedMeshError` when any header, length, bounds, count, or
26/// payload invariant is violated.
27pub fn decode_compressed_mesh(input: &[u8]) -> Result<CompressedMesh<'_>, CompressedMeshError> {
28    let header = parse_header(input)?;
29    let layout = payload_layout(&header)?;
30    validate_total_len(input, layout.payload_end)?;
31    let sections = payload_sections(input, &layout)?;
32    validate_triangles(
33        sections.triangle_bytes,
34        header.triangle_count,
35        header.vertex_count,
36        header.index_width,
37    )?;
38    Ok(CompressedMesh::new(CompressedMeshParts {
39        bounds: header.bounds,
40        vertex_bytes: sections.vertex_bytes,
41        triangle_bytes: sections.triangle_bytes,
42        vertex_count: header.vertex_count,
43        triangle_count: header.triangle_count,
44        index_width: header.index_width,
45    }))
46}
47
48struct PayloadLayout {
49    vertex_len: usize,
50    triangle_len: usize,
51    triangle_start: usize,
52    payload_end: usize,
53}
54
55struct PayloadSections<'a> {
56    vertex_bytes: &'a [u8],
57    triangle_bytes: &'a [u8],
58}
59
60fn payload_layout(header: &read::Header) -> Result<PayloadLayout, CompressedMeshError> {
61    let vertex_len = checked_payload_len(header.vertex_count, VERTEX_STRIDE)?;
62    let triangle_len = checked_payload_len(header.triangle_count, header.index_width.stride())?;
63    let expected_len = checked_add(vertex_len, triangle_len)?;
64    let expected_len_u64 =
65        u64::try_from(expected_len).map_err(|_| CompressedMeshError::IntegerOverflow)?;
66    if header.payload_len != expected_len_u64 {
67        return Err(CompressedMeshError::InvalidPayloadLength);
68    }
69    let triangle_start = checked_add(HEADER_LEN, vertex_len)?;
70    let payload_end = checked_add(HEADER_LEN, expected_len)?;
71
72    Ok(PayloadLayout { vertex_len, triangle_len, triangle_start, payload_end })
73}
74
75const fn validate_total_len(input: &[u8], payload_end: usize) -> Result<(), CompressedMeshError> {
76    if input.len() < payload_end {
77        return Err(CompressedMeshError::PayloadTooShort);
78    }
79    if input.len() != payload_end {
80        return Err(CompressedMeshError::TrailingData);
81    }
82    Ok(())
83}
84
85fn payload_sections<'a>(
86    input: &'a [u8],
87    layout: &PayloadLayout,
88) -> Result<PayloadSections<'a>, CompressedMeshError> {
89    let vertex_bytes = take(input, HEADER_LEN, layout.vertex_len)?;
90    let triangle_bytes = take(input, layout.triangle_start, layout.triangle_len)?;
91    Ok(PayloadSections { vertex_bytes, triangle_bytes })
92}