re: the capture is Stage_S02 -- provenance corrected, oracle grows to 46 buffers

capture_truth_scan places every drawn buffer across all 166 containers by modal
vbase-offset. Stage_S02 wins with 64 matches at one constant against Stage_S01's
16, and the logs carry f101/f105/f106/e105 -- a Stage-02 cast. Stage_S01 looked
consistent because the shared block is duplicated verbatim (twins 0x116F0 apart
in both), so the earlier structural findings hold; only the loaded-container
claim was wrong. The S02 table places 46 buffers, 12 claimed by nobody, and
resolves the six Stage_S01 mystery buffers as e105 parts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-12 02:59:40 +00:00
parent df7a10749a
commit b58f42e46d
3 changed files with 191 additions and 0 deletions

View File

@@ -0,0 +1,112 @@
//! Which container did each captured draw come from, and where in it?
//!
//! Extends the single-container `--map` check in `shared_vbase_check` to a whole
//! `resource3d` directory. For each container it indexes every 4-byte-aligned
//! position triple, looks up each draw's first dumped position, confirms the run
//! at a fixed stride, and reports the modal `vbase offset`. A container the
//! engine loaded shows one dominant constant; an unrelated one shows noise.
//!
//! The output is capture-named ground truth for anchors far beyond the one ship
//! `Stage_S01` gave us.
//!
//! Usage: capture_truth_scan <resource3d_dir> <capture.log>...
use sylpheed_formats::ship_capture::{parse_capture, parse_drawlog, CapturedDraw};
use std::collections::HashMap;
fn q(v: f32) -> i64 {
(v as f64 * 1e4).round() as i64
}
fn main() {
let a: Vec<String> = std::env::args().collect();
let dir = &a[1];
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");
let mut d = parse_capture(&text);
if d.is_empty() {
d = parse_drawlog(&text);
}
for x in d {
// One entry per buffer; the logs are already deduped per transform.
if x.pos.len() >= 8 && x.vcount >= 20 && seen.insert((log.clone(), x.vbase)) {
draws.push(x);
}
}
}
eprintln!("{} distinct (log, vbase) draws to place", draws.len());
let mut files: Vec<_> = std::fs::read_dir(dir)
.unwrap()
.flatten()
.map(|e| e.path())
.filter(|p| p.extension().and_then(|s| s.to_str()) == Some("xpr"))
.collect();
files.sort();
let mut placed = 0usize;
for f in &files {
let Ok(bytes) = std::fs::read(f) else { continue };
// Index quantised position triples. Junk floats (NaN/huge) are skipped,
// which prunes most of a texture-heavy container.
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, Vec<(u32, u32)>> = HashMap::new();
for d in &draws {
let k = (q(d.pos[0][0]), q(d.pos[0][1]), q(d.pos[0][2]));
// ±1 in each axis: the log rounds, our file value may round the
// other way at a tie.
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()
.push((d.vbase, d.vcount));
break;
}
}
}
}
}
}
}
let mut top: Vec<_> = deltas.into_iter().collect();
top.sort_by_key(|(_, v)| std::cmp::Reverse(v.len()));
if let Some((delta, hits)) = top.first() {
if hits.len() >= 3 {
placed += hits.len();
println!(
"{:<22} base=0x{:<10X} buffers={}",
f.file_name().unwrap().to_string_lossy(),
delta,
hits.len()
);
}
}
}
eprintln!("{placed} draws placed in a container");
}

View File

