From 76a433f75c7f214f4da3cbafa2ae7528679f9dd4 Mon Sep 17 00:00:00 2001 From: "Claude (auto-RE)" Date: Wed, 12 Aug 2026 02:11:41 +0000 Subject: [PATCH] re: the proven offsets ARE candidates -- these mis-anchors are selection failures debug_vertex_run_starts exposes the anchor scan's candidate list: Stage_S01 has 15710 stride-24 starts and all three capture-proven offsets (0x3c55d8 twin, 0x40e418 bridge, 0x44a32c eng_02_l) are among them. anchor_pool_mesh takes the first that validates, so an earlier lookalike wins. Scoped to the current decoder's e106 cases; does not overturn the residual-51 finding. Co-Authored-By: Claude Opus 5 (1M context) --- .../examples/shared_vbase_check.rs | 10 +++++-- crates/sylpheed-formats/src/mesh.rs | 21 +++++++++++++ .../stage-s01-capture-truth-offsets.txt | 30 +++++++++---------- docs/re/structures/xbg7-mesh.md | 21 +++++++++++++ 4 files changed, 65 insertions(+), 17 deletions(-) diff --git a/crates/sylpheed-formats/examples/shared_vbase_check.rs b/crates/sylpheed-formats/examples/shared_vbase_check.rs index 308aa61..eaa4ba3 100644 --- a/crates/sylpheed-formats/examples/shared_vbase_check.rs +++ b/crates/sylpheed-formats/examples/shared_vbase_check.rs @@ -154,7 +154,12 @@ fn main() { 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"); + // Is a capture-proven offset even a candidate the scan considers? + // Absent ⇒ the run scan misses it; present ⇒ selection picked another. + let starts: BTreeSet = + 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) @@ -173,7 +178,8 @@ fn main() { .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}"); + let cand = if starts.contains(off) { "yes" } else { "NO" }; + println!("0x{off:<10x} {vcount:>7} {cand:>9} {who:<44} {same:<34} {near}"); } return; } diff --git a/crates/sylpheed-formats/src/mesh.rs b/crates/sylpheed-formats/src/mesh.rs index e0f7334..ce26370 100644 --- a/crates/sylpheed-formats/src/mesh.rs +++ b/crates/sylpheed-formats/src/mesh.rs @@ -613,6 +613,27 @@ impl Xbg7Model { } } +/// Diagnostic: the candidate vertex-buffer starts the stage anchor scan will +/// consider for a given `stride`, for one container. A runtime capture names the +/// offsets the engine really drew from (see `examples/shared_vbase_check.rs`), so +/// asking whether a proven offset is in this list separates the two possible +/// root causes of a mis-anchor: **absent** ⇒ the run scan misses it, **present** +/// ⇒ the scan sees it and the selection picks another. +pub fn debug_vertex_run_starts(bytes: &[u8], stride: usize) -> Vec { + if bytes.len() < 16 || &bytes[..4] != b"XPR2" { + return Vec::new(); + } + let mut cur = Cursor::new(bytes); + let Ok(header) = Xpr2Header::read(&mut cur) else { + return Vec::new(); + }; + let data_base = header.header_size as usize; + if data_base >= bytes.len() { + return Vec::new(); + } + vertex_run_starts(bytes, data_base, stride) +} + /// Scan the data section for offsets that begin a `stride`-sized unit-normal /// vertex run (NORMAL is `f16×4` at vertex offset +12). A run *start* is an /// offset whose normal is unit while the preceding stride slot's is not — i.e. diff --git a/docs/re/captures/stage-s01-capture-truth-offsets.txt b/docs/re/captures/stage-s01-capture-truth-offsets.txt index 2fcbc77..307515a 100644 --- a/docs/re/captures/stage-s01-capture-truth-offsets.txt +++ b/docs/re/captures/stage-s01-capture-truth-offsets.txt @@ -1,15 +1,15 @@ -file offset vcount claimed by our decode our resources with that vcount -0x38788 181 — NOBODY — none -0x4b8b8 93 — NOBODY — none -0xb6574 41 — NOBODY n042_bdy_m e007_bdy_01 @ -0x24864 -0xdbbac 77 — NOBODY — none _rou_e010_break @ -0x5050 -0x133da0 76 — NOBODY e303_wep_01_m, n041_bdy_l _rou_e010_break @ -0x407bc -0x162840 60 — NOBODY — none _rou_e010_break @ -0x6f25c -0x3b3ee8 119 e106_bdy_01_l(119), e106_bdy_02_l(119) e106_bdy_01_l, e106_bdy_02_l e106_bdy_01_l @ -0x0 -0x3c55d8 119 — NOBODY e106_bdy_01_l, e106_bdy_02_l e106_bdy_01_m @ -0x104e8 -0x3dd2c4 146 e106_bdy_03_l(146) e106_bdy_03_l e106_bdy_03_l @ -0x0 -0x40763c 179 e106_bdy_04_l(179) e106_bdy_04_l e106_bdy_04_l @ -0x0 -0x40e418 51 — NOBODY e106_brg_01_b_02, e106_brg_01_l e106_brg_01_b_02 @ -0x5d0 -0x444ccc 58 e106_eng_01_l(58) e106_eng_01_l e106_eng_01_l @ -0x0 -0x44a32c 44 — NOBODY e106_eng_02_l e106_eng_02 @ -0x35b8 -0x45705c 82 e106_wep_02_01_l(82) e106_wep_02_01_l e106_wep_02_01_l @ -0x0 +file offset vcount candidate claimed by our decode our resources with that vcount +0x38788 181 NO — NOBODY — none +0x4b8b8 93 NO — NOBODY — none +0xb6574 41 NO — NOBODY n042_bdy_m e007_bdy_01 @ -0x24864 +0xdbbac 77 NO — NOBODY — none _rou_e010_break @ -0x5050 +0x133da0 76 NO — NOBODY e303_wep_01_m, n041_bdy_l _rou_e010_break @ -0x407bc +0x162840 60 NO — NOBODY — none _rou_e010_break @ -0x6f25c +0x3b3ee8 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 +0x3c55d8 119 yes — NOBODY e106_bdy_01_l, e106_bdy_02_l e106_bdy_01_m @ -0x104e8 +0x3dd2c4 146 yes e106_bdy_03_l(146) e106_bdy_03_l e106_bdy_03_l @ -0x0 +0x40763c 179 yes e106_bdy_04_l(179) e106_bdy_04_l e106_bdy_04_l @ -0x0 +0x40e418 51 yes — NOBODY e106_brg_01_b_02, e106_brg_01_l e106_brg_01_b_02 @ -0x5d0 +0x444ccc 58 yes e106_eng_01_l(58) e106_eng_01_l e106_eng_01_l @ -0x0 +0x44a32c 44 yes — NOBODY e106_eng_02_l e106_eng_02 @ -0x35b8 +0x45705c 82 yes e106_wep_02_01_l(82) e106_wep_02_01_l e106_wep_02_01_l @ -0x0 diff --git a/docs/re/structures/xbg7-mesh.md b/docs/re/structures/xbg7-mesh.md index 5e2e867..5bf3c76 100644 --- a/docs/re/structures/xbg7-mesh.md +++ b/docs/re/structures/xbg7-mesh.md @@ -653,6 +653,27 @@ could not: `0x3b3ee8` stays with whichever twin we already decode there, and the other twin must move to `0x3c55d8`. The invariant is checkable without a capture — *mirrored twins must decode to X-reflected buffers, never identical ones*. +**4. Root cause, for these three: selection, not the run scan.** +`mesh::debug_vertex_run_starts` exposes the candidate list the anchor scan works +from. `Stage_S01` yields **15 710** stride-24 candidate starts, and **all three +capture-proven offsets are in it** — `0x3c55d8` (the mirrored twin), `0x40e418` +(the drawn bridge buffer) and `0x44a32c` (`eng_02_l`). The scan sees the right +offsets; `anchor_pool_mesh` walks the list in ascending order and takes the first +that validates, so an earlier lookalike wins — our bridge resources sit `0x5d0` +before the buffer the engine drew. + +This is scoped: it says the *current* decoder's e106 mis-anchors are selection +failures. It does not overturn the earlier finding that the residual 51 *under +the withdrawn neighbourhood fix* had no validating candidate at all — a different +population, and the two can both be true. + +What the twins suggest as the fix: selection is **per-resource and greedy**, so +two resources can and do claim one buffer while a validating buffer sits unused. +An assignment that is distinct by construction — each candidate used at most once +— resolves the twin case by shape rather than by heuristic. Whether the proven +offsets actually validate for their resources is the next thing to test; if they +do, distinctness alone is the fix. + 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 holds three near-identical 51-vertex runs, so the pair has no oracle yet.