diff options
| author | Valentin Popov <valentin@popov.link> | 2026-02-19 12:46:23 +0300 |
|---|---|---|
| committer | Valentin Popov <valentin@popov.link> | 2026-02-19 12:46:23 +0300 |
| commit | efab61a45c8837d3c2aaec464d8f6243fecb7a38 (patch) | |
| tree | b511f1cab917f5f2931d6bc2ae2676b553bb8ef9 /crates/texm | |
| parent | 0d7ae6a017b8b2bf26c5c14c39cb62b599e8262d (diff) | |
| download | fparkan-efab61a45c8837d3c2aaec464d8f6243fecb7a38.tar.xz fparkan-efab61a45c8837d3c2aaec464d8f6243fecb7a38.zip | |
feat(render-core): add default UV scale and refactor UV mapping logic
- Introduced a constant `DEFAULT_UV_SCALE` for UV scaling.
- Refactored UV mapping in `build_render_mesh` to use the new constant.
- Simplified `compute_bounds` functions by extracting common logic into `compute_bounds_impl`.
test(render-core): add tests for rendering with empty and multi-node models
- Added tests to verify behavior when building render meshes from models with no slots and multiple nodes.
- Ensured UV scaling is correctly applied in tests.
feat(render-demo): add FOV argument and improve error handling
- Added a `--fov` command-line argument to set the field of view.
- Enhanced error messages for texture resolution failures.
- Updated MVP computation to use the new FOV parameter.
fix(rsli): improve error handling in LZH decompression
- Added checks to prevent out-of-bounds access in LZH decoding logic.
refactor(texm): streamline texture parsing and decoding tests
- Created a helper function `build_texm_payload` for constructing test payloads.
- Added tests for various texture formats including RGB565, RGB556, ARGB4444, and Luminance Alpha.
- Improved error handling for invalid TEXM headers and mip bounds.
Diffstat (limited to 'crates/texm')
| -rw-r--r-- | crates/texm/Cargo.toml | 2 | ||||
| -rw-r--r-- | crates/texm/src/error.rs | 1 | ||||
| -rw-r--r-- | crates/texm/src/lib.rs | 14 | ||||
| -rw-r--r-- | crates/texm/src/tests.rs | 228 |
4 files changed, 188 insertions, 57 deletions
diff --git a/crates/texm/Cargo.toml b/crates/texm/Cargo.toml index 7085293..216bb44 100644 --- a/crates/texm/Cargo.toml +++ b/crates/texm/Cargo.toml @@ -3,5 +3,5 @@ name = "texm" version = "0.1.0" edition = "2021" -[dependencies] +[dev-dependencies] nres = { path = "../nres" } diff --git a/crates/texm/src/error.rs b/crates/texm/src/error.rs index 38e32ca..90d618d 100644 --- a/crates/texm/src/error.rs +++ b/crates/texm/src/error.rs @@ -1,6 +1,7 @@ use core::fmt; #[derive(Debug)] +#[non_exhaustive] pub enum Error { HeaderTooSmall { size: usize, diff --git a/crates/texm/src/lib.rs b/crates/texm/src/lib.rs index 5d8b594..7a166f3 100644 --- a/crates/texm/src/lib.rs +++ b/crates/texm/src/lib.rs @@ -36,6 +36,7 @@ impl PixelFormat { match self { Self::Indexed8 => 1, Self::Rgb565 | Self::Rgb556 | Self::Argb4444 | Self::LuminanceAlpha88 => 2, + // Parkan stores format 888 as 32-bit RGBX in texture payloads. Self::Rgb888 | Self::Argb8888 => 4, } } @@ -173,14 +174,8 @@ pub fn parse_texm(payload: &[u8]) -> Result<Texture> { offset: level_offset, size: level_size, }); - w = w.max(1) >> 1; - h = h.max(1) >> 1; - if w == 0 { - w = 1; - } - if h == 0 { - h = 1; - } + w = (w >> 1).max(1); + h = (h >> 1).max(1); } let page_rects = parse_page_tail(payload, offset)?; @@ -240,7 +235,8 @@ pub fn decode_mip_rgba8(texture: &Texture, payload: &[u8], mip_index: usize) -> break; } let poff = usize::from(index).saturating_mul(4); - if poff + 3 >= palette.len() { + // Keep this form to accept the last palette item (index 255). + if poff + 4 > palette.len() { continue; } let out = i.saturating_mul(4); diff --git a/crates/texm/src/tests.rs b/crates/texm/src/tests.rs index 3d990bf..ba8aeeb 100644 --- a/crates/texm/src/tests.rs +++ b/crates/texm/src/tests.rs @@ -35,6 +35,36 @@ fn nres_test_files() -> Vec<PathBuf> { .collect() } +fn build_texm_payload( + width: u32, + height: u32, + format_raw: u32, + flags5: u32, + palette: Option<[u8; 1024]>, + mip_levels: &[&[u8]], +) -> Vec<u8> { + let mut payload = Vec::new(); + payload.extend_from_slice(&TEXM_MAGIC.to_le_bytes()); + payload.extend_from_slice(&width.to_le_bytes()); + payload.extend_from_slice(&height.to_le_bytes()); + payload.extend_from_slice( + &u32::try_from(mip_levels.len()) + .expect("mip level count overflow in test") + .to_le_bytes(), + ); + payload.extend_from_slice(&0u32.to_le_bytes()); // flags4 + payload.extend_from_slice(&flags5.to_le_bytes()); + payload.extend_from_slice(&0u32.to_le_bytes()); // unk6 + payload.extend_from_slice(&format_raw.to_le_bytes()); + if let Some(palette) = palette { + payload.extend_from_slice(&palette); + } + for level in mip_levels { + payload.extend_from_slice(level); + } + payload +} + #[test] fn texm_parse_all_game_textures() { let archives = nres_test_files(); @@ -97,16 +127,7 @@ fn texm_parse_all_game_textures() { #[test] fn texm_parse_minimal_argb8888_no_page() { - let mut payload = Vec::new(); - payload.extend_from_slice(&TEXM_MAGIC.to_le_bytes()); - payload.extend_from_slice(&1u32.to_le_bytes()); // width - payload.extend_from_slice(&1u32.to_le_bytes()); // height - payload.extend_from_slice(&1u32.to_le_bytes()); // mip_count - payload.extend_from_slice(&0u32.to_le_bytes()); // flags4 - payload.extend_from_slice(&0u32.to_le_bytes()); // flags5 - payload.extend_from_slice(&0u32.to_le_bytes()); // unk6 - payload.extend_from_slice(&8888u32.to_le_bytes()); // format - payload.extend_from_slice(&[1, 2, 3, 4]); // one pixel + let payload = build_texm_payload(1, 1, 8888, 0, None, &[&[1, 2, 3, 4]]); let parsed = parse_texm(&payload).expect("failed to parse minimal texm"); assert_eq!(parsed.header.width, 1); @@ -117,17 +138,7 @@ fn texm_parse_minimal_argb8888_no_page() { #[test] fn texm_decode_minimal_argb8888_no_page() { - let mut payload = Vec::new(); - payload.extend_from_slice(&TEXM_MAGIC.to_le_bytes()); - payload.extend_from_slice(&1u32.to_le_bytes()); // width - payload.extend_from_slice(&1u32.to_le_bytes()); // height - payload.extend_from_slice(&1u32.to_le_bytes()); // mip_count - payload.extend_from_slice(&0u32.to_le_bytes()); // flags4 - payload.extend_from_slice(&0u32.to_le_bytes()); // flags5 - payload.extend_from_slice(&0u32.to_le_bytes()); // unk6 - payload.extend_from_slice(&8888u32.to_le_bytes()); // format - payload.extend_from_slice(&[0x40, 0x11, 0x22, 0x33]); // A,R,G,B in little-endian order - + let payload = build_texm_payload(1, 1, 8888, 0, None, &[&[0x40, 0x11, 0x22, 0x33]]); let parsed = parse_texm(&payload).expect("failed to parse minimal texm"); let decoded = decode_mip_rgba8(&parsed, &payload, 0).expect("failed to decode mip"); assert_eq!(decoded.width, 1); @@ -136,18 +147,54 @@ fn texm_decode_minimal_argb8888_no_page() { } #[test] +fn texm_decode_rgb565() { + let word = 0xFFE0u16; // r=31 g=63 b=0 + let payload = build_texm_payload(1, 1, 565, 0, None, &[&word.to_le_bytes()]); + let parsed = parse_texm(&payload).expect("failed to parse rgb565 texm"); + let decoded = decode_mip_rgba8(&parsed, &payload, 0).expect("failed to decode rgb565 texm"); + assert_eq!(decoded.rgba8, vec![255, 255, 0, 255]); +} + +#[test] +fn texm_decode_rgb556() { + let word = 0xF800u16; // r=31 g=0 b=0 + let payload = build_texm_payload(1, 1, 556, 0, None, &[&word.to_le_bytes()]); + let parsed = parse_texm(&payload).expect("failed to parse rgb556 texm"); + let decoded = decode_mip_rgba8(&parsed, &payload, 0).expect("failed to decode rgb556 texm"); + assert_eq!(decoded.rgba8, vec![255, 0, 0, 255]); +} + +#[test] +fn texm_decode_argb4444() { + let word = 0xF12Eu16; // a=F r=1 g=2 b=E + let payload = build_texm_payload(1, 1, 4444, 0, None, &[&word.to_le_bytes()]); + let parsed = parse_texm(&payload).expect("failed to parse argb4444 texm"); + let decoded = decode_mip_rgba8(&parsed, &payload, 0).expect("failed to decode argb4444 texm"); + assert_eq!(decoded.rgba8, vec![17, 34, 238, 255]); +} + +#[test] +fn texm_decode_luminance_alpha88() { + let word = 0x7F40u16; // luminance=0x7F alpha=0x40 + let payload = build_texm_payload(1, 1, 88, 0, None, &[&word.to_le_bytes()]); + let parsed = parse_texm(&payload).expect("failed to parse la88 texm"); + let decoded = decode_mip_rgba8(&parsed, &payload, 0).expect("failed to decode la88 texm"); + assert_eq!(decoded.rgba8, vec![0x7F, 0x7F, 0x7F, 0x40]); +} + +#[test] +fn texm_decode_rgb888x() { + let payload = build_texm_payload(1, 1, 888, 0, None, &[&[0x11, 0x22, 0x33, 0x99]]); + let parsed = parse_texm(&payload).expect("failed to parse rgb888 texm"); + let decoded = decode_mip_rgba8(&parsed, &payload, 0).expect("failed to decode rgb888 texm"); + assert_eq!(decoded.rgba8, vec![0x11, 0x22, 0x33, 255]); +} + +#[test] fn texm_parse_indexed_with_page_chunk() { - let mut payload = Vec::new(); - payload.extend_from_slice(&TEXM_MAGIC.to_le_bytes()); - payload.extend_from_slice(&2u32.to_le_bytes()); // width - payload.extend_from_slice(&2u32.to_le_bytes()); // height - payload.extend_from_slice(&1u32.to_le_bytes()); // mip_count - payload.extend_from_slice(&0u32.to_le_bytes()); // flags4 - payload.extend_from_slice(&0u32.to_le_bytes()); // flags5 - payload.extend_from_slice(&0u32.to_le_bytes()); // unk6 - payload.extend_from_slice(&0u32.to_le_bytes()); // format indexed8 - payload.extend_from_slice(&[0u8; 1024]); // palette - payload.extend_from_slice(&[1, 2, 3, 4]); // pixels + let mut palette = [0u8; 1024]; + palette[4..8].copy_from_slice(&[10, 20, 30, 255]); + let mut payload = build_texm_payload(2, 2, 0, 0, Some(palette), &[&[1, 1, 1, 1]]); payload.extend_from_slice(&PAGE_MAGIC.to_le_bytes()); payload.extend_from_slice(&1u32.to_le_bytes()); // rect_count payload.extend_from_slice(&0i16.to_le_bytes()); // x @@ -170,26 +217,113 @@ fn texm_parse_indexed_with_page_chunk() { } #[test] -fn texm_decode_indexed_with_palette() { - let mut payload = Vec::new(); - payload.extend_from_slice(&TEXM_MAGIC.to_le_bytes()); - payload.extend_from_slice(&2u32.to_le_bytes()); // width - payload.extend_from_slice(&1u32.to_le_bytes()); // height - payload.extend_from_slice(&1u32.to_le_bytes()); // mip_count - payload.extend_from_slice(&0u32.to_le_bytes()); // flags4 - payload.extend_from_slice(&0u32.to_le_bytes()); // flags5 - payload.extend_from_slice(&0u32.to_le_bytes()); // unk6 - payload.extend_from_slice(&0u32.to_le_bytes()); // format indexed8 - +fn texm_decode_indexed_with_palette_last_entry() { let mut palette = [0u8; 1024]; palette[4..8].copy_from_slice(&[10, 20, 30, 255]); // index 1 palette[8..12].copy_from_slice(&[40, 50, 60, 200]); // index 2 - payload.extend_from_slice(&palette); - payload.extend_from_slice(&[1u8, 2u8]); // two pixels + palette[1020..1024].copy_from_slice(&[1, 2, 3, 4]); // index 255 (last) + let payload = build_texm_payload(3, 1, 0, 0, Some(palette), &[&[1u8, 2u8, 255u8]]); let parsed = parse_texm(&payload).expect("failed to parse indexed texm"); let decoded = decode_mip_rgba8(&parsed, &payload, 0).expect("failed to decode indexed texm"); - assert_eq!(decoded.width, 2); + assert_eq!(decoded.width, 3); assert_eq!(decoded.height, 1); - assert_eq!(decoded.rgba8, vec![10, 20, 30, 255, 40, 50, 60, 200]); + assert_eq!( + decoded.rgba8, + vec![10, 20, 30, 255, 40, 50, 60, 200, 1, 2, 3, 4] + ); +} + +#[test] +fn texm_parse_multi_mip_offsets() { + let mip0 = [0x10u8; 32]; // 4*2*4 + let mip1 = [0x20u8; 8]; // 2*1*4 + let mip2 = [0x30u8; 4]; // 1*1*4 + let payload = build_texm_payload(4, 2, 8888, 0, None, &[&mip0, &mip1, &mip2]); + + let parsed = parse_texm(&payload).expect("failed to parse multi-mip texm"); + assert_eq!(parsed.header.mip_count, 3); + assert_eq!(parsed.mip_levels.len(), 3); + assert_eq!( + parsed.mip_levels, + vec![ + MipLevel { + width: 4, + height: 2, + offset: 32, + size: 32 + }, + MipLevel { + width: 2, + height: 1, + offset: 64, + size: 8 + }, + MipLevel { + width: 1, + height: 1, + offset: 72, + size: 4 + }, + ] + ); +} + +#[test] +fn texm_preserves_flags5_for_mip_skip_metadata() { + let payload = build_texm_payload(1, 1, 8888, 0x0000_00A5, None, &[&[0, 0, 0, 0]]); + let parsed = parse_texm(&payload).expect("failed to parse texm"); + assert_eq!(parsed.header.flags5, 0x0000_00A5); +} + +#[test] +fn texm_errors_for_invalid_header_values() { + let mut bad_magic = build_texm_payload(1, 1, 8888, 0, None, &[&[0, 0, 0, 0]]); + bad_magic[0..4].copy_from_slice(&0u32.to_le_bytes()); + assert!(matches!( + parse_texm(&bad_magic), + Err(Error::InvalidMagic { .. }) + )); + + let zero_dims = build_texm_payload(0, 1, 8888, 0, None, &[&[]]); + assert!(matches!( + parse_texm(&zero_dims), + Err(Error::InvalidDimensions { .. }) + )); + + let mut bad_mips = build_texm_payload(1, 1, 8888, 0, None, &[&[0, 0, 0, 0]]); + bad_mips[12..16].copy_from_slice(&0u32.to_le_bytes()); + assert!(matches!( + parse_texm(&bad_mips), + Err(Error::InvalidMipCount { .. }) + )); + + let bad_format = build_texm_payload(1, 1, 12345, 0, None, &[&[0, 0, 0, 0]]); + assert!(matches!( + parse_texm(&bad_format), + Err(Error::UnknownFormat { .. }) + )); +} + +#[test] +fn texm_errors_for_page_chunk_and_mip_bounds() { + let mut bad_page = build_texm_payload(1, 1, 8888, 0, None, &[&[0, 0, 0, 0]]); + bad_page.extend_from_slice(b"X"); + assert!(matches!( + parse_texm(&bad_page), + Err(Error::InvalidPageSize { .. }) + )); + + let payload = build_texm_payload(1, 1, 8888, 0, None, &[&[1, 2, 3, 4]]); + let parsed = parse_texm(&payload).expect("failed to parse valid texm"); + assert!(matches!( + decode_mip_rgba8(&parsed, &payload, 7), + Err(Error::MipIndexOutOfRange { .. }) + )); + + let truncated = &payload[..payload.len() - 1]; + assert!(matches!( + decode_mip_rgba8(&parsed, truncated, 0), + Err(Error::MipDataOutOfBounds { .. }) + )); } |