@@ -0,0 +1,47 @@
file offset vcount candidate claimed by our decode our resources with that vcount
0x135036c 240 yes _rou_f101(240) — none _rou_f101 @ -0x0
0x13519ec 240 NO _rou_f101(240) — none _rou_f101 @ -0x0
0x13fd6e8 25448 yes f101_bdy_01(25448) f101_bdy_01 f101_bdy_01 @ -0x0
0x192961c 13172 yes f101_bdy_02(13172) f101_bdy_02 f101_bdy_02 @ -0x0
0x1a0befc 2296 yes f101_bdy_03(2296) f101_bdy_03 f101_bdy_03 @ -0x0
0x1a22c1c 7694 yes f101_eng_01(7694) f101_eng_01 f101_eng_01 @ -0x0
0x1be5df4 628 yes f101_wep_01_m(628) f101_wep_01_m f101_wep_01_m @ -0x0
0x1bf40f4 261 yes _rou_f105_break(261), f105_bdy_03_m(2336), f105_eng_01_m(1402) f106_eng_01_l _rou_f105_break @ -0x0
0x1bf596c 2446 NO _rou_f105_break(2446) f105_bdy_01_m _rou_f105_break @ -0x0
0x1c03ebc 1960 NO _rou_f105_break(1960) f105_bdy_02_m _rou_f105_break @ -0x0
0x1c0f67c 82 NO _rou_f105_break(82) e106_wep_02_01_l _rou_f105_break @ -0x0
0x1c0fe2c 2336 NO _rou_f105_break(2336) f105_bdy_03_m _rou_f105_break @ -0x0
0x1c1d92c 220 NO _rou_f105_break(220) — none _rou_f105_break @ -0x0
0x1c1edcc 80 NO _rou_f105_break(80) f001_bdy_24_m _rou_f105_break @ -0x0
0x1c1f54c 148 NO _rou_f105_break(148) — none _rou_f105_break @ -0x0
0x1c2032c 156 NO _rou_f105_break(156) e105_bdy_01_l _rou_f105_break @ -0x0
0x1c211cc 1402 NO _rou_f105_break(1402) f105_eng_01_m _rou_f105_break @ -0x0
0x1c2953c 84 NO _rou_f105_break(84) — none _rou_f105_break @ -0x0
0x1c29d1c 192 NO _rou_f105_break(192) f105_sld_01_b01 _rou_f105_break @ -0x0
0x1c2af1c 122 NO _rou_f105_break(122) e106_eng_02_m _rou_f105_break @ -0x0
0x1c2ba8c 24 NO _rou_f105_break(24) — none _rou_f105_break @ -0x0
0x227ac94 1692 yes f106_bdy_02_l(1692) f106_bdy_02_l f106_bdy_02_l @ -0x0
0x22c2ef0 686 yes f106_bdy_03_l(686) f106_bdy_03_l f106_bdy_03_l @ -0x0
0x22ed420 261 yes f106_eng_01_l(261) f106_eng_01_l f106_eng_01_l @ -0x0
0x2326c9c 254 yes f106_sld_01_l(254), f106_sld_02_l(254), e303_wep_02_m(254) f106_sld_01_l, f106_sld_02_l, e303_wep_02_m f106_sld_01_l @ -0x0
0x233b3fc 254 yes — NOBODY f106_sld_01_l, f106_sld_02_l, e303_wep_02_m f106_sld_02 @ -0xcc44
0x24ce5f4 32 yes — NOBODY e302_barrel_l f303_body @ -0x1638
0x24fa1c8 128 yes — NOBODY _rou_f402 _rou_f401_dead @ -0x90fc
0x28ea1d4 156 yes e105_bdy_01_l(156) e105_bdy_01_l e105_bdy_01_l @ -0x0
0x28f7514 107 yes — NOBODY e105_bdy_02_l, e105_brg_m e105_bdy_02_d @ -0x14e8
0x291e1e8 185 yes e105_bdy_03_l(185) e105_bdy_03_l e105_bdy_03_l @ -0x0
0x29a4788 181 yes e105_bdy_04_l(181) e105_bdy_04_l e105_bdy_04_l @ -0x0
0x29b78b8 93 yes — NOBODY e105_bdy_05_l e105_bdy_05_d @ -0x9094
0x2a22574 41 yes — NOBODY — none e105_bdy_06_d @ -0x4863c
0x2a47bac 77 yes e105_brg_l(77) e105_brg_l e105_brg_l @ -0x0
0x2a9fda0 76 yes — NOBODY e105_eng_01_l, e303_wep_01_m e105_eng_01_d @ -0x27a14
0x2ace840 60 yes — NOBODY e105_wep_01_l e105_wep_01 @ -0x2b63c
0x2d1fee8 119 yes e106_bdy_01_l(119), e106_bdy_02_l(119) e106_bdy_01_l, e106_bdy_02_l e106_bdy_01_l @ -0x0
0x2d315d8 119 yes — NOBODY e106_bdy_01_l, e106_bdy_02_l e106_bdy_01_m @ -0x104e8
0x2d492c4 146 yes e106_bdy_03_l(146) e106_bdy_03_l e106_bdy_03_l @ -0x0
0x2d7363c 179 yes — NOBODY e106_bdy_04_l e106_bdy_04 @ -0x25bd4
0x2d7a418 51 yes — NOBODY e106_brg_01_b_02, e106_brg_01_l e106_brg_01_b_02 @ -0x5d0
0x2db0ccc 58 yes e105_wep_01_l(60) e106_eng_01_l e105_wep_01_l @ -0x0
0x2db632c 44 yes — NOBODY e106_eng_02_l, e302_base_m e106_eng_02 @ -0x35b8
0x2dc305c 82 yes e106_wep_02_01_l(82), e201_bdy_02_l(85) e106_wep_02_01_l e106_wep_02_01_l @ -0x0
0x329077c 6000 yes n006_02(6000) n006_02 n006_02 @ -0x0

