Skip to main content

rgpre/import/
glide_splash_dll.rs

1//! Authored Windows Glide splash data, without executing the player's DLL.
2//! The resource and playback contracts are documented in formats/GLIDE-SPLASH-DLL.md.
3
4use super::glide_splash::{SplashFace, SplashMesh, SplashTexture, SplashVertex};
5use crate::error::Error;
6
7/// Installed Windows splash, with timed frames decoded from its command stream.
8#[derive(Debug, Clone)]
9pub struct WindowsGlideSplash {
10    pub frames_per_second: u32,
11    pub meshes: Vec<SplashMesh>,
12    /// Geometry flag bit 3 selects sphere mapping.
13    pub sphere_mapped: Vec<bool>,
14    pub textures: Vec<SplashTexture>,
15    pub frames: Vec<Vec<WindowsSplashDraw>>,
16    /// Untimed final redraws before the fade-out command.
17    pub final_draws: Vec<WindowsSplashDraw>,
18}
19
20/// Complete authored inputs for one draw. Matrices are column-major; FOV is radians.
21#[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
84/// Extracts the unique ANIM/101 resource from a PE32 DLL, through its section map.
85pub 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}