//! Level textures: 8-bit palette indices plus a 256-entry CLUT. //! //! Spec: `docs/formats/texture.md`. //! //! Every level texture is a rectangular grid of **palette indices**, not //! pixels: `width * height` bytes at `core_data[textures_base + data_offset]`. //! The colour comes from a CLUT of 256 RGBA entries (1024 bytes) in GS RAM at //! `palette * 0x100`. Both halves are indexed by the same byte, so nothing //! about the layout is inferred — it is a straight lookup. //! //! Two conventions have to be respected, and both are PS2 hardware details //! rather than choices: //! //! * **CSM1 CLUT order.** Within each 32-entry group the two middle 8-entry //! blocks are swapped. The remap is an involution, so the same function //! converts either way. //! * **Alpha runs 0..=0x80, not 0..=255.** `0x80` is fully opaque, and //! anything above it clamps to opaque. use crate::buf::{Error, ErrorKind, Result}; use crate::level_core::TextureEntry; /// PS2 CSM1 CLUT order to linear: within each 32-entry group, swap the two /// middle 8-entry blocks. An involution. pub fn clut_index(i: u32) -> u32 { if ((i >> 3) & 1) != ((i >> 4) & 1) { i ^ 0x18 } else { i } } /// PS2 alpha 0..=0x80 to 0..=0xff. `0x80` is opaque; above that clamps. pub fn scale_alpha(a: u8) -> u8 { if a < 0x80 { a * 2 } else { 0xff } } /// A decoded RGBA8 image. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Texture { pub width: u32, pub height: u32, /// `width * height * 4` bytes, row-major from the top. pub rgba: Vec, } /// One stored image: palette indices plus the CLUT, exactly as on disc. #[derive(Debug, Clone, Copy)] pub struct IndexedImage<'a> { pub width: u32, pub height: u32, pub indices: &'a [u8], /// 1024 bytes, 256 RGBA entries in CSM1 order. pub clut: &'a [u8], } impl IndexedImage<'_> { /// Look every index up in the CLUT. pub fn decode(&self) -> Result { decode_indexed8(self.indices, self.width, self.height, self.clut) } } /// Decode `width * height` palette indices against a 256-entry CLUT. pub fn decode_indexed8(indices: &[u8], width: u32, height: u32, clut: &[u8]) -> Result { let n = width as usize * height as usize; if indices.len() < n { return Err(Error { offset: 0, context: "indexed texture data too small", kind: ErrorKind::OutOfBounds { needed: n, available: indices.len(), }, }); } if clut.len() < 1024 { return Err(Error { offset: 0, context: "texture palette too small", kind: ErrorKind::OutOfBounds { needed: 1024, available: clut.len(), }, }); } let mut pal = [[0u8; 4]; 256]; for (i, p) in pal.iter_mut().enumerate() { let at = clut_index(i as u32) as usize * 4; let e = &clut[at..at + 4]; *p = [e[0], e[1], e[2], scale_alpha(e[3])]; } let mut rgba = Vec::with_capacity(n * 4); for &ix in &indices[..n] { rgba.extend_from_slice(&pal[ix as usize]); } Ok(Texture { width, height, rgba, }) } /// The base level of a tfrag texture: `width * height` indices at `data_offset`. pub fn base_image<'a>( textures: &'a [u8], gs_ram: &'a [u8], e: &TextureEntry, ) -> Result> { let (px, clut) = pixels_and_clut(textures, gs_ram, e)?; let (w, h) = (e.width as usize, e.height as usize); Ok(IndexedImage { width: w as u32, height: h as u32, indices: px, clut, }) } /// Every mip level of a tfrag texture, base level first. /// /// All tfrag textures are square, so the levels halve evenly. Levels 0 and 1 /// live in the texture block; levels 2 and 3 are resident in GS RAM and are /// addressed by the entry's `mipmap` and `pad` fields. pub fn mip_images<'a>( textures: &'a [u8], gs_ram: &'a [u8], e: &TextureEntry, ) -> Result>> { let (px0, clut) = pixels_and_clut(textures, gs_ram, e)?; let (w, h) = (e.width as usize, e.height as usize); let levels = e.ty as usize; if !(1..=4).contains(&levels) { return Err(Error { offset: 0, context: "tfrag texture level count outside 1..=4", kind: ErrorKind::InvalidLength("tfrag texture level count"), }); } let sub = |data: &'a [u8], at: usize, need: usize, what: &'static str| -> Result<&'a [u8]> { let end = at .checked_add(need) .filter(|&e| e <= data.len()) .ok_or(Error { offset: at, context: what, kind: ErrorKind::OutOfBounds { needed: need, available: data.len().saturating_sub(at), }, })?; Ok(&data[at..end]) }; let mut out = Vec::with_capacity(levels); for level in 0..levels { let (lw, lh) = (w >> level, h >> level); if lw == 0 || lh == 0 { return Err(Error { offset: 0, context: "mip level smaller than one texel", kind: ErrorKind::InvalidLength("mip level size"), }); } let need = lw * lh; let indices = match level { 0 => px0, 1 => sub( textures, e.data_offset as usize + w * h, need, "mip 1 pixels", )?, 2 if e.mipmap >= 0 => sub(gs_ram, e.mipmap as usize * 0x100, need, "mip 2 pixels")?, 3 if e.pad >= 0 => sub(gs_ram, e.pad as usize * 0x100, need, "mip 3 pixels")?, _ => { return Err(Error { offset: 0, context: "mip level without a gs_ram block", kind: ErrorKind::InvalidLength("mip level"), }) } }; out.push(IndexedImage { width: lw as u32, height: lh as u32, indices, clut, }); } Ok(out) } /// Decode the whole mip chain. pub fn decode_tfrag_mips(textures: &[u8], gs_ram: &[u8], e: &TextureEntry) -> Result> { mip_images(textures, gs_ram, e)? .iter() .map(IndexedImage::decode) .collect() } fn pixels_and_clut<'a>( textures: &'a [u8], gs_ram: &'a [u8], e: &TextureEntry, ) -> Result<(&'a [u8], &'a [u8])> { if e.width <= 0 || e.height <= 0 || e.data_offset < 0 || e.palette < 0 { return Err(Error { offset: 0, context: "texture entry has no pixels", kind: ErrorKind::Invalid("texture entry has no pixels"), }); } let need = e.width as usize * e.height as usize; let end = (e.data_offset as usize) .checked_add(need) .filter(|&x| x <= textures.len()) .ok_or(Error { offset: e.data_offset as usize, context: "texture pixels run past the texture block", kind: ErrorKind::OutOfBounds { needed: need, available: textures.len().saturating_sub(e.data_offset.max(0) as usize), }, })?; let clut_at = e.palette as usize * 0x100; let clut_end = clut_at .checked_add(1024) .filter(|&x| x <= gs_ram.len()) .ok_or(Error { offset: clut_at, context: "texture palette runs past gs_ram", kind: ErrorKind::OutOfBounds { needed: 1024, available: gs_ram.len().saturating_sub(clut_at), }, })?; Ok(( &textures[e.data_offset as usize..end], &gs_ram[clut_at..clut_end], )) } #[cfg(test)] mod tests { use super::*; #[test] fn clut_order_swaps_the_middle_blocks() { // Bits 3 and 4 differ inside a 32-entry group, so the remap applies. assert_eq!(clut_index(0x00), 0x00); assert_eq!(clut_index(0x08), 0x10); assert_eq!(clut_index(0x10), 0x08); assert_eq!(clut_index(0x1f), 0x1f); // Outside that pattern the index is unchanged. assert_eq!(clut_index(0x07), 0x07); assert_eq!(clut_index(0x18), 0x18); // The remap is an involution, so it round-trips. for i in 0..256u32 { assert_eq!(clut_index(clut_index(i)), i); } } #[test] fn alpha_is_scaled_from_eight_bit_halves() { assert_eq!(scale_alpha(0), 0); assert_eq!(scale_alpha(0x40), 0x80); assert_eq!(scale_alpha(0x7f), 0xfe); assert_eq!(scale_alpha(0x80), 0xff); assert_eq!(scale_alpha(0xff), 0xff); } /// A 2x1 image whose two texels select distinct CLUT entries. fn fixture() -> (Vec, Vec) { let mut clut = vec![0u8; 1024]; for i in 0..256u32 { let at = clut_index(i) as usize * 4; clut[at] = i as u8; clut[at + 1] = 255 - i as u8; clut[at + 2] = 7; clut[at + 3] = 0x80; } (vec![3, 200], clut) } #[test] fn decodes_indices_through_the_clut() { let (idx, clut) = fixture(); let t = decode_indexed8(&idx, 2, 1, &clut).unwrap(); assert_eq!(t.rgba.len(), 8); // Entry 3 is red, entry 200 has a flipped red and green. assert_eq!(&t.rgba[0..4], &[3, 252, 7, 0xff]); assert_eq!(&t.rgba[4..8], &[200, 55, 7, 0xff]); } #[test] fn short_data_is_an_error_not_a_panic() { let (_idx, clut) = fixture(); assert!(decode_indexed8(&[0, 1], 4, 4, &clut).is_err()); assert!(decode_indexed8(&[0; 16], 2, 2, &clut[..512]).is_err()); assert!(decode_indexed8(&[0; 3], 2, 1, &clut).is_ok()); } #[test] fn a_square_entry_halves_through_its_mip_chain() { // 64x64 base, mip 1 immediately after it, mips 2 and 3 in gs_ram. let mut textures = vec![0u8; 64 * 64 * 2]; for (i, b) in textures.iter_mut().enumerate() { *b = (i % 251) as u8; } let mut gs = vec![0u8; 0x4000]; for (i, b) in gs.iter_mut().enumerate().take(0x3000) { *b = (i % 253) as u8; } let e = TextureEntry { data_offset: 0, width: 64, height: 64, ty: 4, palette: 8, mipmap: 16, pad: 24, }; let mips = mip_images(&textures, &gs, &e).unwrap(); assert_eq!(mips.len(), 4); let sizes: Vec<(u32, u32)> = mips.iter().map(|m| (m.width, m.height)).collect(); assert_eq!(sizes, [(64, 64), (32, 32), (16, 16), (8, 8)]); // Level 0 is the base block; level 1 follows it immediately. assert_eq!(mips[0].indices, &textures[0..64 * 64]); assert_eq!(mips[1].indices, &textures[64 * 64..64 * 64 + 32 * 32]); // Levels 2 and 3 come from gs_ram at `mipmap * 0x100` and `pad * 0x100`. assert_eq!(mips[2].indices, &gs[16 * 0x100..16 * 0x100 + 16 * 16]); assert_eq!(mips[3].indices, &gs[24 * 0x100..24 * 0x100 + 8 * 8]); } #[test] fn an_absent_high_mip_is_an_error() { let textures = vec![0u8; 64 * 64]; let gs = vec![0u8; 0x4000]; let e = TextureEntry { data_offset: 0, width: 64, height: 64, ty: 4, palette: 8, mipmap: 16, pad: -1, }; assert!(mip_images(&textures, &gs, &e).is_err()); } #[test] fn a_palette_past_gs_ram_is_an_error() { let textures = vec![0u8; 64 * 64]; let gs = vec![0u8; 0x100]; let e = TextureEntry { data_offset: 0, width: 64, height: 64, ty: 3, palette: 900, mipmap: -1, pad: -1, }; assert!(base_image(&textures, &gs, &e).is_err()); } }