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.
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 {
let pos: Vec<[f32; 3]> =
m.meshes.iter().flat_map(|s| s.positions.iter().copied()).take(8).collect();
let n: usize = m.meshes.iter().map(|s| s.positions.len()).sum();
for (o, _) in locate_run(&bytes, &pos) {
ours.entry(o).or_default().push((m.name.clone(), n));
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 {
@@ -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 {
let who = ours
.get(off)
@@ -143,7 +162,18 @@ fn main() {
v.iter().map(|(n, c)| format!("{n}({c})")).collect::<Vec<_>>().join(", ")
})
.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;
}

View File

@@ -115,6 +115,12 @@ pub struct GameMesh {
pub indices: Vec<u32>,
/// Sub-mesh / node name from the descriptor, when available.
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
@@ -376,6 +382,7 @@ impl Xbg7Model {
uvs,
indices,
name: None,
vbuf_offset: Some(vb),
});
off = align16(ve) - base;
}
@@ -976,6 +983,7 @@ fn read_pool_mesh(
uvs,
indices,
name: None,
vbuf_offset: Some(vb),
}
}

View File

@@ -3677,6 +3677,7 @@ fn exhaust_cone_mesh() -> sylpheed_formats::mesh::GameMesh {
uvs: Vec::new(),
indices,
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
0x38788 181 — NOBODY
0x4b8b8 93 — NOBODY
0xb6574 41 — NOBODY
0xdbbac 77 — NOBODY
0x133da0 76 — NOBODY
0x162840 60 — NOBODY
0x3b3ee8 119 e106_bdy_01_l(119), e106_bdy_02_l(119)
0x3c55d8 119 — NOBODY
0x3dd2c4 146 e106_bdy_03(815), e106_bdy_03_l(146)
0x40763c 179 e106_bdy_04_l(179)
0x40e418 51 e106_brg_01(202), e106_brg_01_m(92)
0x444ccc 58 e106_eng_01_l(58)
0x44a32c 44 — NOBODY
0x45705c 82 e106_wep_02_01_l(82), e106_wep_02_01_m(294)
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

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
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)):
| file offset | drawn vcount | claimed by our decode |
|---|---|---|
| `0x3b3ee8` | 119 | `e106_bdy_01_l`(119), `e106_bdy_02_l`(119) |
| `0x3c55d8` | 119 | — **nobody** |
| `0x3dd2c4` | 146 | `e106_bdy_03`(815), `e106_bdy_03_l`(146) |
| `0x40763c` | 179 | `e106_bdy_04_l`(179) ✅ |
| `0x40e418` | 51 | `e106_brg_01`(202), `e106_brg_01_m`(92) |
| `0x444ccc` | 58 | `e106_eng_01_l`(58) ✅ |
| `0x45705c` | 82 | `e106_wep_02_01_l`(82), `e106_wep_02_01_m`(294) |
| `0x38788` `0x4b8b8` `0xb6574` `0xdbbac` `0x133da0` `0x162840` `0x44a32c` | 181, 93, 41, 77, 76, 60, 44 | — **nobody** |
| drawn offset | vcount | our resource anchored there | our resources with that vcount |
|---|---|---|---|
| `0x3b3ee8` | 119 | `e106_bdy_01_l`, `e106_bdy_02_l` | `bdy_01_l`, `bdy_02_l` |
| `0x3c55d8` | 119 | — **nobody** | `bdy_01_l`, `bdy_02_l` |
| `0x3dd2c4` | 146 | `e106_bdy_03_l` ✅ | `bdy_03_l` |
| `0x40763c` | 179 | `e106_bdy_04_l` | `bdy_04_l` |
| `0x40e418` | 51 | **nobody** (ours sit `0x5d0` earlier) | `brg_01_b_02`, `brg_01_l` |
| `0x444ccc` | 58 | `e106_eng_01_l` | `eng_01_l` |
| `0x44a32c` | 44 | **nobody** | `eng_02_l` |
| `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
`_m` resource starts at the offset of its own `_l` buffer** — `bdy_03` decodes
815 vertices beginning where the engine's 146-vertex LOD begins — and eight
drawn buffers are claimed by no resource at all. This is the anchor scan taking
the first candidate that validates, seen directly rather than inferred.
Four of the ship's drawn buffers are anchored exactly right. Two are the twin
collapse below. **Two are mis-anchors of a size we do have**: the engine's
51-vertex bridge buffer is at `0x40e418` while both our 51-vertex bridge
resources sit at `0x40de48`, and its 44-vertex `eng_02_l` is at `0x44a32c` while
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
`e106_bdy_01_l``e106_bdy_02_l` the capture shows **two** 119-vertex buffers