fix(mesh): filtered decode must not depend on the requested subset
models_named pruned to the wanted set BEFORE distinct assignment, so collision resolution saw a different resource population and returned different offsets: 27 of 356 resources in Stage_S02 decoded differently when asked for alone, including real geometry (f001_bdy_30, f106_sld_02_l/m/d, f101_wep_01_l). Both the viewer and assemble_ship decode subsets, so both could disagree with the container's own answer. This was a regression from distinct assignment itself. Fixed by filtering the OUTPUT: the assignment always runs over the whole container. One-name, three-name and full decodes now agree exactly. Cost: a single-resource query on a 50MB container goes from near-instant to ~15s; per-container caching is the follow-up. Ten suites green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
53
crates/sylpheed-formats/examples/bounds_in_descriptor.rs
Normal file
53
crates/sylpheed-formats/examples/bounds_in_descriptor.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
//! Does a resource's DESCRIPTOR carry its bounding box?
|
||||
//!
|
||||
//! The last cross-container disagreements are 24-vertex bound boxes swapping
|
||||
//! identities; no anchoring rule can pin them (see docs). If the descriptor
|
||||
//! states the box, that is the missing information. This decodes the resource,
|
||||
//! takes the box its geometry actually spans, and searches the descriptor for
|
||||
//! those float values.
|
||||
//!
|
||||
//! Usage: bounds_in_descriptor <container.xpr> <resource>...
|
||||
use sylpheed_formats::mesh::{xbg7_descriptor_range, Xbg7Model};
|
||||
use std::collections::HashSet;
|
||||
|
||||
fn main() {
|
||||
let a: Vec<String> = std::env::args().collect();
|
||||
let bytes = std::fs::read(&a[1]).expect("container");
|
||||
let want: HashSet<String> = a[2..].iter().cloned().collect();
|
||||
for m in Xbg7Model::models_named(&bytes, &want, &|| false) {
|
||||
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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
let Some((d0, d1)) = xbg7_descriptor_range(&bytes, &m.name) else { continue };
|
||||
println!(
|
||||
"{} descriptor 0x{d0:x}..0x{d1:x} ({} bytes), box lo{:?} hi{:?}",
|
||||
m.name,
|
||||
d1 - d0,
|
||||
lo.map(|v| v.round()),
|
||||
hi.map(|v| v.round())
|
||||
);
|
||||
// Where in the descriptor does each bound value appear (±0.01)?
|
||||
let targets: Vec<(&str, f32)> = vec![
|
||||
("lo.x", lo[0]), ("lo.y", lo[1]), ("lo.z", lo[2]),
|
||||
("hi.x", hi[0]), ("hi.y", hi[1]), ("hi.z", hi[2]),
|
||||
];
|
||||
for (label, v) in targets {
|
||||
let mut at: Vec<usize> = Vec::new();
|
||||
let mut o = d0;
|
||||
while o + 4 <= d1 {
|
||||
let f = f32::from_be_bytes(bytes[o..o + 4].try_into().unwrap());
|
||||
if (f - v).abs() <= 0.01 * (1.0 + v.abs()) {
|
||||
at.push(o - d0);
|
||||
}
|
||||
o += 4;
|
||||
}
|
||||
println!(" {label:5} {v:10.3} at descriptor offsets {:x?}", &at[..at.len().min(6)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -523,11 +523,13 @@ impl Xbg7Model {
|
||||
}
|
||||
let name = read_cstr(bytes, e.name_offset as usize + DIR_BASE)
|
||||
.unwrap_or_else(|| "XBG7".to_string());
|
||||
if let Some(w) = wanted {
|
||||
if !w.contains(&name) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// NOTE: `wanted` is NOT applied here. Distinct assignment resolves
|
||||
// collisions against the whole set of resources, so pruning first
|
||||
// made a filtered decode depend on *which* subset was asked for —
|
||||
// measured 2026-08-12: 27 of 356 resources in `Stage_S02` came out
|
||||
// at a different offset when requested alone. The filter is applied
|
||||
// to the OUTPUT instead, so a subset is always a subset of the
|
||||
// container's own answer.
|
||||
resources.push(Res {
|
||||
name,
|
||||
markers,
|
||||
@@ -722,6 +724,9 @@ impl Xbg7Model {
|
||||
}
|
||||
models.push(m);
|
||||
}
|
||||
if let Some(w) = wanted {
|
||||
models.retain(|m| w.contains(&m.name));
|
||||
}
|
||||
out = models;
|
||||
out
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user