Files
Syplheed-Reborn/crates/sylpheed-formats/examples/shared_vbase_check.rs
Claude (auto-RE) 26f1bdf6fd re: exact anchor offsets (GameMesh.vbuf_offset) -- correcting yesterday's defect table
The first read of the capture-truth table located our resources by searching the
container for their leading vertices, which reads much worse than reality: the
same leading run occurs at several offsets in one container. GameMesh now carries
the offset the anchor scan actually chose, so the comparison is exact -- 4 of the
ship's drawn buffers are anchored correctly, 2 are the twin collapse, and 2 are
real mis-anchors of a size we do decode (brg 51 verts, eng_02_l 44).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 02:04:55 +00:00

257 lines
11 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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 -- \
//! <Stage_SNN.xpr> <capture.log>...
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<String> = std::env::args().collect();
if args.len() < 3 {
eprintln!("usage: shared_vbase_check <Stage_SNN.xpr> <capture.log>...");
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<CapturedDraw>)> = 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<u32> = BTreeSet::new();
let mut delta: BTreeMap<i64, usize> = 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 <base>`: 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<usize, Vec<(String, usize)>> = 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<usize, u32> = 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<usize, Vec<String>> = 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::<Vec<_>>().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<(i64, i64, i64)>, Vec<String>> = 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<u32> = 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<u32> = 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<u32> = 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<u32> = 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<String> =
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(" ") }
);
}
}
}
}