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)
|
let name = read_cstr(bytes, e.name_offset as usize + DIR_BASE)
|
||||||
.unwrap_or_else(|| "XBG7".to_string());
|
.unwrap_or_else(|| "XBG7".to_string());
|
||||||
if let Some(w) = wanted {
|
// NOTE: `wanted` is NOT applied here. Distinct assignment resolves
|
||||||
if !w.contains(&name) {
|
// collisions against the whole set of resources, so pruning first
|
||||||
continue;
|
// 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 {
|
resources.push(Res {
|
||||||
name,
|
name,
|
||||||
markers,
|
markers,
|
||||||
@@ -722,6 +724,9 @@ impl Xbg7Model {
|
|||||||
}
|
}
|
||||||
models.push(m);
|
models.push(m);
|
||||||
}
|
}
|
||||||
|
if let Some(w) = wanted {
|
||||||
|
models.retain(|m| w.contains(&m.name));
|
||||||
|
}
|
||||||
out = models;
|
out = models;
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1097,6 +1097,28 @@ that carry it) and `t901_e01_D` is 58×59×3013 (a mast). And `f101_bdy_01` flag
|
|||||||
at 6× while being **capture-verified exact** in the truth table — a useful
|
at 6× while being **capture-verified exact** in the truth table — a useful
|
||||||
reminder that a bulky hull is not a bug.
|
reminder that a bulky hull is not a bug.
|
||||||
|
|
||||||
|
### ✅ Fixed: a filtered decode no longer depends on what you ask for
|
||||||
|
|
||||||
|
Distinct assignment resolves collisions against the set of resources being
|
||||||
|
decoded — and `models_named` was pruning to the requested subset **before** that
|
||||||
|
pass. So the answer depended on the request: measured on `Stage_S02`, **27 of 356
|
||||||
|
resources came out at a different offset when asked for alone** than in a full
|
||||||
|
decode, and not only boxes — `f001_bdy_30`, `f106_sld_02_l/m/d`, `f101_wep_01_l`
|
||||||
|
among them. Both the viewer and `ship::assemble_ship` decode subsets, so both
|
||||||
|
could get geometry the container's own answer disagrees with.
|
||||||
|
|
||||||
|
This was a regression introduced by distinct assignment itself, and it is fixed
|
||||||
|
by applying `wanted` to the **output** instead of the input: the assignment
|
||||||
|
always runs over the whole container, and a subset is now a subset of the
|
||||||
|
container's own answer. Verified: one-name, three-name and full decodes now
|
||||||
|
return the identical offset for the same resource.
|
||||||
|
|
||||||
|
**Cost, stated plainly:** a single-resource query on a 50 MB container went from
|
||||||
|
near-instant to **~15 s**, because it now anchors every resource. The viewer
|
||||||
|
calls this once per ship view (with cancellation), so it is a slow prepare step
|
||||||
|
rather than a per-frame cost — but caching the per-container decode is the
|
||||||
|
obvious follow-up and is not done here.
|
||||||
|
|
||||||
### The consistency figure is mostly bounding boxes — real disagreement is ONE resource
|
### The consistency figure is mostly bounding boxes — real disagreement is ONE resource
|
||||||
|
|
||||||
`examples/consensus_check.rs` sharpens the cross-container test: with three or
|
`examples/consensus_check.rs` sharpens the cross-container test: with three or
|
||||||
|
|||||||
Reference in New Issue
Block a user