1use super::glide_splash::{SplashFace, SplashMesh, SplashTexture, SplashVertex};
5use crate::error::Error;
6
7#[derive(Debug, Clone)]
9pub struct WindowsGlideSplash {
10 pub frames_per_second: u32,
11 pub meshes: Vec<SplashMesh>,
12 pub sphere_mapped: Vec<bool>,
14 pub textures: Vec<SplashTexture>,
15 pub frames: Vec<Vec<WindowsSplashDraw>>,
16 pub final_draws: Vec<WindowsSplashDraw>,
18}
19
20#[derive(Debug, Clone)]
22pub struct WindowsSplashDraw {
23 pub mesh: usize,
24 pub texture: usize,
25 pub transform: [f32; 16],
26 pub camera: [f32; 16],
27 pub horizontal_fov: f32,
28 pub light_color: [f32; 3],
29 pub light_position: [f32; 3],
30 pub alpha: u8,
31}
32
33fn invalid(message: &str) -> Error {
34 Error::Parse(format!("Windows Glide splash: {message}"))
35}
36
37struct Reader<'a> {
38 data: &'a [u8],
39 position: usize,
40}
41
42impl<'a> Reader<'a> {
43 fn take(&mut self, length: usize) -> Result<&'a [u8], Error> {
44 let end = self
45 .position
46 .checked_add(length)
47 .ok_or_else(|| invalid("range overflow"))?;
48 let result = self
49 .data
50 .get(self.position..end)
51 .ok_or_else(|| invalid("truncated data"))?;
52 self.position = end;
53 Ok(result)
54 }
55
56 fn word(&mut self) -> Result<u32, Error> {
57 Ok(u32::from_le_bytes(self.take(4)?.try_into().unwrap()))
58 }
59
60 fn count(&mut self, size: usize) -> Result<usize, Error> {
61 let count = self.word()? as usize;
62 if count > self.data.len().saturating_sub(self.position) / size {
63 return Err(invalid("count exceeds remaining resource"));
64 }
65 Ok(count)
66 }
67
68 fn floats<const N: usize>(&mut self) -> Result<[f32; N], Error> {
69 let mut result = [0.0; N];
70 for value in &mut result {
71 *value = f32::from_bits(self.word()?);
72 if !value.is_finite() {
73 return Err(invalid("non-finite authored value"));
74 }
75 }
76 Ok(result)
77 }
78}
79
80fn at_word(data: &[u8], position: usize) -> Result<u32, Error> {
81 Reader { data, position }.word()
82}
83
84pub fn parse_windows_glide_splash(data: &[u8]) -> Result<WindowsGlideSplash, Error> {
86 if data.get(..2) != Some(b"MZ") {
87 return Err(invalid("missing DOS header"));
88 }
89 let pe = at_word(data, 60)? as usize;
90 if data.get(pe..pe.saturating_add(4)) != Some(b"PE\0\0") {
91 return Err(invalid("missing PE header"));
92 }
93 let mut header = Reader {
94 data,
95 position: pe + 6,
96 };
97 let sections = u16::from_le_bytes(header.take(2)?.try_into().unwrap()) as usize;
98 header.position = pe + 20;
99 let optional_size = u16::from_le_bytes(header.take(2)?.try_into().unwrap()) as usize;
100 let optional = pe + 24;
101 if data.get(optional..optional + 2) != Some(&[0x0b, 0x01]) || optional_size < 120 {
102 return Err(invalid("unsupported PE optional header"));
103 }
104 let table = optional + optional_size;
105 let rva_to_file = |rva: u32, length: usize| -> Result<usize, Error> {
106 for section in 0..sections {
107 let entry = table + section * 40;
108 let virtual_start = at_word(data, entry + 12)?;
109 let raw_size = at_word(data, entry + 16)? as usize;
110 let raw_start = at_word(data, entry + 20)? as usize;
111 if let Some(delta) = rva.checked_sub(virtual_start)
112 && (delta as usize)
113 .checked_add(length)
114 .is_some_and(|end| end <= raw_size)
115 {
116 let start = raw_start
117 .checked_add(delta as usize)
118 .ok_or_else(|| invalid("section overflow"))?;
119 Reader {
120 data,
121 position: start,
122 }
123 .take(length)?;
124 return Ok(start);
125 }
126 }
127 Err(invalid("resource RVA is outside initialized sections"))
128 };
129 let resource_rva = at_word(data, optional + 112)?;
130 let resource_size = at_word(data, optional + 116)? as usize;
131 let start = rva_to_file(resource_rva, resource_size)?;
132 let resource = &data[start..start + resource_size];
133 let entries = |offset: usize| -> Result<Vec<(u32, u32)>, Error> {
134 let mut r = Reader {
135 data: resource,
136 position: offset + 12,
137 };
138 let counts = r.take(4)?;
139 let count = usize::from(u16::from_le_bytes(counts[..2].try_into().unwrap()))
140 + usize::from(u16::from_le_bytes(counts[2..].try_into().unwrap()));
141 r.take(count * 8)?
142 .as_chunks::<8>()
143 .0
144 .iter()
145 .map(|entry| Ok((at_word(entry, 0)?, at_word(entry, 4)?)))
146 .collect()
147 };
148 let mut matches = Vec::new();
149 for (name, directory) in entries(0)? {
150 if name & 0x80000000 == 0 {
151 continue;
152 }
153 let mut name_reader = Reader {
154 data: resource,
155 position: (name & 0x7fffffff) as usize,
156 };
157 let length = u16::from_le_bytes(name_reader.take(2)?.try_into().unwrap()) as usize;
158 let name = name_reader.take(length * 2)?;
159 if name != [b'A', 0, b'N', 0, b'I', 0, b'M', 0] {
160 continue;
161 }
162 if directory & 0x80000000 == 0 {
163 return Err(invalid("ANIM is not a resource directory"));
164 }
165 for (id, languages) in entries((directory & 0x7fffffff) as usize)? {
166 if id != 101 {
167 continue;
168 }
169 if languages & 0x80000000 == 0 {
170 return Err(invalid("ANIM/101 has no language directory"));
171 }
172 for (_, leaf) in entries((languages & 0x7fffffff) as usize)? {
173 if leaf & 0x80000000 != 0 {
174 return Err(invalid("unexpected resource nesting"));
175 }
176 let rva = at_word(resource, leaf as usize)?;
177 let length = at_word(resource, leaf as usize + 4)? as usize;
178 let start = rva_to_file(rva, length)?;
179 matches.push(&data[start..start + length]);
180 }
181 }
182 }
183 let [resource] = matches.as_slice() else {
184 return Err(invalid("missing or ambiguous ANIM/101 resource"));
185 };
186 parse_animation(resource)
187}
188
189fn parse_animation(data: &[u8]) -> Result<WindowsGlideSplash, Error> {
190 let mut r = Reader { data, position: 0 };
191 if r.take(24)? != b"3Dfx Splash Version 1.2\n" {
192 return Err(invalid("unsupported ANIM version"));
193 }
194 let frame_count = r.word()? as usize;
195 let frames_per_second = r.word()?;
196 if frame_count != 90 || frames_per_second != 30 {
197 return Err(invalid("unsupported animation cadence"));
198 }
199 let mut textures = Vec::new();
200 for _ in 0..r.count(24)? {
201 let small = r.word()?;
202 let large = r.word()?;
203 let aspect = r.word()?;
204 let format = r.word()?;
205 r.word()?;
206 if small != 8 || large > 8 || aspect != 3 || format != 10 {
207 return Err(invalid("unsupported texture descriptor"));
208 }
209 let side = 256u32 >> large;
210 let length = r.count(1)?;
211 let pixels = r.take(length)?;
212 if length < (side * side * 2) as usize {
213 return Err(invalid("truncated texture mip chain"));
214 }
215 let rgba = pixels[..(side * side * 2) as usize]
216 .as_chunks::<2>()
217 .0
218 .iter()
219 .flat_map(|p| {
220 let p = u16::from_le_bytes(*p);
221 [
222 ((p >> 11) * 255 / 31) as u8,
223 (((p >> 5) & 63) * 255 / 63) as u8,
224 ((p & 31) * 255 / 31) as u8,
225 255,
226 ]
227 })
228 .collect();
229 textures.push(SplashTexture {
230 width: side,
231 height: side,
232 rgba,
233 });
234 }
235 let mut materials = Vec::new();
236 for _ in 0..r.count(4)? {
237 if r.word()? != 1 {
238 return Err(invalid("unsupported material group"));
239 }
240 r.floats::<6>()?;
241 let texture = r.word()? as usize;
242 if texture >= textures.len() {
243 return Err(invalid("material texture index is out of range"));
244 }
245 materials.push(texture);
246 }
247 let slots = r.count(4)?;
248 let mut meshes = vec![None; slots];
249 let mut sphere_mapped = vec![false; slots];
250 loop {
251 let slot = r.word()?;
252 if slot == u32::MAX {
253 break;
254 }
255 let slot = slot as usize;
256 if slot >= slots || meshes[slot].is_some() {
257 return Err(invalid("invalid or repeated geometry slot"));
258 }
259 let flags = r.word()?;
260 if flags & !8 != 0 {
261 return Err(invalid("unsupported geometry flags"));
262 }
263 let mut faces = Vec::new();
264 for _ in 0..r.count(12)? {
265 faces.push(SplashFace {
266 vertices: [r.word()? as usize, r.word()? as usize, r.word()? as usize],
267 material: 0,
268 antialiased_edges: 7,
269 });
270 }
271 let mut vertices = Vec::new();
272 for _ in 0..r.count(24)? {
273 let v = r.floats::<6>()?;
274 if v[3..].iter().all(|value| *value == 0.0) {
275 return Err(invalid("zero vertex normal"));
276 }
277 vertices.push(SplashVertex {
278 position: v[..3].try_into().unwrap(),
279 normal: v[3..].try_into().unwrap(),
280 uv: [0.0; 2],
281 });
282 }
283 if faces
284 .iter()
285 .any(|face| face.vertices.iter().any(|index| *index >= vertices.len()))
286 {
287 return Err(invalid("triangle vertex index is out of range"));
288 }
289 for index in 0..r.count(8)? {
290 let uv = r.floats()?;
291 if let Some(vertex) = vertices.get_mut(index) {
292 vertex.uv = uv;
293 }
294 }
295 meshes[slot] = Some(SplashMesh { vertices, faces });
296 sphere_mapped[slot] = flags & 8 != 0;
297 }
298 let meshes = meshes
299 .into_iter()
300 .collect::<Option<Vec<_>>>()
301 .ok_or_else(|| invalid("unfilled geometry slot"))?;
302 let mut frames = Vec::new();
303 let mut draws = Vec::new();
304 let mut camera = None;
305 let mut fov = 0.0;
306 let mut texture = None;
307 let mut light_color = [0.0; 3];
308 let mut light_position = [0.0; 3];
309 let mut alpha = 255;
310 let mut finished = false;
311 while r.position < data.len() {
312 match r.take(1)?[0] {
313 2 => frames.push(std::mem::take(&mut draws)),
314 3 => {
315 let mesh = r.word()? as usize;
316 r.word()?;
317 let transform = r.floats()?;
318 if mesh >= meshes.len() {
319 return Err(invalid("draw geometry index is out of range"));
320 }
321 draws.push(WindowsSplashDraw {
322 mesh,
323 texture: texture.ok_or_else(|| invalid("draw has no material"))?,
324 transform,
325 camera: camera.ok_or_else(|| invalid("draw has no camera"))?,
326 horizontal_fov: fov,
327 light_color,
328 light_position,
329 alpha,
330 });
331 }
332 4 => {
333 fov = r.floats::<1>()?[0];
334 if !(0.0..std::f32::consts::PI).contains(&fov) || fov == 0.0 {
335 return Err(invalid("invalid camera field of view"));
336 }
337 camera = Some(r.floats()?);
338 }
339 5 => {
340 finished = true;
341 break;
342 }
343 6 => {
344 let group = r.word()? as usize;
345 texture = Some(
346 *materials
347 .get(group)
348 .ok_or_else(|| invalid("material group index is out of range"))?,
349 );
350 }
351 7 => {
352 let v = r.floats::<7>()?;
353 if v[..3] != [0.0; 3] {
354 return Err(invalid("unsupported directional light color"));
355 }
356 }
357 8 => {
358 if r.take(1)? != [1] {
359 return Err(invalid("unsupported auxiliary toggle"));
360 }
361 }
362 9 => {
363 let value = r.word()?;
364 alpha = if value == 0 || value == 0xff000000 {
365 255
366 } else {
367 (value >> 24) as u8
368 };
369 }
370 11..=13 => {}
371 14 => {
372 let v = r.floats::<7>()?;
373 light_color = v[..3].try_into().unwrap();
374 light_position = v[4..].try_into().unwrap();
375 }
376 _ => return Err(invalid("unsupported animation command")),
377 }
378 }
379 if !finished || r.position != data.len() || frames.len() != frame_count {
380 return Err(invalid("incomplete animation command stream"));
381 }
382 Ok(WindowsGlideSplash {
383 frames_per_second,
384 meshes,
385 sphere_mapped,
386 textures,
387 frames,
388 final_draws: draws,
389 })
390}
391
392#[cfg(test)]
393mod tests {
394 use super::*;
395
396 fn animation() -> Vec<u8> {
397 let mut bytes = b"3Dfx Splash Version 1.2\n".to_vec();
398 for value in [90u32, 30, 1, 8, 8, 3, 10, 0, 2] {
399 bytes.extend(value.to_le_bytes());
400 }
401 bytes.extend(0xf800u16.to_le_bytes());
402 for value in [1u32, 1] {
403 bytes.extend(value.to_le_bytes());
404 }
405 for value in [1.0f32; 6] {
406 bytes.extend(value.to_le_bytes());
407 }
408 for value in [0u32, 1, 0, 0, 1, 0, 1, 2, 3] {
409 bytes.extend(value.to_le_bytes());
410 }
411 for position in [[0.0f32, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]] {
412 for value in position.into_iter().chain([0.0, 0.0, 1.0]) {
413 bytes.extend(value.to_le_bytes());
414 }
415 }
416 bytes.extend(3u32.to_le_bytes());
417 for value in [0.0f32, 0.0, 1.0, 0.0, 0.0, 1.0] {
418 bytes.extend(value.to_le_bytes());
419 }
420 bytes.extend(u32::MAX.to_le_bytes());
421 bytes.push(4);
422 bytes.extend(1.0f32.to_le_bytes());
423 for value in [
424 1.0f32, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
425 ] {
426 bytes.extend(value.to_le_bytes());
427 }
428 bytes.push(6);
429 bytes.extend(0u32.to_le_bytes());
430 bytes.push(3);
431 bytes.extend([0u8; 8]);
432 for value in [
433 1.0f32, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
434 ] {
435 bytes.extend(value.to_le_bytes());
436 }
437 bytes.extend([2u8; 90]);
438 bytes.push(5);
439 bytes
440 }
441
442 #[test]
443 fn decodes_authored_triangle_texture_and_draw() {
444 let splash = parse_animation(&animation()).unwrap();
445 assert_eq!(splash.textures[0].rgba, [255, 0, 0, 255]);
446 assert_eq!(splash.meshes[0].faces[0].vertices, [0, 1, 2]);
447 assert_eq!(splash.meshes[0].vertices[2].position, [0.0, 1.0, 0.0]);
448 assert_eq!(splash.meshes[0].vertices[2].uv, [0.0, 1.0]);
449 assert_eq!(splash.frames[0][0].mesh, 0);
450 assert_eq!(splash.frames[0][0].horizontal_fov, 1.0);
451 assert_eq!(splash.frames.len(), 90);
452 }
453
454 #[test]
455 fn rejects_every_truncation_of_an_animation() {
456 let bytes = animation();
457 for length in 0..bytes.len() {
458 assert!(
459 parse_animation(&bytes[..length]).is_err(),
460 "length {length}"
461 );
462 }
463 }
464}