Skip to main content

draco_core/
mesh_decoder.rs

1use crate::compression_config::EncodedGeometryType;
2use crate::decoder_buffer::DecoderBuffer;
3use crate::draco_types::DataType;
4use crate::geometry_attribute::{GeometryAttributeType, PointAttribute};
5use crate::mesh::Mesh;
6use crate::point_cloud_decoder::PointCloudDecoder;
7use crate::prediction_scheme::EntryToPointIdMap;
8use crate::sequential_generic_attribute_decoder::SequentialGenericAttributeDecoder;
9use crate::sequential_integer_attribute_decoder::{
10    PortableExtent, SequentialIntegerAttributeDecoder,
11};
12use crate::sequential_normal_attribute_decoder::SequentialNormalAttributeDecoder;
13use crate::sequential_quantization_attribute_decoder::SequentialQuantizationAttributeDecoder;
14use crate::status::{DracoError, Status};
15
16use crate::attribute_octahedron_transform::AttributeOctahedronTransform;
17use crate::attribute_quantization_transform::AttributeQuantizationTransform;
18use crate::attribute_transform::AttributeTransform;
19use crate::corner_table::CornerTable;
20use crate::geometry_indices::AttributeValueIndex;
21use crate::geometry_indices::{
22    CornerIndex, FaceIndex, PointIndex, VertexIndex, INVALID_CORNER_INDEX, INVALID_VERTEX_INDEX,
23};
24
25use crate::mesh_edgebreaker_decoder::MeshEdgebreakerDecoder;
26use crate::metadata::{GeometryMetadata, METADATA_FLAG_MASK};
27use crate::test_event_log;
28use crate::version::version_at_least;
29
30/// Output of an edgebreaker attribute traversal:
31/// `(point ids in traversal order, processed corners, vertex -> data-id map)`.
32type AttributeTraversalArrays = (Vec<PointIndex>, Vec<u32>, Vec<i32>);
33
34/// The traversal observers' test-log arm, out of line.
35///
36/// [`test_event_log::record_event`] takes an owned `String`, so the two
37/// `format!` calls are built at the call site: argument structs, a formatter
38/// call and an allocation, inline in a closure whose live body is three stores.
39/// An inliner prices the whole body, and both observers were being emitted as
40/// real calls -- `17` instructions of prologue and epilogue on a closure taking
41/// six or seven arguments, once per vertex visited. The log is installed only by
42/// the tests that read it, so this is the cold arm; outlined and marked cold, it
43/// leaves an observer the walk can inline.
44#[cold]
45#[inline(never)]
46fn record_vertex_visit_events(corner: CornerIndex, vertex: VertexIndex, point_id: PointIndex) {
47    test_event_log::record_event(format!("MAP:{}->v{}", corner.0, vertex.0));
48    test_event_log::record_event(format!("MAP_POINT:{}->p{}", corner.0, point_id.0));
49}
50
51fn validate_num_attributes_in_decoder(
52    num_attributes_in_decoder: usize,
53    remaining_bytes: usize,
54) -> Result<(), DracoError> {
55    // Each attribute must have at least type, data type, component count,
56    // normalized flag, unique id, and a decoder type byte. Reject impossible
57    // counts before reserving vectors from untrusted input.
58    const MIN_ATTRIBUTE_BYTES: usize = 6;
59    if num_attributes_in_decoder == 0
60        || num_attributes_in_decoder > remaining_bytes / MIN_ATTRIBUTE_BYTES
61    {
62        return Err(DracoError::general(
63            "Invalid number of attributes".to_string(),
64        ));
65    }
66    Ok(())
67}
68
69fn validate_num_components(num_components: u8) -> Result<(), DracoError> {
70    if num_components == 0 {
71        return Err(DracoError::general(
72            "Invalid attribute component count".to_string(),
73        ));
74    }
75    Ok(())
76}
77
78fn copy_point_mapping(
79    source: &PointAttribute,
80    target: &mut PointAttribute,
81    num_points: usize,
82) -> Result<(), DracoError> {
83    // One slice copy when the source's map is already the exact shape asked
84    // for; the per-point loop remains for identity sources and length
85    // mismatches, where mapped_index supplies the per-point answer.
86    if let Some(map) = source.explicit_mapping() {
87        if map.len() == num_points {
88            target.set_explicit_mapping_from(map);
89            return Ok(());
90        }
91    }
92    target.set_explicit_mapping(num_points);
93    for point in 0..num_points {
94        let point_id = PointIndex(point as u32);
95        target.try_set_point_map_entry(point_id, source.mapped_index(point_id))?;
96    }
97    Ok(())
98}
99
100fn build_vertex_to_data_map_from_corner_map(
101    corner_table: &CornerTable,
102    data_to_corner_map: &[u32],
103) -> Result<Vec<i32>, DracoError> {
104    let mut vertex_to_data_map = vec![-1i32; corner_table.num_vertices()];
105    for (i, &corner_id) in data_to_corner_map.iter().enumerate() {
106        let corner = CornerIndex(corner_id);
107        if corner == INVALID_CORNER_INDEX {
108            continue;
109        }
110        if corner.0 as usize >= corner_table.num_corners() {
111            return Err(DracoError::general(
112                "Data-to-corner map references an invalid corner".to_string(),
113            ));
114        }
115        let vertex = corner_table.vertex(corner);
116        if vertex == INVALID_VERTEX_INDEX {
117            continue;
118        }
119        let Some(slot) = vertex_to_data_map.get_mut(vertex.0 as usize) else {
120            return Err(DracoError::general(
121                "Data-to-corner map references an invalid vertex".to_string(),
122            ));
123        };
124        *slot = i as i32;
125    }
126    Ok(vertex_to_data_map)
127}
128
129fn upsert_portable_attribute(
130    portable_attributes_by_id: &mut Vec<(i32, PointAttribute)>,
131    att_id: i32,
132    portable: PointAttribute,
133) {
134    if let Some((_, existing)) = portable_attributes_by_id
135        .iter_mut()
136        .find(|(id, _)| *id == att_id)
137    {
138        *existing = portable;
139    } else {
140        portable_attributes_by_id.push((att_id, portable));
141    }
142}
143
144/// Decoder for Draco triangle mesh bitstreams.
145///
146/// `MeshDecoder` reads a `.drc` bitstream produced by `MeshEncoder` (or C++
147/// Draco) and reconstructs a [`Mesh`]: faces, attributes, and any
148/// metadata. It handles both EdgeBreaker and sequential connectivity and
149/// dequantizes attributes back to their original data types.
150///
151/// A point-cloud bitstream (geometry type 0) is also accepted and decoded into
152/// the mesh's underlying [`PointCloud`](crate::PointCloud) with no faces.
153///
154/// # Examples
155///
156/// ```
157/// use draco_core::{DecoderBuffer, Mesh, MeshDecoder};
158///
159/// # fn decode(drc_bytes: &[u8]) -> Result<(), draco_core::DracoError> {
160/// let mut mesh = Mesh::new();
161/// MeshDecoder::new().decode(&mut DecoderBuffer::new(drc_bytes), &mut mesh)?;
162/// println!("{} faces, {} points", mesh.num_faces(), mesh.num_points());
163/// # Ok(())
164/// # }
165/// ```
166///
167/// A full encode/decode round trip is shown on the `MeshEncoder` type docs.
168pub struct MeshDecoder {
169    geometry_type: EncodedGeometryType,
170    method: u8,
171    flags: u16,
172    version_major: u8,
173    version_minor: u8,
174    corner_table: Option<Box<CornerTable>>,
175    edgebreaker_data_to_corner_map: Option<Vec<u32>>,
176    edgebreaker_attribute_seam_corners: Vec<Vec<u32>>,
177    edgebreaker_attribute_corner_tables: Vec<CornerTable>,
178    edgebreaker_attribute_vertices_on_seam: Vec<Vec<bool>>,
179    edgebreaker_processed_connectivity_corners: Vec<u32>,
180    edgebreaker_vertex_to_corner_map: Vec<u32>,
181    edgebreaker_is_vert_hole: Vec<bool>,
182    traversal_method: u8,
183}
184
185impl Default for MeshDecoder {
186    fn default() -> Self {
187        Self::new()
188    }
189}
190
191impl MeshDecoder {
192    /// Creates a mesh decoder with default state.
193    pub fn new() -> Self {
194        Self {
195            geometry_type: EncodedGeometryType::TriangularMesh,
196            method: 0,
197            flags: 0,
198            version_major: 0,
199            version_minor: 0,
200            corner_table: None,
201            edgebreaker_data_to_corner_map: None,
202            edgebreaker_attribute_seam_corners: Vec::new(),
203            edgebreaker_attribute_corner_tables: Vec::new(),
204            edgebreaker_attribute_vertices_on_seam: Vec::new(),
205            edgebreaker_processed_connectivity_corners: Vec::new(),
206            edgebreaker_vertex_to_corner_map: Vec::new(),
207            edgebreaker_is_vert_hole: Vec::new(),
208            traversal_method: 0,
209        }
210    }
211
212    /// Decodes a Draco mesh from `in_buffer` into `out_mesh`.
213    ///
214    /// Reads the header, optional metadata, connectivity, and attributes,
215    /// replacing whatever `out_mesh` held. Point-cloud bitstreams are decoded
216    /// into the mesh's underlying point cloud (no faces).
217    ///
218    /// `out_mesh` need not be empty, and one mesh can serve a whole sequence of
219    /// decodes: it is cleared here, which keeps the face and attribute lists'
220    /// capacity. On error it is left cleared rather than half-written.
221    ///
222    /// # Errors
223    ///
224    /// Returns an error if the magic/header is invalid, the bitstream version
225    /// is unsupported, the geometry is malformed, or a required feature (such
226    /// as `point_cloud_decode`) is disabled.
227    pub fn decode(&mut self, in_buffer: &mut DecoderBuffer, out_mesh: &mut Mesh) -> Status {
228        // Every stage below adds to the mesh -- attributes are pushed, faces
229        // are written from index zero -- so a mesh that arrives holding a
230        // previous decode would come out carrying both.
231        out_mesh.clear();
232
233        // 1. Decode Header
234        self.decode_header(in_buffer)?;
235
236        // 2. Decode Metadata
237        if version_at_least(
238            self.version_major,
239            self.version_minor,
240            crate::version::VERSION_FLAGS_INTRODUCED,
241        ) && (self.flags & METADATA_FLAG_MASK) != 0
242        {
243            self.decode_metadata(in_buffer, out_mesh)?;
244        }
245
246        if self.geometry_type == EncodedGeometryType::PointCloud {
247            #[cfg(feature = "point_cloud_decode")]
248            {
249                // Point cloud files (geometry_type == 0) have no connectivity.
250                // Delegate to PointCloudDecoder which reads num_points + attributes
251                // directly into the Mesh's underlying PointCloud.
252                let mut pc_decoder = crate::point_cloud_decoder::PointCloudDecoder::new();
253                return pc_decoder.decode_after_header(
254                    self.version_major,
255                    self.version_minor,
256                    self.method,
257                    in_buffer,
258                    &mut *out_mesh,
259                );
260            }
261            #[cfg(not(feature = "point_cloud_decode"))]
262            {
263                return Err(DracoError::general(
264                    "Point cloud decode support is disabled".to_string(),
265                ));
266            }
267        }
268
269        // 3. Decode Connectivity
270        {
271            let _phase = crate::decode_phase_probe::PhaseTimer::start(
272                crate::decode_phase_probe::Phase::Connectivity,
273            );
274            self.decode_connectivity(in_buffer, out_mesh)?;
275        }
276
277        // 4. Decode Attributes
278        let _phase = crate::decode_phase_probe::PhaseTimer::start(
279            crate::decode_phase_probe::Phase::Attributes,
280        );
281        self.decode_attributes(in_buffer, out_mesh)
282    }
283
284    /// Test helper: Returns a reference to the decoded corner table (if any).
285    /// This is useful in unit tests that wish to compare encoder/decoder
286    /// corner table structures without accessing internal decoder types.
287    pub fn get_corner_table_ref(&self) -> Option<&crate::corner_table::CornerTable> {
288        self.corner_table.as_deref()
289    }
290
291    fn decode_metadata(
292        &self,
293        in_buffer: &mut DecoderBuffer,
294        out_mesh: &mut Mesh,
295    ) -> Result<(), DracoError> {
296        let metadata = GeometryMetadata::decode(in_buffer)
297            .map_err(|_| DracoError::general("Failed to decode metadata".to_string()))?;
298        out_mesh.set_metadata(Some(metadata));
299        Ok(())
300    }
301
302    fn decode_header(&mut self, buffer: &mut DecoderBuffer) -> Status {
303        let mut magic = [0u8; 5];
304        buffer.decode_bytes(&mut magic)?;
305        if &magic != b"DRACO" {
306            return Err(DracoError::general("Invalid magic".to_string()));
307        }
308
309        self.version_major = buffer.decode_u8()?;
310        self.version_minor = buffer.decode_u8()?;
311        buffer.set_version(self.version_major, self.version_minor);
312
313        let g_type = buffer.decode_u8()?;
314        self.geometry_type = match g_type {
315            0 => EncodedGeometryType::PointCloud,
316            1 => EncodedGeometryType::TriangularMesh,
317            _ => return Err(DracoError::general("Invalid geometry type".to_string())),
318        };
319
320        self.method = buffer.decode_u8()?;
321
322        // Flags field is always present in the binary header (C++ reads unconditionally).
323        // The VERSION_FLAGS_INTRODUCED constant refers to when flag bits gained meaning,
324        // not when the bytes were added to the format.
325        self.flags = buffer
326            .decode_u16()
327            .map_err(|_| DracoError::general("Failed to decode flags".to_string()))?;
328
329        Ok(())
330    }
331
332    fn decode_connectivity(&mut self, buffer: &mut DecoderBuffer, mesh: &mut Mesh) -> Status {
333        if self.method == 1 {
334            let mut eb_decoder = MeshEdgebreakerDecoder::new();
335            eb_decoder.decode_connectivity(buffer, mesh)?;
336
337            // Preserve edgebreaker-derived maps for attribute decoding.
338            self.edgebreaker_data_to_corner_map = eb_decoder.take_data_to_corner_map();
339            self.edgebreaker_attribute_seam_corners = eb_decoder.take_attribute_seam_corners();
340            self.edgebreaker_processed_connectivity_corners =
341                eb_decoder.take_processed_connectivity_corners();
342            self.edgebreaker_vertex_to_corner_map = eb_decoder.take_vertex_to_corner_map();
343            self.edgebreaker_is_vert_hole = eb_decoder.take_is_vert_hole();
344            self.traversal_method = eb_decoder.get_traversal_decoder_type();
345
346            // Use the edgebreaker decoder's corner table with proper opposite mappings
347            // instead of building a new one from mesh faces
348            if let Some(ct) = eb_decoder.take_corner_table() {
349                self.corner_table = Some(Box::new(ct));
350            } else {
351                return Err(DracoError::general(
352                    "Edgebreaker decoder did not provide corner table".to_string(),
353                ));
354            }
355            self.rebuild_edgebreaker_attribute_corner_tables()?;
356            self.assign_edgebreaker_points_to_corners(mesh)?;
357        } else {
358            // Sequential connectivity encoding
359            // C++ MeshSequentialDecoder uses raw u32 for v < 2.2, varint for v >= 2.2
360            let seq_uses_varint = version_at_least(self.version_major, self.version_minor, (2, 2));
361            let (num_faces, num_points) = if !seq_uses_varint {
362                #[cfg(not(feature = "legacy_bitstream_decode"))]
363                {
364                    return Err(DracoError::bitstream_version_unsupported());
365                }
366                #[cfg(feature = "legacy_bitstream_decode")]
367                {
368                    let nf = buffer.decode_u32()? as usize;
369                    let np = buffer.decode_u32()? as usize;
370                    (nf, np)
371                }
372            } else {
373                let nf = buffer.decode_varint()? as usize;
374                let np = buffer.decode_varint()? as usize;
375                (nf, np)
376            };
377            // No bit-budget guard on these counts. Upstream bounds the face
378            // count by `remaining_size() / 3`, but that premise fails for the
379            // compressed-connectivity branch for the same reason the one-bit
380            // premise failed here: the indices are entropy-coded. Adopting it
381            // would import an upstream bug rather than parity. The allocation
382            // budget below bounds the work instead, and it errs on the side of
383            // reading files upstream refuses rather than the reverse.
384            let num_indices = validate_mesh_index_count(num_faces)?;
385            mesh.set_num_points(num_points);
386
387            if num_faces > 0 && num_points > 0 {
388                let connectivity_method = buffer.decode_u8()?;
389                if connectivity_method == 0 {
390                    // Compressed. The symbol count is bounded before the buffer
391                    // it fills is sized: the ratio alone lets a 26 KB stream
392                    // declare 1,095,910,464 faces and reserve 13 GB for them,
393                    // because a ratio scales with the input an attacker
394                    // supplies. See `decode_budget::ensure_symbols_are_backed`.
395                    crate::decode_budget::ensure_symbols_are_backed(num_indices, buffer.size())?;
396                    // Empty on purpose: `decode_symbols` grows it as symbols
397                    // arrive, so a count the stream cannot deliver costs one
398                    // small reservation instead of the whole array.
399                    let mut encoded_indices = Vec::new();
400                    let options = crate::symbol_encoding::SymbolEncodingOptions::default();
401                    crate::symbol_encoding::decode_symbols(
402                        num_indices,
403                        1,
404                        &options,
405                        buffer,
406                        &mut encoded_indices,
407                    )
408                    .map_err(|err| {
409                        DracoError::general(format!(
410                            "Failed to decode compressed sequential connectivity: {err}"
411                        ))
412                    })?;
413                    // Sized from what the decode produced rather than from what
414                    // the header claimed: on success the two are equal, and on
415                    // failure this line is not reached.
416                    let mut indices = make_zeroed_indices(encoded_indices.len())?;
417                    let mut last_index_value = 0i32;
418                    for (dst, encoded_val) in indices.iter_mut().zip(encoded_indices) {
419                        let mut index_diff = (encoded_val >> 1) as i32;
420                        if (encoded_val & 1) != 0 {
421                            if index_diff > last_index_value {
422                                return Err(DracoError::general(
423                                    "Sequential connectivity index underflow".to_string(),
424                                ));
425                            }
426                            index_diff = -index_diff;
427                        } else if index_diff > i32::MAX - last_index_value {
428                            return Err(DracoError::general(
429                                "Sequential connectivity index overflow".to_string(),
430                            ));
431                        }
432                        let index_value = last_index_value + index_diff;
433                        *dst = index_value as u32;
434                        last_index_value = index_value;
435                    }
436                    set_num_faces_within_limits(mesh, buffer, num_faces)?;
437                    mesh.set_faces_from_flat_indices(&indices);
438                } else if connectivity_method == 1 {
439                    // Raw - bulk read indices from buffer
440                    if num_points < 256 {
441                        let bytes_needed = num_indices;
442                        let bytes = buffer.decode_slice(bytes_needed).map_err(|_| {
443                            DracoError::general("Not enough data for u8 indices".to_string())
444                        })?;
445                        set_num_faces_within_limits(mesh, buffer, num_faces)?;
446                        mesh.set_faces_from_u8_indices(bytes);
447                    } else if num_points < 65536 {
448                        let bytes_needed = num_indices.checked_mul(2).ok_or_else(|| {
449                            DracoError::general("Mesh u16 index byte count overflow".to_string())
450                        })?;
451                        let bytes = buffer.decode_slice(bytes_needed).map_err(|_| {
452                            DracoError::general("Not enough data for u16 indices".to_string())
453                        })?;
454                        set_num_faces_within_limits(mesh, buffer, num_faces)?;
455                        mesh.set_faces_from_le_u16_indices(bytes);
456                    } else if num_points < (1 << 21) && seq_uses_varint {
457                        // Three varints a face, and a varint is at least one
458                        // byte, so a face count past the bytes that remain is
459                        // one this stream cannot deliver. The three branches
460                        // around this one are bounded by the exact byte count
461                        // they read before sizing anything; this one reads
462                        // variable-length indices, so the per-face floor is
463                        // what it can check instead. Without it the count went
464                        // straight into the face array: a 22-byte stream sized
465                        // it for twenty trillion faces and asked for 240 TB,
466                        // which the campaign's AddressSanitizer refused before
467                        // the fallible reservation could report anything.
468                        if num_indices > buffer.remaining_size() {
469                            return Err(DracoError::general(format!(
470                                "Sequential connectivity declares {num_faces} faces, more than the {} bytes left can encode",
471                                buffer.remaining_size()
472                            )));
473                        }
474                        set_num_faces_within_limits(mesh, buffer, num_faces)?;
475                        for face_id in 0..num_faces {
476                            mesh.set_face_from_indices(
477                                face_id,
478                                [
479                                    buffer.decode_varint()? as u32,
480                                    buffer.decode_varint()? as u32,
481                                    buffer.decode_varint()? as u32,
482                                ],
483                            );
484                        }
485                    } else {
486                        let bytes_needed = num_indices.checked_mul(4).ok_or_else(|| {
487                            DracoError::general("Mesh u32 index byte count overflow".to_string())
488                        })?;
489                        let bytes = buffer.decode_slice(bytes_needed).map_err(|_| {
490                            DracoError::general("Not enough data for u32 indices".to_string())
491                        })?;
492                        set_num_faces_within_limits(mesh, buffer, num_faces)?;
493                        mesh.set_faces_from_le_u32_indices(bytes);
494                    }
495                } else {
496                    return Err(DracoError::general(format!(
497                        "Unsupported sequential connectivity method: {}",
498                        connectivity_method
499                    )));
500                }
501                // If sequential mode uses compressed connectivity, we may need
502                // to remap indices for deduplication. For raw mode above,
503                // face indices match the flat array.
504
505                // Note: Sequential encoding does NOT use a CornerTable.
506                // C++ MeshSequentialDecoder::DecodeConnectivity() just calls mesh->AddFace()
507                // and uses LinearSequencer for attribute decoding (identity mapping).
508                // Corner tables are only needed for Edgebreaker's mesh prediction schemes.
509                // self.corner_table remains None for sequential decoding.
510            }
511        }
512
513        Ok(())
514    }
515
516    fn make_attribute_corner_table(
517        base_ct: &CornerTable,
518        seam_corners: &[u32],
519    ) -> Result<(CornerTable, Vec<bool>), DracoError> {
520        // Seam corners come off the wire, so every index gets checked before
521        // it addresses anything; the encoder's own seam detection never needs
522        // this because it can only ever name a corner its own corner table has.
523        let mut is_edge_on_seam = vec![false; base_ct.num_corners()];
524        for &c_u32 in seam_corners {
525            let c = CornerIndex(c_u32);
526            if c == INVALID_CORNER_INDEX {
527                continue;
528            }
529            if c.0 as usize >= base_ct.num_corners() {
530                return Err(DracoError::general(
531                    "Invalid Edgebreaker attribute seam corner".to_string(),
532                ));
533            }
534            is_edge_on_seam[c.0 as usize] = true;
535
536            let opp = base_ct.opposite(c);
537            if opp != INVALID_CORNER_INDEX {
538                if opp.0 as usize >= base_ct.num_corners() {
539                    return Err(DracoError::general(
540                        "Invalid Edgebreaker attribute seam opposite corner".to_string(),
541                    ));
542                }
543                is_edge_on_seam[opp.0 as usize] = true;
544            }
545        }
546
547        crate::mesh_attribute_corner_table::cut_seam_edges_and_recompute_vertices(
548            base_ct,
549            &is_edge_on_seam,
550        )
551    }
552
553    fn rebuild_edgebreaker_attribute_corner_tables(&mut self) -> Status {
554        self.edgebreaker_attribute_corner_tables.clear();
555        self.edgebreaker_attribute_vertices_on_seam.clear();
556        let Some(base_ct) = self.corner_table.as_deref() else {
557            return Ok(());
558        };
559        for seam_corners in &self.edgebreaker_attribute_seam_corners {
560            let (corner_table, vertices_on_seam) =
561                Self::make_attribute_corner_table(base_ct, seam_corners)?;
562            self.edgebreaker_attribute_corner_tables.push(corner_table);
563            self.edgebreaker_attribute_vertices_on_seam
564                .push(vertices_on_seam);
565        }
566        Ok(())
567    }
568
569    fn assign_edgebreaker_points_to_corners(&self, mesh: &mut Mesh) -> Status {
570        if self.edgebreaker_attribute_corner_tables.is_empty() {
571            return Ok(());
572        }
573        let Some(base_ct) = self.corner_table.as_deref() else {
574            return Ok(());
575        };
576
577        let num_corners = base_ct.num_corners();
578        let mut point_to_corner_map: Vec<u32> = Vec::new();
579        let mut corner_to_point_map = vec![u32::MAX; num_corners];
580
581        // The vertex each attribute table gave for the previous corner of the
582        // fan. Walking a fan asks every attribute table for two vertices per
583        // step -- the corner's and its predecessor's -- and the predecessor's
584        // was answered one step earlier by this same walk. One slot per
585        // attribute table, allocated here rather than per vertex: the loop
586        // below runs once per vertex, two hundred thousand times on a mesh the
587        // size of the Bunny.
588        let mut prev_attr_vertex =
589            vec![INVALID_VERTEX_INDEX; self.edgebreaker_attribute_corner_tables.len()];
590
591        for v in 0..base_ct.num_vertices() {
592            let mut c = base_ct.left_most_corner(VertexIndex(v as u32));
593            if c == INVALID_CORNER_INDEX {
594                continue;
595            }
596
597            let mut first_corner = c;
598            let is_vert_hole = self
599                .edgebreaker_is_vert_hole
600                .get(v)
601                .copied()
602                .unwrap_or_else(|| base_ct.is_vertex_on_boundary(VertexIndex(v as u32)));
603            if !is_vert_hole {
604                for (attr_index, attr_ct) in
605                    self.edgebreaker_attribute_corner_tables.iter().enumerate()
606                {
607                    let base_vertex = base_ct.vertex(c);
608                    let Some(vertices_on_seam) =
609                        self.edgebreaker_attribute_vertices_on_seam.get(attr_index)
610                    else {
611                        continue;
612                    };
613                    if base_vertex == crate::geometry_indices::INVALID_VERTEX_INDEX
614                        || !vertices_on_seam
615                            .get(base_vertex.0 as usize)
616                            .copied()
617                            .unwrap_or(false)
618                    {
619                        continue;
620                    }
621                    let vertex_at_first = attr_ct.vertex(c);
622                    let mut act_c = base_ct.swing_right(c);
623                    let mut seam_found = false;
624                    let mut swing_steps = 0usize;
625                    let max_swing_steps = base_ct.num_corners().saturating_add(1);
626                    while act_c != INVALID_CORNER_INDEX && act_c != c {
627                        swing_steps += 1;
628                        if swing_steps > max_swing_steps {
629                            return Err(DracoError::general(
630                                "Edgebreaker seam search traversal did not terminate".to_string(),
631                            ));
632                        }
633                        if attr_ct.vertex(act_c) != vertex_at_first {
634                            first_corner = act_c;
635                            seam_found = true;
636                            break;
637                        }
638                        act_c = base_ct.swing_right(act_c);
639                    }
640                    if seam_found {
641                        break;
642                    }
643                }
644            }
645
646            c = first_corner;
647            corner_to_point_map[c.0 as usize] = point_to_corner_map.len() as u32;
648            point_to_corner_map.push(c.0);
649
650            for (attr_ct, slot) in self
651                .edgebreaker_attribute_corner_tables
652                .iter()
653                .zip(prev_attr_vertex.iter_mut())
654            {
655                *slot = attr_ct.vertex(c);
656            }
657
658            let mut prev_c = c;
659            c = base_ct.swing_right(c);
660            let mut swing_steps = 0usize;
661            let max_swing_steps = base_ct.num_corners().saturating_add(1);
662            while c != INVALID_CORNER_INDEX && c != first_corner {
663                swing_steps += 1;
664                if swing_steps > max_swing_steps {
665                    return Err(DracoError::general(
666                        "Edgebreaker point assignment traversal did not terminate".to_string(),
667                    ));
668                }
669                // Every slot is written on every step, so `fold` rather than
670                // `any`: short-circuiting would leave the later tables' slots
671                // holding a vertex from some earlier corner, and the answer
672                // they gave for this step would go missing from the next one.
673                let attribute_seam = self
674                    .edgebreaker_attribute_corner_tables
675                    .iter()
676                    .zip(prev_attr_vertex.iter_mut())
677                    .fold(false, |seam, (attr_ct, slot)| {
678                        let vertex = attr_ct.vertex(c);
679                        let differs = vertex != *slot;
680                        *slot = vertex;
681                        seam | differs
682                    });
683                if attribute_seam {
684                    corner_to_point_map[c.0 as usize] = point_to_corner_map.len() as u32;
685                    point_to_corner_map.push(c.0);
686                } else {
687                    corner_to_point_map[c.0 as usize] = corner_to_point_map[prev_c.0 as usize];
688                }
689                prev_c = c;
690                c = base_ct.swing_right(c);
691            }
692        }
693
694        for face_id in 0..mesh.num_faces() {
695            let base = face_id * 3;
696            let p0 = corner_to_point_map[base];
697            let p1 = corner_to_point_map[base + 1];
698            let p2 = corner_to_point_map[base + 2];
699            if p0 == u32::MAX || p1 == u32::MAX || p2 == u32::MAX {
700                return Err(DracoError::general(
701                    "Failed to assign Edgebreaker corner point".to_string(),
702                ));
703            }
704            mesh.set_face(
705                FaceIndex(face_id as u32),
706                [PointIndex(p0), PointIndex(p1), PointIndex(p2)],
707            );
708        }
709        mesh.set_num_points(point_to_corner_map.len());
710
711        Ok(())
712    }
713
714    fn decode_attributes(&mut self, buffer: &mut DecoderBuffer, mesh: &mut Mesh) -> Status {
715        // Both MeshSequentialEncoding and MeshEdgebreakerEncoding use a u8 for the number of
716        // attribute decoders.
717        let num_attributes_decoders = buffer.decode_u8()? as usize;
718        let num_points = mesh.num_points();
719
720        // For Edgebreaker, traversal sequencing is controlled per attribute decoder.
721        // We'll derive the correct (point_ids, data_to_corner_map) later for each decoder payload
722        // based on its traversal_method.
723        // Sequential encoding uses the identity mapping. Keep it symbolic: a
724        // point count comes from the bitstream, and materializing [0, 1, ...]
725        // here would spend four bytes per claimed point before the attributes
726        // have provided any data. EdgeBreaker still builds real traversal
727        // arrays below, because those arrays describe a non-identity order.
728        let point_ids = if self.method == 0 {
729            EntryToPointIdMap::identity(num_points)
730        } else {
731            EntryToPointIdMap::identity(0)
732        };
733        let data_to_corner_map: Option<Vec<u32>> = None;
734
735        // This decoder never read a header of its own -- the mesh path parsed
736        // one -- so it is told the version, which the attribute decoders read
737        // when they bind a prediction parent.
738        let mut pc_decoder = PointCloudDecoder::new();
739        pc_decoder.set_bitstream_version(self.version_major, self.version_minor);
740        let bitstream_version: u16 =
741            crate::version::bitstream_version(self.version_major, self.version_minor);
742
743        struct PendingQuant {
744            att_id: i32,
745            portable: PointAttribute,
746            transform: AttributeQuantizationTransform,
747        }
748
749        struct PendingNormal {
750            att_id: i32,
751            portable: PointAttribute,
752            quantization_bits: u8,
753        }
754
755        // (1) Attribute decoder identifiers.
756        // For Edgebreaker this ties each decoder payload to attribute connectivity data.
757        let mut att_data_id_by_decoder: Vec<u8> = vec![0; num_attributes_decoders];
758        let mut encoder_type_by_decoder: Vec<u8> = vec![0; num_attributes_decoders];
759        let mut traversal_method_by_decoder: Vec<u8> = vec![0; num_attributes_decoders];
760        if self.method == 1 {
761            for i in 0..num_attributes_decoders {
762                att_data_id_by_decoder[i] = buffer.decode_u8()?;
763                encoder_type_by_decoder[i] = buffer.decode_u8()?;
764                // traversal_method was added in v1.2. For older streams, default to
765                // DEPTH_FIRST (0).
766                if bitstream_version >= 0x0102 {
767                    traversal_method_by_decoder[i] = buffer.decode_u8()?;
768                } else if !cfg!(feature = "legacy_bitstream_decode") {
769                    return Err(DracoError::bitstream_version_unsupported());
770                }
771            }
772        }
773
774        // (2) Attribute decoder data.
775        let mut att_ids_by_decoder: Vec<Vec<i32>> = Vec::with_capacity(num_attributes_decoders);
776        let mut decoder_types_by_decoder: Vec<Vec<u8>> =
777            Vec::with_capacity(num_attributes_decoders);
778
779        for _ in 0..num_attributes_decoders {
780            let num_attributes_in_decoder: usize = if bitstream_version < 0x0200 {
781                if !cfg!(feature = "legacy_bitstream_decode") {
782                    return Err(DracoError::bitstream_version_unsupported());
783                }
784                buffer.decode_u32()? as usize
785            } else {
786                buffer.decode_varint()? as usize
787            };
788            if num_attributes_in_decoder == 0 {
789                return Err(DracoError::general(
790                    "Invalid number of attributes".to_string(),
791                ));
792            }
793            validate_num_attributes_in_decoder(num_attributes_in_decoder, buffer.remaining_size())?;
794
795            let mut att_ids: Vec<i32> = Vec::with_capacity(num_attributes_in_decoder);
796            let mut decoder_types: Vec<u8> = Vec::with_capacity(num_attributes_in_decoder);
797
798            for _ in 0..num_attributes_in_decoder {
799                let att_type_val = buffer.decode_u8()?;
800                let att_type = GeometryAttributeType::try_from(att_type_val)?;
801
802                let data_type_val = buffer.decode_u8()?;
803                let data_type = DataType::try_from(data_type_val)?;
804
805                let num_components = buffer.decode_u8()?;
806                validate_num_components(num_components)?;
807                let normalized = buffer.decode_u8()? != 0;
808                let unique_id: u32 = if bitstream_version < 0x0103 {
809                    if !cfg!(feature = "legacy_bitstream_decode") {
810                        return Err(DracoError::bitstream_version_unsupported());
811                    }
812                    buffer.decode_u16()? as u32
813                } else {
814                    buffer.decode_varint()? as u32
815                };
816
817                buffer.charge_decoded_bytes(
818                    (num_components as usize)
819                        .saturating_mul(data_type.byte_length())
820                        .saturating_mul(num_points),
821                )?;
822                let mut att = PointAttribute::new();
823                att.init_deferred(att_type, num_components, data_type, normalized, num_points)?;
824                att.set_unique_id(unique_id);
825                let att_id = mesh.add_attribute_preserve_unique_id(att);
826                att_ids.push(att_id);
827
828                // No mapping is written here for EdgeBreaker: upstream leaves
829                // it untouched until the traversal sequencer sets the explicit
830                // point-to-value map (the loop over `point_to_value` below),
831                // which overwrites every entry. An identity fill at this point
832                // is one fallible call per point per attribute that the
833                // sequencer then discards.
834            }
835
836            for _ in 0..num_attributes_in_decoder {
837                decoder_types.push(buffer.decode_u8()?);
838            }
839
840            att_ids_by_decoder.push(att_ids);
841            decoder_types_by_decoder.push(decoder_types);
842        }
843
844        // (3) Attribute decoder payloads.
845        let mut portable_attributes_by_id: Vec<(i32, PointAttribute)> = Vec::new();
846        // Every edgebreaker attribute decoder asks for the same seed-free depth-first
847        // traversal of the same corner table, and that walk is a seventh of a decode on
848        // a 69k-face mesh. The corner table is fixed once connectivity is decoded, so
849        // the walk happens once here and the later decoders copy its answer.
850        let mut dfs_traversal: Option<AttributeTraversalArrays> = None;
851        for dec_i in 0..num_attributes_decoders {
852            let att_ids = &att_ids_by_decoder[dec_i];
853            let decoder_types = &decoder_types_by_decoder[dec_i];
854
855            // For edgebreaker, pick the attribute-specific corner table
856            // (seams) if one applies. Corner indices remain stable because
857            // seam-breaking only removes opposite links, and the table is
858            // read-only from here on -- borrowed, not cloned: a clone is a
859            // whole corner table per decoder (three arrays the size of the
860            // corner and vertex counts) for data nothing writes to.
861            let mut attr_corner_table: Option<&CornerTable> = None;
862            if self.method == 1 {
863                let att_data_id = att_data_id_by_decoder[dec_i] as usize;
864                let uses_attribute_connectivity =
865                    att_data_id_by_decoder[dec_i] != u8::MAX && encoder_type_by_decoder[dec_i] != 0;
866                if uses_attribute_connectivity
867                    && att_data_id < self.edgebreaker_attribute_seam_corners.len()
868                {
869                    if let Some(ct) = self.edgebreaker_attribute_corner_tables.get(att_data_id) {
870                        attr_corner_table = Some(ct);
871                    }
872                }
873            }
874
875            // Determine the corner table used for prediction within this decoder.
876            // For edgebreaker, seams may split vertex fans and change the effective
877            // traversal sequence used by predictors.
878            let mut point_ids_for_decoder: Option<Vec<PointIndex>> = None;
879            let mut data_to_corner_map_for_decoder: Option<Vec<u32>> = None;
880            let mut vertex_to_data_map_for_decoder: Option<Vec<i32>> = None;
881            if self.method == 1 {
882                // If we have an attribute-specific seam corner table, recompute vertex
883                // corners after breaking opposites so we can derive the correct number
884                // of entries for this decoder.
885                if let Some(ct) = attr_corner_table {
886                    // A seam-broken table, not the main table this decode
887                    // already validated -- keep the check.
888                    let (ids, map, v_map) =
889                        Self::generate_point_ids_and_corners_dfs_for_table(mesh, ct, &[], false)?;
890                    point_ids_for_decoder = Some(ids);
891                    data_to_corner_map_for_decoder = Some(map);
892                    vertex_to_data_map_for_decoder = Some(v_map);
893                }
894
895                // Note: For edgebreaker, we intentionally do NOT take a traversal
896                // mapping from `MeshEdgebreakerDecoder::assign_points_to_corners()`.
897                // The C++ decoder derives its attribute traversal from
898                // `MeshTraversalSequencer` (with no corner_order set), i.e. from
899                // deterministic traversal over the reconstructed corner table.
900                // Mixing a connectivity-derived map with a separately generated
901                // vertex_to_data_map can desynchronize prediction decoding.
902            }
903
904            let corner_table_for_decoder: Option<&CornerTable> =
905                attr_corner_table.or(self.corner_table.as_deref());
906
907            // Optional vertex_to_data_map derived from the chosen data_to_corner_map.
908            // (Needed by mesh prediction schemes to map corner-table vertices -> data ids.)
909            // For edgebreaker, derive per-decoder traversal sequencing when seams are not
910            // applied (per-vertex attributes). This sequencing must match the bitstream
911            // traversal_method to keep prediction-scheme side streams (e.g. crease flags)
912            // synchronized.
913            let mut sequenced_point_ids: Option<Vec<PointIndex>> = None;
914            let mut sequenced_data_to_corner_map: Option<Vec<u32>> = None;
915            let mut sequenced_vertex_to_data_map: Option<Vec<i32>> = None;
916
917            // Generate point_ids using traversal method.
918            // For Edgebreaker, the decoder should match the encoder's traversal method.
919            // The per-decoder traversal method is stored in traversal_method_by_decoder.
920            // - traversal_method == 1 (PREDICTION_DEGREE): uses MaxPredictionDegree traversal
921            // - traversal_method == 0 (DEPTH_FIRST): uses DFS traversal
922            // Note: self.traversal_method is the edgebreaker decoder type (0=Standard, 1=Predictive, 2=Valence),
923            // which is different from the per-decoder traversal method.
924            if sequenced_point_ids.is_none() {
925                // Get the per-decoder traversal method (Speed 0 uses PREDICTION_DEGREE=1, others use DEPTH_FIRST=0)
926                let per_decoder_traversal =
927                    if self.method == 1 && dec_i < traversal_method_by_decoder.len() {
928                        traversal_method_by_decoder[dec_i]
929                    } else {
930                        0
931                    };
932                // For sequential encoding (method 0), use identity permutation
933                // because the encoder writes positions in point ID order [0, 1, 2, ...].
934                // For edgebreaker (method 1), use DFS/prediction traversal to match encoder.
935                if self.method == 0 {
936                    // Sequential encoding: C++ uses LinearSequencer which generates
937                    // identity mapping [0, 1, 2, ..., num_points-1] and calls
938                    // SetIdentityMapping() for attributes. No corner table or
939                    // data_to_corner_map is needed.
940                    // Use the mesh-wide symbolic identity sequence instead of
941                    // rebuilding an identical vector for each decoder.
942                    // sequenced_data_to_corner_map remains None - not needed for sequential
943                } else {
944                    // Edgebreaker decoding: traversal method depends on the per-decoder
945                    // traversal method written by the encoder.
946                    // - per_decoder_traversal == 1 (PREDICTION_DEGREE): MaxPredictionDegree traversal (speed 0)
947                    // - per_decoder_traversal == 0 (DEPTH_FIRST): DFS traversal (speed >= 1)
948
949                    if per_decoder_traversal == 1 {
950                        // Speed 0: use MaxPredictionDegree traversal
951                        let (ids, map, v_map) = self
952                            .generate_point_ids_and_corners_max_prediction_degree(
953                                mesh,
954                                &self.edgebreaker_processed_connectivity_corners,
955                            )?;
956                        sequenced_point_ids = Some(ids);
957                        sequenced_data_to_corner_map = Some(map);
958                        sequenced_vertex_to_data_map = Some(v_map); // Use directly from traversal
959                    } else {
960                        // Speed >= 1: use DFS with sequential faces. The traversal helper
961                        // already uses CornerIndex(3 * face_id) when no explicit seeds are
962                        // provided, so avoid allocating a temporary seed vector here.
963                        let (ids, map, v_map) = match &dfs_traversal {
964                            Some(cached) => cached.clone(),
965                            None => {
966                                let arrays = self.generate_point_ids_and_corners_dfs(mesh, &[])?;
967                                // The cache exists for later decoders; with
968                                // none left to read it (a single-decoder
969                                // decode, the common case), storing it would
970                                // clone three point-sized arrays for nobody.
971                                if dec_i + 1 < num_attributes_decoders {
972                                    dfs_traversal = Some(arrays.clone());
973                                }
974                                arrays
975                            }
976                        };
977                        sequenced_point_ids = Some(ids);
978                        sequenced_data_to_corner_map = Some(map);
979                        sequenced_vertex_to_data_map = Some(v_map); // Use directly from DFS traversal
980                    }
981                }
982            }
983
984            // Generate vertex_to_data_map from the traversal result (only if not already set).
985            // This is needed by predictors (like Parallelogram) to find references by point index.
986            // Only needed for Edgebreaker (method 1) since sequential encoding uses only
987            // Difference prediction which doesn't need mesh connectivity.
988            if self.method == 1 && sequenced_vertex_to_data_map.is_none() {
989                if let Some(ref map) = sequenced_data_to_corner_map {
990                    let ct = self.corner_table.as_ref().ok_or_else(|| {
991                        DracoError::general(
992                            "Edgebreaker attribute traversal missing corner table".to_string(),
993                        )
994                    })?;
995                    sequenced_vertex_to_data_map =
996                        Some(build_vertex_to_data_map_from_corner_map(ct, map)?);
997                }
998            }
999
1000            // Choose which point sequence to use for decoding values in this decoder.
1001            // If seams were applied, we derived a per-decoder point id list (possibly
1002            // containing repeats). Otherwise, fall back to the mesh-wide sequence.
1003            let point_ids_for_values = if let Some(ref ids) = point_ids_for_decoder {
1004                EntryToPointIdMap::from_point_indices(ids)
1005            } else if let Some(ref ids) = sequenced_point_ids {
1006                EntryToPointIdMap::from_point_indices(ids)
1007            } else {
1008                point_ids
1009            };
1010            let data_to_corner_map_override_for_values: Option<&[u32]> =
1011                if let Some(ref map) = data_to_corner_map_for_decoder {
1012                    Some(map.as_slice())
1013                } else if let Some(ref map) = sequenced_data_to_corner_map {
1014                    Some(map.as_slice())
1015                } else {
1016                    data_to_corner_map.as_deref()
1017                };
1018            let vertex_to_data_map_override_for_values: Option<&[i32]> =
1019                if point_ids_for_decoder.is_some() {
1020                    vertex_to_data_map_for_decoder.as_deref()
1021                } else {
1022                    sequenced_vertex_to_data_map.as_deref()
1023                };
1024
1025            let mut pending_quant: Vec<PendingQuant> = Vec::new();
1026            let mut pending_normals: Vec<PendingNormal> = Vec::new();
1027
1028            for (local_i, &att_id) in att_ids.iter().enumerate() {
1029                let decoder_type = decoder_types[local_i];
1030                {
1031                    let att = mesh.try_attribute_mut(att_id)?;
1032                    if att.size() != point_ids_for_values.len() {
1033                        att.resize_unique_entries(point_ids_for_values.len())?;
1034                    }
1035                }
1036                match decoder_type {
1037                    0 => {
1038                        let mut att_decoder = SequentialGenericAttributeDecoder::new();
1039                        att_decoder.init(&pc_decoder, att_id);
1040                        {
1041                            let _phase = crate::decode_phase_probe::PhaseTimer::start(
1042                                crate::decode_phase_probe::Phase::Values,
1043                            );
1044                            att_decoder.decode_values(mesh, point_ids_for_values, buffer)?;
1045                        }
1046                    }
1047                    1 => {
1048                        let mut att_decoder = SequentialIntegerAttributeDecoder::new();
1049                        att_decoder.init(&pc_decoder, att_id);
1050                        let portable_parent_attribute = {
1051                            let pos_att_id =
1052                                mesh.named_attribute_id(GeometryAttributeType::Position);
1053                            portable_attributes_by_id
1054                                .iter()
1055                                .find(|(id, _)| *id == pos_att_id)
1056                                .map(|(_, att)| att)
1057                        };
1058                        {
1059                            let _phase = crate::decode_phase_probe::PhaseTimer::start(
1060                                crate::decode_phase_probe::Phase::Values,
1061                            );
1062                            att_decoder.decode_values(
1063                                mesh,
1064                                point_ids_for_values,
1065                                buffer,
1066                                corner_table_for_decoder,
1067                                data_to_corner_map_override_for_values,
1068                                vertex_to_data_map_override_for_values,
1069                                None,
1070                                portable_parent_attribute,
1071                                None,
1072                            )?;
1073                        }
1074                    }
1075                    2 => {
1076                        let mut att_decoder = SequentialQuantizationAttributeDecoder::new();
1077                        att_decoder.init(&pc_decoder, mesh, att_id)?;
1078                        let portable_parent_attribute = {
1079                            let pos_att_id =
1080                                mesh.named_attribute_id(GeometryAttributeType::Position);
1081                            portable_attributes_by_id
1082                                .iter()
1083                                .find(|(id, _)| *id == pos_att_id)
1084                                .map(|(_, att)| att)
1085                        };
1086                        let portable = {
1087                            let _phase = crate::decode_phase_probe::PhaseTimer::start(
1088                                crate::decode_phase_probe::Phase::Values,
1089                            );
1090                            att_decoder.decode_values(
1091                                mesh,
1092                                point_ids_for_values,
1093                                buffer,
1094                                bitstream_version,
1095                                PortableExtent::of(point_ids_for_values),
1096                                corner_table_for_decoder,
1097                                data_to_corner_map_override_for_values,
1098                                vertex_to_data_map_override_for_values,
1099                                portable_parent_attribute,
1100                            )?
1101                        };
1102                        // Below 2.0, upstream has no separate "portable" concept
1103                        // at all: an attribute's own decode dequantizes it in
1104                        // place immediately, so a later attribute in this same
1105                        // loop that reads `mesh.attribute()` as a prediction
1106                        // parent (`MeshPredictionGeometricNormal`,
1107                        // `MeshPredictionTexCoordsPortable`) sees real values.
1108                        // 2.0+ defers this to a later shared pass instead, which
1109                        // is exactly the branch above that reads
1110                        // `portable_attributes_by_id` -- so applying it early
1111                        // here only where the bitstream cannot describe that
1112                        // deferral keeps both paths matching what a version's
1113                        // own decoder actually does. Applying it again in the
1114                        // later "inverse transforms" pass is redundant but
1115                        // harmless: the same input produces the same output.
1116                        if bitstream_version < 0x0200 {
1117                            let dst = mesh.try_attribute_mut(att_id)?;
1118                            if dst.size() != portable.size() {
1119                                dst.resize_unique_entries(portable.size())?;
1120                            }
1121                            att_decoder
1122                                .transform()
1123                                .inverse_transform_attribute(&portable, dst)?;
1124                        }
1125                        pending_quant.push(PendingQuant {
1126                            att_id,
1127                            portable,
1128                            transform: att_decoder.into_transform(),
1129                        });
1130                    }
1131                    3 => {
1132                        let mut att_decoder = SequentialNormalAttributeDecoder::new();
1133                        att_decoder.init(&pc_decoder, mesh, att_id)?;
1134                        let portable_parent_attribute = {
1135                            let pos_att_id =
1136                                mesh.named_attribute_id(GeometryAttributeType::Position);
1137                            portable_attributes_by_id
1138                                .iter()
1139                                .find(|(id, _)| *id == pos_att_id)
1140                                .map(|(_, att)| att)
1141                        };
1142                        let portable = {
1143                            let _phase = crate::decode_phase_probe::PhaseTimer::start(
1144                                crate::decode_phase_probe::Phase::Values,
1145                            );
1146                            att_decoder.decode_values(
1147                                mesh,
1148                                point_ids_for_values,
1149                                buffer,
1150                                bitstream_version,
1151                                PortableExtent::of(point_ids_for_values),
1152                                corner_table_for_decoder,
1153                                data_to_corner_map_override_for_values,
1154                                vertex_to_data_map_override_for_values,
1155                                portable_parent_attribute,
1156                            )?
1157                        };
1158                        pending_normals.push(PendingNormal {
1159                            att_id,
1160                            portable,
1161                            quantization_bits: att_decoder.quantization_bits(),
1162                        });
1163                    }
1164                    _ => {
1165                        return Err(DracoError::general(format!(
1166                            "Unsupported sequential decoder type: {}",
1167                            decoder_type
1168                        )));
1169                    }
1170                }
1171            }
1172
1173            // Decode transform data for all attributes.
1174            // For C++ files with bitstream version < 2.0, quantization params were already
1175            // decoded before integer values (legacy peek-ahead above). For v >= 2.0
1176            // (including all Rust-generated files), they are decoded here after all values.
1177            for (local_i, &att_id) in att_ids.iter().enumerate() {
1178                match decoder_types[local_i] {
1179                    2 if bitstream_version >= 0x0200 => {
1180                        let idx = pending_quant
1181                            .iter()
1182                            .position(|p| p.att_id == att_id)
1183                            .ok_or_else(|| {
1184                                DracoError::general("Missing pending quant entry".to_string())
1185                            })?;
1186                        let original = mesh.try_attribute(att_id)?;
1187                        pending_quant[idx]
1188                            .transform
1189                            .decode_parameters(original, buffer)
1190                            .map_err(|e| {
1191                                DracoError::general(format!(
1192                                    "Failed to decode quantization parameters: {e}"
1193                                ))
1194                            })?;
1195                    }
1196                    3 if bitstream_version >= 0x0200 => {
1197                        let idx = pending_normals
1198                            .iter()
1199                            .position(|p| p.att_id == att_id)
1200                            .ok_or_else(|| {
1201                                DracoError::general("Missing pending normal entry".to_string())
1202                            })?;
1203                        let bits = buffer.decode_u8()?;
1204                        if !AttributeOctahedronTransform::is_valid_quantization_bits(bits as i32) {
1205                            return Err(DracoError::general(
1206                                "Invalid normal quantization bits".to_string(),
1207                            ));
1208                        }
1209                        pending_normals[idx].quantization_bits = bits;
1210                    }
1211                    _ => {}
1212                }
1213            }
1214
1215            // Apply inverse transforms.
1216            for q in &pending_quant {
1217                let dst = mesh.try_attribute_mut(q.att_id)?;
1218                if dst.size() != q.portable.size() {
1219                    dst.resize_unique_entries(q.portable.size())?;
1220                }
1221                q.transform
1222                    .inverse_transform_attribute(&q.portable, dst)
1223                    .map_err(|e| {
1224                        DracoError::general(format!("Failed to dequantize attribute: {e}"))
1225                    })?;
1226            }
1227            for n in &pending_normals {
1228                let mut oct = AttributeOctahedronTransform::new(-1);
1229                oct.set_parameters(n.quantization_bits as i32)?;
1230                let dst = mesh.try_attribute_mut(n.att_id)?;
1231                if dst.size() != n.portable.size() {
1232                    dst.resize_unique_entries(n.portable.size())?;
1233                }
1234                oct.inverse_transform_attribute_with_legacy_octahedron(
1235                    &n.portable,
1236                    dst,
1237                    bitstream_version < 0x0200,
1238                )
1239                .map_err(|e| DracoError::general(format!("Failed to decode normals: {e}")))?;
1240            }
1241
1242            let _phase = crate::decode_phase_probe::PhaseTimer::start(
1243                crate::decode_phase_probe::Phase::MapFix,
1244            );
1245            // Apply UpdatePointToAttributeIndexMapping for Edgebreaker (method 1)
1246            // This creates the final mapping from mesh points to attribute values,
1247            // matching C++ MeshTraversalSequencer::UpdatePointToAttributeIndexMapping.
1248            //
1249            // The key insight: values are stored in data_id order (determined by DFS).
1250            // vertex_to_data_map[v] tells us which data_id holds vertex v's value.
1251            // In the decoder, mesh point == corner table vertex (since faces are built from CT).
1252            // So point p should get value from data_id = vertex_to_data_map[p].
1253            if self.method == 1 {
1254                let mapping_v_map = vertex_to_data_map_for_decoder
1255                    .as_deref()
1256                    .or(sequenced_vertex_to_data_map.as_deref());
1257                if let Some(v_map) = mapping_v_map {
1258                    let num_points = mesh.num_points();
1259                    // The finished map, assembled once and copied into each
1260                    // attribute whole. Entries no corner reaches stay INVALID,
1261                    // exactly what set_explicit_mapping leaves for a point no
1262                    // try_set_point_map_entry call touches.
1263                    let mut point_to_value: Vec<AttributeValueIndex> =
1264                        vec![crate::geometry_indices::INVALID_ATTRIBUTE_VALUE_INDEX; num_points];
1265                    if let Some(ct) = corner_table_for_decoder {
1266                        // Corner `c` of the mesh is entry `c` of the corner
1267                        // table and entry `c` of the flattened face list, so
1268                        // the walk is over two slices side by side rather than
1269                        // over faces with a corner index derived from each --
1270                        // that derivation was a multiply and an add per corner
1271                        // and every read behind it re-proved a bound the
1272                        // iterator now carries. Zipping also ends at the
1273                        // shorter of the two, which is where the derived form
1274                        // stopped anyway: a corner past the table read as the
1275                        // invalid vertex and was skipped.
1276                        for (&vertex, &point) in ct
1277                            .corner_to_vertex_map
1278                            .iter()
1279                            .zip(mesh.faces().as_flattened())
1280                        {
1281                            if vertex == INVALID_VERTEX_INDEX {
1282                                continue;
1283                            }
1284                            let Some(&data_id) = v_map.get(vertex.0 as usize) else {
1285                                continue;
1286                            };
1287                            if data_id < 0 {
1288                                continue;
1289                            }
1290                            if let Some(slot) = point_to_value.get_mut(point.0 as usize) {
1291                                *slot = AttributeValueIndex(data_id as u32);
1292                            }
1293                        }
1294                    } else {
1295                        for p in 0..num_points {
1296                            if p < v_map.len() && v_map[p] >= 0 {
1297                                point_to_value[p] = AttributeValueIndex(v_map[p] as u32);
1298                            }
1299                        }
1300                    }
1301
1302                    for &att_id in att_ids {
1303                        let att = mesh.try_attribute_mut(att_id)?;
1304                        att.set_explicit_mapping_from(&point_to_value);
1305                    }
1306                }
1307            }
1308
1309            for q in pending_quant {
1310                let mut portable = q.portable;
1311                copy_point_mapping(
1312                    mesh.try_attribute(q.att_id)?,
1313                    &mut portable,
1314                    mesh.num_points(),
1315                )?;
1316                upsert_portable_attribute(&mut portable_attributes_by_id, q.att_id, portable);
1317            }
1318            for n in pending_normals {
1319                let mut portable = n.portable;
1320                copy_point_mapping(
1321                    mesh.try_attribute(n.att_id)?,
1322                    &mut portable,
1323                    mesh.num_points(),
1324                )?;
1325                upsert_portable_attribute(&mut portable_attributes_by_id, n.att_id, portable);
1326            }
1327        }
1328
1329        Ok(())
1330    }
1331
1332    /// Discovery-order traversal: use the order points were created during reconstruction.
1333    #[allow(dead_code)]
1334    fn generate_point_ids_and_corners_discovery(&self, mesh: &Mesh) -> (Vec<PointIndex>, Vec<u32>) {
1335        let num_points = mesh.num_points();
1336        let mut point_ids = Vec::with_capacity(num_points);
1337        let mut data_to_corner_map = Vec::with_capacity(num_points);
1338
1339        for i in 0..num_points {
1340            let pid = PointIndex(i as u32);
1341            point_ids.push(pid);
1342            let corner = self
1343                .edgebreaker_vertex_to_corner_map
1344                .get(i)
1345                .cloned()
1346                .unwrap_or(u32::MAX);
1347            data_to_corner_map.push(if corner == u32::MAX { 0 } else { corner });
1348        }
1349
1350        (point_ids, data_to_corner_map)
1351    }
1352
1353    #[allow(dead_code)]
1354    fn generate_point_ids_and_corners_dfs(
1355        &self,
1356        mesh: &Mesh,
1357        processed_connectivity_corners: &[u32],
1358    ) -> Result<AttributeTraversalArrays, DracoError> {
1359        let corner_table = self.corner_table.as_ref().ok_or_else(|| {
1360            DracoError::general(
1361                "Edgebreaker DFS attribute traversal missing corner table".to_string(),
1362            )
1363        })?;
1364        // `self.corner_table` was already validated by
1365        // `MeshEdgebreakerDecoder::assign_points_to_corners` during connectivity
1366        // decode -- the only way this method (`self.method == 1`) reaches this
1367        // field is through that call, and it returns before setting the field
1368        // if the check fails. Re-scanning it here streams the same 1.67 MB
1369        // twice for an answer already known.
1370        Self::generate_point_ids_and_corners_dfs_for_table(
1371            mesh,
1372            corner_table,
1373            processed_connectivity_corners,
1374            true,
1375        )
1376    }
1377
1378    /// `already_validated` -- true when `corner_table` is known consistent from
1379    /// an earlier check on this exact table (the main corner table, checked in
1380    /// `assign_points_to_corners`); false for a seam-broken clone built fresh
1381    /// for one attribute, which has no other check standing in for this one.
1382    fn generate_point_ids_and_corners_dfs_for_table(
1383        mesh: &Mesh,
1384        corner_table: &CornerTable,
1385        processed_connectivity_corners: &[u32],
1386        already_validated: bool,
1387    ) -> Result<AttributeTraversalArrays, DracoError> {
1388        let _phase =
1389            crate::decode_phase_probe::PhaseTimer::start(crate::decode_phase_probe::Phase::Setup);
1390        // Reject an inconsistent (e.g. seam-modified) corner table before the DFS
1391        // indexes per-vertex / per-face arrays by table-derived ids.
1392        if !already_validated && !corner_table.is_index_consistent() {
1393            return Err(DracoError::general(
1394                "Inconsistent corner table for attribute traversal".to_string(),
1395            ));
1396        }
1397        let num_vertices = corner_table.num_vertices();
1398        let num_faces = corner_table.num_faces();
1399
1400        // Sized rather than reserved, and cut back to what the walk visited at
1401        // the end. `visited_vertices` gates the observer to one call per vertex
1402        // and is this long itself, so the walk cannot fill more than this --
1403        // which is what lets the observer write by index instead of pushing.
1404        // The push carried the reallocation path with it, and that path is what
1405        // an inliner priced: outlined, the observer cost `18` instructions of
1406        // prologue and epilogue per vertex on top of its three stores.
1407        let mut point_ids = vec![PointIndex(0); num_vertices];
1408        let mut data_to_corner_map = vec![0u32; num_vertices];
1409        let mut num_visited = 0usize;
1410        let mut vertex_to_data_map = vec![-1i32; num_vertices];
1411        let mut visited_vertices = vec![false; num_vertices];
1412        let mut visited_faces = vec![false; num_faces];
1413        let event_log_enabled = test_event_log::enabled();
1414
1415        // Helper to get mesh PointIndex from corner (matches C++ Mesh::CornerToPointId)
1416        let corner_to_point_id = |c: CornerIndex| -> PointIndex {
1417            if c == INVALID_CORNER_INDEX {
1418                return PointIndex(u32::MAX);
1419            }
1420            let face_id = FaceIndex(c.0 / 3);
1421            let corner_offset = (c.0 % 3) as usize;
1422            mesh.face(face_id)[corner_offset]
1423        };
1424
1425        // Visit a corner table vertex and record it as a point ID.
1426        // This matches C++ MeshAttributeIndicesEncodingObserver::OnNewVertexVisited
1427        // which gets point_id from mesh_->face(corner / 3)[corner % 3]
1428
1429        // Records one newly visited vertex: the point it maps to, its slot in
1430        // the attribute data order, and the corner it was discovered through.
1431        // C++ MeshAttributeIndicesEncodingObserver::OnNewVertexVisited.
1432        let mut on_new_vertex = |vertex: VertexIndex, corner: CornerIndex| {
1433            let point_id = corner_to_point_id(corner);
1434            vertex_to_data_map[vertex.0 as usize] = num_visited as i32;
1435            if event_log_enabled {
1436                record_vertex_visit_events(corner, vertex, point_id);
1437            }
1438            // Reached through `get_mut` rather than indexed so the observer
1439            // carries no panic path either: the slot exists by the sizing
1440            // above, and what kept this a real call was its size.
1441            if let (Some(point_slot), Some(corner_slot)) = (
1442                point_ids.get_mut(num_visited),
1443                data_to_corner_map.get_mut(num_visited),
1444            ) {
1445                *point_slot = point_id;
1446                *corner_slot = corner.0;
1447                num_visited += 1;
1448            }
1449        };
1450
1451        // Scratch for the depth-first walk, reused across seeds.
1452        let mut corner_stack: Vec<CornerIndex> = Vec::new();
1453
1454        // Run the traverser in the same way as C++ MeshTraversalSequencer:
1455        // - If a corner_order is provided, process only those corners.
1456        // - Otherwise, process sequential CornerIndex(3 * face_id).
1457        if !processed_connectivity_corners.is_empty() {
1458            for &c in processed_connectivity_corners {
1459                crate::corner_traversal::traverse_from_corner(
1460                    corner_table,
1461                    CornerIndex(c),
1462                    &mut corner_stack,
1463                    &mut visited_faces,
1464                    &mut visited_vertices,
1465                    &mut on_new_vertex,
1466                );
1467            }
1468        } else {
1469            for f in 0..num_faces {
1470                if !visited_faces[f] {
1471                    crate::corner_traversal::traverse_from_corner(
1472                        corner_table,
1473                        CornerIndex((f * 3) as u32),
1474                        &mut corner_stack,
1475                        &mut visited_faces,
1476                        &mut visited_vertices,
1477                        &mut on_new_vertex,
1478                    );
1479                }
1480            }
1481        }
1482
1483        // Back to the length the pushes would have left: a walk that reaches
1484        // fewer vertices than the table holds -- an isolated one, a component
1485        // the seeds miss -- must not hand back slots it never wrote.
1486        point_ids.truncate(num_visited);
1487        data_to_corner_map.truncate(num_visited);
1488
1489        Ok((point_ids, data_to_corner_map, vertex_to_data_map))
1490    }
1491
1492    #[allow(dead_code)]
1493    fn generate_point_ids_and_corners_max_prediction_degree(
1494        &self,
1495        mesh: &Mesh,
1496        _processed_connectivity_corners: &[u32],
1497    ) -> Result<AttributeTraversalArrays, DracoError> {
1498        let _phase =
1499            crate::decode_phase_probe::PhaseTimer::start(crate::decode_phase_probe::Phase::Setup);
1500        // Matches C++ MaxPredictionDegreeTraverser (MESH_TRAVERSAL_PREDICTION_DEGREE).
1501        let corner_table = self.corner_table.as_ref().ok_or_else(|| {
1502            DracoError::general(
1503                "Edgebreaker prediction-degree traversal missing corner table".to_string(),
1504            )
1505        })?;
1506        // No consistency check here: this method only ever runs on
1507        // `self.corner_table` (never a caller-supplied table), reachable only
1508        // when `self.method == 1`, which is exactly the condition under which
1509        // `MeshEdgebreakerDecoder::assign_points_to_corners` already checked
1510        // this same table during connectivity decode.
1511        let num_vertices = corner_table.num_vertices();
1512        let num_faces = corner_table.num_faces();
1513
1514        let mut point_ids = Vec::with_capacity(num_vertices);
1515        let mut data_to_corner_map = Vec::with_capacity(num_vertices);
1516        // Build vertex_to_data_map during traversal: vertex_to_data_map[vertex_id] = data_id
1517        // where data_id is the index into point_ids where this vertex was first visited.
1518        let mut vertex_to_data_map: Vec<i32> = vec![-1; num_vertices];
1519
1520        let mut visited_vertices = vec![false; num_vertices];
1521        let mut visited_faces = vec![false; num_faces];
1522        let mut prediction_degree: Vec<i32> = vec![0; num_vertices];
1523        let event_log_enabled = test_event_log::enabled();
1524
1525        // Buckets (stacks) for priorities 0..2.
1526        let mut stacks: [Vec<CornerIndex>; 3] = [Vec::new(), Vec::new(), Vec::new()];
1527        let mut best_priority: usize = 0;
1528
1529        // Helper to get mesh PointIndex from corner (matches C++ Mesh::CornerToPointId)
1530        let corner_to_point_id = |c: CornerIndex| -> PointIndex {
1531            if c == INVALID_CORNER_INDEX {
1532                return PointIndex(u32::MAX);
1533            }
1534            let face_id = FaceIndex(c.0 / 3);
1535            let corner_offset = (c.0 % 3) as usize;
1536            mesh.face(face_id)[corner_offset]
1537        };
1538
1539        let visit_vertex = |v: VertexIndex,
1540                            c: CornerIndex,
1541                            point_ids: &mut Vec<PointIndex>,
1542                            data_to_corner_map: &mut Vec<u32>,
1543                            visited_vertices: &mut [bool],
1544                            vertex_to_data_map: &mut [i32]| {
1545            if v == INVALID_VERTEX_INDEX {
1546                return;
1547            }
1548            let vi = v.0 as usize;
1549            if vi >= visited_vertices.len() {
1550                return;
1551            }
1552            if !visited_vertices[vi] {
1553                visited_vertices[vi] = true;
1554                // Record vertex->data_id mapping BEFORE pushing to point_ids
1555                // data_id is current length of point_ids (0-indexed sequence number)
1556                vertex_to_data_map[vi] = point_ids.len() as i32;
1557                // Use corner_to_point_id to get mesh PointIndex from corner
1558                let point_id = corner_to_point_id(c);
1559                if event_log_enabled {
1560                    record_vertex_visit_events(c, v, point_id);
1561                }
1562                point_ids.push(point_id);
1563                data_to_corner_map.push(c.0);
1564            }
1565        };
1566
1567        let compute_priority = |corner_id: CornerIndex,
1568                                visited_vertices: &[bool],
1569                                prediction_degree: &mut [i32]|
1570         -> usize {
1571            if corner_id == INVALID_CORNER_INDEX {
1572                return 2;
1573            }
1574            let v_tip = corner_table.vertex(corner_id);
1575            if v_tip == INVALID_VERTEX_INDEX {
1576                return 2;
1577            }
1578            let vi = v_tip.0 as usize;
1579            if vi < visited_vertices.len() && visited_vertices[vi] {
1580                return 0;
1581            }
1582            if vi < prediction_degree.len() {
1583                prediction_degree[vi] += 1;
1584                if prediction_degree[vi] > 1 {
1585                    1
1586                } else {
1587                    2
1588                }
1589            } else {
1590                2
1591            }
1592        };
1593
1594        let add_corner_to_stack = |ci: CornerIndex,
1595                                   priority: usize,
1596                                   stacks: &mut [Vec<CornerIndex>; 3],
1597                                   best_priority: &mut usize| {
1598            let p = priority.min(2);
1599            stacks[p].push(ci);
1600            if p < *best_priority {
1601                *best_priority = p;
1602            }
1603        };
1604
1605        let pop_next_corner =
1606            |stacks: &mut [Vec<CornerIndex>; 3], best_priority: &mut usize| -> CornerIndex {
1607                for p in *best_priority..3 {
1608                    if let Some(ci) = stacks[p].pop() {
1609                        *best_priority = p;
1610                        return ci;
1611                    }
1612                }
1613                INVALID_CORNER_INDEX
1614            };
1615
1616        let traverse_from_corner =
1617            |start_corner: CornerIndex,
1618             point_ids: &mut Vec<PointIndex>,
1619             data_to_corner_map: &mut Vec<u32>,
1620             visited_vertices: &mut Vec<bool>,
1621             visited_faces: &mut Vec<bool>,
1622             prediction_degree: &mut Vec<i32>,
1623             stacks: &mut [Vec<CornerIndex>; 3],
1624             best_priority: &mut usize,
1625             vertex_to_data_map: &mut Vec<i32>| {
1626                if corner_table.face(start_corner) == crate::geometry_indices::INVALID_FACE_INDEX {
1627                    return;
1628                }
1629
1630                // Deliberately no visited-face guard and no stack reset, matching
1631                // upstream MaxPredictionDegreeTraverser::TraverseFromCorner. The
1632                // depth-first traverser has both; this one has neither, because a
1633                // face is marked visited having visited only one of its vertices,
1634                // so the three pre-visits below still have work to do on a face
1635                // that was already traversed.
1636                stacks[0].push(start_corner);
1637                *best_priority = 0;
1638
1639                // Pre-visit next, prev and tip vertices.
1640                let next_c = corner_table.next(start_corner);
1641                let prev_c = corner_table.previous(start_corner);
1642                visit_vertex(
1643                    corner_table.vertex(next_c),
1644                    next_c,
1645                    point_ids,
1646                    data_to_corner_map,
1647                    visited_vertices,
1648                    vertex_to_data_map,
1649                );
1650                visit_vertex(
1651                    corner_table.vertex(prev_c),
1652                    prev_c,
1653                    point_ids,
1654                    data_to_corner_map,
1655                    visited_vertices,
1656                    vertex_to_data_map,
1657                );
1658                visit_vertex(
1659                    corner_table.vertex(start_corner),
1660                    start_corner,
1661                    point_ids,
1662                    data_to_corner_map,
1663                    visited_vertices,
1664                    vertex_to_data_map,
1665                );
1666
1667                loop {
1668                    let mut corner_id = pop_next_corner(stacks, best_priority);
1669                    if corner_id == INVALID_CORNER_INDEX {
1670                        break;
1671                    }
1672                    let face_id0 = corner_table.face(corner_id);
1673                    if face_id0 == crate::geometry_indices::INVALID_FACE_INDEX {
1674                        continue;
1675                    }
1676                    if visited_faces[face_id0.0 as usize] {
1677                        continue;
1678                    }
1679
1680                    loop {
1681                        let face_id = corner_table.face(corner_id);
1682                        if face_id == crate::geometry_indices::INVALID_FACE_INDEX {
1683                            break;
1684                        }
1685                        visited_faces[face_id.0 as usize] = true;
1686
1687                        let vert_id = corner_table.vertex(corner_id);
1688                        if vert_id != INVALID_VERTEX_INDEX {
1689                            let vi = vert_id.0 as usize;
1690                            if vi < visited_vertices.len() && !visited_vertices[vi] {
1691                                visit_vertex(
1692                                    vert_id,
1693                                    corner_id,
1694                                    point_ids,
1695                                    data_to_corner_map,
1696                                    visited_vertices,
1697                                    vertex_to_data_map,
1698                                );
1699                            }
1700                        }
1701
1702                        let right_corner_id = corner_table.right_corner(corner_id);
1703                        let left_corner_id = corner_table.left_corner(corner_id);
1704                        let right_face_id = if right_corner_id == INVALID_CORNER_INDEX {
1705                            crate::geometry_indices::INVALID_FACE_INDEX
1706                        } else {
1707                            corner_table.face(right_corner_id)
1708                        };
1709                        let left_face_id = if left_corner_id == INVALID_CORNER_INDEX {
1710                            crate::geometry_indices::INVALID_FACE_INDEX
1711                        } else {
1712                            corner_table.face(left_corner_id)
1713                        };
1714
1715                        let is_right_face_visited = right_face_id
1716                            == crate::geometry_indices::INVALID_FACE_INDEX
1717                            || visited_faces[right_face_id.0 as usize];
1718                        let is_left_face_visited = left_face_id
1719                            == crate::geometry_indices::INVALID_FACE_INDEX
1720                            || visited_faces[left_face_id.0 as usize];
1721
1722                        if !is_left_face_visited {
1723                            let priority = compute_priority(
1724                                left_corner_id,
1725                                visited_vertices,
1726                                prediction_degree,
1727                            );
1728                            if is_right_face_visited && priority <= *best_priority {
1729                                corner_id = left_corner_id;
1730                                continue;
1731                            }
1732                            add_corner_to_stack(left_corner_id, priority, stacks, best_priority);
1733                        }
1734
1735                        if !is_right_face_visited {
1736                            let priority = compute_priority(
1737                                right_corner_id,
1738                                visited_vertices,
1739                                prediction_degree,
1740                            );
1741                            if priority <= *best_priority {
1742                                corner_id = right_corner_id;
1743                                continue;
1744                            }
1745                            add_corner_to_stack(right_corner_id, priority, stacks, best_priority);
1746                        }
1747
1748                        break;
1749                    }
1750                }
1751            };
1752
1753        // C++ DECODER traverses faces SEQUENTIALLY (face 0, face 1, face 2, ...)
1754        // NOT using processed_connectivity_corners (that's only for the ENCODER)!
1755        // See C++ MeshTraversalSequencer::GenerateSequenceInternal() - when corner_order_ is null,
1756        // it does: for (int i = 0; i < num_faces; ++i) ProcessCorner(CornerIndex(3 * i));
1757        for f in 0..num_faces {
1758            let first_corner = corner_table.first_corner(FaceIndex(f as u32));
1759            traverse_from_corner(
1760                first_corner,
1761                &mut point_ids,
1762                &mut data_to_corner_map,
1763                &mut visited_vertices,
1764                &mut visited_faces,
1765                &mut prediction_degree,
1766                &mut stacks,
1767                &mut best_priority,
1768                &mut vertex_to_data_map,
1769            );
1770        }
1771
1772        Ok((point_ids, data_to_corner_map, vertex_to_data_map))
1773    }
1774}
1775
1776fn validate_mesh_index_count(num_faces: usize) -> Result<usize, DracoError> {
1777    num_faces
1778        .checked_mul(3)
1779        .ok_or_else(|| DracoError::general("Mesh face index count overflow".to_string()))
1780}
1781
1782/// The index array, sized from a count that already has an array behind it.
1783///
1784/// Uncharged, and the caller is why: the length handed in is the length of the
1785/// symbols the decode actually produced, so this reservation is backed by one
1786/// that already exists in memory. The count that *did* come from the header
1787/// was bounded before a symbol was read, at one bit each, by
1788/// `decode_budget::ensure_symbols_are_backed`. Billing the budget a second
1789/// time here would put a ceiling on legitimate meshes -- 256 MiB is 22 million
1790/// faces -- for a reservation nothing about it is unbacked.
1791fn make_zeroed_indices(num_indices: usize) -> Result<Vec<u32>, DracoError> {
1792    let mut indices = Vec::new();
1793    indices
1794        .try_reserve_exact(num_indices)
1795        .map_err(|_| DracoError::general("Failed to allocate mesh indices".to_string()))?;
1796    indices.resize(num_indices, 0);
1797    Ok(indices)
1798}
1799
1800/// Sizes the face array, after the caller's ceiling has had its say.
1801///
1802/// The ceiling is checked here rather than where the count is read, and the
1803/// order is the point: every structural refusal on the way to this call --
1804/// a count no stream that size can carry, an index that overflows, bytes that
1805/// are not there -- says the *file* is wrong, and must be what a caller sees.
1806/// [`ErrorKind::LimitExceeded`](crate::ErrorKind::LimitExceeded) says the file
1807/// may be fine and the caller declined to decode something this large, which
1808/// is only a truthful answer once the file has been found coherent.
1809#[cfg(feature = "decoder")]
1810fn set_num_faces_within_limits(
1811    mesh: &mut Mesh,
1812    buffer: &DecoderBuffer,
1813    num_faces: usize,
1814) -> Status {
1815    buffer.check_faces(num_faces)?;
1816    mesh.try_set_num_faces(num_faces)
1817}
1818
1819#[cfg(test)]
1820mod tests {
1821    use super::*;
1822
1823    #[test]
1824    fn attribute_corner_table_rejects_out_of_range_seam_corner() {
1825        let mut corner_table = CornerTable::new(1);
1826        corner_table.set_face_vertices(FaceIndex(0), PointIndex(0), PointIndex(1), PointIndex(2));
1827
1828        let invalid_corner = corner_table.num_corners() as u32;
1829        let status = MeshDecoder::make_attribute_corner_table(&corner_table, &[invalid_corner]);
1830
1831        assert!(status.is_err());
1832    }
1833
1834    #[test]
1835    fn vertex_to_data_map_rejects_out_of_range_corner() {
1836        let mut corner_table = CornerTable::new(1);
1837        corner_table.set_face_vertices(FaceIndex(0), PointIndex(0), PointIndex(1), PointIndex(2));
1838
1839        let invalid_corner = corner_table.num_corners() as u32;
1840        let status = build_vertex_to_data_map_from_corner_map(&corner_table, &[invalid_corner]);
1841
1842        assert!(status.is_err());
1843    }
1844}