80 findings, not the 14 the first run showed -- clippy stops at the first failing compilation unit, so `--keep-going` is what makes the list complete. 60 were machine-applicable (`cargo clippy --fix`). The rest by hand: * five descending `sort_by` -> `sort_by_key(Reverse(..))` * `chunks_exact(4)` on both sides of four zips, so the compared items stay `[u8; 4]` rather than one array against one slice * three `type` aliases for the census maps and the captured-quad tuple * `&PathBuf` -> `&Path` in two disc tests * two range loops; one of them keeps `#[allow(needless_range_loop)]` with the reason -- the index is into a map's value, which changes each iteration * the module doc list in `invert_capture` re-indented to markdown's rules * `blit`'s eight arguments get `#[allow(too_many_arguments)]`, not a struct One dead `let off = b.len();` in a `ratc` test is dropped rather than renamed. The sibling test at :162 is the one that asserts an offset; if this one was meant to as well, that is a test change and not a lint fix. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
252 lines
10 KiB
Rust
252 lines
10 KiB
Rust
//! Where does a drawn block's INDEX buffer really live?
|
||
//!
|
||
//! Our XBG7 anchor scan only ever *assumes* the layout `[index buffer][vertex
|
||
//! buffer]` with a pad of at most 3 bytes between them (`anchor_pool_mesh`:
|
||
//! `ib = vb - idx_count*2 - pad`). Nothing on disc states it, and it is the gate
|
||
//! that rejected the capture-proven `e106_eng_02_l` block
|
||
//! (docs/re/captures/…): the block's index data was not where the decoder
|
||
//! looked. The F10 ship capture was extended on 2026-08-13 to log each draw's
|
||
//! index-buffer base, count and min/max index value, so the assumption is now
|
||
//! directly checkable:
|
||
//!
|
||
//! * `vbase - ibase` is the real gap in guest memory, and a stage container is
|
||
//! uploaded contiguously (see `shared_vbase_check`), so the same difference
|
||
//! holds in the file;
|
||
//! * `max index vs vcount` says whether a draw covers its whole vertex pool —
|
||
//! the "buffer not covered" miss class is a *sub-range draw* if it does not.
|
||
//!
|
||
//! Usage:
|
||
//! cargo run --release --example capture_ib_truth -- <Stage_SNN.xpr> <capture.log>...
|
||
use std::collections::HashMap;
|
||
use sylpheed_formats::mesh::{debug_resource_params, xbg7_resource_names, Xbg7Model};
|
||
use sylpheed_formats::ship_capture::{parse_capture, CapturedDraw};
|
||
|
||
fn q(v: f32) -> i64 {
|
||
(v as f64 * 1e4).round() as i64
|
||
}
|
||
|
||
fn main() {
|
||
let a: Vec<String> = std::env::args().collect();
|
||
if a.len() < 3 {
|
||
eprintln!("usage: capture_ib_truth <container.xpr> <capture.log>...");
|
||
std::process::exit(2);
|
||
}
|
||
let bytes = std::fs::read(&a[1]).expect("container");
|
||
|
||
// One entry per (log, vbase): the capture already de-dups per placement, and
|
||
// a buffer drawn at several transforms has the same index buffer each time.
|
||
let mut draws: Vec<CapturedDraw> = 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) {
|
||
// One entry per (log, vbase, index range): the engine issues SEVERAL
|
||
// draws over one vertex buffer, each with its own index sub-range, and
|
||
// it is their UNION that describes the block. (Captures taken before
|
||
// 2026-08-13 de-dup by (vbase, transform) and so hold only the first
|
||
// batch — such a log reads as a mysteriously short draw.)
|
||
let k = d.ib.map(|i| (i.ibase, i.icount)).unwrap_or((0, 0));
|
||
if d.ib.is_some() && d.pos.len() >= 4 && seen.insert((log.clone(), d.vbase, k)) {
|
||
draws.push(d);
|
||
}
|
||
}
|
||
}
|
||
eprintln!("{} drawn buffers with an index buffer", draws.len());
|
||
|
||
// ── Place the drawn buffers in the file: POSITION is f32×3 big-endian at
|
||
// vertex offset 0, so the dumped positions are a literal byte pattern.
|
||
let be = |at: usize| f32::from_be_bytes(bytes[at..at + 4].try_into().unwrap());
|
||
let mut index: HashMap<(i64, i64, i64), Vec<u32>> = 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<i64, usize> = HashMap::new();
|
||
let mut hits: Vec<(i64, usize, &CapturedDraw)> = Vec::new(); // (delta, file offset, draw)
|
||
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 {
|
||
let delta = d.vbase as i64 - off as i64;
|
||
*deltas.entry(delta).or_default() += 1;
|
||
hits.push((delta, off as usize, d));
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
let Some((&base_delta, &n)) = deltas.iter().max_by_key(|(_, n)| **n) else {
|
||
eprintln!("no draw could be placed in this container");
|
||
std::process::exit(1);
|
||
};
|
||
println!(
|
||
"container load constant: vbase - file_offset = 0x{base_delta:X} ({n} buffers agree)"
|
||
);
|
||
|
||
// ── Our decoder's view of the same container.
|
||
let models = Xbg7Model::stage_models(&bytes);
|
||
let mut by_off: HashMap<usize, Vec<(String, usize, usize)>> = 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.len(),
|
||
));
|
||
}
|
||
}
|
||
}
|
||
println!(
|
||
"decoded {} resources, {} distinct vertex offsets\n",
|
||
models.len(),
|
||
by_off.len()
|
||
);
|
||
|
||
// Declared-but-not-decoded resources, indexed by their first marker's
|
||
// (vertex, index) counts. A drawn buffer our decoder cannot name is the one
|
||
// thing a capture can give the residual misses: ground truth for where the
|
||
// block actually is. Matching on counts is enough to propose an identity —
|
||
// then `debug_try_anchor` at that offset says which gate rejects it.
|
||
let decoded_names: std::collections::HashSet<String> =
|
||
models.iter().map(|m| m.name.clone()).collect();
|
||
let mut undecoded_by_counts: HashMap<(usize, usize), Vec<String>> = HashMap::new();
|
||
for n in xbg7_resource_names(&bytes) {
|
||
if decoded_names.contains(&n) {
|
||
continue;
|
||
}
|
||
if let Some((markers, _)) = debug_resource_params(&bytes, &n) {
|
||
if let Some(&(v, i)) = markers.first() {
|
||
undecoded_by_counts.entry((v, i)).or_default().push(n);
|
||
}
|
||
}
|
||
}
|
||
|
||
// ── The report: one row per drawn BUFFER, aggregating its index batches.
|
||
let mut per_buf: HashMap<
|
||
u32,
|
||
(
|
||
usize,
|
||
Vec<sylpheed_formats::ship_capture::CapturedIndexBuffer>,
|
||
u32,
|
||
),
|
||
> = HashMap::new();
|
||
for (delta, voff, d) in &hits {
|
||
if *delta != base_delta {
|
||
continue;
|
||
}
|
||
let e = per_buf
|
||
.entry(d.vbase)
|
||
.or_insert((*voff, Vec::new(), d.vcount));
|
||
let ib = d.ib.unwrap();
|
||
if !e.1.contains(&ib) {
|
||
e.1.push(ib);
|
||
}
|
||
}
|
||
|
||
let (mut pad0, mut pad_small, mut pad_off, mut unnamed) = (0usize, 0usize, 0usize, 0usize);
|
||
let (mut cover_exact, mut cover_short, mut idx_equal, mut idx_partial) =
|
||
(0usize, 0usize, 0usize, 0usize);
|
||
let mut rows: Vec<(usize, String)> = Vec::new();
|
||
for (voff, ibs, vcount) in per_buf.values() {
|
||
let batches = ibs.len();
|
||
let total: u32 = ibs.iter().map(|i| i.icount).sum();
|
||
let lo = ibs.iter().map(|i| i.ibase).min().unwrap() as i64 - base_delta;
|
||
let hi = ibs.iter().map(|i| i.ibase + i.icount * 2).max().unwrap() as i64 - base_delta;
|
||
let umax = ibs.iter().map(|i| i.imax).max().unwrap();
|
||
let gap = *voff as i64 - hi; // bytes from the end of the index data to the vertex buffer
|
||
let names = by_off.get(voff);
|
||
let dec_idx = names.and_then(|v| {
|
||
v.iter()
|
||
.find(|(_, p, _)| *p as u32 == *vcount)
|
||
.map(|(_, _, i)| *i as u32)
|
||
});
|
||
// The decoder's assumption, scored: it expects the whole index buffer at
|
||
// `vb - 2*idx_count - pad`, pad ≤ 3.
|
||
let dec_pad = dec_idx.map(|i| *voff as i64 - (i as i64) * 2 - lo);
|
||
match dec_pad {
|
||
Some(0) => pad0 += 1,
|
||
Some(p) if (1..=3).contains(&p) => pad_small += 1,
|
||
Some(_) => pad_off += 1,
|
||
None => unnamed += 1,
|
||
}
|
||
if umax + 1 == *vcount {
|
||
cover_exact += 1;
|
||
} else {
|
||
cover_short += 1;
|
||
}
|
||
match dec_idx {
|
||
Some(i) if i == total => idx_equal += 1,
|
||
Some(_) => idx_partial += 1,
|
||
None => {}
|
||
}
|
||
rows.push((
|
||
*voff,
|
||
format!(
|
||
"vb 0x{:07X} v={:<6} batches {:<3} idx {:<6} span {:<7} gap {:<8} decpad {:<7} cover {:<10} {}",
|
||
voff,
|
||
vcount,
|
||
batches,
|
||
total,
|
||
hi - lo,
|
||
gap,
|
||
dec_pad.map(|p| p.to_string()).unwrap_or_else(|| "?".into()),
|
||
if umax + 1 == *vcount { "exact".to_string() } else { format!("{}/{}", umax, vcount - 1) },
|
||
names
|
||
.map(|v| v
|
||
.iter()
|
||
.map(|(n, p, i)| format!("{n}(v{p},i{i})"))
|
||
.collect::<Vec<_>>()
|
||
.join(" "))
|
||
.unwrap_or_else(|| {
|
||
// Nothing of ours sits here — is it a resource that never
|
||
// decodes? Propose it by (vertex, index) counts.
|
||
undecoded_by_counts
|
||
.get(&(*vcount as usize, total as usize))
|
||
.map(|v| format!("MISSED? {}", v.join(" ")))
|
||
.unwrap_or_else(|| "-".into())
|
||
})
|
||
),
|
||
));
|
||
}
|
||
rows.sort();
|
||
for (_, r) in &rows {
|
||
println!("{r}");
|
||
}
|
||
println!("\nplaced {} drawn buffers in this container", rows.len());
|
||
println!(
|
||
"DECODER layout assumption — whole index buffer at vb - 2*idx_count - pad: pad 0 {pad0} · pad 1..3 {pad_small} · elsewhere {pad_off} · not decoded here {unnamed}"
|
||
);
|
||
println!(
|
||
"index extent: our idx_count == sum of captured batches for {idx_equal} buffers, differs for {idx_partial}"
|
||
);
|
||
println!(
|
||
"vertex-pool coverage by the union of batches: exact {cover_exact} · short {cover_short}"
|
||
);
|
||
}
|