`cargo fmt --all -- --check` has failed on every run in this repository's history, identically on `main` and on every branch. This is #12. Mechanical: `cargo fmt --all`, nothing else. 154 files, all `.rs`, no other extension touched. `cargo check --workspace` exits 0 afterwards, so nothing changed semantically. ON THE ORDERING, WHICH WAS THE REAL QUESTION. HANDOFF-2026-09-06 section 7 warns this is the expensive fix: a whole-tree reformat before #7 and #8 return "would put a conflict in every file of 861 commits and make the reviews those items exist to enable unreadable". That is measurably too pessimistic, and it had been reasoned rather than tested. Measured here by three-way merging a rustfmt'd `main` against both unmerged branches, file by file: file/branch pairs tested 32 merges CLEAN 28 merges CONFLICTING 4 (8 conflict hunks total) sylpheed-cli/src/main.rs 1 hunk sylpheed-export/src/check.rs 1 sylpheed-export/src/screen.rs 4 sylpheed-export/src/video.rs 2 All four are against `auto/frame-blend-draw-path` only; `auto/port-p6-audio` does not conflict anywhere. The earlier framing -- 154 dirty files, 133 that cannot collide, 21 that can, the collision set carrying 147 of 774 hunks (19%) -- reproduces exactly. What it did not say is that most of the 21 still merge cleanly, because rustfmt's edits and the branches' edits rarely land on the same lines. So the cost of sweeping now is 4 files and 8 hunks for one branch, against a check that is otherwise red forever. Deliberately NOT folded into the WASM PR: 154 reformatted files would make that one unreviewable. Closes #12
295 lines
12 KiB
Rust
295 lines
12 KiB
Rust
//! 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 std::collections::{BTreeMap, BTreeSet};
|
||
use sylpheed_formats::mesh::Xbg7Model;
|
||
use sylpheed_formats::ship_capture::{parse_capture, parse_drawlog, CapturedDraw};
|
||
|
||
/// 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());
|
||
}
|
||
// Is a capture-proven offset even a candidate the scan considers?
|
||
// Absent ⇒ the run scan misses it; present ⇒ selection picked another.
|
||
let starts: BTreeSet<usize> = sylpheed_formats::mesh::debug_vertex_run_starts(&bytes, 24)
|
||
.into_iter()
|
||
.collect();
|
||
eprintln!(
|
||
"{} stride-24 candidate starts in this container",
|
||
starts.len()
|
||
);
|
||
println!(
|
||
"{:<12} {:>7} {:>9} {:<44} our resources with that vcount",
|
||
"file offset", "vcount", "candidate", "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();
|
||
let cand = if starts.contains(off) { "yes" } else { "NO" };
|
||
println!("0x{off:<10x} {vcount:>7} {cand:>9} {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(" ")
|
||
}
|
||
);
|
||
}
|
||
}
|
||
}
|
||
}
|