`mesh_consistency_disc.rs` had the only conflict: this branch added a `Sightings` type alias where #22 replaced the file's private `disc_root()` with the shared `common::disc_root`. Both kept — they are unrelated edits that happened to land in the same lines. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
152 lines
5.6 KiB
Rust
152 lines
5.6 KiB
Rust
//! Cross-container consistency for XBG7 geometry.
|
|
//!
|
|
//! A geometry resource shared by several stage containers must decode to the
|
|
//! same bounds in each. This needs no ground truth, and on 2026-08-11 it found
|
|
//! **125 of 681** shared resources decoding to different bounds while reporting
|
|
//! identical vertex and triangle counts — the anchor scan locating a different
|
|
//! buffer of the same size (see `docs/re/structures/xbg7-mesh.md`).
|
|
//!
|
|
//! The test is `#[ignore]`d because the decoder does not satisfy it yet. It is
|
|
//! written as the *target* state so that fixing the anchor scan makes it pass,
|
|
//! rather than as a snapshot of the bug.
|
|
|
|
use std::collections::BTreeMap;
|
|
use std::path::PathBuf;
|
|
|
|
use sylpheed_formats::mesh::Xbg7Model;
|
|
|
|
/// Every place one model name was seen: (span, verts, tris, container).
|
|
type Sightings = BTreeMap<String, Vec<([i64; 3], usize, usize, String)>>;
|
|
|
|
mod common;
|
|
use common::disc_root;
|
|
|
|
/// Rounded (w, h, d) of a model's own geometry.
|
|
fn span(m: &Xbg7Model) -> Option<[i64; 3]> {
|
|
let (mut lo, mut hi) = ([f32::MAX; 3], [f32::MIN; 3]);
|
|
for s in &m.meshes {
|
|
for q in &s.positions {
|
|
for k in 0..3 {
|
|
lo[k] = lo[k].min(q[k]);
|
|
hi[k] = hi[k].max(q[k]);
|
|
}
|
|
}
|
|
}
|
|
if lo[0] == f32::MAX {
|
|
return None;
|
|
}
|
|
Some([
|
|
(hi[0] - lo[0]).round() as i64,
|
|
(hi[1] - lo[1]).round() as i64,
|
|
(hi[2] - lo[2]).round() as i64,
|
|
])
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "known-failing: 62 of 714 shared resources decode inconsistently (was 125 of 681; distinct anchor assignment + the 0.42 connectivity cap fixed the rest). Note this metric is the WEAKER witness — a systematic mis-anchor is consistent — see docs/re/structures/xbg7-mesh.md"]
|
|
fn shared_resources_decode_identically_in_every_container() {
|
|
let Some(root) = disc_root() else {
|
|
eprintln!("SKIP: extracted disc not found (set SYLPHEED_DISC to enable)");
|
|
return;
|
|
};
|
|
let dir = root.join("hidden/resource3d");
|
|
let mut files: Vec<PathBuf> = std::fs::read_dir(&dir)
|
|
.expect("resource3d/")
|
|
.flatten()
|
|
.map(|e| e.path())
|
|
.filter(|p| p.extension().and_then(|s| s.to_str()) == Some("xpr"))
|
|
.collect();
|
|
files.sort();
|
|
|
|
// name -> (verts, tris) -> set of spans seen
|
|
let mut seen: Sightings = BTreeMap::new();
|
|
for f in &files {
|
|
let Ok(bytes) = std::fs::read(f) else {
|
|
continue;
|
|
};
|
|
let where_ = f.file_name().unwrap().to_string_lossy().to_string();
|
|
for m in Xbg7Model::anchor_models_cancellable(&bytes, 0.0, &|| false) {
|
|
let Some(sp) = span(&m) else { continue };
|
|
let v: usize = m.meshes.iter().map(|s| s.positions.len()).sum();
|
|
let t: usize = m.meshes.iter().map(|s| s.indices.len() / 3).sum();
|
|
seen.entry(m.name.clone())
|
|
.or_default()
|
|
.push((sp, v, t, where_.clone()));
|
|
}
|
|
}
|
|
|
|
let mut bad: Vec<String> = Vec::new();
|
|
for (name, list) in &seen {
|
|
if list.len() < 2 {
|
|
continue;
|
|
}
|
|
// Only compare decodes that agree on how much geometry they found;
|
|
// a differing vertex/triangle count is a different question.
|
|
if !list.iter().all(|e| e.1 == list[0].1 && e.2 == list[0].2) {
|
|
continue;
|
|
}
|
|
let spans: std::collections::BTreeSet<[i64; 3]> = list.iter().map(|e| e.0).collect();
|
|
if spans.len() > 1 && bad.len() < 10 {
|
|
bad.push(format!("{name}: {spans:?}"));
|
|
}
|
|
}
|
|
assert!(
|
|
bad.is_empty(),
|
|
"{} shared resources decode to different bounds; first: {bad:?}",
|
|
bad.len()
|
|
);
|
|
}
|
|
|
|
/// Port/starboard twins must not decode to the *same* buffer.
|
|
///
|
|
/// A runtime capture showed the container stores both halves of the `e106` hull
|
|
/// as separate X-reflected buffers, so a `…_01`/`…_02` pair of equal vertex
|
|
/// count should come out mirrored (or related by another axis / vertex order) —
|
|
/// never identical, which is the collapse distinct assignment fixes. Since that
|
|
/// fix reached grouped pools too, this holds for **every** twin pair on the disc
|
|
/// — including `n206`, which was the last exception.
|
|
#[test]
|
|
fn twin_pairs_do_not_share_a_buffer() {
|
|
let Some(root) = disc_root() else {
|
|
eprintln!("SKIP: extracted disc not found (set SYLPHEED_DISC to enable)");
|
|
return;
|
|
};
|
|
let mut files: Vec<PathBuf> = std::fs::read_dir(root.join("hidden/resource3d"))
|
|
.expect("resource3d/")
|
|
.flatten()
|
|
.map(|e| e.path())
|
|
.filter(|p| p.extension().and_then(|s| s.to_str()) == Some("xpr"))
|
|
.collect();
|
|
files.sort();
|
|
|
|
let mut collapsed: Vec<String> = Vec::new();
|
|
for f in &files {
|
|
let Ok(bytes) = std::fs::read(f) else {
|
|
continue;
|
|
};
|
|
let models = Xbg7Model::anchor_models_cancellable(&bytes, 0.0, &|| false);
|
|
let by_name: BTreeMap<&str, &Xbg7Model> =
|
|
models.iter().map(|m| (m.name.as_str(), m)).collect();
|
|
for m in &models {
|
|
let Some(stem) = m.name.strip_suffix("_01") else {
|
|
continue;
|
|
};
|
|
let Some(t) = by_name.get(format!("{stem}_02").as_str()) else {
|
|
continue;
|
|
};
|
|
let (a, b) = (m.meshes[0].vbuf_offset, t.meshes[0].vbuf_offset);
|
|
if a.is_some() && a == b {
|
|
collapsed.push(format!(
|
|
"{}/{stem}_02 in {}",
|
|
m.name,
|
|
f.file_name().unwrap().to_string_lossy()
|
|
));
|
|
}
|
|
}
|
|
}
|
|
assert!(
|
|
collapsed.is_empty(),
|
|
"twin pairs sharing one buffer: {collapsed:?}"
|
|
);
|
|
}
|