Skip to main content

rage_rpf/
ytd.rs

1/// YTD (Texture Dictionary) parser for GTA V (Gen8 / PC format).
2///
3/// Accepts the standalone RSC7 bytes as returned by `RpfArchive::extract_entry`.
4use anyhow::{bail, Context, Result};
5use flate2::read::DeflateDecoder;
6use std::io::Read;
7
8use crate::archive::{resource_size_from_flags, RSC7_MAGIC};
9
10// ─── Public types ─────────────────────────────────────────────────────────────
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13#[repr(u32)]
14pub enum TextureFormat {
15    A8R8G8B8 = 21,
16    X8R8G8B8 = 22,
17    A1R5G5B5 = 25,
18    A8       = 28,
19    A8B8G8R8 = 32,
20    L8       = 50,
21    DXT1     = 0x31545844,
22    DXT3     = 0x33545844,
23    DXT5     = 0x35545844,
24    ATI1     = 0x31495441,
25    ATI2     = 0x32495441,
26    BC7      = 0x20374342,
27    Unknown  = 0,
28}
29
30impl TextureFormat {
31    pub fn from_u32(v: u32) -> Self {
32        match v {
33            21          => Self::A8R8G8B8,
34            22          => Self::X8R8G8B8,
35            25          => Self::A1R5G5B5,
36            28          => Self::A8,
37            32          => Self::A8B8G8R8,
38            50          => Self::L8,
39            0x31545844  => Self::DXT1,
40            0x33545844  => Self::DXT3,
41            0x35545844  => Self::DXT5,
42            0x31495441  => Self::ATI1,
43            0x32495441  => Self::ATI2,
44            0x20374342  => Self::BC7,
45            _           => Self::Unknown,
46        }
47    }
48
49    pub fn is_block_compressed(self) -> bool {
50        matches!(self, Self::DXT1 | Self::DXT3 | Self::DXT5 | Self::ATI1 | Self::ATI2 | Self::BC7)
51    }
52}
53
54impl std::fmt::Display for TextureFormat {
55    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56        let s = match self {
57            Self::A8R8G8B8 => "A8R8G8B8",
58            Self::X8R8G8B8 => "X8R8G8B8",
59            Self::A1R5G5B5 => "A1R5G5B5",
60            Self::A8       => "A8",
61            Self::A8B8G8R8 => "A8B8G8R8",
62            Self::L8       => "L8",
63            Self::DXT1     => "DXT1",
64            Self::DXT3     => "DXT3",
65            Self::DXT5     => "DXT5",
66            Self::ATI1     => "ATI1",
67            Self::ATI2     => "ATI2",
68            Self::BC7      => "BC7",
69            Self::Unknown  => "Unknown",
70        };
71        f.write_str(s)
72    }
73}
74
75/// One texture entry extracted from a YTD.
76#[derive(Debug)]
77pub struct YtdTexture {
78    pub name: String,
79    pub name_hash: u32,
80    pub width: u16,
81    pub height: u16,
82    pub depth: u16,
83    pub format: TextureFormat,
84    pub levels: u8,
85    pub stride: u16,
86    pub pixel_data: Vec<u8>,
87}
88
89impl YtdTexture {
90    /// Serialize this texture to a DDS file.
91    pub fn to_dds(&self) -> Vec<u8> {
92        let mut out = Vec::new();
93        // DDS magic
94        out.extend_from_slice(b"DDS ");
95
96        // DDS_HEADER (124 bytes)
97        let has_mips = self.levels > 1;
98        let is_compressed = self.format.is_block_compressed();
99
100        let mut flags: u32 = 0x1 | 0x2 | 0x4 | 0x1000; // CAPS | HEIGHT | WIDTH | PIXELFORMAT
101        if has_mips { flags |= 0x20000; } // MIPMAPCOUNT
102        if is_compressed { flags |= 0x80000; } else { flags |= 0x8; } // LINEARSIZE or PITCH
103
104        let pitch_or_linear: u32 = self.stride as u32 * self.height as u32;
105
106        out.extend_from_slice(&124u32.to_le_bytes());            // dwSize
107        out.extend_from_slice(&flags.to_le_bytes());             // dwFlags
108        out.extend_from_slice(&(self.height as u32).to_le_bytes()); // dwHeight
109        out.extend_from_slice(&(self.width as u32).to_le_bytes());  // dwWidth
110        out.extend_from_slice(&pitch_or_linear.to_le_bytes());   // dwPitchOrLinearSize
111        out.extend_from_slice(&(self.depth as u32).to_le_bytes()); // dwDepth
112        out.extend_from_slice(&(self.levels as u32).to_le_bytes()); // dwMipMapCount
113        out.extend_from_slice(&[0u8; 44]);                       // dwReserved1[11]
114
115        // DDS_PIXELFORMAT (32 bytes)
116        self.write_pixelformat(&mut out);
117
118        let mut caps: u32 = 0x1000; // DDSCAPS_TEXTURE
119        if has_mips { caps |= 0x8 | 0x400000; } // COMPLEX | MIPMAP
120        out.extend_from_slice(&caps.to_le_bytes());
121        out.extend_from_slice(&[0u8; 16]); // Caps2/3/4 + Reserved2
122
123        // Pixel data
124        if self.format == TextureFormat::BC7 {
125            // Prepend DX10 extension header after pixel format signals DX10
126            // Note: the DX10 header is placed before pixel data but after DDS_HEADER
127            // The FourCC "DX10" in the pixelformat signals this header follows
128            write_dx10_header(&mut out);
129        }
130        out.extend_from_slice(&self.pixel_data);
131
132        out
133    }
134
135    fn write_pixelformat(&self, out: &mut Vec<u8>) {
136        out.extend_from_slice(&32u32.to_le_bytes()); // dwSize
137        match self.format {
138            TextureFormat::DXT1 | TextureFormat::DXT3 | TextureFormat::DXT5
139            | TextureFormat::ATI1 | TextureFormat::ATI2 => {
140                out.extend_from_slice(&0x4u32.to_le_bytes()); // DDPF_FOURCC
141                out.extend_from_slice(&(self.format as u32).to_le_bytes()); // FourCC
142                out.extend_from_slice(&[0u8; 20]); // RGB counts + masks
143            }
144            TextureFormat::BC7 => {
145                out.extend_from_slice(&0x4u32.to_le_bytes()); // DDPF_FOURCC
146                out.extend_from_slice(b"DX10");               // FourCC = DX10
147                out.extend_from_slice(&[0u8; 20]);
148            }
149            TextureFormat::A8R8G8B8 => {
150                out.extend_from_slice(&(0x1 | 0x40u32).to_le_bytes()); // ALPHAPIXELS | RGB
151                out.extend_from_slice(&0u32.to_le_bytes()); // no FourCC
152                out.extend_from_slice(&32u32.to_le_bytes()); // bit count
153                out.extend_from_slice(&0x00FF0000u32.to_le_bytes()); // R
154                out.extend_from_slice(&0x0000FF00u32.to_le_bytes()); // G
155                out.extend_from_slice(&0x000000FFu32.to_le_bytes()); // B
156                out.extend_from_slice(&0xFF000000u32.to_le_bytes()); // A
157            }
158            TextureFormat::X8R8G8B8 => {
159                out.extend_from_slice(&0x40u32.to_le_bytes()); // RGB
160                out.extend_from_slice(&0u32.to_le_bytes());
161                out.extend_from_slice(&32u32.to_le_bytes());
162                out.extend_from_slice(&0x00FF0000u32.to_le_bytes());
163                out.extend_from_slice(&0x0000FF00u32.to_le_bytes());
164                out.extend_from_slice(&0x000000FFu32.to_le_bytes());
165                out.extend_from_slice(&0u32.to_le_bytes()); // no alpha
166            }
167            TextureFormat::A8B8G8R8 => {
168                out.extend_from_slice(&(0x1 | 0x40u32).to_le_bytes());
169                out.extend_from_slice(&0u32.to_le_bytes());
170                out.extend_from_slice(&32u32.to_le_bytes());
171                out.extend_from_slice(&0x000000FFu32.to_le_bytes()); // R
172                out.extend_from_slice(&0x0000FF00u32.to_le_bytes()); // G
173                out.extend_from_slice(&0x00FF0000u32.to_le_bytes()); // B
174                out.extend_from_slice(&0xFF000000u32.to_le_bytes()); // A
175            }
176            TextureFormat::A1R5G5B5 => {
177                out.extend_from_slice(&(0x1 | 0x40u32).to_le_bytes());
178                out.extend_from_slice(&0u32.to_le_bytes());
179                out.extend_from_slice(&16u32.to_le_bytes());
180                out.extend_from_slice(&0x7C00u32.to_le_bytes()); // R (5 bits)
181                out.extend_from_slice(&0x03E0u32.to_le_bytes()); // G (5 bits)
182                out.extend_from_slice(&0x001Fu32.to_le_bytes()); // B (5 bits)
183                out.extend_from_slice(&0x8000u32.to_le_bytes()); // A (1 bit)
184            }
185            TextureFormat::A8 => {
186                out.extend_from_slice(&0x2u32.to_le_bytes()); // ALPHA
187                out.extend_from_slice(&0u32.to_le_bytes());
188                out.extend_from_slice(&8u32.to_le_bytes());
189                out.extend_from_slice(&[0u8; 16]);
190                // alpha mask is last u32 — overwrite last 4 bytes
191                let len = out.len();
192                out[len - 4..len].copy_from_slice(&0xFFu32.to_le_bytes());
193            }
194            TextureFormat::L8 => {
195                out.extend_from_slice(&0x20000u32.to_le_bytes()); // LUMINANCE
196                out.extend_from_slice(&0u32.to_le_bytes());
197                out.extend_from_slice(&8u32.to_le_bytes());
198                out.extend_from_slice(&0xFFu32.to_le_bytes()); // R mask
199                out.extend_from_slice(&[0u8; 12]);
200            }
201            TextureFormat::Unknown => {
202                // best-effort fallback: write empty pixelformat
203                out.extend_from_slice(&[0u8; 28]);
204            }
205        }
206    }
207}
208
209fn write_dx10_header(out: &mut Vec<u8>) {
210    out.extend_from_slice(&98u32.to_le_bytes()); // DXGI_FORMAT_BC7_UNORM
211    out.extend_from_slice(&3u32.to_le_bytes());  // D3D10_RESOURCE_DIMENSION_TEXTURE2D
212    out.extend_from_slice(&0u32.to_le_bytes());  // miscFlag
213    out.extend_from_slice(&1u32.to_le_bytes());  // arraySize
214    out.extend_from_slice(&0u32.to_le_bytes());  // miscFlags2
215}
216
217// ─── Parser ───────────────────────────────────────────────────────────────────
218
219/// Parse a YTD file from standalone RSC7 bytes.
220///
221/// `data` is the output of `RpfArchive::extract_entry` for a `.ytd` entry.
222pub fn parse_ytd(data: &[u8]) -> Result<Vec<YtdTexture>> {
223    if data.len() < 16 {
224        bail!("YTD data too short");
225    }
226
227    let magic = u32::from_le_bytes(data[0..4].try_into().unwrap());
228    if magic != RSC7_MAGIC {
229        bail!("Not an RSC7 file (magic = 0x{:08X})", magic);
230    }
231
232    let system_flags  = u32::from_le_bytes(data[8..12].try_into().unwrap());
233    let graphics_flags = u32::from_le_bytes(data[12..16].try_into().unwrap());
234
235    let sys_size  = resource_size_from_flags(system_flags);
236    let gfx_size  = resource_size_from_flags(graphics_flags);
237    let body      = &data[16..];
238
239    // Decompress
240    let decompressed = {
241        let mut out = Vec::new();
242        if DeflateDecoder::new(body).read_to_end(&mut out).is_ok() && !out.is_empty() {
243            out
244        } else {
245            // Try raw (uncompressed) body
246            body.to_vec()
247        }
248    };
249
250    if decompressed.len() < sys_size {
251        bail!(
252            "Decompressed size {} < expected system size {}",
253            decompressed.len(), sys_size
254        );
255    }
256
257    let system   = &decompressed[..sys_size];
258    let graphics = if decompressed.len() >= sys_size + gfx_size {
259        &decompressed[sys_size..sys_size + gfx_size]
260    } else {
261        &decompressed[sys_size..]
262    };
263
264    let reader = ResReader { system, graphics };
265    parse_texture_dict(&reader)
266}
267
268// ─── Internal virtual-memory reader ──────────────────────────────────────────
269
270struct ResReader<'a> {
271    system:   &'a [u8],
272    graphics: &'a [u8],
273}
274
275impl<'a> ResReader<'a> {
276    fn resolve(&self, va: u64, len: usize) -> Option<&'a [u8]> {
277        if va == 0 { return None; }
278        if (va & 0x50000000) == 0x50000000 && (va & 0x60000000) != 0x60000000 {
279            let off = (va - 0x50000000) as usize;
280            self.system.get(off..off + len)
281        } else if (va & 0x60000000) == 0x60000000 {
282            let off = (va - 0x60000000) as usize;
283            self.graphics.get(off..off + len)
284        } else {
285            None
286        }
287    }
288
289    fn string_at(&self, va: u64) -> Option<String> {
290        if (va & 0x50000000) == 0x50000000 && (va & 0x60000000) != 0x60000000 {
291            let off = (va - 0x50000000) as usize;
292            let slice = self.system.get(off..)?;
293            let end = slice.iter().position(|&b| b == 0).unwrap_or(slice.len());
294            Some(String::from_utf8_lossy(&slice[..end]).into_owned())
295        } else {
296            None
297        }
298    }
299}
300
301// ─── Struct parsing helpers ───────────────────────────────────────────────────
302
303fn u16_le(b: &[u8], off: usize) -> u16 {
304    u16::from_le_bytes(b[off..off + 2].try_into().unwrap())
305}
306fn u32_le(b: &[u8], off: usize) -> u32 {
307    u32::from_le_bytes(b[off..off + 4].try_into().unwrap())
308}
309fn u64_le(b: &[u8], off: usize) -> u64 {
310    u64::from_le_bytes(b[off..off + 8].try_into().unwrap())
311}
312
313// ─── TextureDictionary ────────────────────────────────────────────────────────
314
315fn parse_texture_dict(reader: &ResReader<'_>) -> Result<Vec<YtdTexture>> {
316    let sys = reader.system;
317    if sys.len() < 64 {
318        bail!("system section too small for TextureDictionary");
319    }
320
321    // ResourceFileBase at 0x00 (16 bytes): VFT, FileUnknown, FilePagesInfoPointer
322    // TextureDictionary fields at 0x10:
323    // 0x10..0x1F: four u32 unknowns
324    // 0x20: ResourceSimpleList64_uint (TextureNameHashes) — 16 bytes
325    let hash_ptr   = u64_le(sys, 0x20);
326    let hash_count = u32_le(sys, 0x28) as usize;
327    // capacity at 0x2C
328
329    // 0x30: ResourcePointerList64<Texture> (Textures) — 16 bytes
330    let tex_ptr_array = u64_le(sys, 0x30);
331    let tex_count     = u32_le(sys, 0x38) as usize;
332
333    // Read name hashes (u32 array in system section)
334    let hash_data = if hash_count > 0 {
335        reader.resolve(hash_ptr, hash_count * 4)
336    } else {
337        None
338    };
339
340    // Read texture pointer array (u64 per texture, in system section)
341    let ptr_bytes = tex_count * 8;
342    let ptr_data = if tex_count > 0 {
343        reader.resolve(tex_ptr_array, ptr_bytes)
344            .with_context(|| format!("texture pointer array out of bounds (va=0x{:X})", tex_ptr_array))?
345    } else {
346        return Ok(vec![]);
347    };
348
349    let mut textures = Vec::with_capacity(tex_count);
350    for i in 0..tex_count {
351        let tex_va = u64_le(ptr_data, i * 8);
352        if tex_va == 0 { continue; }
353
354        let name_hash = hash_data
355            .and_then(|h| h.get(i * 4..i * 4 + 4))
356            .map(|b| u32_le(b, 0))
357            .unwrap_or(0);
358
359        match parse_texture(tex_va, name_hash, reader) {
360            Ok(tex) => textures.push(tex),
361            Err(e) => eprintln!("[YTD] Warning: texture {} parse error: {}", i, e),
362        }
363    }
364
365    Ok(textures)
366}
367
368fn parse_texture(tex_va: u64, name_hash: u32, reader: &ResReader<'_>) -> Result<YtdTexture> {
369    // Texture struct is 144 bytes (0x90) in the system section
370    let raw = reader.resolve(tex_va, 0x90)
371        .with_context(|| format!("texture struct out of bounds (va=0x{:X})", tex_va))?;
372
373    // TextureBase fields (offset within Texture struct)
374    let name_ptr = u64_le(raw, 0x28);
375
376    // Texture-specific fields (starting at 0x50)
377    let width  = u16_le(raw, 0x50);
378    let height = u16_le(raw, 0x52);
379    let depth  = u16_le(raw, 0x54);
380    let stride = u16_le(raw, 0x56);
381    let fmt    = TextureFormat::from_u32(u32_le(raw, 0x58));
382    let levels = raw[0x5D];
383    let data_ptr = u64_le(raw, 0x70);
384
385    let name = reader.string_at(name_ptr).unwrap_or_default();
386
387    // Compute pixel data size (same formula as CodeWalker TextureData.Read)
388    let pixel_size = calc_pixel_data_size(stride, height, levels);
389
390    let pixel_data = if pixel_size > 0 && data_ptr != 0 {
391        reader.resolve(data_ptr, pixel_size)
392            .with_context(|| format!("pixel data out of bounds (va=0x{:X}, size={})", data_ptr, pixel_size))?
393            .to_vec()
394    } else {
395        vec![]
396    };
397
398    Ok(YtdTexture { name, name_hash, width, height, depth, format: fmt, levels, stride, pixel_data })
399}
400
401fn calc_pixel_data_size(stride: u16, height: u16, levels: u8) -> usize {
402    let mut total = 0usize;
403    let mut length = stride as usize * height as usize;
404    for _ in 0..levels {
405        total += length;
406        length /= 4;
407    }
408    total
409}