`mesh_consistency_disc.rs` had the only conflict: this branch added a `Sightings` type alias where #22 replaced the file's private `disc_root()` with the shared `common::disc_root`. Both kept — they are unrelated edits that happened to land in the same lines. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
152 lines
5.1 KiB
Rust
152 lines
5.1 KiB
Rust
//! Integration test: run the XPR2 texture pipeline against REAL `.xpr` files
|
|
//! read directly from the retail disc image, reproducing exactly what the
|
|
//! viewer's texture-preview path does (`identify_format` → `X360Texture::
|
|
//! from_xpr2`). Skipped unless `SYLPHEED_ISO` points at the disc image.
|
|
//!
|
|
//! Run: `cargo test -p sylpheed-formats --test texture_disc -- --ignored --nocapture`
|
|
|
|
use sylpheed_formats::texture::X360Texture;
|
|
use sylpheed_formats::vfs::identify_format;
|
|
|
|
mod common;
|
|
use common::iso_path;
|
|
|
|
#[tokio::test]
|
|
#[ignore = "requires the retail ISO — set SYLPHEED_ISO"]
|
|
async fn xpr_pipeline_over_disc_sample() {
|
|
let Some(iso) = iso_path() else {
|
|
eprintln!("SKIP: ISO not found (set SYLPHEED_ISO)");
|
|
return;
|
|
};
|
|
|
|
let mut reader = sylpheed_formats::xiso::open_iso(&iso).await.unwrap();
|
|
let all = reader.list_all_files().await.unwrap();
|
|
let mut xprs: Vec<String> = all
|
|
.into_iter()
|
|
.filter(|f| f.to_lowercase().ends_with(".xpr"))
|
|
.collect();
|
|
xprs.sort();
|
|
println!("found {} .xpr files", xprs.len());
|
|
|
|
// Sample across the set so we hit different texture sizes/formats.
|
|
let sample: Vec<String> = xprs
|
|
.iter()
|
|
.step_by((xprs.len() / 24).max(1))
|
|
.cloned()
|
|
.collect();
|
|
|
|
let mut ok = 0usize;
|
|
let mut fail = 0usize;
|
|
let mut by_format: std::collections::BTreeMap<String, usize> = Default::default();
|
|
let mut nonxpr = 0usize;
|
|
|
|
// Optionally dump the sampled .xpr bytes so the CLI can export them to PNG
|
|
// for visual de-tiling validation: `DUMP_XPR_DIR=/tmp/xpr cargo test …`.
|
|
let dump_dir = std::env::var("DUMP_XPR_DIR").ok();
|
|
if let Some(d) = &dump_dir {
|
|
std::fs::create_dir_all(d).unwrap();
|
|
}
|
|
|
|
for path in &sample {
|
|
let bytes = reader.read_file(path).await.unwrap();
|
|
if let Some(d) = &dump_dir {
|
|
let base = path.rsplit('/').next().unwrap();
|
|
std::fs::write(format!("{d}/{base}"), &bytes).unwrap();
|
|
}
|
|
let fmt = identify_format(&bytes);
|
|
let magic: String = bytes
|
|
.iter()
|
|
.take(4)
|
|
.map(|b| {
|
|
if b.is_ascii_graphic() {
|
|
*b as char
|
|
} else {
|
|
'.'
|
|
}
|
|
})
|
|
.collect();
|
|
|
|
if fmt != sylpheed_formats::vfs::FileFormat::Xpr2Texture {
|
|
nonxpr += 1;
|
|
println!(
|
|
" [{path}] NOT XPR2 (magic {magic:?}, {} bytes)",
|
|
bytes.len()
|
|
);
|
|
continue;
|
|
}
|
|
|
|
match X360Texture::from_xpr2(&bytes) {
|
|
Ok(t) => {
|
|
ok += 1;
|
|
*by_format.entry(format!("{:?}", t.format)).or_default() += 1;
|
|
// Sanity: does the decoded data length match the descriptor
|
|
// dimensions (what the GPU upload will require)?
|
|
let bs = t.format.block_size() as u32;
|
|
let bw = t.width.div_ceil(bs).max(1) as usize;
|
|
let bh = t.height.div_ceil(bs).max(1) as usize;
|
|
let need = bw * bh * t.format.bytes_per_block();
|
|
let size_ok = if need == t.data.len() {
|
|
"ok"
|
|
} else {
|
|
"MISMATCH"
|
|
};
|
|
println!(
|
|
" [{path}] {:?} {}x{} mips={} data={} need={} {size_ok}",
|
|
t.format,
|
|
t.width,
|
|
t.height,
|
|
t.mip_levels,
|
|
t.data.len(),
|
|
need
|
|
);
|
|
}
|
|
Err(e) => {
|
|
fail += 1;
|
|
println!(" [{path}] from_xpr2 FAILED: {e}");
|
|
dump_xpr2_structure(&bytes);
|
|
}
|
|
}
|
|
}
|
|
|
|
println!("\nSUMMARY: ok={ok} fail={fail} non-xpr2={nonxpr} formats={by_format:?}");
|
|
}
|
|
|
|
/// Dump the XPR2 header + resource directory the way `from_xpr2` reads it,
|
|
/// so we can see why a file's TX2D scan comes up empty.
|
|
fn dump_xpr2_structure(bytes: &[u8]) {
|
|
let be = |o: usize| u32::from_be_bytes(bytes[o..o + 4].try_into().unwrap());
|
|
let magic: String = bytes[..4].iter().map(|b| *b as char).collect();
|
|
println!(
|
|
" hdr magic={magic:?} header_size=0x{:X} data_size=0x{:X} num_resources={}",
|
|
be(0x04),
|
|
be(0x08),
|
|
be(0x0C)
|
|
);
|
|
let n = be(0x0C).min(16); // guard against garbage counts
|
|
for i in 0..n {
|
|
let base = 0x10 + i as usize * 16;
|
|
if base + 16 > bytes.len() {
|
|
break;
|
|
}
|
|
let tag: String = bytes[base..base + 4]
|
|
.iter()
|
|
.map(|b| {
|
|
if b.is_ascii_graphic() {
|
|
*b as char
|
|
} else {
|
|
'.'
|
|
}
|
|
})
|
|
.collect();
|
|
println!(
|
|
" res[{i}] tag={tag:?} data_off=0x{:X} desc_size=0x{:X} name_off=0x{:X}",
|
|
be(base + 4),
|
|
be(base + 8),
|
|
be(base + 12)
|
|
);
|
|
}
|
|
// First 48 bytes hex for orientation.
|
|
let hx: String = bytes.iter().take(48).map(|b| format!("{b:02X} ")).collect();
|
|
println!(" hex[0..48] {hx}");
|
|
}
|