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>
This commit is contained in:
2026-08-12 02:04:55 +00:00
parent 3245aca8cb
commit 26f1bdf6fd
5 changed files with 90 additions and 38 deletions

View File

@@ -117,15 +117,27 @@ fn main() {
// scan chose for each resource — a direct read-out of what we got wrong. // 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=")) { 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"); 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(); let mut ours: BTreeMap<usize, Vec<(String, usize)>> = BTreeMap::new();
for m in &models { for m in &models {
let pos: Vec<[f32; 3]> = for sub in &m.meshes {
m.meshes.iter().flat_map(|s| s.positions.iter().copied()).take(8).collect(); if let Some(o) = sub.vbuf_offset {
let n: usize = m.meshes.iter().map(|s| s.positions.len()).sum(); ours.entry(o).or_default().push((m.name.clone(), sub.positions.len()));
for (o, _) in locate_run(&bytes, &pos) {
ours.entry(o).or_default().push((m.name.clone(), n));
} }
} }
}
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(); let mut drawn: BTreeMap<usize, u32> = BTreeMap::new();
for (_, draws) in &logs { for (_, draws) in &logs {
for d in draws { for d in draws {
@@ -135,7 +147,14 @@ fn main() {
} }
} }
} }
println!("{:<12} {:>7} claimed by our decode", "file offset", "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 { for (off, vcount) in &drawn {
let who = ours let who = ours
.get(off) .get(off)
@@ -143,7 +162,18 @@ fn main() {
v.iter().map(|(n, c)| format!("{n}({c})")).collect::<Vec<_>>().join(", ") v.iter().map(|(n, c)| format!("{n}({c})")).collect::<Vec<_>>().join(", ")
}) })
.unwrap_or_else(|| "— NOBODY".into()); .unwrap_or_else(|| "— NOBODY".into());
println!("0x{off:<10x} {vcount:>7} {who}"); 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; return;
} }

View File

@@ -115,6 +115,12 @@ pub struct GameMesh {
pub indices: Vec<u32>, pub indices: Vec<u32>,
/// Sub-mesh / node name from the descriptor, when available. /// Sub-mesh / node name from the descriptor, when available.
pub name: Option<String>, pub name: Option<String>,
/// Byte offset of this sub-mesh's vertex buffer inside the container, when
/// the decode path knows it. The content-anchored stage path does — and a
/// runtime capture names the same offset (a draw's `vbase` is this plus the
/// container's load address), so this is what makes an anchor checkable
/// against ground truth. See `examples/shared_vbase_check.rs`.
pub vbuf_offset: Option<usize>,
} }
/// A model = the set of sub-meshes recovered from one XPR2 container's first /// A model = the set of sub-meshes recovered from one XPR2 container's first
@@ -376,6 +382,7 @@ impl Xbg7Model {
uvs, uvs,
indices, indices,
name: None, name: None,
vbuf_offset: Some(vb),
}); });
off = align16(ve) - base; off = align16(ve) - base;
} }
@@ -976,6 +983,7 @@ fn read_pool_mesh(
uvs, uvs,
indices, indices,
name: None, name: None,
vbuf_offset: Some(vb),
} }
} }

View File

@@ -3677,6 +3677,7 @@ fn exhaust_cone_mesh() -> sylpheed_formats::mesh::GameMesh {
uvs: Vec::new(), uvs: Vec::new(),
indices, indices,
name: Some("exhaust".to_string()), name: Some("exhaust".to_string()),
vbuf_offset: None, // procedural, not read from a container
} }
} }

View File

@@ -1,15 +1,15 @@
file offset vcount claimed by our decode file offset vcount claimed by our decode our resources with that vcount
0x38788 181 — NOBODY 0x38788 181 — NOBODY — none
0x4b8b8 93 — NOBODY 0x4b8b8 93 — NOBODY — none
0xb6574 41 — NOBODY 0xb6574 41 — NOBODY n042_bdy_m e007_bdy_01 @ -0x24864
0xdbbac 77 — NOBODY 0xdbbac 77 — NOBODY — none _rou_e010_break @ -0x5050
0x133da0 76 — NOBODY 0x133da0 76 — NOBODY e303_wep_01_m, n041_bdy_l _rou_e010_break @ -0x407bc
0x162840 60 — NOBODY 0x162840 60 — NOBODY — none _rou_e010_break @ -0x6f25c
0x3b3ee8 119 e106_bdy_01_l(119), e106_bdy_02_l(119) 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 0x3c55d8 119 — NOBODY e106_bdy_01_l, e106_bdy_02_l e106_bdy_01_m @ -0x104e8
0x3dd2c4 146 e106_bdy_03(815), e106_bdy_03_l(146) 0x3dd2c4 146 e106_bdy_03_l(146) e106_bdy_03_l e106_bdy_03_l @ -0x0
0x40763c 179 e106_bdy_04_l(179) 0x40763c 179 e106_bdy_04_l(179) e106_bdy_04_l e106_bdy_04_l @ -0x0
0x40e418 51 e106_brg_01(202), e106_brg_01_m(92) 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) 0x444ccc 58 e106_eng_01_l(58) e106_eng_01_l e106_eng_01_l @ -0x0
0x44a32c 44 — NOBODY 0x44a32c 44 — NOBODY e106_eng_02_l e106_eng_02 @ -0x35b8
0x45705c 82 e106_wep_02_01_l(82), e106_wep_02_01_m(294) 0x45705c 82 e106_wep_02_01_l(82) e106_wep_02_01_l e106_wep_02_01_l @ -0x0

