Reworks the XPR2 texture path after RE against the retail disc (viewer previews were scrambled/failed). - de-tile: replace the naive Morton-over-32×32 approximation with the faithful Xenos Tiled2D bank/pipe/macro-tile address formula (ported from xenia texture_address.h). Verified correct: the Acheron backdrop decodes to a sharp planet with its spiral storm. - endian: undo the X360 word byte-swap per the fetch-constant endianness field (k8in16/k8in32/k16in32) — without it BCn/ARGB data is noise. - cubemaps: parse TXCM resources (skybox/backdrops), decoding face 0, instead of erroring "No TX2D found". - A8R8G8B8: correct channel order after the k8in32 swap ([A,R,G,B]→RGBA) — the Acheron backdrop is now correctly green, not red. - XPR files are texture PACKS; add XPR_RES_INDEX to select a resource. - CLI `texture export` now writes real PNGs (texpresso BCn decode + image), replacing the stub. Adds texpresso + image deps. - tests/texture_disc.rs: integration test running the pipeline over real disc .xpr files (28/28 parse). RE debug knobs: XPR_NO_DETILE / XPR_NO_ENDIAN / XPR_FORCE_ENDIAN. KNOWN ISSUE: mipmapped DXT1 textures still decode to noise — mip 0 is not at base_address in the multi-resource packs (localized to a mip storage-offset quirk; uncompressed + de-tile + endian are all verified). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
141 lines
5.1 KiB
Rust
141 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 (or the
|
|
//! default dev path exists).
|
|
//!
|
|
//! Run: `cargo test -p sylpheed-formats --test texture_disc -- --ignored --nocapture`
|
|
|
|
use std::path::PathBuf;
|
|
|
|
use sylpheed_formats::texture::X360Texture;
|
|
use sylpheed_formats::vfs::identify_format;
|
|
|
|
fn iso_path() -> Option<PathBuf> {
|
|
if let Ok(p) = std::env::var("SYLPHEED_ISO") {
|
|
let p = PathBuf::from(p);
|
|
if p.is_file() {
|
|
return Some(p);
|
|
}
|
|
}
|
|
let default = PathBuf::from(
|
|
"/home/fabi/RE Project Sylpheed/Project Sylpheed - Arc of Deception (USA, Europe) (En,Ja).iso",
|
|
);
|
|
default.is_file().then_some(default)
|
|
}
|
|
|
|
#[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 + bs - 1) / bs).max(1) as usize;
|
|
let bh = ((t.height + bs - 1) / 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}");
|
|
}
|