Skip to main content

qualia_core_db/container_10d/
mesh_section.rs

1//! `.10d` QuantizedMesh section — the geometry half of a mesh asset in the
2//! container (P0.4).
3//!
4//! A QuantizedMesh section wraps a [`Mesh`](crate::render::assets::Mesh) as a
5//! self-describing `.10d` section. Vertex positions are quantized to **u16
6//! per axis within the mesh's bounding box** — 6 bytes/vertex vs 12 for raw
7//! f32 (2×), and the bbox is exactly what the semantic quins already carry,
8//! so it doubles as the dequantization frame (no information is invented).
9//! Triangle indices are u16 when the mesh has ≤65 536 vertices (6 bytes/tri
10//! vs 12). Quantization error is `bbox_extent / 65535` per axis — sub-micron
11//! at organ scale, visually lossless.
12//!
13//! **Layout:** a 40-byte [`MeshMiniHeader`] (flags + counts + dequantization
14//! bbox + reserved) followed by the quantized vertex data (N × 6 bytes:
15//! u16×3 per vertex) and then the triangle indices (u16×3 or u32×3 per
16//! triangle, selected by the `FLAG_U16_INDICES` flag). The mini-header is
17//! `repr(C)`, naturally aligned, no implicit padding.
18//!
19//! **This replaces the erroneous legacy mesh build artifact** that lived in
20//! `render/mesh_asset.rs` — a pre-release format that was never shipped and
21//! has been refactored out rather than carried forward. The legacy 48-byte
22//! header with its per-format magic is gone; the `.10d` section-type tag
23//! (`SectionType::QuantizedMesh = 1`) replaces the magic, and the `.10d`
24//! container version replaces the per-format version. No backward-compat is
25//! provided — the legacy format was an erroneous build artifact, not a
26//! shipped format anyone depends on.
27//!
28//! **Determinism + CRC:** two encodes of the same mesh are byte-identical
29//! (the quantization is deterministic). The per-section CRC-32C (P0.2)
30//! catches a flipped bit in the payload. The whole-file CRC-32C (P0.3)
31//! catches header corruption.
32
33use bytemuck::{bytes_of, from_bytes, Pod, Zeroable};
34
35use crate::render::assets::Mesh;
36
37/// Section payload mini-header size in bytes.
38pub const MESH_MINI_HEADER_SIZE: usize = 40;
39
40/// `flags` bit 0: triangle indices are u16 (else u32).
41pub const FLAG_U16_INDICES: u16 = 0x0001;
42
43/// Maximum vertex count the mesh section will accept. Bounds against a
44/// hostile/malformed file. u16 indices cap at 65 536; above that the encoder
45/// switches to u32. The practical ceiling is the 42MB Sentinel: 40MB of
46/// vertex data / 6 bytes per vertex ≈ 6.7M vertices. 4M (2^22) is a
47/// comfortable upper bound.
48pub const MAX_VERTEX_COUNT: usize = 4_194_304; // 2^22
49
50/// Maximum triangle count. Similarly bounded by the Sentinel: 40MB / 6 bytes
51/// per u16-indexed triangle ≈ 6.7M triangles. 4M is the matching ceiling.
52pub const MAX_TRIANGLE_COUNT: usize = 4_194_304; // 2^22
53
54/// The 40-byte QuantizedMesh-section mini-header. `repr(C)`, naturally
55/// aligned, no implicit padding.
56///
57/// ```text
58/// offset  size  field
59/// 0       2     flags:u16         (bit 0 = u16 indices, else u32)
60/// 2       2     reserved_u16      (must be zero)
61/// 4       4     vertex_count:u32
62/// 8       4     triangle_count:u32
63/// 12      12    min:[f32;3]       (dequantization frame: position = min + (q/65535)*(max-min))
64/// 24      12    max:[f32;3]
65/// 36      4     reserved_u32      (must be zero — future: LOD tier, material index)
66/// ```
67#[repr(C)]
68#[derive(Debug, Clone, Copy, PartialEq, Pod, Zeroable)]
69pub struct MeshMiniHeader {
70    pub flags: u16,
71    pub reserved_u16: u16,
72    pub vertex_count: u32,
73    pub triangle_count: u32,
74    pub min: [f32; 3],
75    pub max: [f32; 3],
76    pub reserved_u32: u32,
77}
78
79impl MeshMiniHeader {
80    /// Total payload byte length (mini-header + vertex data + index data).
81    #[inline]
82    pub fn payload_bytes(vertex_count: usize, triangle_count: usize, u16_idx: bool) -> usize {
83        let idx_bytes = if u16_idx { 6 } else { 12 };
84        MESH_MINI_HEADER_SIZE + vertex_count * 6 + triangle_count * idx_bytes
85    }
86}
87
88/// Mesh-section read/write error.
89#[derive(Debug, Clone, PartialEq, Eq)]
90pub enum MeshSectionError {
91    /// The payload is too short for the mini-header.
92    PayloadTooShort { got: usize, need: usize },
93    /// A reserved field in the mini-header is non-zero.
94    NonZeroReserved { field: &'static str },
95    /// `vertex_count` exceeds `MAX_VERTEX_COUNT`.
96    VertexCountTooLarge { got: u32, max: usize },
97    /// `triangle_count` exceeds `MAX_TRIANGLE_COUNT`.
98    TriangleCountTooLarge { got: u32, max: usize },
99    /// The payload is too short for the declared counts.
100    PayloadTruncated { expected: usize, got: usize },
101    /// The output buffer is too small.
102    OutputBufferTooSmall { needed: usize, have: usize },
103    /// Unknown flags bit set (only bit 0 is defined in v1).
104    UnknownFlags { got: u16 },
105}
106
107impl std::fmt::Display for MeshSectionError {
108    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109        match self {
110            Self::PayloadTooShort { got, need } => {
111                write!(f, "10d MESH payload too short: got {got}, need {need}")
112            }
113            Self::NonZeroReserved { field } => {
114                write!(f, "10d MESH non-zero reserved field {field:?}")
115            }
116            Self::VertexCountTooLarge { got, max } => {
117                write!(f, "10d MESH vertex_count {got} exceeds max {max}")
118            }
119            Self::TriangleCountTooLarge { got, max } => {
120                write!(f, "10d MESH triangle_count {got} exceeds max {max}")
121            }
122            Self::PayloadTruncated { expected, got } => write!(
123                f,
124                "10d MESH payload truncated: expected {expected}, got {got}"
125            ),
126            Self::OutputBufferTooSmall { needed, have } => write!(
127                f,
128                "10d MESH output buffer too small: need {needed}, have {have}"
129            ),
130            Self::UnknownFlags { got } => write!(
131                f,
132                "10d MESH unknown flags bits {got:#06x} (only bit 0 defined in v1)"
133            ),
134        }
135    }
136}
137
138impl std::error::Error for MeshSectionError {}
139
140/// Whether a mesh with `vertex_count` vertices can use u16 indices.
141#[inline]
142pub fn fits_u16_indices(vertex_count: usize) -> bool {
143    vertex_count <= u16::MAX as usize + 1 // indices 0..=65535
144}
145
146/// Encoded length in bytes for a mesh of the given size (for size reporting without allocating).
147#[inline]
148pub fn encoded_len(vertex_count: usize, triangle_count: usize) -> usize {
149    let idx_bytes = if fits_u16_indices(vertex_count) {
150        6
151    } else {
152        12
153    };
154    MESH_MINI_HEADER_SIZE + vertex_count * 6 + triangle_count * idx_bytes
155}
156
157/// Raw in-memory geometry size (f32 positions + u32 triangle indices) — the baseline we shrink from.
158#[inline]
159pub fn raw_geometry_len(vertex_count: usize, triangle_count: usize) -> usize {
160    vertex_count * 12 + triangle_count * 12
161}
162
163#[inline]
164fn quantize(v: f32, min: f32, extent: f32) -> u16 {
165    if extent <= 0.0 {
166        return 0;
167    }
168    let n = ((v - min) / extent).clamp(0.0, 1.0);
169    (n * 65535.0 + 0.5) as u16
170}
171
172#[inline]
173fn dequantize(q: u16, min: f32, extent: f32) -> f32 {
174    min + (q as f32 / 65535.0) * extent
175}
176
177fn bbox(positions: &[[f32; 3]]) -> ([f32; 3], [f32; 3]) {
178    let mut min = [f32::INFINITY; 3];
179    let mut max = [f32::NEG_INFINITY; 3];
180    for p in positions {
181        for a in 0..3 {
182            if p[a] < min[a] {
183                min[a] = p[a];
184            }
185            if p[a] > max[a] {
186                max[a] = p[a];
187            }
188        }
189    }
190    if positions.is_empty() {
191        min = [0.0; 3];
192        max = [0.0; 3];
193    }
194    (min, max)
195}
196
197/// Parse and validate the mesh-section mini-header. Returns the header and the
198/// total payload byte length it claims.
199pub fn parse_mesh_header(bytes: &[u8]) -> Result<(MeshMiniHeader, usize), MeshSectionError> {
200    if bytes.len() < MESH_MINI_HEADER_SIZE {
201        return Err(MeshSectionError::PayloadTooShort {
202            got: bytes.len(),
203            need: MESH_MINI_HEADER_SIZE,
204        });
205    }
206    let header: MeshMiniHeader = {
207        let mut buf = [0u8; MESH_MINI_HEADER_SIZE];
208        buf.copy_from_slice(&bytes[..MESH_MINI_HEADER_SIZE]);
209        *from_bytes(&buf)
210    };
211    if header.reserved_u16 != 0 {
212        return Err(MeshSectionError::NonZeroReserved {
213            field: "reserved_u16",
214        });
215    }
216    if header.reserved_u32 != 0 {
217        return Err(MeshSectionError::NonZeroReserved {
218            field: "reserved_u32",
219        });
220    }
221    // Only bit 0 (FLAG_U16_INDICES) is defined in v1.
222    if header.flags & !FLAG_U16_INDICES != 0 {
223        return Err(MeshSectionError::UnknownFlags { got: header.flags });
224    }
225    let vcount = header.vertex_count as usize;
226    let tcount = header.triangle_count as usize;
227    if vcount > MAX_VERTEX_COUNT {
228        return Err(MeshSectionError::VertexCountTooLarge {
229            got: header.vertex_count,
230            max: MAX_VERTEX_COUNT,
231        });
232    }
233    if tcount > MAX_TRIANGLE_COUNT {
234        return Err(MeshSectionError::TriangleCountTooLarge {
235            got: header.triangle_count,
236            max: MAX_TRIANGLE_COUNT,
237        });
238    }
239    let u16_idx = header.flags & FLAG_U16_INDICES != 0;
240    let total = MeshMiniHeader::payload_bytes(vcount, tcount, u16_idx);
241    if bytes.len() < total {
242        return Err(MeshSectionError::PayloadTruncated {
243            expected: total,
244            got: bytes.len(),
245        });
246    }
247    Ok((header, total))
248}
249
250/// Encode a [`Mesh`] into a `.10d` QuantizedMesh section payload in a
251/// caller-supplied buffer. Returns the bytes written. Zero-heap. The bbox is
252/// recomputed from the positions (independent of any stale `mesh.min/max`)
253/// so it is a faithful dequantization frame.
254pub fn encode_mesh_section(mesh: &Mesh, out: &mut [u8]) -> Result<usize, MeshSectionError> {
255    let vcount = mesh.positions.len();
256    let tcount = mesh.triangles.len();
257    if vcount > MAX_VERTEX_COUNT {
258        return Err(MeshSectionError::VertexCountTooLarge {
259            got: vcount as u32,
260            max: MAX_VERTEX_COUNT,
261        });
262    }
263    if tcount > MAX_TRIANGLE_COUNT {
264        return Err(MeshSectionError::TriangleCountTooLarge {
265            got: tcount as u32,
266            max: MAX_TRIANGLE_COUNT,
267        });
268    }
269    let u16_idx = fits_u16_indices(vcount);
270    let (min, max) = bbox(&mesh.positions);
271    let extent = [max[0] - min[0], max[1] - min[1], max[2] - min[2]];
272    let need = encoded_len(vcount, tcount);
273    if out.len() < need {
274        return Err(MeshSectionError::OutputBufferTooSmall {
275            needed: need,
276            have: out.len(),
277        });
278    }
279    let header = MeshMiniHeader {
280        flags: if u16_idx { FLAG_U16_INDICES } else { 0 },
281        reserved_u16: 0,
282        vertex_count: vcount as u32,
283        triangle_count: tcount as u32,
284        min,
285        max,
286        reserved_u32: 0,
287    };
288    let header_bytes = bytes_of(&header);
289    out[..MESH_MINI_HEADER_SIZE].copy_from_slice(header_bytes);
290    let mut off = MESH_MINI_HEADER_SIZE;
291    for p in &mesh.positions {
292        for a in 0..3 {
293            out[off..off + 2].copy_from_slice(&quantize(p[a], min[a], extent[a]).to_le_bytes());
294            off += 2;
295        }
296    }
297    for t in &mesh.triangles {
298        for &idx in t {
299            if u16_idx {
300                out[off..off + 2].copy_from_slice(&(idx as u16).to_le_bytes());
301                off += 2;
302            } else {
303                out[off..off + 4].copy_from_slice(&idx.to_le_bytes());
304                off += 4;
305            }
306        }
307    }
308    debug_assert_eq!(off, need, "encode must fill exactly the computed length");
309    Ok(off)
310}
311
312/// Decode a `.10d` QuantizedMesh section payload back into a [`Mesh`]
313/// (dequantized positions, exact indices). This is the ingest path (not a
314/// hot path), so `Vec` allocation is fine per AGENTS.md §2-B.
315pub fn decode_mesh_section(bytes: &[u8]) -> Result<Mesh, MeshSectionError> {
316    let (header, total) = parse_mesh_header(bytes)?;
317    let vcount = header.vertex_count as usize;
318    let tcount = header.triangle_count as usize;
319    let u16_idx = header.flags & FLAG_U16_INDICES != 0;
320    let extent = [
321        header.max[0] - header.min[0],
322        header.max[1] - header.min[1],
323        header.max[2] - header.min[2],
324    ];
325    debug_assert_eq!(
326        total,
327        MESH_MINI_HEADER_SIZE + vcount * 6 + tcount * if u16_idx { 6 } else { 12 }
328    );
329
330    let mut positions = Vec::with_capacity(vcount);
331    let mut off = MESH_MINI_HEADER_SIZE;
332    for _ in 0..vcount {
333        let mut p = [0.0f32; 3];
334        for a in 0..3 {
335            let q = u16::from_le_bytes([bytes[off], bytes[off + 1]]);
336            p[a] = dequantize(q, header.min[a], extent[a]);
337            off += 2;
338        }
339        positions.push(p);
340    }
341
342    let mut triangles = Vec::with_capacity(tcount);
343    for _ in 0..tcount {
344        let mut t = [0u32; 3];
345        for corner in t.iter_mut() {
346            if u16_idx {
347                *corner = u16::from_le_bytes([bytes[off], bytes[off + 1]]) as u32;
348                off += 2;
349            } else {
350                *corner = u32::from_le_bytes([
351                    bytes[off],
352                    bytes[off + 1],
353                    bytes[off + 2],
354                    bytes[off + 3],
355                ]);
356                off += 4;
357            }
358        }
359        triangles.push(t);
360    }
361
362    Ok(Mesh {
363        positions,
364        triangles,
365        min: header.min,
366        max: header.max,
367    })
368}
369
370#[cfg(test)]
371mod tests {
372    use super::*;
373    use crate::container_10d::header::Container10dHeader;
374    use crate::container_10d::integrity::{seal_whole_file_crc32c, verify_whole_file_crc32c};
375    use crate::container_10d::section::{
376        encode_container, parse_section_table, AlignmentTier, SectionInput, SectionType,
377    };
378
379    /// A unit cube: 8 vertices, 12 triangles.
380    fn cube() -> Mesh {
381        let positions = vec![
382            [0.0, 0.0, 0.0],
383            [1.0, 0.0, 0.0],
384            [1.0, 1.0, 0.0],
385            [0.0, 1.0, 0.0],
386            [0.0, 0.0, 1.0],
387            [1.0, 0.0, 1.0],
388            [1.0, 1.0, 1.0],
389            [0.0, 1.0, 1.0],
390        ];
391        let triangles = vec![
392            [0, 1, 2],
393            [0, 2, 3],
394            [4, 5, 6],
395            [4, 6, 7],
396            [0, 1, 5],
397            [0, 5, 4],
398            [2, 3, 7],
399            [2, 7, 6],
400            [1, 2, 6],
401            [1, 6, 5],
402            [0, 3, 7],
403            [0, 7, 4],
404        ];
405        Mesh {
406            positions,
407            triangles,
408            min: [0.0; 3],
409            max: [1.0; 3],
410        }
411    }
412
413    #[test]
414    fn mini_header_is_pod_with_exact_size() {
415        assert_eq!(std::mem::size_of::<MeshMiniHeader>(), MESH_MINI_HEADER_SIZE);
416        assert_eq!(std::mem::offset_of!(MeshMiniHeader, flags), 0);
417        assert_eq!(std::mem::offset_of!(MeshMiniHeader, reserved_u16), 2);
418        assert_eq!(std::mem::offset_of!(MeshMiniHeader, vertex_count), 4);
419        assert_eq!(std::mem::offset_of!(MeshMiniHeader, triangle_count), 8);
420        assert_eq!(std::mem::offset_of!(MeshMiniHeader, min), 12);
421        assert_eq!(std::mem::offset_of!(MeshMiniHeader, max), 24);
422        assert_eq!(std::mem::offset_of!(MeshMiniHeader, reserved_u32), 36);
423    }
424
425    #[test]
426    fn round_trips_within_quantization_tolerance_and_indices_exact() {
427        let mesh = cube();
428        let need = encoded_len(mesh.positions.len(), mesh.triangles.len());
429        let mut buf = vec![0u8; need];
430        let n = encode_mesh_section(&mesh, &mut buf).expect("encode");
431        assert_eq!(n, need);
432        let back = decode_mesh_section(&buf).expect("decode");
433
434        assert_eq!(back.positions.len(), mesh.positions.len());
435        assert_eq!(back.triangles, mesh.triangles, "indices are exact");
436
437        // Positions are within one quantization step of the original (extent/65535 per axis).
438        let extent = 1.0f32; // unit cube
439        let tol = extent / 65535.0 * 2.0;
440        for (a, b) in mesh.positions.iter().zip(&back.positions) {
441            for k in 0..3 {
442                assert!((a[k] - b[k]).abs() <= tol, "axis {k}: {} vs {}", a[k], b[k]);
443            }
444        }
445    }
446
447    #[test]
448    fn encoded_is_smaller_than_raw_f32_geometry() {
449        let vcount = 50_000usize;
450        let tcount = 100_000usize;
451        let raw = raw_geometry_len(vcount, tcount);
452        let enc = encoded_len(vcount, tcount);
453        assert!(enc < raw, "encoded {enc} !< raw {raw}");
454        let ratio = enc as f64 / raw as f64;
455        assert!(ratio < 0.52 && ratio > 0.48, "ratio {ratio} not ~0.5");
456    }
457
458    #[test]
459    fn u32_indices_when_over_65k_vertices() {
460        let positions = vec![[0.0f32, 0.0, 0.0]; 70_000];
461        let triangles = vec![[0u32, 1, 69_999]];
462        let mesh = Mesh {
463            positions,
464            triangles: triangles.clone(),
465            min: [0.0; 3],
466            max: [0.0; 3],
467        };
468        let need = encoded_len(mesh.positions.len(), mesh.triangles.len());
469        let mut buf = vec![0u8; need];
470        encode_mesh_section(&mesh, &mut buf).expect("encode");
471        let (header, _) = parse_mesh_header(&buf).expect("parse header");
472        assert_eq!(header.flags & FLAG_U16_INDICES, 0, "u32 indices selected");
473        let back = decode_mesh_section(&buf).expect("decode");
474        assert_eq!(back.triangles, triangles, "large indices survive");
475    }
476
477    #[test]
478    fn rejects_bad_payload_and_truncation() {
479        assert!(parse_mesh_header(&[0u8; 8]).is_err());
480        let mesh = cube();
481        let need = encoded_len(mesh.positions.len(), mesh.triangles.len());
482        let mut buf = vec![0u8; need];
483        encode_mesh_section(&mesh, &mut buf).expect("encode");
484        buf.truncate(MESH_MINI_HEADER_SIZE + 4); // header ok, body truncated
485        assert!(parse_mesh_header(&buf).is_err());
486    }
487
488    #[test]
489    fn rejects_non_zero_reserved() {
490        let mesh = cube();
491        let need = encoded_len(mesh.positions.len(), mesh.triangles.len());
492        let mut buf = vec![0u8; need];
493        encode_mesh_section(&mesh, &mut buf).expect("encode");
494        buf[2] = 1; // reserved_u16
495        let err = parse_mesh_header(&buf).expect_err("non-zero reserved_u16 must reject");
496        assert!(
497            matches!(
498                err,
499                MeshSectionError::NonZeroReserved {
500                    field: "reserved_u16"
501                }
502            ),
503            "{err}"
504        );
505        // Restore and corrupt reserved_u32 (offset 36).
506        buf[2] = 0;
507        buf[36] = 1;
508        let err = parse_mesh_header(&buf).expect_err("non-zero reserved_u32 must reject");
509        assert!(
510            matches!(
511                err,
512                MeshSectionError::NonZeroReserved {
513                    field: "reserved_u32"
514                }
515            ),
516            "{err}"
517        );
518    }
519
520    #[test]
521    fn rejects_unknown_flags() {
522        let mesh = cube();
523        let need = encoded_len(mesh.positions.len(), mesh.triangles.len());
524        let mut buf = vec![0u8; need];
525        encode_mesh_section(&mesh, &mut buf).expect("encode");
526        buf[0] = 0x02; // bit 1 is undefined in v1
527        let err = parse_mesh_header(&buf).expect_err("unknown flags must reject");
528        assert!(
529            matches!(err, MeshSectionError::UnknownFlags { .. }),
530            "{err}"
531        );
532    }
533
534    #[test]
535    fn rejects_vertex_count_too_large() {
536        let header = MeshMiniHeader {
537            flags: FLAG_U16_INDICES,
538            reserved_u16: 0,
539            vertex_count: (MAX_VERTEX_COUNT + 1) as u32,
540            triangle_count: 0,
541            min: [0.0; 3],
542            max: [0.0; 3],
543            reserved_u32: 0,
544        };
545        let mut buf = vec![0u8; MESH_MINI_HEADER_SIZE];
546        buf[..MESH_MINI_HEADER_SIZE].copy_from_slice(bytemuck::bytes_of(&header));
547        let err = parse_mesh_header(&buf).expect_err("too-large vertex count must reject");
548        assert!(
549            matches!(err, MeshSectionError::VertexCountTooLarge { .. }),
550            "{err}"
551        );
552    }
553
554    #[test]
555    fn determinism_two_encodes_byte_identical() {
556        let mesh = cube();
557        let need = encoded_len(mesh.positions.len(), mesh.triangles.len());
558        let mut a = vec![0u8; need];
559        let mut b = vec![0u8; need];
560        encode_mesh_section(&mesh, &mut a).expect("encode a");
561        encode_mesh_section(&mesh, &mut b).expect("encode b");
562        assert_eq!(a, b, "two encodes of the same mesh must be byte-identical");
563    }
564
565    #[test]
566    fn mesh_section_round_trips_through_10d_container_with_crc() {
567        let mesh = cube();
568        let mesh_need = encoded_len(mesh.positions.len(), mesh.triangles.len());
569        let mut mesh_payload = vec![0u8; mesh_need];
570        encode_mesh_section(&mesh, &mut mesh_payload).expect("mesh encode");
571
572        let h = Container10dHeader::proposed();
573        let inputs = [SectionInput {
574            section_type: SectionType::QuantizedMesh,
575            alignment_tier: AlignmentTier::Word,
576            stride: 0,
577            element_count: 0,
578            payload: &mesh_payload,
579        }];
580        let mut out = vec![0u8; 512];
581        let n = encode_container(&h, &inputs, &mut out).expect("container encode");
582        seal_whole_file_crc32c(&mut out[..n]);
583        verify_whole_file_crc32c(&mut out[..n]).expect("whole-file CRC");
584
585        let parsed_h = Container10dHeader::parse(&out[..n]).expect("header parse");
586        let descs = parse_section_table(&out[..n], &parsed_h).expect("table parse");
587        assert_eq!(descs.len(), 1);
588        assert_eq!(descs[0].section_type, SectionType::QuantizedMesh as u8);
589
590        let p_off = descs[0].byte_offset as usize;
591        let p_len = descs[0].byte_length as usize;
592        let mesh_back = decode_mesh_section(&out[p_off..p_off + p_len]).expect("mesh decode");
593        assert_eq!(
594            mesh_back.triangles, mesh.triangles,
595            "indices exact through container"
596        );
597        // Positions within quantization tolerance.
598        let tol = 1.0f32 / 65535.0 * 2.0;
599        for (a, b) in mesh.positions.iter().zip(&mesh_back.positions) {
600            for k in 0..3 {
601                assert!((a[k] - b[k]).abs() <= tol, "axis {k}: {} vs {}", a[k], b[k]);
602            }
603        }
604    }
605
606    #[test]
607    fn flipped_payload_bit_in_mesh_section_is_caught_by_per_section_crc() {
608        let mesh = cube();
609        let mesh_need = encoded_len(mesh.positions.len(), mesh.triangles.len());
610        let mut mesh_payload = vec![0u8; mesh_need];
611        encode_mesh_section(&mesh, &mut mesh_payload).expect("mesh encode");
612
613        let h = Container10dHeader::proposed();
614        let inputs = [SectionInput {
615            section_type: SectionType::QuantizedMesh,
616            alignment_tier: AlignmentTier::Word,
617            stride: 0,
618            element_count: 0,
619            payload: &mesh_payload,
620        }];
621        let mut out = vec![0u8; 512];
622        let n = encode_container(&h, &inputs, &mut out).expect("encode");
623        let parsed_h = Container10dHeader::parse(&out[..n]).expect("header parse");
624        let descs = parse_section_table(&out[..n], &parsed_h).expect("clean table parses");
625        let p_off = descs[0].byte_offset as usize;
626        // Flip a bit in the mesh payload (past the mini-header, in vertex data).
627        out[p_off + MESH_MINI_HEADER_SIZE + 1] ^= 0x01;
628        let err =
629            parse_section_table(&out[..n], &parsed_h).expect_err("flipped bit must be caught");
630        assert!(
631            matches!(
632                err,
633                crate::container_10d::section::SectionTableError::CrcMismatch { .. }
634            ),
635            "{err}"
636        );
637    }
638
639    #[test]
640    fn empty_mesh_round_trips() {
641        let mesh = Mesh {
642            positions: vec![],
643            triangles: vec![],
644            min: [0.0; 3],
645            max: [0.0; 3],
646        };
647        let need = encoded_len(0, 0);
648        assert_eq!(need, MESH_MINI_HEADER_SIZE);
649        let mut buf = vec![0u8; need];
650        encode_mesh_section(&mesh, &mut buf).expect("empty encode");
651        let back = decode_mesh_section(&buf).expect("empty decode");
652        assert!(back.positions.is_empty());
653        assert!(back.triangles.is_empty());
654    }
655}