//! Do our decoded triangle indices equal the ones the GPU actually read? //! //! `capture_ib_truth` established *where* a block's index buffer lives and that //! its length matches ours. This asks the stronger question: are the index VALUES //! the same, in the same order? The capture prints the first 24 indices of every //! draw batch verbatim (`idx: …`), and a batch's `ibase` locates it inside the //! block's index buffer — so for each drawn buffer we can line the captured run //! up against `GameMesh::indices` at the right offset and compare element by //! element. That tests the whole index path at once: the anchor, the u16 //! big-endian read, the triangle-list interpretation (a strip would disagree //! immediately), and the sub-mesh carve. //! //! Usage: //! cargo run --release --example capture_index_bytes -- ... use sylpheed_formats::mesh::Xbg7Model; use sylpheed_formats::ship_capture::{parse_capture, CapturedDraw}; use std::collections::HashMap; fn q(v: f32) -> i64 { (v as f64 * 1e4).round() as i64 } fn main() { let a: Vec = std::env::args().collect(); if a.len() < 3 { eprintln!("usage: capture_index_bytes ..."); std::process::exit(2); } let bytes = std::fs::read(&a[1]).expect("container"); let mut draws: Vec = Vec::new(); let mut seen = std::collections::HashSet::new(); for log in &a[2..] { let text = std::fs::read_to_string(log).expect("log"); for d in parse_capture(&text) { let k = d.ib.map(|i| (i.ibase, i.icount)).unwrap_or((0, 0)); if d.ib.map_or(false, |i| i.head_len > 0) && d.pos.len() >= 4 && seen.insert((log.clone(), d.vbase, k)) { draws.push(d); } } } eprintln!("{} draw batches with an index head", draws.len()); // Place each drawn buffer in the file by its dumped positions (see // capture_ib_truth for the method) and keep the modal load constant. let be = |at: usize| f32::from_be_bytes(bytes[at..at + 4].try_into().unwrap()); let mut index: HashMap<(i64, i64, i64), Vec> = HashMap::new(); let mut o = 0usize; while o + 12 <= bytes.len() { let (x, y, z) = (be(o), be(o + 4), be(o + 8)); if x.is_finite() && y.is_finite() && z.is_finite() && x.abs() < 1e6 && y.abs() < 1e6 && z.abs() < 1e6 { index.entry((q(x), q(y), q(z))).or_default().push(o as u32); } o += 4; } let mut deltas: HashMap = HashMap::new(); let mut hits: Vec<(i64, usize, &CapturedDraw)> = Vec::new(); for d in &draws { let k = (q(d.pos[0][0]), q(d.pos[0][1]), q(d.pos[0][2])); for dx in -1..=1i64 { for dy in -1..=1i64 { for dz in -1..=1i64 { let Some(cands) = index.get(&(k.0 + dx, k.1 + dy, k.2 + dz)) else { continue }; for &off in cands { for stride in (12..=64).step_by(4) { let ok = (1..4).all(|j| { let at = off as usize + j * stride; at + 12 <= bytes.len() && (0..3).all(|c| (be(at + c * 4) - d.pos[j][c]).abs() <= 1e-4) }); if ok { *deltas.entry(d.vbase as i64 - off as i64).or_default() += 1; hits.push((d.vbase as i64 - off as i64, off as usize, d)); break; } } } } } } } let Some((&base_delta, _)) = deltas.iter().max_by_key(|(_, n)| **n) else { eprintln!("no draw could be placed in this container"); std::process::exit(1); }; println!("load constant 0x{base_delta:X}"); // Our decode, indexed by vertex offset. A model may hold several sub-meshes; // compare against the one whose vertex count matches the draw. let models = Xbg7Model::stage_models(&bytes); let mut by_off: HashMap)>> = HashMap::new(); for m in &models { for sm in &m.meshes { if let Some(off) = sm.vbuf_offset { by_off .entry(off) .or_default() .push((m.name.clone(), sm.positions.len(), sm.indices.clone())); } } } // Where does each buffer's index data START? Take it from the capture, not // from an assumed pad: `capture_ib_truth` established that a buffer's batches // tile its index buffer exactly (sum of counts == our idx_count, span == // 2*count), so the lowest `ibase` over a buffer's batches IS the block's // index start. Assuming `vb - 2*len` instead is wrong for the buffers that // sit at pad 2 and shifts the whole comparison by one element. let mut ib_start: HashMap = HashMap::new(); for (delta, _, d) in &hits { if *delta != base_delta { continue; } let ibase = d.ib.unwrap().ibase as i64; ib_start.entry(d.vbase).and_modify(|e| *e = (*e).min(ibase)).or_insert(ibase); } let (mut agree, mut disagree, mut unmatched, mut nooverlap) = (0usize, 0usize, 0usize, 0usize); let mut bad: Vec = Vec::new(); let mut checked_elems = 0usize; let mut done: std::collections::HashSet<(u32, u32)> = std::collections::HashSet::new(); for (delta, voff, d) in &hits { if *delta != base_delta { continue; } let ib = d.ib.unwrap(); if !done.insert((d.vbase, ib.ibase)) { continue; } let Some(cands) = by_off.get(voff) else { unmatched += 1; continue; }; let Some((name, _, ours)) = cands.iter().find(|(_, p, _)| *p as u32 == d.vcount) else { unmatched += 1; continue; }; // Element 0 of our index list is the block's index start, as observed. let ours_start = ib_start[&d.vbase] - base_delta; let batch_off = ib.ibase as i64 - base_delta - ours_start; if batch_off < 0 || batch_off % 2 != 0 { nooverlap += 1; continue; } let first = (batch_off / 2) as usize; let n = (ib.head_len as usize).min(ours.len().saturating_sub(first)); if n == 0 { nooverlap += 1; continue; } let mism = (0..n).find(|&k| ours[first + k] != ib.head[k]); checked_elems += n; match mism { None => agree += 1, Some(k) => { disagree += 1; if bad.len() < 8 { bad.push(format!( "{name} vb 0x{:07X} batch@{first}: ours {:?} != captured {:?} (first differs at {k})", voff, &ours[first..first + n.min(12)], &ib.head[..n.min(12)] )); } } } } println!( "index runs compared: {agree} identical · {disagree} differing · {unmatched} no decoded resource · {nooverlap} batch outside our buffer" ); println!("{checked_elems} index elements checked against the GPU"); for b in &bad { println!(" MISMATCH {b}"); } }