diff --git a/crates/sylpheed-formats/src/texture.rs b/crates/sylpheed-formats/src/texture.rs index c2c866a..f4561cc 100644 --- a/crates/sylpheed-formats/src/texture.rs +++ b/crates/sylpheed-formats/src/texture.rs @@ -1,16 +1,43 @@ //! Xbox 360 texture format parsing and de-tiling. //! -//! ## The Problem: GPU Tiling -//! The Xbox 360 Xenos GPU stores textures in a "tiled" memory layout for -//! cache efficiency. The tiles are 32×32 texel macro-tiles, and within -//! each macro-tile the DXT compression blocks are arranged in Morton -//! (Z-order curve) order. Before we can upload a texture to a modern GPU, -//! we must "de-tile" it back to a standard linear (row-major) layout. +//! ## XPR2 Container Layout //! -//! ## Reference implementations -//! - Xenia emulator: `src/xenia/gpu/texture_util.cc` -//! - RareView (C#): Xbox 360 texture de-tiling -//! - swizzleinator crate: general console texture unswizzling +//! ```text +//! Offset Size Field +//! 0x00 4 Magic: "XPR2" +//! 0x04 4 header_size — pixel data section starts at this file offset (e.g. 0x2800) +//! 0x08 4 data_size — size of the pixel data section (e.g. 0x8A000) +//! 0x0C 4 num_resources +//! 0x10 16*n Resource directory: n × 16-byte Xpr2ResourceEntry structs +//! [type_tag:4][data_offset:4][data_size:4][name_offset:4] +//! … … Resource descriptors (TX2D = 52-byte D3DBaseTexture2D structs) +//! 0x2800 … Pixel data section +//! ``` +//! +//! ## GPUTEXTURE_FETCH_CONSTANT (GPUFC) +//! +//! Each TX2D descriptor is 52 bytes. The 6-dword (24-byte) GPUFC starts at +//! descriptor offset +0x18: +//! +//! ```text +//! GPUFC[0] (+0x18): tiled flag at bit 31, pitch at bits[23:8] +//! GPUFC[1] (+0x1C): TextureFormat at bits[5:0], base_address at bits[31:12] +//! GPUFC[2] (+0x20): width-1 at bits[12:0], height-1 at bits[25:13] (size_2d) +//! GPUFC[3] (+0x24): swizzle, filter +//! GPUFC[4] (+0x28): mip_max at bits[9:6] → mip_count = mip_max + 1 +//! GPUFC[5] (+0x2C): mip_address, packed_mips, dimension +//! ``` +//! +//! ## GPU Tiling +//! +//! Xbox 360 Xenos stores textures in 32×32 texel macro-tiles. Within each +//! macro-tile the DXT blocks are arranged in Morton (Z-order) order. +//! GPUFC[0] bit 31 = 1 → tiled (de-tiling required); = 0 → linear. +//! +//! ## References +//! +//! - Xenia: `src/xenia/gpu/xenos.h` (GPUTEXTUREFORMAT enum, GPUFC bitfields) +//! - Xenia: `src/xenia/gpu/texture_util.cc` (de-tiling algorithm) use binrw::{BinRead, binread}; use thiserror::Error; @@ -22,6 +49,9 @@ pub enum TextureError { #[error("Invalid texture header magic: expected {expected:?}, got {got:?}")] BadMagic { expected: [u8; 4], got: [u8; 4] }, + #[error("No TX2D texture resource found in XPR2 file")] + NoTextureFound, + #[error("Unsupported texture format: 0x{0:02X}")] UnsupportedFormat(u8), @@ -37,56 +67,58 @@ pub enum TextureError { // ── Texture formats ─────────────────────────────────────────────────────────── -/// D3DFORMAT values used by the Xbox 360 SDK for texture data. -/// These appear in XPR2 headers and in-memory texture descriptors. +/// GPUTEXTUREFORMAT values from Xenia's `xenos.h`. /// -/// Format codes sourced from: -/// - Xbox 360 SDK documentation (leaked) -/// - Xenia emulator source (gpu/xenos.h) -/// - ZenHAX community research +/// These 6-bit codes live in GPUFC dword_1 bits[5:0] — NOT the old D3DFORMAT +/// codes. The mapping is: +/// k_8_8_8_8 = 6, k_DXT1 = 18, k_DXT2_3 = 19, k_DXT4_5 = 20, +/// k_DXN = 49, k_DXT5A = 59 #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u8)] pub enum X360TextureFormat { - /// DXT1 / BC1 — 4 bpp, 1-bit alpha - Dxt1 = 0x52, - /// DXT3 / BC2 — 8 bpp, 4-bit explicit alpha - Dxt3 = 0x53, - /// DXT5 / BC3 — 8 bpp, 8-bit interpolated alpha - Dxt5 = 0x54, - /// DXN / BC5 / ATI2 — normal maps, two-channel - Dxn = 0x71, - /// Uncompressed A8R8G8B8 — 32 bpp - A8R8G8B8 = 0x06, - /// Uncompressed X8R8G8B8 — 32 bpp, no alpha - X8R8G8B8 = 0x07, + /// A8R8G8B8 — uncompressed 32 bpp (k_8_8_8_8 = 6) + A8R8G8B8 = 6, + /// X8R8G8B8 — uncompressed 32 bpp, no alpha (k_8_8_8_8_AS_16_16_16_16 = 7) + X8R8G8B8 = 7, + /// DXT1 / BC1 — 4 bpp, 1-bit alpha (k_DXT1 = 18) + Dxt1 = 18, + /// DXT2/3 / BC2 — 8 bpp, 4-bit explicit alpha (k_DXT2_3 = 19) + Dxt3 = 19, + /// DXT4/5 / BC3 — 8 bpp, 8-bit interpolated alpha (k_DXT4_5 = 20) + Dxt5 = 20, + /// DXN / BC5 / ATI2N — two-channel normal maps (k_DXN = 49) + Dxn = 49, + /// DXT5A / BC4 / ATI1N — single alpha channel (k_DXT5A = 59) + /// Used for gloss, specular, luminance, and reflection maps in Project Sylpheed. + Dxt5A = 59, } impl X360TextureFormat { pub fn from_u8(v: u8) -> Option { match v { - 0x52 => Some(Self::Dxt1), - 0x53 => Some(Self::Dxt3), - 0x54 => Some(Self::Dxt5), - 0x71 => Some(Self::Dxn), - 0x06 => Some(Self::A8R8G8B8), - 0x07 => Some(Self::X8R8G8B8), - _ => None, + 6 => Some(Self::A8R8G8B8), + 7 => Some(Self::X8R8G8B8), + 18 => Some(Self::Dxt1), + 19 => Some(Self::Dxt3), + 20 => Some(Self::Dxt5), + 49 => Some(Self::Dxn), + 59 => Some(Self::Dxt5A), + _ => None, } } - /// Bytes per compressed block (4×4 texel group). - /// For uncompressed formats, bytes per pixel instead. + /// Bytes per compressed block (4×4 texel group) or per pixel for uncompressed. pub fn bytes_per_block(&self) -> usize { match self { - Self::Dxt1 => 8, - Self::Dxt3 | Self::Dxt5 | Self::Dxn => 16, - Self::A8R8G8B8 | Self::X8R8G8B8 => 4, + Self::Dxt1 | Self::Dxt5A => 8, + Self::Dxt3 | Self::Dxt5 | Self::Dxn => 16, + Self::A8R8G8B8 | Self::X8R8G8B8 => 4, } } /// Is this a BCn block-compressed format? pub fn is_block_compressed(&self) -> bool { - matches!(self, Self::Dxt1 | Self::Dxt3 | Self::Dxt5 | Self::Dxn) + matches!(self, Self::Dxt1 | Self::Dxt3 | Self::Dxt5 | Self::Dxn | Self::Dxt5A) } /// Texels per block side (4 for BCn, 1 for uncompressed). @@ -97,176 +129,160 @@ impl X360TextureFormat { // ── XPR2 container format ───────────────────────────────────────────────────── -/// XPR2 is Microsoft's Xbox Packed Resource v2 format. -/// It stores D3D resources (textures, vertex buffers, index buffers) -/// in a single blob that can be DMA'd directly into GPU memory. -/// -/// The header is big-endian (Xbox 360 is big-endian PowerPC). -/// NOTE: Project Sylpheed may use a custom container. If files don't -/// start with b"XPR2", check for game-specific magic bytes instead. +/// XPR2 container header (big-endian, 16 bytes total including magic). #[binread] #[br(magic = b"XPR2", big)] #[derive(Debug, Clone)] pub struct Xpr2Header { - /// Total file size in bytes - pub total_size: u32, - /// Size of the header section (texture data starts after this) + /// Byte offset where the pixel data section starts (= size of header region). + /// Example value: 0x2800 = 10240. pub header_size: u32, - /// Number of resource entries in this file + /// Size of the pixel data section in bytes. + /// Example value: 0x8A000 = 565248. + pub data_size: u32, + /// Number of 16-byte resource entries in the directory at offset 0x10. pub num_resources: u32, } -/// A single resource entry within an XPR2 file. -/// Each entry describes one D3D resource (texture, buffer, etc.) +/// One 16-byte entry in the XPR2 resource directory (starts at file offset 0x10). #[binread] #[br(big)] #[derive(Debug, Clone)] pub struct Xpr2ResourceEntry { - /// Encoded type and reference count. - /// Bits [0..15] = ref_count - /// Bits [16..18] = resource type (4 = texture) - /// Bits [19..31] = flags - pub resource_type_and_flags: u32, - /// Offset of this resource's data within the XPR2 file + /// ASCII type tag: b"TX2D" for 2D textures, b"XBG7" for geometry, etc. + pub type_tag: [u8; 4], + /// Byte offset of this resource's descriptor, relative to directory base 0x10. + /// Actual file offset = data_offset + 0x10. pub data_offset: u32, - /// Reserved / unknown - pub _unknown: u32, + /// Size of the resource descriptor in bytes (e.g. 0x34 = 52 for TX2D). + pub descriptor_size: u32, + /// Byte offset of this resource's name string, relative to directory base 0x10. + pub name_offset: u32, } impl Xpr2ResourceEntry { - pub fn resource_type(&self) -> u8 { - ((self.resource_type_and_flags >> 16) & 0x7) as u8 - } - pub fn is_texture(&self) -> bool { - self.resource_type() == 4 - } -} - -/// Xbox 360 D3D texture descriptor embedded in an XPR2 resource. -/// This is what the GPU register `NV097_SET_TEXTURE_FORMAT` receives. -/// -/// Reference: xboxdevwiki.net/XPR -#[binread] -#[br(big)] -#[derive(Debug, Clone)] -pub struct X360TextureDesc { - /// Packed GPU texture format register - /// Bits [0..3] = DMA channel - /// Bits [4..7] = dimensionality (2 = 2D) - /// Bits [8..15] = D3DFORMAT (see X360TextureFormat) - /// Bits [16..19] = mip levels - /// Bits [20..23] = width as power-of-two: actual = 1 << value - /// Bits [24..27] = height as power-of-two: actual = 1 << value - /// Bits [28..31] = depth (for 3D textures) - pub gpu_format: u32, - - /// For non-power-of-two textures, encodes actual dimensions - pub npot_size: u32, -} - -impl X360TextureDesc { - pub fn format_code(&self) -> u8 { - ((self.gpu_format >> 8) & 0xFF) as u8 - } - - pub fn format(&self) -> Option { - X360TextureFormat::from_u8(self.format_code()) - } - - pub fn mip_levels(&self) -> u32 { - (self.gpu_format >> 16) & 0xF - } - - /// Width from the packed power-of-two field. - pub fn width_pow2(&self) -> u32 { - 1 << ((self.gpu_format >> 20) & 0xF) - } - - /// Height from the packed power-of-two field. - pub fn height_pow2(&self) -> u32 { - 1 << ((self.gpu_format >> 24) & 0xF) + &self.type_tag == b"TX2D" } } // ── Decoded texture ─────────────────────────────────────────────────────────── -/// A decoded Xbox 360 texture, ready to upload to a modern GPU. +/// A decoded Xbox 360 texture ready for GPU upload. /// -/// After `from_xpr2()` or `from_raw_tiled()`, the `data` field contains -/// the texture in standard linear layout that Bevy / wgpu can consume. +/// After `from_xpr2()` the `data` field holds the texture in standard linear +/// (row-major) layout. BCn formats are kept as compressed block data; the GPU +/// decompresses in hardware. #[derive(Debug, Clone)] pub struct X360Texture { pub width: u32, pub height: u32, pub format: X360TextureFormat, pub mip_levels: u32, - /// De-tiled texture data in linear (row-major) order. - /// For BCn formats: standard DDS-style packed block data. - /// For ARGB: standard RGBA8 pixel data. + /// De-tiled texture data in linear order. + /// BCn: standard packed block data (DDS layout). + /// Uncompressed: BGRA8 pixel data. pub data: Vec, } impl X360Texture { - /// Parse a texture from a raw XPR2 file's bytes. + /// Parse the first TX2D texture from an XPR2 file's raw bytes. /// - /// This handles the full pipeline: - /// 1. Parse XPR2 header + resource descriptors - /// 2. Locate the texture resource - /// 3. De-tile the GPU memory layout → linear layout + /// Pipeline: + /// 1. Parse XPR2 header + resource directory + /// 2. Locate the first TX2D entry + /// 3. Read its GPUTEXTURE_FETCH_CONSTANT (GPUFC) at descriptor +0x18 + /// 4. De-tile the pixel data (if tiled) → linear layout pub fn from_xpr2(bytes: &[u8]) -> Result { use std::io::Cursor; let mut cur = Cursor::new(bytes); - // Parse main header (validates "XPR2" magic) + // Parse header — validates "XPR2" magic, reads 3 × u32 (total 16 bytes) let header = Xpr2Header::read(&mut cur)?; - // Parse resource entries + // Resource directory begins immediately after the 16-byte header (offset 0x10) let mut entries = Vec::new(); for _ in 0..header.num_resources { entries.push(Xpr2ResourceEntry::read(&mut cur)?); } - // Find the first texture resource + // Find the first TX2D texture resource let tex_entry = entries.iter() .find(|e| e.is_texture()) - .ok_or_else(|| TextureError::UnsupportedFormat(0))?; + .ok_or(TextureError::NoTextureFound)?; - // The texture descriptor immediately follows the resource entries - let desc = X360TextureDesc::read(&mut cur)?; + // The descriptor is at file offset = data_offset + 0x10 (directory base) + const DIR_BASE: usize = 0x10; + let desc_file_offset = tex_entry.data_offset as usize + DIR_BASE; - let format = desc.format() - .ok_or(TextureError::UnsupportedFormat(desc.format_code()))?; + // GPUFC is a 6-dword (24-byte) block at descriptor offset +0x18 + let gpufc_base = desc_file_offset + 0x18; + if bytes.len() < gpufc_base + 6 * 4 { + return Err(TextureError::BufferTooSmall { + needed: gpufc_base + 24, + have: bytes.len(), + }); + } - let width = desc.width_pow2(); - let height = desc.height_pow2(); + // Read one big-endian u32 at the given file offset + let be_u32 = |offset: usize| -> u32 { + u32::from_be_bytes(bytes[offset..offset + 4].try_into().unwrap()) + }; - // Texture data is at header_size offset - let data_start = header.header_size as usize; + let gpufc0 = be_u32(gpufc_base); // +0x18 + let gpufc1 = be_u32(gpufc_base + 0x04); // +0x1C + let gpufc2 = be_u32(gpufc_base + 0x08); // +0x20 + let gpufc4 = be_u32(gpufc_base + 0x10); // +0x28 + + // GPUFC[1] bits[5:0] = GPUTEXTUREFORMAT + let fmt_code = (gpufc1 & 0x3F) as u8; + let format = X360TextureFormat::from_u8(fmt_code) + .ok_or(TextureError::UnsupportedFormat(fmt_code))?; + + // GPUFC[1] bits[31:12] = base_address (4KB-aligned byte offset into data section) + let base_address = (gpufc1 & 0xFFFFF000) as usize; + + // GPUFC[2] / size_2d: width-1 in bits[12:0], height-1 in bits[25:13] + let width = (gpufc2 & 0x1FFF) + 1; + let height = ((gpufc2 >> 13) & 0x1FFF) + 1; + + // GPUFC[4]: mip_max in bits[9:6]; mip_count = mip_max + 1 + let mip_count = ((gpufc4 >> 6) & 0xF) + 1; + + // GPUFC[0] bit 31 = 1 → tiled memory layout (requires de-tiling) + let is_tiled = (gpufc0 >> 31) != 0; + + // Pixel data for this texture starts at: header_size + base_address + let data_start = header.header_size as usize + base_address; if bytes.len() <= data_start { return Err(TextureError::BufferTooSmall { needed: data_start + 1, have: bytes.len(), }); } - let tiled_data = &bytes[data_start..]; + let raw_data = &bytes[data_start..]; - // De-tile! - let linear_data = detile(tiled_data, width, height, format)?; + let linear_data = if is_tiled { + detile(raw_data, width, height, format)? + } else { + // Linear layout — copy only the mip-0 slice + let block_size = format.block_size() as u32; + let bw = ((width + block_size - 1) / block_size).max(1); + let bh = ((height + block_size - 1) / block_size).max(1); + let needed = bw as usize * bh as usize * format.bytes_per_block(); + if raw_data.len() < needed { + return Err(TextureError::BufferTooSmall { needed, have: raw_data.len() }); + } + raw_data[..needed].to_vec() + }; - Ok(X360Texture { - width, - height, - format, - mip_levels: desc.mip_levels().max(1), - data: linear_data, - }) + Ok(X360Texture { width, height, format, mip_levels: mip_count, data: linear_data }) } /// Parse a texture from already-known parameters + raw tiled data. /// - /// Use this when you've reverse-engineered a game-specific container - /// and extracted the raw tiled texture bytes yourself. + /// Use when you have reverse-engineered a container and extracted the + /// raw tiled bytes yourself. pub fn from_raw_tiled( tiled_data: &[u8], width: u32, @@ -274,30 +290,19 @@ impl X360Texture { format: X360TextureFormat, ) -> Result { let linear_data = detile(tiled_data, width, height, format)?; - Ok(X360Texture { - width, - height, - format, - mip_levels: 1, - data: linear_data, - }) + Ok(X360Texture { width, height, format, mip_levels: 1, data: linear_data }) } } // ── Core de-tiling algorithm ────────────────────────────────────────────────── -/// De-tile an Xbox 360 GPU texture from tiled to linear layout. +/// De-tile an Xbox 360 GPU texture from tiled to linear (row-major) layout. /// -/// Xbox 360 stores textures in a hierarchical tiled format: -/// - The texture is divided into 32×32 texel **macro-tiles** -/// - Within each macro-tile, DXT blocks are in **Morton (Z-order)** order +/// Xbox 360 stores textures in 32×32 texel macro-tiles. Within each +/// macro-tile the DXT blocks (or raw pixels) are in Morton (Z-order) order. +/// This function reverses that ordering for the mip-0 level. /// -/// This is required for ALL textures regardless of whether they are -/// DXT-compressed or uncompressed — the GPU expects tiled memory. -/// -/// Algorithm based on: -/// - Xenia: `texture_util.cc` `TileTexture()` -/// - GTA IV Xbox 360 Texture Editor by Pimpin Tyler and Anthony +/// Algorithm based on Xenia's `texture_util.cc` `TileTexture()`. pub fn detile( src: &[u8], width: u32, @@ -305,25 +310,21 @@ pub fn detile( format: X360TextureFormat, ) -> Result, TextureError> { let block_size = format.block_size() as u32; - let bpb = format.bytes_per_block(); // bytes per block + let bpb = format.bytes_per_block(); - // Dimensions in blocks - let blocks_wide = ((width + block_size - 1) / block_size).max(1); + // Texture dimensions in blocks + let blocks_wide = ((width + block_size - 1) / block_size).max(1); let blocks_tall = ((height + block_size - 1) / block_size).max(1); let expected = blocks_wide as usize * blocks_tall as usize * bpb; if src.len() < expected { - return Err(TextureError::BufferTooSmall { - needed: expected, - have: src.len(), - }); + return Err(TextureError::BufferTooSmall { needed: expected, have: src.len() }); } let mut dst = vec![0u8; expected]; - // Macro-tile dimensions (in blocks) - // Xbox 360 always uses 32×32 texel macro-tiles - let macro_tile_blocks = 32 / block_size; // = 8 for BCn (4×4 texels/block) + // Xbox 360 macro-tiles are always 32×32 texels → 8×8 blocks for BCn (4-texel blocks) + let macro_tile_blocks = 32 / block_size; let macro_tiles_wide = (blocks_wide + macro_tile_blocks - 1) / macro_tile_blocks; let macro_tiles_tall = (blocks_tall + macro_tile_blocks - 1) / macro_tile_blocks; @@ -342,7 +343,7 @@ pub fn detile( let block_x = macro_x * macro_tile_blocks + lx; let block_y = macro_y * macro_tile_blocks + ly; - // Skip blocks that fall outside the actual texture dimensions + // Skip blocks outside the actual texture if block_x >= blocks_wide || block_y >= blocks_tall { continue; } @@ -363,10 +364,7 @@ pub fn detile( /// Decode a Morton (Z-order curve) index into (x, y) coordinates. /// -/// Morton encoding interleaves the bits of x and y coordinates: -/// index = ...y3 x3 y2 x2 y1 x1 y0 x0 -/// -/// This is the inverse operation — extract interleaved bits back to x, y. +/// Morton encoding interleaves bits: index = …y2 x2 y1 x1 y0 x0 #[inline] pub fn morton_decode(index: u32) -> (u32, u32) { let x = compact_bits(index); @@ -374,15 +372,15 @@ pub fn morton_decode(index: u32) -> (u32, u32) { (x, y) } -/// Compact every other bit — the "de-interleave" operation. -/// Used by `morton_decode` to separate X and Y from a Morton index. +/// Extract every other bit and pack them into the low bits. +/// Used by `morton_decode` to de-interleave X and Y. #[inline] fn compact_bits(mut x: u32) -> u32 { - x &= 0x5555_5555; // x = -f-e -d-c -b-a -9-8 -7-6 -5-4 -3-2 -1-0 - x = (x ^ (x >> 1)) & 0x3333_3333; // x = --fe --dc --ba --98 --76 --54 --32 --10 - x = (x ^ (x >> 2)) & 0x0f0f_0f0f; // x = ----fedc ----ba98 ----7654 ----3210 - x = (x ^ (x >> 4)) & 0x00ff_00ff; // x = --------fedcba98 --------76543210 - x = (x ^ (x >> 8)) & 0x0000_ffff; // x = ----------------fedcba9876543210 + x &= 0x5555_5555; // keep even-position bits + x = (x ^ (x >> 1)) & 0x3333_3333; + x = (x ^ (x >> 2)) & 0x0f0f_0f0f; + x = (x ^ (x >> 4)) & 0x00ff_00ff; + x = (x ^ (x >> 8)) & 0x0000_ffff; x } @@ -394,20 +392,15 @@ mod tests { #[test] fn morton_decode_corners() { - // Index 0 → (0, 0) assert_eq!(morton_decode(0), (0, 0)); - // Index 1 → (1, 0) — bit 0 is x - assert_eq!(morton_decode(1), (1, 0)); - // Index 2 → (0, 1) — bit 1 is y - assert_eq!(morton_decode(2), (0, 1)); - // Index 3 → (1, 1) + assert_eq!(morton_decode(1), (1, 0)); // bit 0 → x + assert_eq!(morton_decode(2), (0, 1)); // bit 1 → y assert_eq!(morton_decode(3), (1, 1)); } #[test] fn detile_noop_for_1x1_block() { - // A 4×4 DXT1 texture = exactly 1 block = 8 bytes - // De-tiling a single block should be identity + // A 4×4 DXT1 texture = exactly 1 block = 8 bytes; de-tiling is identity let src = vec![0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x02, 0x03, 0x04]; let result = detile(&src, 4, 4, X360TextureFormat::Dxt1).unwrap(); assert_eq!(result, src); @@ -415,8 +408,17 @@ mod tests { #[test] fn x360_format_bytes_per_block() { - assert_eq!(X360TextureFormat::Dxt1.bytes_per_block(), 8); + assert_eq!(X360TextureFormat::Dxt1.bytes_per_block(), 8); + assert_eq!(X360TextureFormat::Dxt5A.bytes_per_block(), 8); assert_eq!(X360TextureFormat::Dxt5.bytes_per_block(), 16); assert_eq!(X360TextureFormat::A8R8G8B8.bytes_per_block(), 4); } + + #[test] + fn format_from_u8_roundtrip() { + for code in [6u8, 7, 18, 19, 20, 49, 59] { + assert!(X360TextureFormat::from_u8(code).is_some(), "missing format {code}"); + } + assert!(X360TextureFormat::from_u8(0x52).is_none(), "old D3DFORMAT 0x52 must not match"); + } } diff --git a/crates/sylpheed-formats/src/xiso.rs b/crates/sylpheed-formats/src/xiso.rs index 3adc475..64bf069 100644 --- a/crates/sylpheed-formats/src/xiso.rs +++ b/crates/sylpheed-formats/src/xiso.rs @@ -14,25 +14,48 @@ use std::path::Path; use anyhow::{Context, Result}; use tracing::{debug, info}; +use xdvdfs::blockdev::OffsetWrapper; use xdvdfs::layout::VolumeDescriptor; /// A handle to an open XISO image. -pub struct XisoReader { +/// +/// The inner file is wrapped in an [`OffsetWrapper`] which probes the four +/// known XGD partition offsets (raw XISO, XGD1, XGD2, XGD3) at open time, +/// so both single-layer raw dumps **and** full dual-layer disc images are +/// supported transparently. +pub struct XisoReader { volume: VolumeDescriptor, - file: F, + /// Offset-aware block device — all sector reads are shifted by the + /// detected partition start offset automatically. + file: OffsetWrapper, } impl XisoReader { - pub async fn open(mut file: F) -> Result { - let volume = xdvdfs::read::read_volume(&mut file) + pub async fn open(file: F) -> Result { + // OffsetWrapper::new probes four known partition offsets: + // 0x00000000 — raw XISO (trimmed, sector 0 = XDVDFS start) + // 0x183E0000 — XGD1 (original Xbox) + // 0x0FD90000 — XGD2 (Xbox 360, most retail titles) + // 0x02080000 — XGD3 (Xbox 360, later dual-layer titles) + let mut wrapper = OffsetWrapper::new(file).await.map_err(|e| { + anyhow::anyhow!( + "No valid XDVDFS partition found in this disc image. \ + Tried raw XISO, XGD1, XGD2, and XGD3 offsets. \ + Is this a valid Xbox 360 (or original Xbox) disc image? \ + (internal error: {e:?})" + ) + })?; + + // Re-read the volume descriptor via the wrapper (now at the correct offset). + let volume = xdvdfs::read::read_volume(&mut wrapper) .await - .context("Failed to read XDVDFS volume descriptor. Is this a valid Xbox 360 ISO?")?; + .context("Found XDVDFS partition but failed to parse volume descriptor")?; info!( "Opened XISO: root directory table at sector {}", { let s = volume.root_table.region.sector; s } ); - Ok(Self { volume, file }) + Ok(Self { volume, file: wrapper }) } /// List all files in the disc image, recursively (directories excluded). @@ -135,10 +158,13 @@ impl XisoReader { } } -/// Open an XISO from a file path (the common case). +/// Open a disc image from a file path. +/// +/// Accepts raw XISO dumps and full XGD1/XGD2/XGD3 disc images — the correct +/// partition offset is detected automatically. pub async fn open_iso(path: &Path) -> Result> { let file = std::fs::File::open(path) - .with_context(|| format!("Cannot open ISO: {}", path.display()))?; + .with_context(|| format!("Cannot open disc image: {}", path.display()))?; XisoReader::open(file).await }