Skip to main content

draco_io/
meshopt.rs

1//! Decoders for the `EXT_meshopt_compression` bitstreams.
2//!
3//! The vertex, index and filter codecs are a port of the reference decoders in
4//! meshoptimizer (`src/vertexcodec.cpp`, `src/indexcodec.cpp`,
5//! `src/vertexfilter.cpp`), Copyright (c) 2016-2025 Arseny Kapoulkine, used
6//! under the MIT license. The bitstream is fixed by the glTF extension, so the
7//! port follows the reference control flow closely; every read is bounds
8//! checked instead of relying on the C decoder's pre-validated cursors.
9
10use crate::gltf_error::{GltfError, Result};
11
12/// Encoded layout of one `EXT_meshopt_compression` buffer view.
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14pub enum MeshoptMode {
15    /// Vertex attribute stream, `count` elements of `stride` bytes.
16    Attributes,
17    /// Triangle index stream restricted to triangle lists.
18    Triangles,
19    /// Index stream with no connectivity assumptions.
20    Indices,
21}
22
23/// Reversible transform applied to a decoded attribute stream.
24#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
25pub enum MeshoptFilter {
26    /// Attribute data is used as decoded.
27    #[default]
28    None,
29    /// Octahedral unit vectors, 4 or 8 byte stride.
30    Octahedral,
31    /// Quaternions stored with the largest component dropped, 8 byte stride.
32    Quaternion,
33    /// Floats stored as shared-exponent mantissa pairs.
34    Exponential,
35    /// RGBA stored as luma / chroma with the scale folded into alpha, 4 or 8
36    /// byte stride.
37    Color,
38}
39
40impl MeshoptMode {
41    /// Parses the extension's `mode` string.
42    pub fn from_name(name: &str) -> Result<Self> {
43        match name {
44            "ATTRIBUTES" => Ok(Self::Attributes),
45            "TRIANGLES" => Ok(Self::Triangles),
46            "INDICES" => Ok(Self::Indices),
47            other => Err(GltfError::Unsupported(format!(
48                "EXT_meshopt_compression mode {other}"
49            ))),
50        }
51    }
52}
53
54impl MeshoptFilter {
55    /// Parses the extension's `filter` string; absent means [`MeshoptFilter::None`].
56    pub fn from_name(name: &str) -> Result<Self> {
57        match name {
58            "NONE" => Ok(Self::None),
59            "OCTAHEDRAL" => Ok(Self::Octahedral),
60            "QUATERNION" => Ok(Self::Quaternion),
61            "EXPONENTIAL" => Ok(Self::Exponential),
62            "COLOR" => Ok(Self::Color),
63            other => Err(GltfError::Unsupported(format!(
64                "EXT_meshopt_compression filter {other}"
65            ))),
66        }
67    }
68}
69
70/// Decodes one compressed buffer view into its `count * stride` bytes.
71///
72/// `destination` must be exactly the decoded size; `source` is the compressed
73/// range named by the extension object.
74pub fn decode_buffer_view(
75    destination: &mut [u8],
76    source: &[u8],
77    mode: MeshoptMode,
78    filter: MeshoptFilter,
79    count: usize,
80    stride: usize,
81) -> Result<()> {
82    let expected = count
83        .checked_mul(stride)
84        .ok_or_else(|| invalid("buffer view size overflow"))?;
85    if destination.len() != expected {
86        return Err(invalid(
87            "buffer view byteLength does not match count times byteStride",
88        ));
89    }
90    match mode {
91        MeshoptMode::Attributes => {
92            decode_vertex_buffer(destination, count, stride, source)?;
93            apply_filter(destination, filter, count, stride)
94        }
95        MeshoptMode::Triangles => {
96            if filter != MeshoptFilter::None {
97                return Err(invalid("index streams cannot carry a filter"));
98            }
99            decode_index_buffer(destination, count, stride, source)
100        }
101        MeshoptMode::Indices => {
102            if filter != MeshoptFilter::None {
103                return Err(invalid("index streams cannot carry a filter"));
104            }
105            decode_index_sequence(destination, count, stride, source)
106        }
107    }
108}
109
110fn invalid(message: &str) -> GltfError {
111    GltfError::InvalidGltf(format!("EXT_meshopt_compression: {message}"))
112}
113
114const VERTEX_HEADER: u8 = 0xa0;
115const MAX_VERTEX_VERSION: u8 = 1;
116const VERTEX_BLOCK_SIZE_BYTES: usize = 8192;
117const VERTEX_BLOCK_MAX_SIZE: usize = 256;
118const BYTE_GROUP_SIZE: usize = 16;
119const BYTE_GROUP_DECODE_LIMIT: usize = 24;
120const TAIL_MIN_SIZE_V0: usize = 32;
121const TAIL_MIN_SIZE_V1: usize = 24;
122const BITS_V0: [u32; 4] = [0, 2, 4, 8];
123const BITS_V1: [u32; 5] = [0, 1, 2, 4, 8];
124
125fn vertex_block_size(vertex_size: usize) -> usize {
126    let result = (VERTEX_BLOCK_SIZE_BYTES / vertex_size) & !(BYTE_GROUP_SIZE - 1);
127    result.min(VERTEX_BLOCK_MAX_SIZE)
128}
129
130/// Decodes a vertex attribute stream of `count` elements of `stride` bytes.
131pub fn decode_vertex_buffer(
132    destination: &mut [u8],
133    count: usize,
134    stride: usize,
135    source: &[u8],
136) -> Result<()> {
137    if stride == 0 || stride > 256 || !stride.is_multiple_of(4) {
138        return Err(invalid(
139            "attribute byteStride must be 4..=256 and a multiple of 4",
140        ));
141    }
142    let header = *source
143        .first()
144        .ok_or_else(|| invalid("empty vertex stream"))?;
145    if header & 0xf0 != VERTEX_HEADER {
146        return Err(invalid("vertex stream header is invalid"));
147    }
148    let version = header & 0x0f;
149    if version > MAX_VERTEX_VERSION {
150        return Err(GltfError::Unsupported(format!(
151            "EXT_meshopt_compression vertex codec version {version}"
152        )));
153    }
154    let mut pos = 1usize;
155
156    let tail_size = stride + if version == 0 { 0 } else { stride / 4 };
157    let tail_min = if version == 0 {
158        TAIL_MIN_SIZE_V0
159    } else {
160        TAIL_MIN_SIZE_V1
161    };
162    let tail_padded = tail_size.max(tail_min);
163    if source.len() - pos < tail_padded {
164        return Err(invalid("vertex stream is truncated"));
165    }
166    let tail = source.len() - tail_size;
167
168    let mut last_vertex = [0u8; 256];
169    last_vertex[..stride].copy_from_slice(&source[tail..tail + stride]);
170    let channels = if version == 0 {
171        Vec::new()
172    } else {
173        source[tail + stride..tail + tail_size].to_vec()
174    };
175
176    let block_capacity = vertex_block_size(stride);
177    let mut scratch = vec![0u8; VERTEX_BLOCK_MAX_SIZE * 4];
178    let mut offset = 0usize;
179    while offset < count {
180        let block = block_capacity.min(count - offset);
181        let start = offset * stride;
182        pos = decode_vertex_block(
183            source,
184            pos,
185            &mut destination[start..start + block * stride],
186            block,
187            stride,
188            &mut last_vertex,
189            &channels,
190            version,
191            &mut scratch,
192        )?;
193        offset += block;
194    }
195
196    if source.len() - pos != tail_padded {
197        return Err(invalid("vertex stream has trailing data"));
198    }
199    Ok(())
200}
201
202#[allow(clippy::too_many_arguments)]
203fn decode_vertex_block(
204    source: &[u8],
205    mut pos: usize,
206    destination: &mut [u8],
207    count: usize,
208    stride: usize,
209    last_vertex: &mut [u8; 256],
210    channels: &[u8],
211    version: u8,
212    scratch: &mut [u8],
213) -> Result<usize> {
214    debug_assert!(count > 0 && count <= VERTEX_BLOCK_MAX_SIZE);
215    let count_aligned = (count + BYTE_GROUP_SIZE - 1) & !(BYTE_GROUP_SIZE - 1);
216
217    let control_size = if version == 0 { 0 } else { stride / 4 };
218    if source.len() - pos < control_size {
219        return Err(invalid("vertex block control bytes are truncated"));
220    }
221    let control = source[pos..pos + control_size].to_vec();
222    pos += control_size;
223
224    for k in (0..stride).step_by(4) {
225        let control_byte = if version == 0 { 0 } else { control[k / 4] };
226        for j in 0..4usize {
227            let plane = j * count;
228            match (control_byte >> (j * 2)) & 3 {
229                3 => {
230                    if source.len() - pos < count {
231                        return Err(invalid("vertex block literal plane is truncated"));
232                    }
233                    scratch[plane..plane + count].copy_from_slice(&source[pos..pos + count]);
234                    pos += count;
235                }
236                2 => scratch[plane..plane + count].fill(0),
237                control => {
238                    let bits: &[u32] = if version == 0 {
239                        &BITS_V0
240                    } else {
241                        &BITS_V1[control as usize..]
242                    };
243                    pos = decode_bytes(
244                        source,
245                        pos,
246                        &mut scratch[plane..plane + count_aligned],
247                        bits,
248                    )?;
249                }
250            }
251        }
252
253        let channel = if version == 0 { 0 } else { channels[k / 4] };
254        match channel & 3 {
255            0 => decode_deltas(
256                scratch,
257                destination,
258                count,
259                stride,
260                last_vertex,
261                k,
262                1,
263                false,
264                0,
265            ),
266            1 => decode_deltas(
267                scratch,
268                destination,
269                count,
270                stride,
271                last_vertex,
272                k,
273                2,
274                false,
275                0,
276            ),
277            2 => {
278                let rotation = (32 - u32::from(channel >> 4)) & 31;
279                decode_deltas(
280                    scratch,
281                    destination,
282                    count,
283                    stride,
284                    last_vertex,
285                    k,
286                    4,
287                    true,
288                    rotation,
289                )
290            }
291            _ => return Err(invalid("vertex block channel type is invalid")),
292        }
293    }
294
295    last_vertex[..stride].copy_from_slice(&destination[stride * (count - 1)..stride * count]);
296    Ok(pos)
297}
298
299/// Reconstructs one four-byte channel group from its transposed byte planes.
300#[allow(clippy::too_many_arguments)]
301fn decode_deltas(
302    scratch: &[u8],
303    destination: &mut [u8],
304    count: usize,
305    stride: usize,
306    last_vertex: &[u8; 256],
307    k: usize,
308    size: usize,
309    xor: bool,
310    rotation: u32,
311) {
312    let mask = if size == 4 {
313        u32::MAX
314    } else {
315        (1u32 << (8 * size)) - 1
316    };
317    let mut plane = 0usize;
318    for sub in (0..4).step_by(size) {
319        let mut previous = 0u32;
320        for byte in 0..size {
321            previous |= u32::from(last_vertex[k + sub + byte]) << (8 * byte);
322        }
323        let mut offset = k + sub;
324        for i in 0..count {
325            let mut value = 0u32;
326            for byte in 0..size {
327                value |= u32::from(scratch[plane + i + count * byte]) << (8 * byte);
328            }
329            value = if xor {
330                (value.rotate_left(rotation) ^ previous) & mask
331            } else {
332                unzigzag(value).wrapping_add(previous) & mask
333            };
334            for byte in 0..size {
335                destination[offset + byte] = (value >> (8 * byte)) as u8;
336            }
337            previous = value;
338            offset += stride;
339        }
340        plane += count * size;
341    }
342}
343
344fn unzigzag(value: u32) -> u32 {
345    (0u32.wrapping_sub(value & 1)) ^ (value >> 1)
346}
347
348fn decode_bytes(
349    source: &[u8],
350    mut pos: usize,
351    destination: &mut [u8],
352    bits: &[u32],
353) -> Result<usize> {
354    debug_assert!(destination.len().is_multiple_of(BYTE_GROUP_SIZE));
355    let header_size = (destination.len() / BYTE_GROUP_SIZE).div_ceil(4);
356    if source.len() - pos < header_size {
357        return Err(invalid("byte group header is truncated"));
358    }
359    let header = pos;
360    pos += header_size;
361
362    for (group, chunk) in destination.chunks_mut(BYTE_GROUP_SIZE).enumerate() {
363        if source.len() - pos < BYTE_GROUP_DECODE_LIMIT {
364            return Err(invalid("byte group data is truncated"));
365        }
366        let selector = (source[header + group / 4] >> ((group % 4) * 2)) & 3;
367        pos = decode_bytes_group(source, pos, chunk, bits[selector as usize]);
368    }
369    Ok(pos)
370}
371
372/// Expands one 16 byte group; the caller guarantees the worst-case 24 bytes.
373fn decode_bytes_group(source: &[u8], pos: usize, destination: &mut [u8], bits: u32) -> usize {
374    match bits {
375        0 => {
376            destination.fill(0);
377            pos
378        }
379        8 => {
380            destination.copy_from_slice(&source[pos..pos + BYTE_GROUP_SIZE]);
381            pos + BYTE_GROUP_SIZE
382        }
383        bits => {
384            let per_byte = 8 / bits as usize;
385            let control_bytes = BYTE_GROUP_SIZE / per_byte;
386            let sentinel = (1u8 << bits) - 1;
387            let mut extra = pos + control_bytes;
388            for group in 0..control_bytes {
389                let mut byte = source[pos + group];
390                if bits == 1 {
391                    // 1-bit groups store their values in reverse bit order.
392                    byte = byte.reverse_bits();
393                }
394                for slot in 0..per_byte {
395                    let encoded = byte >> (8 - bits);
396                    byte <<= bits;
397                    destination[group * per_byte + slot] = if encoded == sentinel {
398                        let value = source[extra];
399                        extra += 1;
400                        value
401                    } else {
402                        encoded
403                    };
404                }
405            }
406            extra
407        }
408    }
409}
410
411const INDEX_HEADER: u8 = 0xe0;
412const SEQUENCE_HEADER: u8 = 0xd0;
413const MAX_INDEX_VERSION: u8 = 1;
414
415/// Decodes a triangle-list index stream of `count` indices of `size` bytes.
416pub fn decode_index_buffer(
417    destination: &mut [u8],
418    count: usize,
419    size: usize,
420    source: &[u8],
421) -> Result<()> {
422    if !count.is_multiple_of(3) {
423        return Err(invalid("triangle index count is not a multiple of 3"));
424    }
425    if size != 2 && size != 4 {
426        return Err(invalid("index byteStride must be 2 or 4"));
427    }
428    if source.len() < 1 + count / 3 + 16 {
429        return Err(invalid("index stream is truncated"));
430    }
431    if source[0] & 0xf0 != INDEX_HEADER {
432        return Err(invalid("index stream header is invalid"));
433    }
434    let version = source[0] & 0x0f;
435    if version > MAX_INDEX_VERSION {
436        return Err(GltfError::Unsupported(format!(
437            "EXT_meshopt_compression index codec version {version}"
438        )));
439    }
440
441    let mut edge_fifo = [[0u32; 2]; 16];
442    let mut vertex_fifo = [0u32; 16];
443    let mut edge_offset = 0usize;
444    let mut vertex_offset = 0usize;
445    let mut next = 0u32;
446    let mut last = 0u32;
447    let fec_max = if version >= 1 { 13 } else { 15 };
448
449    let code_end = 1 + count / 3;
450    let mut data = code_end;
451    let safe_end = source.len() - 16;
452    let table = safe_end;
453    let mut written = 0usize;
454
455    for code in 1..code_end {
456        let code_tri = source[code];
457        if code_tri < 0xf0 {
458            let fe = usize::from(code_tri >> 4);
459            let edge = edge_fifo[(edge_offset.wrapping_sub(1 + fe)) & 15];
460            let (a, b) = (edge[0], edge[1]);
461            let c;
462
463            let fec = i32::from(code_tri & 15);
464            if fec < fec_max {
465                let cached = vertex_fifo[(vertex_offset.wrapping_sub(1 + fec as usize)) & 15];
466                c = if fec == 0 { next } else { cached };
467                let first = usize::from(fec == 0);
468                next += u32::from(fec == 0);
469                push_vertex_fifo(&mut vertex_fifo, c, &mut vertex_offset, first);
470            } else {
471                if data > safe_end {
472                    return Err(invalid("index stream data is truncated"));
473                }
474                c = if fec != 15 {
475                    last.wrapping_add((fec * 2 - 27) as u32)
476                } else {
477                    decode_index(source, &mut data, last)?
478                };
479                last = c;
480                push_vertex_fifo(&mut vertex_fifo, c, &mut vertex_offset, 1);
481            }
482
483            push_edge_fifo(&mut edge_fifo, c, b, &mut edge_offset);
484            push_edge_fifo(&mut edge_fifo, a, c, &mut edge_offset);
485            write_triangle(destination, &mut written, size, a, b, c);
486        } else if code_tri < 0xfe {
487            let code_aux = source[table + usize::from(code_tri & 15)];
488            let feb = usize::from(code_aux >> 4);
489            let fec = usize::from(code_aux & 15);
490
491            let a = next;
492            next += 1;
493
494            let b = if feb == 0 {
495                next
496            } else {
497                vertex_fifo[(vertex_offset.wrapping_sub(feb)) & 15]
498            };
499            let feb0 = usize::from(feb == 0);
500            next += feb0 as u32;
501
502            let c = if fec == 0 {
503                next
504            } else {
505                vertex_fifo[(vertex_offset.wrapping_sub(fec)) & 15]
506            };
507            let fec0 = usize::from(fec == 0);
508            next += fec0 as u32;
509
510            write_triangle(destination, &mut written, size, a, b, c);
511
512            push_vertex_fifo(&mut vertex_fifo, a, &mut vertex_offset, 1);
513            push_vertex_fifo(&mut vertex_fifo, b, &mut vertex_offset, feb0);
514            push_vertex_fifo(&mut vertex_fifo, c, &mut vertex_offset, fec0);
515
516            push_edge_fifo(&mut edge_fifo, b, a, &mut edge_offset);
517            push_edge_fifo(&mut edge_fifo, c, b, &mut edge_offset);
518            push_edge_fifo(&mut edge_fifo, a, c, &mut edge_offset);
519        } else {
520            if data > safe_end {
521                return Err(invalid("index stream data is truncated"));
522            }
523            let code_aux = source[data];
524            data += 1;
525
526            let fea = if code_tri == 0xfe { 0usize } else { 15 };
527            let feb = usize::from(code_aux >> 4);
528            let fec = usize::from(code_aux & 15);
529
530            // A codeaux of 0 outside the table is the encoder's index reset.
531            if code_aux == 0 {
532                next = 0;
533            }
534
535            let mut a = 0u32;
536            if fea == 0 {
537                a = next;
538                next += 1;
539            }
540            let mut b = if feb == 0 {
541                let value = next;
542                next += 1;
543                value
544            } else {
545                vertex_fifo[(vertex_offset.wrapping_sub(feb)) & 15]
546            };
547            let mut c = if fec == 0 {
548                let value = next;
549                next += 1;
550                value
551            } else {
552                vertex_fifo[(vertex_offset.wrapping_sub(fec)) & 15]
553            };
554
555            if fea == 15 {
556                a = decode_index(source, &mut data, last)?;
557                last = a;
558            }
559            if feb == 15 {
560                b = decode_index(source, &mut data, last)?;
561                last = b;
562            }
563            if fec == 15 {
564                c = decode_index(source, &mut data, last)?;
565                last = c;
566            }
567
568            write_triangle(destination, &mut written, size, a, b, c);
569
570            push_vertex_fifo(&mut vertex_fifo, a, &mut vertex_offset, 1);
571            push_vertex_fifo(
572                &mut vertex_fifo,
573                b,
574                &mut vertex_offset,
575                usize::from(feb == 0 || feb == 15),
576            );
577            push_vertex_fifo(
578                &mut vertex_fifo,
579                c,
580                &mut vertex_offset,
581                usize::from(fec == 0 || fec == 15),
582            );
583
584            push_edge_fifo(&mut edge_fifo, b, a, &mut edge_offset);
585            push_edge_fifo(&mut edge_fifo, c, b, &mut edge_offset);
586            push_edge_fifo(&mut edge_fifo, a, c, &mut edge_offset);
587        }
588    }
589
590    if data != safe_end {
591        return Err(invalid("index stream has trailing data"));
592    }
593    Ok(())
594}
595
596/// Decodes an index stream that carries no triangle connectivity.
597pub fn decode_index_sequence(
598    destination: &mut [u8],
599    count: usize,
600    size: usize,
601    source: &[u8],
602) -> Result<()> {
603    if size != 2 && size != 4 {
604        return Err(invalid("index byteStride must be 2 or 4"));
605    }
606    if source.len() < 1 + count + 4 {
607        return Err(invalid("index sequence is truncated"));
608    }
609    if source[0] & 0xf0 != SEQUENCE_HEADER {
610        return Err(invalid("index sequence header is invalid"));
611    }
612    let version = source[0] & 0x0f;
613    if version > MAX_INDEX_VERSION {
614        return Err(GltfError::Unsupported(format!(
615            "EXT_meshopt_compression index codec version {version}"
616        )));
617    }
618
619    let mut data = 1usize;
620    let safe_end = source.len() - 4;
621    let mut last = [0u32; 2];
622
623    for i in 0..count {
624        if data >= safe_end {
625            return Err(invalid("index sequence data is truncated"));
626        }
627        let value = decode_vbyte(source, &mut data)?;
628        let baseline = (value & 1) as usize;
629        let value = value >> 1;
630        let delta = (value >> 1) ^ (0u32.wrapping_sub(value & 1));
631        let index = last[baseline].wrapping_add(delta);
632        last[baseline] = index;
633        write_index(destination, i * size, size, index);
634    }
635
636    if data != safe_end {
637        return Err(invalid("index sequence has trailing data"));
638    }
639    Ok(())
640}
641
642fn push_edge_fifo(fifo: &mut [[u32; 2]; 16], a: u32, b: u32, offset: &mut usize) {
643    fifo[*offset] = [a, b];
644    *offset = (*offset + 1) & 15;
645}
646
647fn push_vertex_fifo(fifo: &mut [u32; 16], v: u32, offset: &mut usize, advance: usize) {
648    fifo[*offset] = v;
649    *offset = (*offset + advance) & 15;
650}
651
652fn decode_vbyte(source: &[u8], pos: &mut usize) -> Result<u32> {
653    let lead = *source
654        .get(*pos)
655        .ok_or_else(|| invalid("variable-length index is truncated"))?;
656    *pos += 1;
657    if lead < 128 {
658        return Ok(u32::from(lead));
659    }
660    let mut result = u32::from(lead & 127);
661    let mut shift = 7;
662    for _ in 0..4 {
663        let group = *source
664            .get(*pos)
665            .ok_or_else(|| invalid("variable-length index is truncated"))?;
666        *pos += 1;
667        result |= u32::from(group & 127) << shift;
668        shift += 7;
669        if group < 128 {
670            break;
671        }
672    }
673    Ok(result)
674}
675
676fn decode_index(source: &[u8], pos: &mut usize, last: u32) -> Result<u32> {
677    let value = decode_vbyte(source, pos)?;
678    let delta = (value >> 1) ^ (0u32.wrapping_sub(value & 1));
679    Ok(last.wrapping_add(delta))
680}
681
682fn write_triangle(
683    destination: &mut [u8],
684    written: &mut usize,
685    size: usize,
686    a: u32,
687    b: u32,
688    c: u32,
689) {
690    write_index(destination, *written, size, a);
691    write_index(destination, *written + size, size, b);
692    write_index(destination, *written + 2 * size, size, c);
693    *written += 3 * size;
694}
695
696fn write_index(destination: &mut [u8], offset: usize, size: usize, index: u32) {
697    if size == 2 {
698        destination[offset..offset + 2].copy_from_slice(&(index as u16).to_le_bytes());
699    } else {
700        destination[offset..offset + 4].copy_from_slice(&index.to_le_bytes());
701    }
702}
703
704/// Applies one of the reversible attribute filters in place.
705pub fn apply_filter(
706    data: &mut [u8],
707    filter: MeshoptFilter,
708    count: usize,
709    stride: usize,
710) -> Result<()> {
711    match filter {
712        MeshoptFilter::None => Ok(()),
713        MeshoptFilter::Octahedral => {
714            if stride != 4 && stride != 8 {
715                return Err(invalid("OCTAHEDRAL filter needs a 4 or 8 byte stride"));
716            }
717            filter_octahedral(data, count, stride);
718            Ok(())
719        }
720        MeshoptFilter::Quaternion => {
721            if stride != 8 {
722                return Err(invalid("QUATERNION filter needs an 8 byte stride"));
723            }
724            filter_quaternion(data, count);
725            Ok(())
726        }
727        MeshoptFilter::Exponential => {
728            if !stride.is_multiple_of(4) {
729                return Err(invalid("EXPONENTIAL filter needs a 4 byte aligned stride"));
730            }
731            filter_exponential(data, count * (stride / 4));
732            Ok(())
733        }
734        MeshoptFilter::Color => {
735            if stride != 4 && stride != 8 {
736                return Err(invalid("COLOR filter needs a 4 or 8 byte stride"));
737            }
738            filter_color(data, count, stride);
739            Ok(())
740        }
741    }
742}
743
744/// Recovers RGBA from the luma/chroma form the color filter stores.
745///
746/// Colors are kept as Y/Co/Cg with the per-vertex scale folded into alpha: the
747/// alpha channel's highest set bit gives the range the three chroma components
748/// were quantized against, so alpha carries both the scale and, one bit lower,
749/// the alpha value itself. Co and Cg are signed; Y and alpha are not.
750fn filter_color(data: &mut [u8], count: usize, stride: usize) {
751    let component = stride / 4;
752    let max = if component == 1 {
753        f32::from(u8::MAX)
754    } else {
755        f32::from(u16::MAX)
756    };
757    for i in 0..count {
758        let base = i * stride;
759        let unsigned =
760            |data: &[u8], index: usize| read_unsigned(data, base + index * component, component);
761        let signed =
762            |data: &[u8], index: usize| read_signed(data, base + index * component, component);
763
764        // Smear the highest set bit of alpha down: the result is the range the
765        // chroma components were quantized against.
766        let mut scale = unsigned(data, 3);
767        scale |= scale >> 1;
768        scale |= scale >> 2;
769        scale |= scale >> 4;
770        scale |= scale >> 8;
771
772        let y = unsigned(data, 0);
773        let co = signed(data, 1);
774        let cg = signed(data, 2);
775        let r = y + co - cg;
776        let g = y + cg;
777        let b = y - co - cg;
778
779        // Alpha gains the bit the scale took from it, so it spans the same
780        // range as the three colour components before scaling.
781        let alpha = unsigned(data, 3);
782        let a = ((alpha << 1) & scale) | (alpha & 1);
783
784        let factor = max / scale as f32;
785        let round = |value: i32| (value as f32 * factor + 0.5) as i32;
786        for (index, value) in [r, g, b, a].into_iter().enumerate() {
787            write_unsigned(data, base + index * component, component, round(value));
788        }
789    }
790}
791
792fn filter_octahedral(data: &mut [u8], count: usize, stride: usize) {
793    let component = stride / 4;
794    let max = if component == 1 {
795        f32::from(i8::MAX)
796    } else {
797        f32::from(i16::MAX)
798    };
799    for i in 0..count {
800        let base = i * stride;
801        let read = |index: usize| read_signed(data, base + index * component, component);
802        let x = read(0) as f32;
803        let y = read(1) as f32;
804        let z = read(2) as f32 - x.abs() - y.abs();
805
806        // Points below the octahedron's equator fold back over its edges.
807        let t = z.min(0.0);
808        let x = x + if x >= 0.0 { t } else { -t };
809        let y = y + if y >= 0.0 { t } else { -t };
810
811        let scale = max / (x * x + y * y + z * z).sqrt();
812        write_signed(data, base, component, round_signed(x * scale));
813        write_signed(data, base + component, component, round_signed(y * scale));
814        write_signed(
815            data,
816            base + 2 * component,
817            component,
818            round_signed(z * scale),
819        );
820    }
821}
822
823fn filter_quaternion(data: &mut [u8], count: usize) {
824    let scale = 32767.0 / 2.0f32.sqrt();
825    for i in 0..count {
826        let base = i * 8;
827        let input = [
828            read_signed(data, base, 2) as f32,
829            read_signed(data, base + 2, 2) as f32,
830            read_signed(data, base + 4, 2) as f32,
831        ];
832        let packed = read_signed(data, base + 6, 2);
833
834        // The low two bits name the dropped component; the rest is the scale.
835        let s = (packed | 3) as f32;
836        let ww = s * s * 2.0 - input[0] * input[0] - input[1] * input[1] - input[2] * input[2];
837        let w = ww.max(0.0).sqrt();
838        let ss = scale / s;
839
840        let component = (packed & 3) as usize;
841        for (axis, value) in input.iter().enumerate() {
842            let slot = (component + axis + 1) & 3;
843            write_signed(data, base + slot * 2, 2, round_signed(value * ss));
844        }
845        write_signed(data, base + component * 2, 2, round_signed(w * ss));
846    }
847}
848
849fn filter_exponential(data: &mut [u8], count: usize) {
850    for i in 0..count {
851        let base = i * 4;
852        let value =
853            u32::from_le_bytes([data[base], data[base + 1], data[base + 2], data[base + 3]]);
854        let mantissa = ((value << 8) as i32) >> 8;
855        let exponent = (value as i32) >> 24;
856        // ldexp(mantissa, exponent) without touching libm.
857        let scale = f32::from_bits(((exponent + 127) as u32) << 23);
858        data[base..base + 4].copy_from_slice(&(scale * mantissa as f32).to_bits().to_le_bytes());
859    }
860}
861
862fn read_signed(data: &[u8], offset: usize, size: usize) -> i32 {
863    if size == 1 {
864        i32::from(data[offset] as i8)
865    } else {
866        i32::from(i16::from_le_bytes([data[offset], data[offset + 1]]))
867    }
868}
869
870fn write_signed(data: &mut [u8], offset: usize, size: usize, value: i32) {
871    if size == 1 {
872        data[offset] = value as u8;
873    } else {
874        data[offset..offset + 2].copy_from_slice(&(value as i16).to_le_bytes());
875    }
876}
877
878fn read_unsigned(data: &[u8], offset: usize, size: usize) -> i32 {
879    if size == 1 {
880        i32::from(data[offset])
881    } else {
882        i32::from(u16::from_le_bytes([data[offset], data[offset + 1]]))
883    }
884}
885
886fn write_unsigned(data: &mut [u8], offset: usize, size: usize, value: i32) {
887    if size == 1 {
888        data[offset] = value as u8;
889    } else {
890        data[offset..offset + 2].copy_from_slice(&(value as u16).to_le_bytes());
891    }
892}
893
894fn round_signed(value: f32) -> i32 {
895    (value + if value >= 0.0 { 0.5 } else { -0.5 }) as i32
896}
897
898#[cfg(test)]
899mod tests {
900    use super::*;
901
902    /// Builds a version 0 vertex stream from raw per-byte delta planes.
903    ///
904    /// Every byte group is stored literally, which is the one encoding a test
905    /// can spell out without pulling in an encoder.
906    fn vertex_stream(planes: &[Vec<u8>], baseline: &[u8]) -> Vec<u8> {
907        let count = planes[0].len();
908        let aligned = (count + BYTE_GROUP_SIZE - 1) & !(BYTE_GROUP_SIZE - 1);
909        let mut stream = vec![VERTEX_HEADER];
910        for plane in planes {
911            let groups = aligned / BYTE_GROUP_SIZE;
912            let mut header = vec![0u8; groups.div_ceil(4)];
913            for group in 0..groups {
914                // Selector 3 is the literal 8-bit encoding in kBitsV0.
915                header[group / 4] |= 3 << ((group % 4) * 2);
916            }
917            stream.extend_from_slice(&header);
918            stream.extend_from_slice(plane);
919            stream.resize(stream.len() + aligned - count, 0);
920        }
921        // The baseline vertex sits at the very end of the padded tail.
922        stream.resize(stream.len() + TAIL_MIN_SIZE_V0 - baseline.len(), 0);
923        stream.extend_from_slice(baseline);
924        stream
925    }
926
927    fn zigzag(value: i8) -> u8 {
928        ((value << 1) ^ (value >> 7)) as u8
929    }
930
931    #[test]
932    fn vertex_deltas_accumulate_from_the_stream_tail() {
933        let planes = vec![
934            vec![zigzag(1), zigzag(2)],
935            vec![zigzag(0), zigzag(-1)],
936            vec![zigzag(0), zigzag(0)],
937            vec![zigzag(-4), zigzag(0)],
938        ];
939        let stream = vertex_stream(&planes, &[10, 20, 30, 40]);
940
941        let mut decoded = [0u8; 8];
942        decode_vertex_buffer(&mut decoded, 2, 4, &stream).unwrap();
943
944        // Each vertex is the previous one plus the unzigzagged delta, and the
945        // first vertex starts from the baseline stored in the tail.
946        assert_eq!(decoded, [11, 20, 30, 36, 13, 19, 30, 36]);
947    }
948
949    #[test]
950    fn vertex_stream_rejects_a_truncated_tail() {
951        let planes = vec![vec![0u8], vec![0], vec![0], vec![0]];
952        let mut stream = vertex_stream(&planes, &[1, 2, 3, 4]);
953        stream.truncate(stream.len() - 1);
954
955        let mut decoded = [0u8; 4];
956        assert!(decode_vertex_buffer(&mut decoded, 1, 4, &stream).is_err());
957    }
958
959    #[test]
960    fn index_buffer_decodes_a_restarted_triangle() {
961        // 0xfe selects the slow path with a full codeaux byte; a zero codeaux
962        // resets the index counter and emits the next three fresh indices.
963        let mut stream = vec![INDEX_HEADER | 1, 0xfe, 0x00];
964        stream.resize(stream.len() + 16, 0);
965
966        let mut decoded = [0u8; 6];
967        decode_index_buffer(&mut decoded, 3, 2, &stream).unwrap();
968
969        assert_eq!(decoded, [0, 0, 1, 0, 2, 0]);
970    }
971
972    #[test]
973    fn index_sequence_decodes_zigzag_deltas() {
974        // Each byte is (zigzag(delta) << 1) | baseline selector.
975        let stream = vec![SEQUENCE_HEADER, 0x00, 0x04, 0x04, 0, 0, 0, 0];
976
977        let mut decoded = [0u8; 12];
978        decode_index_sequence(&mut decoded, 3, 4, &stream).unwrap();
979
980        assert_eq!(
981            decoded,
982            [0, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0],
983            "sequence indices are delta coded against two baselines"
984        );
985    }
986
987    /// Expected values come from an independent transcription of the reference
988    /// `decodeFilterColor`, not from this port: alpha's highest set bit gives
989    /// the range Y/Co/Cg were quantized against, so the same bytes decode to
990    /// different colours depending on it, and only fixed numbers pin that.
991    ///
992    /// The third vertex of each case carries an alpha below the scale it
993    /// implies, which is the only arrangement where the one-bit expansion of
994    /// alpha changes the result. The reference scales in single precision, and
995    /// at 16 bits that is visible: the third red component lands on 52531 in
996    /// `f32` and on 52530 in double. The fourth carries an alpha whose highest
997    /// set bit stands eight places clear of the next one, which is the only
998    /// arrangement where the last step of the scale smear changes the range.
999    #[test]
1000    fn color_filter_recovers_rgba_from_luma_chroma() {
1001        // Three vertices at 8 bits per component: scales 15, 63 and 15.
1002        let mut narrow = vec![
1003            10u8,
1004            (-3i8) as u8,
1005            2,
1006            15,
1007            40,
1008            5,
1009            (-7i8) as u8,
1010            63,
1011            8,
1012            (-2i8) as u8,
1013            3,
1014            9,
1015        ];
1016        apply_filter(&mut narrow, MeshoptFilter::Color, 3, 4).unwrap();
1017        assert_eq!(
1018            narrow,
1019            [85, 204, 187, 255, 210, 134, 170, 255, 51, 187, 119, 51]
1020        );
1021
1022        // The same shape at 16 bits, where the scale smear needs its last step.
1023        let mut wide = Vec::new();
1024        for value in [
1025            800u16,
1026            (-100i16) as u16,
1027            60,
1028            1023,
1029            300,
1030            25,
1031            (-40i16) as u16,
1032            511,
1033            700,
1034            90,
1035            (-30i16) as u16,
1036            600,
1037            50000,
1038            (-3000i16) as u16,
1039            1000,
1040            32769,
1041        ] {
1042            wide.extend_from_slice(&value.to_le_bytes());
1043        }
1044        apply_filter(&mut wide, MeshoptFilter::Color, 4, 8).unwrap();
1045        let decoded: Vec<u16> = wide
1046            .chunks_exact(2)
1047            .map(|bytes| u16::from_le_bytes([bytes[0], bytes[1]]))
1048            .collect();
1049        assert_eq!(
1050            decoded,
1051            [
1052                40999, 55093, 53812, 65535, 46811, 33345, 40398, 65535, 52531, 42921, 40999, 11275,
1053                46000, 51000, 52000, 3
1054            ]
1055        );
1056    }
1057
1058    #[test]
1059    fn octahedral_filter_restores_unit_length_vectors() {
1060        let mut data = vec![0u8, 0, 127, 0, 64, 0, 63, 7];
1061        apply_filter(&mut data, MeshoptFilter::Octahedral, 2, 4).unwrap();
1062
1063        assert_eq!(&data[..4], &[0, 0, 127, 0]);
1064        let x = data[4] as i8 as f32;
1065        let y = data[5] as i8 as f32;
1066        let z = data[6] as i8 as f32;
1067        assert!(
1068            ((x * x + y * y + z * z).sqrt() - 127.0).abs() < 1.0,
1069            "decoded normal {x},{y},{z} is not unit length"
1070        );
1071        assert_eq!(data[7], 7, "the fourth component stays untouched");
1072    }
1073
1074    #[test]
1075    fn quaternion_filter_restores_the_dropped_component() {
1076        // The low two bits of the last component name the dropped axis.
1077        let mut data = vec![0u8, 0, 0, 0, 0, 0, 3, 0];
1078        apply_filter(&mut data, MeshoptFilter::Quaternion, 1, 8).unwrap();
1079
1080        let components: Vec<i16> = data
1081            .chunks_exact(2)
1082            .map(|bytes| i16::from_le_bytes([bytes[0], bytes[1]]))
1083            .collect();
1084        assert_eq!(components, [0, 0, 0, 32767]);
1085    }
1086
1087    #[test]
1088    fn exponential_filter_rebuilds_floats() {
1089        let mut data = Vec::new();
1090        // Mantissa 1 with exponent 0, then mantissa -3 with exponent 1.
1091        data.extend_from_slice(&1u32.to_le_bytes());
1092        data.extend_from_slice(&(((1i32 << 24) | 0x00fffffd) as u32).to_le_bytes());
1093        apply_filter(&mut data, MeshoptFilter::Exponential, 2, 4).unwrap();
1094
1095        let decoded: Vec<f32> = data
1096            .chunks_exact(4)
1097            .map(|bytes| f32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
1098            .collect();
1099        assert_eq!(decoded, [1.0, -6.0]);
1100    }
1101
1102    #[test]
1103    fn buffer_view_size_must_match_the_declared_layout() {
1104        let mut destination = [0u8; 7];
1105        let error = decode_buffer_view(
1106            &mut destination,
1107            &[],
1108            MeshoptMode::Attributes,
1109            MeshoptFilter::None,
1110            2,
1111            4,
1112        )
1113        .unwrap_err();
1114        assert!(matches!(error, GltfError::InvalidGltf(_)));
1115    }
1116}