View File

@@ -781,6 +781,38 @@ capture covering more ships and stages (the same `--truth` method extends to any
container the engine drew from) or a discriminator that does not degrade for container the engine drew from) or a discriminator that does not degrade for
coarse geometry. Both are recorded as the next step rather than guessed at. coarse geometry. Both are recorded as the next step rather than guessed at.
**8. Provenance correction — the capture is `Stage_S02`, and the oracle is
4× bigger than reported.** `examples/capture_truth_scan.rs` runs the
offset-locating pass over **every** container in `resource3d` and reports each
one's modal `vbase offset`. `Stage_S02` places **64** buffer-matches at a
single constant (`0x17FE3FF4`); `Stage_S01`, which sections 17 above used,
places only 16. The logs also contain `f101` (the ACROPOLIS escort, 25 448
verts), `f105`/`f106` (TCAF cruiser and destroyer) and `e105` — a Stage-02 cast.
**The loaded container was `Stage_S02`.**
Why `Stage_S01` nevertheless produced a consistent constant: the two containers
carry the shared block **verbatim and contiguously**. The e106 twin buffers sit
at `0x3b3ee8`/`0x3c55d8` in `Stage_S01` and `0x2d1fee8`/`0x2d315d8` in
`Stage_S02` — the same `0x116F0` apart. So the earlier findings are still true
statements about `Stage_S01`'s content (both mirrored buffers are in it, and our
decoder collapses them), and they reproduce in `Stage_S02`; only the claim that
`0x1A94FFF4` was *the loaded container's* base was an artifact.
The `Stage_S02` table
([`captures/stage-s02-capture-truth-offsets.txt`](../captures/stage-s02-capture-truth-offsets.txt))
places **46 drawn buffers**: 34 have a claimant (29 of them a single resource of
exactly the drawn size), **12 are claimed by nobody**. It also resolves the six
mystery buffers of the `Stage_S01` table — 181, 93, 77, 76, 60, 41 vertices —
as **`e105` parts** (`bdy_04_l`, `bdy_05_l`, `brg_l`, `eng_01_l`, `wep_01_l`),
another ship in the same mission, several of which are the same "right size,
wrong place" failure as `eng_02_l`.
**A new defect class to chase**: `_rou_f105_break` — a destruction composite —
claims a run of **twelve consecutive** drawn offsets whose sizes match *other*
ships' LODs (`f105_bdy_01_m`, `f105_bdy_03_m`, `e106_wep_02_01_l`, …). Either the
break model genuinely mirrors those buffers or our decoder is handing a whole
address range to one composite. Not resolved here.
Not settled: `e106_brg_01_b_02``e106_brg_01_l` (51 verts). A second 51-vertex Not settled: `e106_brg_01_b_02``e106_brg_01_l` (51 verts). A second 51-vertex
`vbase` exists in the logs but is **not** from this container, and the container `vbase` exists in the logs but is **not** from this container, and the container
holds three near-identical 51-vertex runs, so the pair has no oracle yet. holds three near-identical 51-vertex runs, so the pair has no oracle yet.