//! 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 { 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 = 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 = 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 = 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}"); }