View File

@@ -605,25 +605,38 @@ is uploaded contiguously and **a capture names the exact file offset of every
buffer the engine drew**. Log 03 loaded the container at a different address, so buffer the engine drew**. Log 03 loaded the container at a different address, so
the constant is per-run, not baked. the constant is per-run, not baked.
**2. Read against our anchor scan, that is a defect list** **2. Read against our anchor scan, that is a defect list.** `GameMesh` now
carries `vbuf_offset` — the offset the anchor scan actually placed a sub-mesh at
— so the comparison is exact
([`captures/stage-s01-capture-truth-offsets.txt`](../captures/stage-s01-capture-truth-offsets.txt)): ([`captures/stage-s01-capture-truth-offsets.txt`](../captures/stage-s01-capture-truth-offsets.txt)):
| file offset | drawn vcount | claimed by our decode | | drawn offset | vcount | our resource anchored there | our resources with that vcount |
|---|---|---| |---|---|---|---|
| `0x3b3ee8` | 119 | `e106_bdy_01_l`(119), `e106_bdy_02_l`(119) | | `0x3b3ee8` | 119 | `e106_bdy_01_l`, `e106_bdy_02_l` | `bdy_01_l`, `bdy_02_l` |
| `0x3c55d8` | 119 | — **nobody** | | `0x3c55d8` | 119 | — **nobody** | `bdy_01_l`, `bdy_02_l` |
| `0x3dd2c4` | 146 | `e106_bdy_03`(815), `e106_bdy_03_l`(146) | | `0x3dd2c4` | 146 | `e106_bdy_03_l` ✅ | `bdy_03_l` |
| `0x40763c` | 179 | `e106_bdy_04_l`(179) ✅ | | `0x40763c` | 179 | `e106_bdy_04_l` | `bdy_04_l` |
| `0x40e418` | 51 | `e106_brg_01`(202), `e106_brg_01_m`(92) | | `0x40e418` | 51 | **nobody** (ours sit `0x5d0` earlier) | `brg_01_b_02`, `brg_01_l` |
| `0x444ccc` | 58 | `e106_eng_01_l`(58) ✅ | | `0x444ccc` | 58 | `e106_eng_01_l` | `eng_01_l` |
| `0x45705c` | 82 | `e106_wep_02_01_l`(82), `e106_wep_02_01_m`(294) | | `0x44a32c` | 44 | **nobody** | `eng_02_l` |
| `0x38788` `0x4b8b8` `0xb6574` `0xdbbac` `0x133da0` `0x162840` `0x44a32c` | 181, 93, 41, 77, 76, 60, 44 | — **nobody** | | `0x45705c` | 82 | `e106_wep_02_01_l` ✅ | `wep_02_01_l` |
| `0x38788` `0x4b8b8` `0xb6574` `0xdbbac` `0x133da0` `0x162840` | 181, 93, 41, 77, 76, 60 | — nobody | mostly none (other objects in the stage) |
Two resources land exactly (`bdy_04_l`, `eng_01_l`). Everywhere else a **full or Four of the ship's drawn buffers are anchored exactly right. Two are the twin
`_m` resource starts at the offset of its own `_l` buffer** — `bdy_03` decodes collapse below. **Two are mis-anchors of a size we do have**: the engine's
815 vertices beginning where the engine's 146-vertex LOD begins — and eight 51-vertex bridge buffer is at `0x40e418` while both our 51-vertex bridge
drawn buffers are claimed by no resource at all. This is the anchor scan taking resources sit at `0x40de48`, and its 44-vertex `eng_02_l` is at `0x44a32c` while
the first candidate that validates, seen directly rather than inferred. ours is elsewhere entirely. The remaining six belong to other objects in the
stage (`n041`, `n042`, `e303`), only two of which we decode at the right size.
> **A trap worth recording.** The first version of this table located our
> resources by *searching the container for their leading vertices* instead of
> asking the decoder, and it read much worse — full and `_m` resources appearing
> to start inside their own `_l` buffer. That was an artifact: **the same leading
> vertex run occurs at several offsets in one container** (`e106_bdy_03`'s first
> eight positions occur at four, `bdy_01`'s at three). That multiplicity is
> itself the reason the anchor scan is ambiguous — but it makes a position search
> useless for asking where a resource *was* anchored. Hence `vbuf_offset`.
**3. The twin pair is an anchoring error, and the mirror is in the data.** For **3. The twin pair is an anchoring error, and the mirror is in the data.** For
`e106_bdy_01_l``e106_bdy_02_l` the capture shows **two** 119-vertex buffers `e106_bdy_01_l``e106_bdy_02_l` the capture shows **two** 119-vertex buffers