diff --git a/crates/sylpheed-formats/examples/try_anchor.rs b/crates/sylpheed-formats/examples/try_anchor.rs new file mode 100644 index 0000000..268f638 --- /dev/null +++ b/crates/sylpheed-formats/examples/try_anchor.rs @@ -0,0 +1,17 @@ +//! Does the capture-proven offset validate for the resource that should own it? +use sylpheed_formats::mesh::debug_try_anchor; +fn main() { + let a: Vec = std::env::args().collect(); + let bytes = std::fs::read(&a[1]).unwrap(); + // The production scan tries pads 0..=3; pass a bigger one to ask whether the + // block would validate at all with a wider index/vertex gap. + let max_pad: usize = std::env::var("MAX_PAD").ok().and_then(|v| v.parse().ok()).unwrap_or(3); + for pair in a[2..].iter() { + let (name, off) = pair.split_once('@').unwrap(); + let off = usize::from_str_radix(off.trim_start_matches("0x"), 16).unwrap(); + match debug_try_anchor(&bytes, name, off, max_pad) { + Some((v, i, pad)) => println!("{name:22} @ 0x{off:x} ACCEPTED v={v} idx={i} pad={pad}"), + None => println!("{name:22} @ 0x{off:x} rejected"), + } + } +} diff --git a/crates/sylpheed-formats/src/mesh.rs b/crates/sylpheed-formats/src/mesh.rs index ce26370..27023bc 100644 --- a/crates/sylpheed-formats/src/mesh.rs +++ b/crates/sylpheed-formats/src/mesh.rs @@ -613,6 +613,66 @@ impl Xbg7Model { } } +/// Diagnostic: would the anchor scan accept `vb` as the vertex buffer of the +/// named resource's first sub-mesh? A runtime capture proves which offset the +/// engine drew from, so this answers whether the correct block is *acceptable* +/// and merely lost the first-match race, or is rejected outright by +/// `validate_block`. Returns `(vtx_count, index_count, accepted_pad)`. +pub fn debug_try_anchor( + bytes: &[u8], + name: &str, + vb: usize, + max_pad: usize, +) -> Option<(usize, usize, usize)> { + if bytes.len() < 16 || &bytes[..4] != b"XPR2" { + return None; + } + let mut cur = Cursor::new(bytes); + let header = Xpr2Header::read(&mut cur).ok()?; + const DIR_BASE: usize = 0x10; + for _ in 0..header.num_resources { + let Ok(e) = Xpr2ResourceEntry::read(&mut cur) else { break }; + if &e.type_tag != b"XBG7" { + continue; + } + let desc = e.data_offset as usize + DIR_BASE; + let desc_end = (desc + e.descriptor_size as usize).min(bytes.len()); + if desc >= bytes.len() || desc_end <= desc { + continue; + } + let rname = read_cstr(bytes, e.name_offset as usize + DIR_BASE) + .unwrap_or_else(|| "XBG7".to_string()); + if rname != name { + continue; + } + let d = &bytes[desc..desc_end]; + let markers = all_index_markers(d); + let decl = parse_vertex_decl(d)?; + let (vtx_count, index_count) = *markers.first()?; + let idx_bytes = index_count * 2; + for pad in 0..=max_pad { + if vb < idx_bytes + pad { + continue; + } + let mc = if pad == 0 { 0.0 } else { 0.85 }; + if validate_block( + bytes, + vb - idx_bytes - pad, + vb, + vtx_count, + index_count, + &decl, + mc, + true, + ) { + return Some((vtx_count, index_count, pad)); + } + } + return None; + } + None +} + /// 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 diff --git a/docs/re/structures/xbg7-mesh.md b/docs/re/structures/xbg7-mesh.md index 5bf3c76..ab32ecb 100644 --- a/docs/re/structures/xbg7-mesh.md +++ b/docs/re/structures/xbg7-mesh.md @@ -674,6 +674,36 @@ An assignment that is distinct by construction — each candidate used at most o offsets actually validate for their resources is the next thing to test; if they do, distinctness alone is the fix. +**5. Do the proven offsets validate? Two of three — and that splits the fix.** +`mesh::debug_try_anchor(bytes, name, vb, max_pad)` asks `validate_block` directly +(`examples/try_anchor.rs`): + +| resource | proven offset | verdict | +|---|---|---| +| `e106_bdy_01_l` / `e106_bdy_02_l` | `0x3b3ee8` **and** `0x3c55d8` | **accepted for both, at both** (v=119, idx=246, pad=0) | +| `e106_brg_01_l` / `e106_brg_01_b_02` | `0x40e418` | **accepted for both** (v=51, idx=126, pad=0) | +| `e106_eng_02_l` | `0x44a32c` | **rejected** — and still rejected with the pad widened to 64 | + +So the twin case is exactly what it looked like: the correct block is perfectly +acceptable and simply lost the first-match race, and a **distinct assignment** +(each candidate buffer claimed by at most one resource) fixes it — the two +resources have two accepted offsets between them. The bridge pair is weaker: +`0x40e418` is accepted by both, our current `0x40de48` is accepted too, so +distinctness would separate them but not choose correctly. + +`eng_02_l` is a different failure: the offset the engine drew from is **not +acceptable at all**, so no selection policy can reach it. That is the +"residual" class this file describes above, now with one member pinned to a +concrete offset for the first time. + +❔ **An open discrepancy, recorded not explained.** The capture's `DRAW` lines +carry an `indices=` field that does not agree with the descriptor's index count: +the 119-vertex twin draws log `indices=21` where our marker says 246, and the +44-vertex draw logs `indices=12`. Whether that field is an index *count* of a +sub-range, a different unit, or a Xenia-side artifact is unknown — it may matter +for `eng_02_l`, whose block validation is exactly what an index-count mismatch +would break. + 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.