Skip to main content

rgpre/import/
glide_splash.rs

1//! Authored splash geometry, transforms, and textures in the GOG Glide overlay.
2//! This reads data tables, without executing or translating executable code.
3
4use crate::error::Error;
5
6#[derive(Debug, Clone)]
7pub struct SplashVertex {
8    pub position: [f32; 3],
9    pub normal: [f32; 3],
10    /// Glide texture coordinates, measured on a 256-unit square.
11    pub uv: [f32; 2],
12}
13
14#[derive(Debug, Clone)]
15pub struct SplashFace {
16    pub vertices: [usize; 3],
17    pub material: u32,
18    pub antialiased_edges: u32,
19}
20
21#[derive(Debug, Clone)]
22pub struct SplashMesh {
23    pub vertices: Vec<SplashVertex>,
24    pub faces: Vec<SplashFace>,
25}
26
27#[derive(Debug, Clone)]
28pub struct SplashTexture {
29    pub width: u32,
30    pub height: u32,
31    /// Largest mip level, in row-major RGBA order. Intensity textures have equal RGB channels and opaque alpha.
32    pub rgba: Vec<u8>,
33}
34
35#[derive(Debug, Clone)]
36pub struct GlideSplash {
37    /// Shield, logo, and outer shield, in that order.
38    pub meshes: [SplashMesh; 3],
39    /// Column-major affine matrices, one per mesh in each authored frame.
40    pub frames: Vec<[[f32; 16]; 3]>,
41    /// Logo color, highlight intensity, and shadow intensity textures.
42    pub textures: [SplashTexture; 3],
43}
44
45fn invalid(message: &str) -> Error {
46    Error::Parse(format!("Glide splash: {message}"))
47}
48
49fn bytes(data: &[u8], start: usize, length: usize) -> Result<&[u8], Error> {
50    data.get(
51        start
52            ..start
53                .checked_add(length)
54                .ok_or_else(|| invalid("range overflow"))?,
55    )
56    .ok_or_else(|| invalid("truncated data"))
57}
58
59fn word(data: &[u8], start: usize) -> Result<u32, Error> {
60    Ok(u32::from_le_bytes(
61        bytes(data, start, 4)?.try_into().unwrap(),
62    ))
63}
64
65fn floats<const N: usize>(data: &[u8], start: usize) -> Result<[f32; N], Error> {
66    let mut result = [0.0; N];
67    for (i, value) in result.iter_mut().enumerate() {
68        *value = f32::from_bits(word(data, start + i * 4)?);
69        if !value.is_finite() {
70            return Err(invalid("non-finite geometry or transform"));
71        }
72    }
73    Ok(result)
74}
75
76fn unique(data: &[u8], words: &[u32]) -> Result<usize, Error> {
77    let pattern: Vec<_> = words.iter().flat_map(|value| value.to_le_bytes()).collect();
78    let mut matches = data
79        .windows(pattern.len())
80        .enumerate()
81        .filter(|(_, bytes)| *bytes == pattern);
82    let first = matches
83        .next()
84        .ok_or_else(|| invalid("unsupported data-table layout"))?
85        .0;
86    if matches.next().is_some() {
87        return Err(invalid("ambiguous data-table layout"));
88    }
89    Ok(first)
90}
91
92/// Extracts the splash in the GOG `DOSBOX/glide2x_emu.ovl` LE overlay.
93/// Other overlay revisions return an unsupported-layout error. Pointer values
94/// are interpreted relative to their LE data object, never as host pointers.
95pub fn parse_glide_splash(data: &[u8]) -> Result<GlideSplash, Error> {
96    if bytes(data, 0, 2)? != b"MZ" {
97        return Err(invalid("missing DOS header"));
98    }
99    let le = word(data, 60)? as usize;
100    if bytes(data, le, 4)? != b"LE\0\0" {
101        return Err(invalid("unsupported executable header"));
102    }
103    let page_size = word(data, le + 0x28)? as usize;
104    let object_table = le + word(data, le + 0x40)? as usize;
105    let object_count = word(data, le + 0x44)? as usize;
106    let page_table = le + word(data, le + 0x48)? as usize;
107    let file_pages = word(data, le + 0x80)? as usize;
108    if !(1..=64).contains(&object_count) || page_size != 4096 {
109        return Err(invalid("unsupported LE object/page layout"));
110    }
111    let mut candidates = Vec::new();
112    for object in 0..object_count {
113        let entry = object_table + object * 24;
114        let length = word(data, entry)? as usize;
115        let flags = word(data, entry + 8)?;
116        if flags & 2 == 0 {
117            continue;
118        }
119        if length > 8 * 1024 * 1024 {
120            return Err(invalid("data object is too large"));
121        }
122        let first_page = word(data, entry + 12)? as usize;
123        let page_count = word(data, entry + 16)? as usize;
124        if first_page == 0 || page_count > length.div_ceil(page_size) {
125            return Err(invalid("invalid data-object page range"));
126        }
127        let mut image = vec![0; length];
128        for page in 0..page_count {
129            let map = bytes(data, page_table + (first_page - 1 + page) * 4, 4)?;
130            if map[3] != 0 {
131                return Err(invalid("unsupported encoded LE page"));
132            }
133            let number =
134                (usize::from(map[0]) << 16) | (usize::from(map[1]) << 8) | usize::from(map[2]);
135            if number == 0 {
136                return Err(invalid("invalid LE file-page number"));
137            }
138            let size = if number == word(data, le + 0x14)? as usize {
139                word(data, le + 0x2c)? as usize
140            } else {
141                page_size
142            };
143            if size > page_size || page * page_size + size > image.len() {
144                return Err(invalid("invalid LE page size"));
145            }
146            image[page * page_size..page * page_size + size].copy_from_slice(bytes(
147                data,
148                file_pages + (number - 1) * page_size,
149                size,
150            )?);
151        }
152        if let Ok(root) = unique(&image, &[68, 1694, 34]) {
153            candidates.push((image, root));
154        }
155    }
156    if candidates.len() != 1 {
157        return Err(invalid("unsupported or ambiguous splash object"));
158    }
159    let (image, counts) = candidates.pop().unwrap();
160    let root = counts
161        .checked_sub(12)
162        .ok_or_else(|| invalid("invalid mesh-table location"))?;
163    if [
164        word(&image, root + 36)?,
165        word(&image, root + 40)?,
166        word(&image, root + 44)?,
167        word(&image, root + 48)?,
168    ] != [100, 1606, 32, 75]
169    {
170        return Err(invalid("unsupported splash mesh/frame counts"));
171    }
172    let mut meshes = Vec::new();
173    for mesh in 0..3 {
174        let vertex_start = word(&image, root + mesh * 4)? as usize;
175        let vertex_count = word(&image, root + 12 + mesh * 4)? as usize;
176        let face_start = word(&image, root + 24 + mesh * 4)? as usize;
177        let face_count = word(&image, root + 36 + mesh * 4)? as usize;
178        let vertices = (0..vertex_count)
179            .map(|i| {
180                let values = floats::<8>(&image, vertex_start + i * 32)?;
181                Ok(SplashVertex {
182                    position: values[..3].try_into().unwrap(),
183                    normal: values[3..6].try_into().unwrap(),
184                    uv: values[6..].try_into().unwrap(),
185                })
186            })
187            .collect::<Result<Vec<_>, Error>>()?;
188        let faces = (0..face_count)
189            .map(|i| {
190                let start = face_start + i * 20;
191                let vertices = [
192                    word(&image, start)? as usize,
193                    word(&image, start + 4)? as usize,
194                    word(&image, start + 8)? as usize,
195                ];
196                let material = word(&image, start + 12)?;
197                let antialiased_edges = word(&image, start + 16)?;
198                if vertices.iter().any(|index| *index >= vertex_count)
199                    || material > 4
200                    || antialiased_edges > 7
201                {
202                    return Err(invalid("invalid face index, material, or edge flags"));
203                }
204                Ok(SplashFace {
205                    vertices,
206                    material,
207                    antialiased_edges,
208                })
209            })
210            .collect::<Result<Vec<_>, Error>>()?;
211        meshes.push(SplashMesh { vertices, faces });
212    }
213    let frames = (0..75)
214        .map(|frame| {
215            let start = root + 52 + frame * 192;
216            Ok([
217                floats(&image, start)?,
218                floats(&image, start + 64)?,
219                floats(&image, start + 128)?,
220            ])
221        })
222        .collect::<Result<Vec<_>, Error>>()?;
223    let textures = [
224        texture(&image, [64, 64, 8, 2, 3, 1])?,
225        texture(&image, [32, 32, 8, 3, 3, 3])?,
226        texture(&image, [64, 32, 8, 2, 2, 3])?,
227    ];
228    Ok(GlideSplash {
229        meshes: meshes.try_into().unwrap(),
230        frames,
231        textures,
232    })
233}
234
235fn texture(data: &[u8], header: [u32; 6]) -> Result<SplashTexture, Error> {
236    let start = unique(data, &header)?;
237    let [width, height, _, _, _, format] = header;
238    let mip_size = word(data, start + 1052)? as usize;
239    if mip_size < (width * height) as usize || mip_size > 64 * 1024 {
240        return Err(invalid("invalid texture mip-chain size"));
241    }
242    let pixels = bytes(data, start + 1056, mip_size)?;
243    let table = bytes(data, start + 24, 64)?;
244    let mut rgba = Vec::with_capacity((width * height * 4) as usize);
245    for &pixel in &pixels[..(width * height) as usize] {
246        if format == 3 {
247            rgba.extend([pixel, pixel, pixel, 255]);
248        } else {
249            for channel in 0..3 {
250                let i = 16 + (usize::from((pixel >> 2) & 3) * 3 + channel) * 2;
251                let q = 40 + (usize::from(pixel & 3) * 3 + channel) * 2;
252                let component = |offset| {
253                    let bits = u16::from_le_bytes(table[offset..offset + 2].try_into().unwrap());
254                    i32::from(bits & 255) - i32::from(bits & 256)
255                };
256                let value = i32::from(table[usize::from(pixel >> 4)]) + component(i) + component(q);
257                rgba.push(value.clamp(0, 255) as u8);
258            }
259            rgba.push(255);
260        }
261    }
262    Ok(SplashTexture {
263        width,
264        height,
265        rgba,
266    })
267}
268
269#[cfg(test)]
270mod tests {
271    use super::*;
272
273    #[test]
274    fn yiq_sign_extends_nine_bit_color_components() {
275        let header = [64, 64, 8, 2, 3, 1];
276        let mut data = vec![0; 1056 + 4096];
277        for (i, value) in header.into_iter().enumerate() {
278            data[i * 4..i * 4 + 4].copy_from_slice(&u32::to_le_bytes(value));
279        }
280        data[24 + 8] = 128;
281        for (start, vector) in [
282            (24 + 16 + 6, [502u16, 20, 30]),
283            (24 + 40 + 12, [5, 507, 492]),
284        ] {
285            for (channel, value) in vector.into_iter().enumerate() {
286                data[start + channel * 2..start + channel * 2 + 2]
287                    .copy_from_slice(&value.to_le_bytes());
288            }
289        }
290        data[1052..1056].copy_from_slice(&4096u32.to_le_bytes());
291        data[1056] = (8 << 4) | (1 << 2) | 2;
292        assert_eq!(
293            &texture(&data, header).unwrap().rgba[..4],
294            &[123, 143, 138, 255]
295        );
296    }
297
298    #[test]
299    fn intensity_is_opaque_grayscale() {
300        let header = [32, 32, 8, 3, 3, 3];
301        let mut data = vec![0; 1056 + 1024];
302        for (i, value) in header.into_iter().enumerate() {
303            data[i * 4..i * 4 + 4].copy_from_slice(&u32::to_le_bytes(value));
304        }
305        data[1052..1056].copy_from_slice(&1024u32.to_le_bytes());
306        data[1056..1060].copy_from_slice(&[0, 64, 128, 255]);
307        let decoded = texture(&data, header).unwrap();
308        assert_eq!(
309            &decoded.rgba[..16],
310            &[
311                0, 0, 0, 255, 64, 64, 64, 255, 128, 128, 128, 255, 255, 255, 255, 255
312            ]
313        );
314    }
315}