re: eng_02_l's real block is adjacent -- the connectivity heuristic rejects it
debug_find_index_buffer scans the container for an index buffer that validates against a capture-proven vertex buffer. For eng_02_l nothing validates with the connectivity test on; with it off the nearest hit is exact pad-0 adjacency (vb-ib = 144 = 72*2). The block's mean_edge/diag is 0.417 against a 0.28 cap -- the documented false positive for coarse LODs, now caught with ground truth. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
27
crates/sylpheed-formats/examples/find_ib.rs
Normal file
27
crates/sylpheed-formats/examples/find_ib.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
//! Where could a resource's index buffer be, given the vertex buffer a runtime
|
||||
//! capture proves the engine drew from? Tests the anchor scan's adjacency
|
||||
//! assumption against ground truth.
|
||||
use sylpheed_formats::mesh::{debug_find_index_buffer, debug_resource_params};
|
||||
fn main() {
|
||||
let a: Vec<String> = std::env::args().collect();
|
||||
let bytes = std::fs::read(&a[1]).unwrap();
|
||||
for pair in a[2..].iter() {
|
||||
let (name, off) = pair.split_once('@').unwrap();
|
||||
let vb = usize::from_str_radix(off.trim_start_matches("0x"), 16).unwrap();
|
||||
let params = debug_resource_params(&bytes, name);
|
||||
let markers = params.as_ref().map(|(m, _)| m.clone()).unwrap_or_default();
|
||||
println!("{name} @ 0x{vb:x} markers={markers:?}");
|
||||
let hits = debug_find_index_buffer(&bytes, name, vb);
|
||||
if hits.is_empty() {
|
||||
println!(" no index buffer anywhere in the container validates this block");
|
||||
}
|
||||
let mut hits = hits;
|
||||
hits.sort_by_key(|(_, d)| d.abs());
|
||||
for (ib, d) in hits.iter().take(8) {
|
||||
println!(" ib 0x{ib:x} vb-ib = {d} bytes");
|
||||
}
|
||||
if hits.len() > 8 {
|
||||
println!(" … {} total", hits.len());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -673,6 +673,100 @@ pub fn debug_try_anchor(
|
||||
None
|
||||
}
|
||||
|
||||
/// Diagnostic: for a resource whose vertex buffer is *known* (a runtime capture
|
||||
/// names it), where could its index buffer be? The anchor scan assumes the index
|
||||
/// buffer sits immediately before the vertex buffer; this scans the whole
|
||||
/// container instead and returns every offset that validates as this resource's
|
||||
/// index buffer. An empty result means the block is unreadable at that `vb` for
|
||||
/// another reason; a hit far from `vb` means the adjacency assumption is what
|
||||
/// fails. Returns `(ib_offset, signed distance vb - ib)` pairs.
|
||||
pub fn debug_find_index_buffer(bytes: &[u8], name: &str, vb: usize) -> Vec<(usize, i64)> {
|
||||
let Some(decl) = decl_of(bytes, name) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let markers = decl.1;
|
||||
let decl = decl.0;
|
||||
let Some(&(vtx_count, index_count)) = markers.first() else {
|
||||
return Vec::new();
|
||||
};
|
||||
let mut out = Vec::new();
|
||||
let end = bytes.len().saturating_sub(index_count * 2);
|
||||
let mut ib = 0usize;
|
||||
while ib < end {
|
||||
// Cheap prefilter: the first few indices must be in range, and a real
|
||||
// index buffer is not a run of zeros.
|
||||
let ok = (0..6).all(|k| (be16(bytes, ib + k * 2) as usize) < vtx_count)
|
||||
&& (0..6).any(|k| be16(bytes, ib + k * 2) != 0);
|
||||
// `SOFT_IB=1` drops the connectivity requirement, to separate "no index
|
||||
// buffer fits" from "our connectivity test is too strict".
|
||||
let strict = std::env::var("SOFT_IB").is_err();
|
||||
if ok && validate_block(bytes, ib, vb, vtx_count, index_count, &decl, 0.0, strict) {
|
||||
out.push((ib, vb as i64 - ib as i64));
|
||||
}
|
||||
ib += 2;
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Internal: the descriptor parameters the diagnostics need.
|
||||
fn decl_of(bytes: &[u8], name: &str) -> Option<(VertexDecl, Vec<(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];
|
||||
return Some((parse_vertex_decl(d)?, all_index_markers(d)));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Diagnostic: a resource's `(vtx_count, idx_count)` markers and vertex stride,
|
||||
/// as the stage anchor scan reads them from the descriptor.
|
||||
pub fn debug_resource_params(bytes: &[u8], name: &str) -> Option<(Vec<(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];
|
||||
return Some((all_index_markers(d), parse_vertex_decl(d)?.stride));
|
||||
}
|
||||
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
|
||||
|
||||
@@ -714,6 +714,40 @@ sub-range, a different unit, or a Xenia-side artifact is unknown — it may matt
|
||||
for `eng_02_l`, whose block validation is exactly what an index-count mismatch
|
||||
would break.
|
||||
|
||||
**6. Why `eng_02_l`'s real block is rejected: the connectivity heuristic.**
|
||||
`mesh::debug_find_index_buffer` scans the *whole container* for an index buffer
|
||||
that validates against a known vertex buffer, instead of assuming adjacency
|
||||
(`examples/find_ib.rs`). For `e106_eng_02_l` at the capture-proven `0x44a32c`,
|
||||
with the connectivity test on, **nothing in the container validates**. With it
|
||||
off (`SOFT_IB=1`) the nearest hit is `ib 0x44a29c` — `vb − ib = 144 = 72 × 2`,
|
||||
i.e. **exact pad-0 adjacency**. So the index buffer is exactly where the decoder
|
||||
assumes it is; the block is thrown out by one heuristic.
|
||||
|
||||
That heuristic is `mean_edge / bbox_diag > 0.28 → reject`. Measured on the real
|
||||
blocks:
|
||||
|
||||
| block | mean edge | bbox diagonal | ratio | verdict |
|
||||
|---|---|---|---|---|
|
||||
| `eng_02_l` (24 tris) `ib 0x44a29c → vb 0x44a32c` | 109.21 | 261.96 | **0.417** | rejected (cap 0.28) |
|
||||
| `bdy_02_l` (82 tris) `ib 0x3c53ec → vb 0x3c55d8` | 131.70 | 786.66 | 0.167 | passes |
|
||||
| `bdy_01_l` (82 tris) `ib 0x3b3cfc → vb 0x3b3ee8` | 131.70 | 786.66 | 0.167 | passes |
|
||||
|
||||
This is precisely the false positive the check's own comment predicts — "a small
|
||||
flat sub-mesh legitimately has large edges relative to its own diagonal" — caught
|
||||
in the wild for the first time, with the runtime naming the block it rejects. A
|
||||
24-triangle engine LOD is coarse by construction, so its edges *are* a large
|
||||
fraction of its size.
|
||||
|
||||
(The twins' two real blocks having **identical** mean edge and diagonal is a free
|
||||
corroboration that they are mirror images: reflection preserves lengths.)
|
||||
|
||||
So the residual class is not one bug. `eng_02_l` has its vertex start in the
|
||||
candidate list *and* its index buffer exactly adjacent, and still fails — a
|
||||
**validator** problem, not a scan or selection one. Raising the cap is not the
|
||||
fix to reach for blind: the threshold trades against false anchors, and now that
|
||||
a capture can name true blocks, it can be **calibrated** against them rather than
|
||||
guessed. Not changed here.
|
||||
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user