1use bytemuck::{bytes_of, from_bytes, Pod, Zeroable};
34
35use crate::render::assets::Mesh;
36
37pub const MESH_MINI_HEADER_SIZE: usize = 40;
39
40pub const FLAG_U16_INDICES: u16 = 0x0001;
42
43pub const MAX_VERTEX_COUNT: usize = 4_194_304; pub const MAX_TRIANGLE_COUNT: usize = 4_194_304; #[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 #[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#[derive(Debug, Clone, PartialEq, Eq)]
90pub enum MeshSectionError {
91 PayloadTooShort { got: usize, need: usize },
93 NonZeroReserved { field: &'static str },
95 VertexCountTooLarge { got: u32, max: usize },
97 TriangleCountTooLarge { got: u32, max: usize },
99 PayloadTruncated { expected: usize, got: usize },
101 OutputBufferTooSmall { needed: usize, have: usize },
103 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#[inline]
142pub fn fits_u16_indices(vertex_count: usize) -> bool {
143 vertex_count <= u16::MAX as usize + 1 }
145
146#[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#[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
197pub 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 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
250pub 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
312pub 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 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 let extent = 1.0f32; 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); 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; 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 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; 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 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 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}