Skip to main content

plasma_prp/resource/
vert_decoder.rs

1//! plVertCoder — decode compressed vertex data from .prp files
2//!
3//! Direct translation of plVertCoder.cpp from the Plasma C++ source.
4//! Each function cites the exact C++ function it translates.
5//!
6//! C++ source: Plasma/Sources/Plasma/PubUtilLib/plDrawable/plVertCoder.cpp
7//! C++ header: Plasma/Sources/Plasma/PubUtilLib/plDrawable/plVertCoder.h
8
9use anyhow::Result;
10use std::io::Read;
11use super::prp::PlasmaRead;
12
13// C++ plVertCoder.cpp lines 50-52
14const K_POS_QUANTUM: f32 = 1.0 / (1 << 10) as f32;
15const K_WEIGHT_QUANTUM: f32 = 1.0 / (1 << 15) as f32;
16const K_UVW_QUANTUM: f32 = 1.0 / (1u32 << 16) as f32;
17
18// C++ plVertCoder.h lines 51-55
19const K_POSITION: usize = 0;
20const K_WEIGHT: usize = 1;
21const K_UVW: usize = 2;
22const K_NUM_FLOAT_FIELDS: usize = K_UVW + 8;
23
24// C++ plVertCoder.cpp lines 59-73
25const K_QUANTA: [f32; K_NUM_FLOAT_FIELDS] = [
26    K_POS_QUANTUM,
27    K_WEIGHT_QUANTUM,
28    K_UVW_QUANTUM, K_UVW_QUANTUM, K_UVW_QUANTUM, K_UVW_QUANTUM,
29    K_UVW_QUANTUM, K_UVW_QUANTUM, K_UVW_QUANTUM, K_UVW_QUANTUM,
30];
31
32// C++ plVertCoder.cpp line 275
33const K_SAME_MASK: u16 = 0x8000;
34
35// C++ plGBufferGroup.h lines 156-169
36const K_UV_COUNT_MASK: u8 = 0x0f;
37const K_SKIN_WEIGHT_MASK: u8 = 0x30;
38const K_SKIN_INDICES: u8 = 0x40;
39
40// C++ plVertCoder.h lines 60-66
41struct FloatCode {
42    offset: f32,
43    all_same: bool,
44    count: u16,
45}
46
47// C++ plVertCoder.h lines 70-76
48struct ByteCode {
49    count: u16,
50    val: u8,
51    same: bool,
52}
53
54/// Direct translation of C++ plVertCoder class
55pub struct VertDecoder {
56    // C++ plVertCoder.h line 68
57    floats: [[FloatCode; 3]; K_NUM_FLOAT_FIELDS],
58    // C++ plVertCoder.h line 78
59    colors: [ByteCode; 4],
60}
61
62impl VertDecoder {
63    // C++ plVertCoder.cpp lines 459-462
64    pub fn new() -> Self {
65        Self {
66            floats: std::array::from_fn(|_| std::array::from_fn(|_| FloatCode {
67                offset: 0.0, all_same: false, count: 0,
68            })),
69            colors: std::array::from_fn(|_| ByteCode {
70                count: 0, val: 0, same: false,
71            }),
72        }
73    }
74
75    // C++ plVertCoder.cpp lines 126-136: IReadFloat
76    fn i_read_float(reader: &mut impl Read, dst: &mut [u8], pos: &mut usize,
77                    offset: f32, quantum: f32) -> Result<()> {
78        let ival = reader.read_u16()?;
79        let fval = ival as f32 * quantum + offset;
80        dst[*pos..*pos + 4].copy_from_slice(&fval.to_le_bytes());
81        *pos += 4;
82        Ok(())
83    }
84
85    // C++ plVertCoder.cpp lines 157-175: IDecodeFloat
86    fn i_decode_float(&mut self, reader: &mut impl Read, field: usize, chan: usize,
87                      dst: &mut [u8], pos: &mut usize) -> Result<()> {
88        if self.floats[field][chan].count == 0 {
89            self.floats[field][chan].offset = reader.read_f32()?;
90            self.floats[field][chan].all_same = reader.read_u8()? != 0;
91            self.floats[field][chan].count = reader.read_u16()?;
92        }
93        if !self.floats[field][chan].all_same {
94            Self::i_read_float(reader, dst, pos, self.floats[field][chan].offset, K_QUANTA[field])?;
95        } else {
96            let bytes = self.floats[field][chan].offset.to_le_bytes();
97            dst[*pos..*pos + 4].copy_from_slice(&bytes);
98            *pos += 4;
99        }
100        self.floats[field][chan].count -= 1;
101        Ok(())
102    }
103
104    // C++ plVertCoder.cpp lines 201-218: IDecodeNormal
105    fn i_decode_normal(reader: &mut impl Read, dst: &mut [u8], pos: &mut usize) -> Result<()> {
106        // 3 encoded bytes → 3 floats in [-1, 1]
107        for _ in 0..3 {
108            let ix = reader.read_u8()?;
109            let x = (ix as f32 / 255.9 - 0.5) * 2.0;
110            dst[*pos..*pos + 4].copy_from_slice(&x.to_le_bytes());
111            *pos += 4;
112        }
113        Ok(())
114    }
115
116    // C++ plVertCoder.cpp lines 299-324: IDecodeByte
117    fn i_decode_byte(&mut self, reader: &mut impl Read, chan: usize,
118                     dst: &mut [u8], pos: &mut usize) -> Result<()> {
119        if self.colors[chan].count == 0 {
120            let cnt = reader.read_u16()?;
121            if cnt & K_SAME_MASK != 0 {
122                self.colors[chan].same = true;
123                self.colors[chan].val = reader.read_u8()?;
124                self.colors[chan].count = cnt & !K_SAME_MASK;
125            } else {
126                self.colors[chan].same = false;
127                self.colors[chan].count = cnt;
128            }
129        }
130        if !self.colors[chan].same {
131            dst[*pos] = reader.read_u8()?;
132        } else {
133            dst[*pos] = self.colors[chan].val;
134        }
135        *pos += 1;
136        self.colors[chan].count -= 1;
137        Ok(())
138    }
139
140    // C++ plVertCoder.cpp lines 334-340: IDecodeColor
141    fn i_decode_color(&mut self, reader: &mut impl Read,
142                      dst: &mut [u8], pos: &mut usize) -> Result<()> {
143        self.i_decode_byte(reader, 0, dst, pos)?;
144        self.i_decode_byte(reader, 1, dst, pos)?;
145        self.i_decode_byte(reader, 2, dst, pos)?;
146        self.i_decode_byte(reader, 3, dst, pos)?;
147        Ok(())
148    }
149
150    // C++ plVertCoder.cpp lines 177-180: INumWeights
151    fn i_num_weights(format: u8) -> usize {
152        ((format & K_SKIN_WEIGHT_MASK) >> 4) as usize
153    }
154
155    // C++ plVertCoder.cpp lines 381-420: IDecode
156    fn i_decode(&mut self, reader: &mut impl Read, dst: &mut [u8], pos: &mut usize,
157                format: u8) -> Result<()> {
158        // Position (3 floats)
159        self.i_decode_float(reader, K_POSITION, 0, dst, pos)?;
160        self.i_decode_float(reader, K_POSITION, 1, dst, pos)?;
161        self.i_decode_float(reader, K_POSITION, 2, dst, pos)?;
162
163        // Weights
164        let num_weights = Self::i_num_weights(format);
165        for j in 0..num_weights {
166            self.i_decode_float(reader, K_WEIGHT, j, dst, pos)?;
167        }
168        // C++ ref: plVertCoder.cpp:395-400 — kSkinIndices checked independently
169        if format & K_SKIN_INDICES != 0 {
170            let idx = reader.read_u32()?;
171            dst[*pos..*pos + 4].copy_from_slice(&idx.to_le_bytes());
172            *pos += 4;
173        }
174
175        // Normal
176        Self::i_decode_normal(reader, dst, pos)?;
177
178        // Color (diffuse)
179        self.i_decode_color(reader, dst, pos)?;
180
181        // COLOR2 (specular) — zeroed
182        dst[*pos..*pos + 4].copy_from_slice(&[0u8; 4]);
183        *pos += 4;
184
185        // UVWs
186        let num_uvws = (format & K_UV_COUNT_MASK) as usize;
187        for i in 0..num_uvws {
188            self.i_decode_float(reader, K_UVW + i, 0, dst, pos)?;
189            self.i_decode_float(reader, K_UVW + i, 1, dst, pos)?;
190            self.i_decode_float(reader, K_UVW + i, 2, dst, pos)?;
191        }
192
193        Ok(())
194    }
195
196    // C++ plVertCoder.cpp lines 422-429: Read
197    pub fn read(&mut self, reader: &mut impl Read, format: u8, stride: usize,
198                num_verts: u16) -> Result<Vec<u8>> {
199        // C++ calls Clear() at start — we start fresh (new instance)
200        let total_size = num_verts as usize * stride;
201        let mut data = vec![0u8; total_size];
202
203        for i in 0..num_verts as usize {
204            let mut pos = i * stride;
205            if let Err(e) = self.i_decode(reader, &mut data, &mut pos, format) {
206                if i == 0 {
207                    return Err(e);
208                }
209                log::warn!("Vertex decode failed at vertex {}/{}: {}, returning partial data",
210                           i, num_verts, e);
211                data.truncate(i * stride);
212                return Ok(data);
213            }
214        }
215
216        Ok(data)
217    }
218}
219
220/// Compute vertex stride for a buffer group format byte.
221/// Direct translation of C++ plGBufferGroup::ICalcVertexSize (lines 307-333).
222///
223/// The stride ALWAYS includes 8 bytes for diffuse+specular colors,
224/// regardless of whether the data is compressed or uncompressed.
225/// For uncompressed data, colors are stored in a SEPARATE buffer but
226/// the stride still accounts for them in the interleaved layout.
227pub fn calc_vertex_stride(format: u8) -> usize {
228    // C++ line 312: pos + normal
229    let mut size: usize = std::mem::size_of::<f32>() * (3 + 3); // 24
230
231    // C++ line 313: UVs
232    let num_uvs = (format & K_UV_COUNT_MASK) as usize;
233    size += std::mem::size_of::<f32>() * 3 * num_uvs;
234
235    // C++ lines 315-328: weights and indices
236    let num_weights = ((format & K_SKIN_WEIGHT_MASK) >> 4) as usize;
237    if num_weights > 0 {
238        size += std::mem::size_of::<f32>() * num_weights;
239        if format & K_SKIN_INDICES != 0 {
240            size += std::mem::size_of::<u32>();
241        }
242    }
243
244    // C++ line 331: ALWAYS add diffuse + specular (8 bytes)
245    // This is the key difference from the old code which only added colors
246    // for compressed data. The C++ ALWAYS includes colors in the stride.
247    size += std::mem::size_of::<u32>() * 2;
248
249    size
250}