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