//! Ask the runtime capture whether two resources that our decoder gives the //! **same geometry** really are the same geometry. //! //! Our XBG7 anchor scan sometimes lands two different resource names on one //! vertex buffer. Statics cannot separate "the container genuinely reuses a //! buffer" from "the scan picked the wrong candidate" — but a capture can: the //! engine uploads a buffer per resource and reuses one only 3.4 % of the time //! (see docs/re/structures/xbg7-mesh.md), so a group of `k` resources our //! decoder collapses onto one buffer should show up as `k` distinct `vbase`s //! carrying that same vertex count and those same positions. Fewer means at //! most one member of the group is really that geometry. //! //! Usage: //! cargo run --release --example shared_vbase_check -- \ //! ... use sylpheed_formats::mesh::Xbg7Model; use sylpheed_formats::ship_capture::{parse_capture, parse_drawlog, CapturedDraw}; use std::collections::{BTreeMap, BTreeSet}; /// Quantised position key — the logs print 4 decimals, so compare at that scale. fn key(p: [f32; 3]) -> (i64, i64, i64) { ( (p[0] as f64 * 1e4).round() as i64, (p[1] as f64 * 1e4).round() as i64, (p[2] as f64 * 1e4).round() as i64, ) } /// Where in the container does a captured buffer live? POSITION is `f32×3` big /// endian at vertex offset 0, so a draw's dumped positions are a literal byte /// pattern: find the first one, then confirm the next few at a fixed stride. /// This turns a capture into ground truth for a resource we mis-anchored. fn locate_run(bytes: &[u8], pos: &[[f32; 3]]) -> Vec<(usize, usize)> { if pos.len() < 4 { return Vec::new(); } // The log prints 4 decimals, so match on value with the printing tolerance // rather than on bytes. let be = |b: &[u8], at: usize| f32::from_be_bytes(b[at..at + 4].try_into().unwrap()); let same = |b: &[u8], at: usize, p: [f32; 3]| { at + 12 <= b.len() && (0..3).all(|c| (be(b, at + c * 4) - p[c]).abs() <= 1e-4) }; let mut out = Vec::new(); for o in (0..bytes.len().saturating_sub(12)).step_by(4) { if !same(bytes, o, pos[0]) { continue; } for stride in (12..=64).step_by(4) { if (1..4).all(|k| same(bytes, o + k * stride, pos[k])) { out.push((o, stride)); break; } } } out } fn main() { let args: Vec = std::env::args().collect(); if args.len() < 3 { eprintln!("usage: shared_vbase_check ..."); std::process::exit(2); } let bytes = std::fs::read(&args[1]).expect("read container"); // Every draw from every log, keyed by vertex count. // Keep the logs apart: each is its own emulator run, so a `vbase` only // means something within one log. let mut logs: Vec<(String, Vec)> = Vec::new(); for log in args[2..].iter().filter(|a| !a.starts_with("--")) { let text = std::fs::read_to_string(log).expect("read log"); let mut d = parse_capture(&text); if d.is_empty() { d = parse_drawlog(&text); } eprintln!("{log}: {} draws", d.len()); logs.push((log.rsplit('/').next().unwrap_or(log).to_string(), d)); } // `--map`: is a draw's guest `vbase` just the container file offset plus a // constant? If the container is uploaded contiguously it is — and then a // capture names the exact offset of every buffer the engine drew, which is // ground truth the anchor scan currently has to guess at. if args.iter().any(|a| a == "--map") { for (log, draws) in &logs { let mut seen: BTreeSet = BTreeSet::new(); let mut delta: BTreeMap = BTreeMap::new(); let mut unfound = 0usize; for d in draws { if d.pos.len() < 8 || d.vcount < 20 || !seen.insert(d.vbase) { continue; } let at = locate_run(&bytes, &d.pos); if at.is_empty() { unfound += 1; continue; } for (o, _) in at { *delta.entry(d.vbase as i64 - o as i64).or_default() += 1; } } let mut top: Vec<_> = delta.iter().collect(); top.sort_by_key(|(_, n)| std::cmp::Reverse(**n)); println!("{log}: {} distinct vbases located, {unfound} not in this container", seen.len() - unfound); for (d, n) in top.iter().take(5) { println!(" vbase - offset = 0x{:X} ×{n}", d); } } return; } // Decode the container and group resources by the exact geometry they got. let models = Xbg7Model::anchor_models_cancellable(&bytes, 0.0, &|| false); // `--truth `: with the container's guest load address (from `--map`), // every draw names a file offset. Print it against the offset our anchor // scan chose for each resource — a direct read-out of what we got wrong. if let Some(a) = args.iter().find_map(|a| a.strip_prefix("--truth=")) { let base = u32::from_str_radix(a.trim_start_matches("0x"), 16).expect("base"); // Where the anchor scan actually put each sub-mesh — exact, from the // decoder, not inferred by searching for its leading vertices (the same // leading run occurs at several offsets in a container, so a search // cannot tell where a resource was anchored). let mut ours: BTreeMap> = BTreeMap::new(); for m in &models { for sub in &m.meshes { if let Some(o) = sub.vbuf_offset { ours.entry(o).or_default().push((m.name.clone(), sub.positions.len())); } } } if args.iter().any(|a| a == "--anchors") { println!("{:<12} where our decode put each resource", "file offset"); for (o, v) in &ours { for (n, c) in v { println!("0x{o:<10x} {n} ({c} verts)"); } } return; } let mut drawn: BTreeMap = BTreeMap::new(); for (_, draws) in &logs { for d in draws { let off = d.vbase.wrapping_sub(base) as usize; if off < bytes.len() && d.vcount >= 20 { drawn.insert(off, d.vcount); } } } // Which resources have the drawn vertex count, wherever we put them? // Right size + wrong place is a different bug from never finding it. let mut by_count: BTreeMap> = BTreeMap::new(); for m in &models { let n: usize = m.meshes.iter().map(|s| s.positions.len()).sum(); by_count.entry(n).or_default().push(m.name.clone()); } println!("{:<12} {:>7} {:<44} our resources with that vcount", "file offset", "vcount", "claimed by our decode"); for (off, vcount) in &drawn { let who = ours .get(off) .map(|v| { v.iter().map(|(n, c)| format!("{n}({c})")).collect::>().join(", ") }) .unwrap_or_else(|| "— NOBODY".into()); let same = by_count .get(&(*vcount as usize)) .map(|v| v.join(", ")) .unwrap_or_else(|| "— none".into()); // Nearest resource we anchored at or before this offset — the // likely owner of a buffer nobody claims. let near = ours .range(..=*off) .next_back() .map(|(o, v)| format!("{} @ -0x{:x}", v[0].0, off - o)) .unwrap_or_default(); println!("0x{off:<10x} {vcount:>7} {who:<44} {same:<34} {near}"); } return; } let mut groups: BTreeMap, Vec> = BTreeMap::new(); for m in &models { let pos: Vec<(i64, i64, i64)> = m.meshes.iter().flat_map(|s| s.positions.iter().copied()).map(key).collect(); if pos.is_empty() { continue; } groups.entry(pos).or_default().push(m.name.clone()); } let shared: Vec<_> = groups.iter().filter(|(_, n)| n.len() > 1).collect(); eprintln!( "{} models, {} distinct geometries, {} shared by >1 resource", models.len(), groups.len(), shared.len() ); for (pos, names) in shared { let vcount = pos.len() as u32; // A draw belongs to this geometry if every dumped position is one of // the decoded ones (the log dumps at most the first 64). let want: BTreeSet<(i64, i64, i64)> = pos.iter().copied().collect(); println!("\n{} ({vcount} verts, {} resources)", names.join(" ≡ "), names.len()); for (log, draws) in &logs { let hits: Vec<&CapturedDraw> = draws.iter().filter(|d| d.vcount == vcount).collect(); let all: BTreeSet = hits.iter().map(|d| d.vbase).collect(); let matching: Vec<&&CapturedDraw> = hits .iter() .filter(|d| !d.pos.is_empty() && d.pos.iter().all(|p| want.contains(&key(*p)))) .collect(); let ok: BTreeSet = matching.iter().map(|d| d.vbase).collect(); // A buffer we do NOT match may still be the mirrored twin: same // geometry with x negated. That is the case our assembler papers // over with `apply_twin_mirrors`. let mirrored: BTreeSet = hits .iter() .filter(|d| !ok.contains(&d.vbase)) .filter(|d| { !d.pos.is_empty() && d.pos.iter().all(|p| want.contains(&key([-p[0], p[1], p[2]]))) }) .map(|d| d.vbase) .collect(); println!( " {log:32} draws={:<5} vbases@vcount={:<3} ours={} mirrored={} other={}", hits.len(), all.len(), ok.len(), mirrored.len(), all.len() - ok.len() - mirrored.len() ); // Where does each captured buffer live in the container? One // representative draw per vbase is enough. let mut done: BTreeSet = BTreeSet::new(); for d in &hits { if d.pos.len() < 8 || !done.insert(d.vbase) { continue; } let kind = if ok.contains(&d.vbase) { "ours" } else if mirrored.contains(&d.vbase) { "mirror" } else { "other" }; let at = locate_run(&bytes, &d.pos); let shown: Vec = at.iter().take(4).map(|(o, s)| format!("0x{o:x}/stride{s}")).collect(); println!( " vbase=0x{:08X} [{kind:6}] in container at: {}", d.vbase, if shown.is_empty() { "NOT FOUND".into() } else { shown.join(" ") } ); } } } }