//! Invert the capture↔part match: instead of asking, per ship part, "is there a //! draw with this vertex count?", ask of the **capture's** biggest draws "which //! decoded resource in this stage container has that vertex count?". //! //! This is the diagnostic for the 2026-07-31 negative result (Stage_S02 capture, //! zero parts correlated). It separates three hypotheses: //! 1. LOD/variant vcount not covered by the correlator's variant list //! → the big draws DO map to named resources, just not to the `_m`/`_l`/`_d` //! set the correlator tries; //! 2. position validation over-rejects //! → the vcounts match the very parts we asked for (so the vcount key was //! fine and the rejection happened later); //! 3. a different draw path (instanced/batched/merged buffers) //! → the big draws match NO resource in the container at all. //! //! Usage: //! SYLPHEED_ISO=... cargo run --release --example invert_capture -- \ //! [top_n] [--all] //! `--all` lists every capture vcount, not just the `top_n` (default 40) largest. use sylpheed_formats::mesh::{xbg7_resource_names, Xbg7Model}; use sylpheed_formats::ship_capture::{parse_capture, parse_drawlog}; use sylpheed_formats::xiso::open_iso; use std::collections::{HashMap, HashSet}; use std::path::Path; fn main() { let args: Vec = std::env::args().collect(); let positional: Vec<&String> = args[1..].iter().filter(|a| !a.starts_with("--")).collect(); let all = args.iter().any(|a| a == "--all"); if positional.len() < 2 { eprintln!("usage: invert_capture [top_n] [--all]"); std::process::exit(2); } let (log, stage) = (positional[0], positional[1]); let top_n: usize = positional.get(2).and_then(|s| s.parse().ok()).unwrap_or(40); let iso = std::env::var("SYLPHEED_ISO").expect("SYLPHEED_ISO"); let text = std::fs::read_to_string(log).expect("read log"); let mut draws = parse_capture(&text); if draws.is_empty() { draws = parse_drawlog(&text); println!("parsed {} draws (draw-logger format)", draws.len()); } else { println!("parsed {} draws (F10 capture format)", draws.len()); } // Decode EVERY geometry resource in the stage container, not just one ship's. let bytes = { let rt = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap(); rt.block_on(async { let mut r = open_iso(Path::new(&iso)).await.unwrap(); r.read_file(&format!("hidden/resource3d/{stage}.xpr")).await.unwrap() }) }; let names = xbg7_resource_names(&bytes); println!("{stage}.xpr: {} XBG7 resources", names.len()); let want: HashSet = names.iter().cloned().collect(); let models = Xbg7Model::models_named(&bytes, &want, &|| false); println!("decoded {} models", models.len()); // vcount -> resource names with that many vertices. let mut by_vcount: HashMap> = HashMap::new(); for m in &models { let v: usize = m.meshes.iter().map(|s| s.positions.len()).sum(); by_vcount.entry(v as u32).or_default().push(m.name.clone()); } // Per-submesh counts too: a draw may be one sub-mesh of a multi-mesh resource. let mut by_sub_vcount: HashMap> = HashMap::new(); for m in &models { for (i, s) in m.meshes.iter().enumerate() { if m.meshes.len() > 1 { by_sub_vcount .entry(s.positions.len() as u32) .or_default() .push(format!("{}#{i}", m.name)); } } } // Capture vcounts, de-duped by (vbase, vcount) so a re-drawn part counts once // per distinct buffer. let mut draw_count: HashMap = HashMap::new(); let mut bufs: HashMap> = HashMap::new(); for d in &draws { *draw_count.entry(d.vcount).or_default() += 1; bufs.entry(d.vcount).or_default().insert(d.vbase); } let mut vcounts: Vec = draw_count.keys().copied().collect(); vcounts.sort_unstable_by(|a, b| b.cmp(a)); let matched_draws: usize = draws .iter() .filter(|d| by_vcount.contains_key(&d.vcount) || by_sub_vcount.contains_key(&d.vcount)) .count(); println!( "\n{} distinct vcounts; {}/{} draws have a vcount present in {stage}.xpr ({:.1}%)", vcounts.len(), matched_draws, draws.len(), 100.0 * matched_draws as f64 / draws.len().max(1) as f64 ); let shown = if all { vcounts.len() } else { top_n.min(vcounts.len()) }; println!("\nlargest capture vcounts (draws / distinct vbufs) → matching resources:"); for &v in vcounts.iter().take(shown) { let n = draw_count[&v]; let b = bufs[&v].len(); let mut hit: Vec = by_vcount.get(&v).cloned().unwrap_or_default(); let sub: Vec = by_sub_vcount.get(&v).cloned().unwrap_or_default(); hit.extend(sub.into_iter().map(|s| format!("{s} (sub)"))); let label = if hit.is_empty() { "— no resource".to_string() } else { let mut h = hit.clone(); h.sort(); h.truncate(6); format!("{}{}", h.join(", "), if hit.len() > 6 { ", …" } else { "" }) }; println!(" vcount {v:6} draws {n:4} bufs {b:3} {label}"); } // `--ship `: every resource of one ship family, with its vertex count and // whether the capture drew it — this is what shows an all-`_l` (far-LOD) frame. if let Some(i) = args.iter().position(|a| a == "--ship") { if let Some(id) = args.get(i + 1) { let mut rows: Vec<(String, u32, usize)> = models .iter() .filter(|m| m.name.contains(id.as_str())) .map(|m| { let v = m.meshes.iter().map(|s| s.positions.len()).sum::() as u32; (m.name.clone(), v, draw_count.get(&v).copied().unwrap_or(0)) }) .collect(); rows.sort_by(|a, b| a.0.cmp(&b.0)); let drawn = rows.iter().filter(|r| r.2 > 0).count(); println!("\n{id} resources in {stage}.xpr ({drawn}/{} with a drawn vcount):", rows.len()); for (name, v, n) in rows { println!(" {name:28} vcount {v:6} {}", if n > 0 { format!("DRAWN ×{n}") } else { "—".into() }); } } } // The other direction, for orientation: the container's biggest resources and // whether the capture ever drew that many vertices. let mut sizes: Vec<(u32, String)> = models .iter() .map(|m| (m.meshes.iter().map(|s| s.positions.len()).sum::() as u32, m.name.clone())) .collect(); sizes.sort_unstable_by(|a, b| b.0.cmp(&a.0)); println!("\nlargest resources in {stage}.xpr → drawn in the capture?"); for (v, name) in sizes.iter().take(top_n.min(sizes.len())) { let n = draw_count.get(v).copied().unwrap_or(0); println!(" {name:28} vcount {v:6} {}", if n > 0 { format!("DRAWN ×{n}") } else { "not drawn".into() }); } }