From a204d8e5f1a38ff5b274c2a16a33af508c7609e4 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Tue, 9 Dec 2025 22:31:03 -0700 Subject: [PATCH] Prime 4 TXTR support --- lib/src/format/txtr.rs | 180 ++++++++++++++++++++++++++++++------ lib/src/util/compression.rs | 16 ++-- lib/src/util/lzss.rs | 24 ++--- 3 files changed, 175 insertions(+), 45 deletions(-) diff --git a/lib/src/format/txtr.rs b/lib/src/format/txtr.rs index bc37356..71f9c44 100644 --- a/lib/src/format/txtr.rs +++ b/lib/src/format/txtr.rs @@ -112,8 +112,9 @@ pub struct STextureHeader { pub width: u32, pub height: u32, pub layers: u32, - pub tile_mode: u32, - pub swizzle: u32, + pub components: [u8; 4], + // pub tile_mode: u32, + // pub swizzle: u32, #[bw(try_calc = mip_sizes.len().try_into())] pub mip_count: u32, #[br(count = mip_count)] @@ -141,14 +142,32 @@ pub struct STextureReadInfo { pub size: u32, } +// #[binrw] +// #[derive(Clone, Debug)] +// pub struct STextureCompressedBufferInfo { +// pub index: u32, +// pub offset: u32, +// pub size: u32, +// pub dest_offset: u32, +// pub dest_size: u32, +// } + #[binrw] #[derive(Clone, Debug)] -pub struct STextureCompressedBufferInfo { - pub index: u32, - pub offset: u32, - pub size: u32, - pub dest_offset: u32, - pub dest_size: u32, +pub struct STextureCompressedBufferInfo2 { + pub first_index: u32, + pub first_size: u32, + pub first_dest_offset: u32, + pub first_dest_size: u32, + pub unk: u32, + pub second_index: u32, + pub second_size: u32, + pub second_dest_offset: u32, + pub second_dest_size: u32, + // Within the second buffer, part of the data may be uncompressed. + pub second_decompressed_len: u32, + pub second_compressed_len: u32, + pub second_compressed_offset: u32, } #[binrw] @@ -164,10 +183,11 @@ pub struct STextureMetaData { pub info_count: u32, #[br(count = info_count)] pub info: Vec, - #[bw(try_calc = buffers.len().try_into())] - pub buffer_count: u32, - #[br(count = buffer_count)] - pub buffers: Vec, + pub buffers: STextureCompressedBufferInfo2, + // #[bw(try_calc = buffers.len().try_into())] + // pub buffer_count: u32, + // #[br(count = buffer_count)] + // pub buffers: Vec, } #[binrw] @@ -574,8 +594,8 @@ impl TextureData { pub fn slice(data: &[u8], meta: &[u8]) -> Result { let (txtr_desc, txtr_data, _) = FormDescriptor::::slice(data)?; ensure!(txtr_desc.id == K_FORM_TXTR); - ensure!(txtr_desc.reader_version.get() == 47); - ensure!(txtr_desc.writer_version.get() == 51); + ensure!(txtr_desc.reader_version.get() == 65); + ensure!(txtr_desc.writer_version.get() == 66); let (head_desc, head_data, _) = ChunkDescriptor::::slice(txtr_data)?; ensure!(head_desc.id == K_CHUNK_HEAD); @@ -586,22 +606,126 @@ impl TextureData { let meta: STextureMetaData = Cursor::new(meta).read_type(Endian::Little)?; let mut buffer = vec![0u8; meta.decompressed_size as usize]; - for info in &meta.buffers { - let (read_idx, read) = meta - .info - .iter() - .enumerate() - .find(|(_, i)| i.index as u32 == info.index) - .ok_or_else(|| anyhow!("Failed to locate read info for buffer {}", info.index))?; - ensure!(read.index as usize == read_idx); // do these ever differ? + // for info in &meta.buffers { + // let (read_idx, read) = meta + // .info + // .iter() + // .enumerate() + // .find(|(_, i)| i.index as u32 == info.index) + // .ok_or_else(|| anyhow!("Failed to locate read info for buffer {}", info.index))?; + // ensure!(read.index as usize == read_idx); // do these ever differ? + // let read_buf = &data[read.offset as usize..(read.offset + read.size) as usize]; + // let comp_buf = &read_buf[info.offset as usize..(info.offset + info.size) as usize]; + // decompress_into( + // comp_buf, + // &mut buffer + // [info.dest_offset as usize..(info.dest_offset + info.dest_size) as usize], + // )?; + // } + + // First buffer + if meta.buffers.first_size > 0 { + let read = meta.info.get(meta.buffers.first_index as usize).ok_or_else(|| { + anyhow!("Failed to locate read info for buffer {}", meta.buffers.first_index) + })?; let read_buf = &data[read.offset as usize..(read.offset + read.size) as usize]; - let comp_buf = &read_buf[info.offset as usize..(info.offset + info.size) as usize]; - decompress_into( - comp_buf, - &mut buffer - [info.dest_offset as usize..(info.dest_offset + info.dest_size) as usize], - )?; + let comp_buf = &read_buf[..meta.buffers.first_size as usize]; + + let dst_start = meta.buffers.first_dest_offset; + let dst_end = dst_start + meta.buffers.first_dest_size; + + log::debug!( + "Decompressing first buffer: {:#x}-{:#x} ({:#x}) -> {:#x}-{:#x} ({:#x})", + read.offset, + read.offset + meta.buffers.first_size, + meta.buffers.first_size, + dst_start, + dst_end, + dst_end - dst_start + ); + decompress_into(comp_buf, &mut buffer[dst_start as usize..dst_end as usize]) + .context("Decompressing first buffer")?; } + + if meta.buffers.second_size > 0 { + let read = meta.info.get(meta.buffers.second_index as usize).ok_or_else(|| { + anyhow!("Failed to locate read info for buffer {}", meta.buffers.second_index) + })?; + let read_buf = &data[read.offset as usize..(read.offset + read.size) as usize]; + + // Compressed part of second buffer + if meta.buffers.second_compressed_offset < meta.buffers.second_size { + let comp_buf = &read_buf[meta.buffers.second_compressed_offset as usize + ..(meta.buffers.second_compressed_offset + meta.buffers.second_compressed_len) + as usize]; + + let dst_start = meta.buffers.second_dest_offset; + let dst_end = dst_start + meta.buffers.second_decompressed_len; + + log::debug!( + "Decompressing second buffer: {:#x}-{:#x} ({:#x}) -> {:#x}-{:#x} ({:#x})", + read.offset + meta.buffers.second_compressed_offset, + read.offset + + meta.buffers.second_compressed_offset + + meta.buffers.second_compressed_len, + meta.buffers.second_compressed_len, + dst_start, + dst_end, + dst_end - dst_start + ); + decompress_into(comp_buf, &mut buffer[dst_start as usize..dst_end as usize]) + .context("Decompressing second buffer")?; + } + + // Uncompressed start of second buffer + if meta.buffers.second_compressed_offset > 0 { + let src_start = 0; + let src_end = meta.buffers.second_compressed_offset; + let comp_buf = &read_buf[src_start as usize..src_end as usize]; + + let dst_start = + meta.buffers.second_dest_offset + meta.buffers.second_decompressed_len; + let dst_end = dst_start + meta.buffers.second_compressed_offset; + + log::debug!( + "Copying uncompressed data from {:#x}-{:#x} ({:#x}) to {:#x}-{:#x} ({:#x})", + read.offset + src_start, + read.offset + src_end, + src_end - src_start, + dst_start, + dst_end, + dst_end - dst_start + ); + buffer[dst_start as usize..dst_end as usize].copy_from_slice(comp_buf); + } + + // Uncompressed end of second buffer + if meta.buffers.second_compressed_offset + meta.buffers.second_compressed_len + < meta.buffers.second_size + { + let src_start = + meta.buffers.second_compressed_offset + meta.buffers.second_compressed_len; + let src_end = meta.buffers.second_size; + let comp_buf = &read_buf[src_start as usize..src_end as usize]; + + let dst_start = meta.buffers.second_dest_offset + + meta.buffers.second_compressed_offset + + meta.buffers.second_decompressed_len; + let dst_end = meta.buffers.second_dest_offset + meta.buffers.second_dest_size; + + log::debug!( + "Copying uncompressed data from {:#x}-{:#x} ({:#x}) to {:#x}-{:#x} ({:#x})", + read.offset + src_start, + read.offset + src_end, + src_end - src_start, + dst_start, + dst_end, + dst_end - dst_start + ); + buffer[dst_start as usize..dst_end as usize].copy_from_slice(comp_buf); + } + } + let deswizzled = deswizzle(&head, &buffer)?; Ok(Self { head, data: deswizzled, _marker: PhantomData }) } diff --git a/lib/src/util/compression.rs b/lib/src/util/compression.rs index 941683e..0a488c8 100644 --- a/lib/src/util/compression.rs +++ b/lib/src/util/compression.rs @@ -24,6 +24,10 @@ pub fn decompress_into(compressed_data: &[u8], out: &mut [u8]) -> Result { if compressed_data.len() < 4 { bail!("Invalid compressed data size: {}", compressed_data.len()); } + if compressed_data.len() == out.len() { + out.copy_from_slice(compressed_data); + return Ok(0); + } let mode = u32::from_le_bytes(compressed_data[0..4].try_into().unwrap()); let data = &compressed_data[4..]; if !match mode { @@ -35,12 +39,12 @@ pub fn decompress_into(compressed_data: &[u8], out: &mut [u8]) -> Result { false } } - 1 => lzss::decompress::<1>(data, out), - 2 => lzss::decompress::<2>(data, out), - 3 => lzss::decompress::<3>(data, out), - 12 => lzss::decompress_huffman::<1>(data, out), - 13 => lzss::decompress_huffman::<2>(data, out), - 14 => lzss::decompress_huffman::<3>(data, out), + 1 => lzss::decompress::<0>(data, out), + 2 => lzss::decompress::<1>(data, out), + 3 => lzss::decompress::<2>(data, out), + 12 => lzss::decompress_huffman::<0>(data, out), + 13 => lzss::decompress_huffman::<1>(data, out), + 14 => lzss::decompress_huffman::<2>(data, out), _ => bail!("Unsupported compression mode {}", mode), } { bail!("Decompression failed"); diff --git a/lib/src/util/lzss.rs b/lib/src/util/lzss.rs index 48a9848..9247a75 100644 --- a/lib/src/util/lzss.rs +++ b/lib/src/util/lzss.rs @@ -1,6 +1,6 @@ /// https://wiki.axiodl.com/w/LZSS_Compression pub fn decompress(input: &[u8], output: &mut [u8]) -> bool { - let group_size: usize = 1 << (M - 1); + let group_size: usize = 1 << M; let mut state = DecompressionState::new(input); let mut out_cur: usize = 0; @@ -14,8 +14,8 @@ pub fn decompress(input: &[u8], output: &mut [u8]) -> bool { if !state.read_bytes_direct(&mut b, 0, 2) { return false; } - let count = (b[0] as usize >> 4) + (4 - M as usize); - let offset = (((b[0] as usize & 0xF) << 8) | b[1] as usize) << (M - 1); + let count = (b[0] as usize >> 4) + (3 - M as usize); + let offset = (((b[0] as usize & 0xF) << 8) | b[1] as usize) << M; if offset > out_cur { return false; } @@ -42,15 +42,15 @@ pub fn decompress(input: &[u8], output: &mut [u8]) -> bool { /// Decompresses Huffman-coded LZSS variants introduced in Metroid Prime 4. /// -/// M = 1: 0xC format (8-bit groups) -/// M = 2: 0xD format (16-bit groups) -/// M = 3: 0xE format (32-bit groups) +/// M = 0: 0xC format (8-bit groups) +/// M = 1: 0xD format (16-bit groups) +/// M = 2: 0xE format (32-bit groups) /// /// This format uses: /// - Flag bits indicating literal (0) or back-reference (1) /// - Huffman-coded length and offset values for back-references pub fn decompress_huffman(input: &[u8], output: &mut [u8]) -> bool { - let group_size: usize = 1 << (M - 1); + let group_size: usize = 1 << M; let mut state = DecompressionState::new(input); let mut out_cur: usize = 0; @@ -106,8 +106,9 @@ impl<'a> DecompressionState<'a> { #[inline] fn read_bytes_direct(&mut self, output: &mut [u8], pos: usize, count: usize) -> bool { if self.input.len() >= count && output.len() >= pos + count { - output[pos..pos + count].copy_from_slice(&self.input[..count]); - self.input = &self.input[count..]; + let (data, rest) = self.input.split_at(count); + output[pos..pos + count].copy_from_slice(data); + self.input = rest; true } else { false @@ -181,15 +182,16 @@ impl<'a> DecompressionState<'a> { let Some(offset_hi) = self.decode_huffman(&HUFFMAN_OFFSET_HI) else { return false; }; - let offset = ((offset_hi as usize) << (8 + M - 1)) | ((offset_lo as usize) << (M - 1)); + let offset = ((offset_hi as usize) << (8 + M)) | ((offset_lo as usize) << M); let cur = *out_cur; if offset > cur { return false; } - let copy_len = (length_code as usize + (4 - M as usize)) * (1 << (M - 1)); + let copy_len = (length_code as usize + (3 - M as usize)) * (1 << M); if copy_len > output.len() - cur { return false; } + // println!("Copying {} bytes ({}/group) from offset {} to {} (overlaps? {})", copy_len, M, cur - offset, cur, copy_len > offset); for i in 0..copy_len { output[cur + i] = output[cur - offset + i]; }