Prime 4 TXTR support

This commit is contained in:
Luke Street
2025-12-09 22:31:03 -07:00
parent 96c6506673
commit a204d8e5f1
3 changed files with 175 additions and 45 deletions
+152 -28
View File
@@ -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<STextureReadInfo>,
#[bw(try_calc = buffers.len().try_into())]
pub buffer_count: u32,
#[br(count = buffer_count)]
pub buffers: Vec<STextureCompressedBufferInfo>,
pub buffers: STextureCompressedBufferInfo2,
// #[bw(try_calc = buffers.len().try_into())]
// pub buffer_count: u32,
// #[br(count = buffer_count)]
// pub buffers: Vec<STextureCompressedBufferInfo>,
}
#[binrw]
@@ -574,8 +594,8 @@ impl<O: ByteOrder> TextureData<O> {
pub fn slice(data: &[u8], meta: &[u8]) -> Result<Self> {
let (txtr_desc, txtr_data, _) = FormDescriptor::<O>::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::<O>::slice(txtr_data)?;
ensure!(head_desc.id == K_CHUNK_HEAD);
@@ -586,22 +606,126 @@ impl<O: ByteOrder> TextureData<O> {
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 })
}
+10 -6
View File
@@ -24,6 +24,10 @@ pub fn decompress_into(compressed_data: &[u8], out: &mut [u8]) -> Result<u32> {
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<u32> {
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");
+13 -11
View File
@@ -1,6 +1,6 @@
/// https://wiki.axiodl.com/w/LZSS_Compression
pub fn decompress<const M: u8>(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<const M: u8>(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<const M: u8>(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<const M: u8>(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];
}