anchor_pool_mesh took the FIRST candidate in file order from a container-global vertex-run scan, so a resource could be handed another resource's block whenever both shared (stride, vertex count, index count). Both blocks are real geometry and both pass every quality gate, so only position separates them. anchor_pool_mesh_near now tries candidates in order of distance from a reference, and anchor_models_filtered runs two passes: pass 1 anchors first-match to learn where resources land, pass 2 re-anchors each resource preferring the median anchor of its +/-2 descriptor neighbours. Too few anchored neighbours -> keep pass 1, so nothing regresses to guesswork. before decoded 5480/6294 shared 681 inconsistent 125 after decoded 5480/6294 shared 681 inconsistent 63 Coverage unchanged, inconsistency halved. e303_wep_01 decodes to 49x23x42 in ALL containers now, and e106 renders as a destroyer instead of a slab -- its two shared turrets symmetric at X[-203,-154] and X[154,203]. That resolves the user-reported "capital ships assemble wrong" for this cause. The filtered path needed care: models_named (what the viewer uses) dropped non-wanted resources, which would have left filtered decodes with no neighbourhood and silently kept the old behaviour. Resources are now collected regardless of the filter, but only the asked-for ones and their +/-2 neighbours are decoded in pass 1, so a filtered decode stays proportional to what was asked. 63 cases remain; mesh_consistency_disc stays ignored and now records 63, not 125. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
105 lines
3.7 KiB
Rust
105 lines
3.7 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::{Path, PathBuf};
|
|
|
|
use sylpheed_formats::mesh::Xbg7Model;
|
|
|
|
fn disc_root() -> Option<PathBuf> {
|
|
if let Ok(p) = std::env::var("SYLPHEED_DISC") {
|
|
let p = PathBuf::from(p);
|
|
if p.join("dat").is_dir() {
|
|
return Some(p);
|
|
}
|
|
}
|
|
let default = Path::new(
|
|
"/home/fabi/RE - Project Sylpheed/Project Sylpheed - Arc of Deception (USA, Europe) (En,Ja)",
|
|
);
|
|
if default.join("dat").is_dir() {
|
|
return Some(default.to_path_buf());
|
|
}
|
|
None
|
|
}
|
|
|
|
/// 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: 63 of 681 shared resources still decode inconsistently (was 125 before the neighbourhood anchor, 2026-08-12)"]
|
|
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: BTreeMap<String, Vec<([i64; 3], usize, usize, String)>> = 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()
|
|
);
|
|
}
|