From c79c0f6ccfba615a5f61e11c7d9cb8705aeb2587 Mon Sep 17 00:00:00 2001 From: "Claude (auto-RE)" Date: Wed, 12 Aug 2026 18:47:41 +0000 Subject: [PATCH] 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) --- .../examples/bounds_in_descriptor.rs | 53 +++++++++++++++++++ crates/sylpheed-formats/src/mesh.rs | 15 ++++-- docs/re/structures/xbg7-mesh.md | 22 ++++++++ 3 files changed, 85 insertions(+), 5 deletions(-) create mode 100644 crates/sylpheed-formats/examples/bounds_in_descriptor.rs diff --git a/crates/sylpheed-formats/examples/bounds_in_descriptor.rs b/crates/sylpheed-formats/examples/bounds_in_descriptor.rs new file mode 100644 index 0000000..c3c0bf7 --- /dev/null +++ b/crates/sylpheed-formats/examples/bounds_in_descriptor.rs @@ -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 ... +use sylpheed_formats::mesh::{xbg7_descriptor_range, Xbg7Model}; +use std::collections::HashSet; + +fn main() { + let a: Vec = std::env::args().collect(); + let bytes = std::fs::read(&a[1]).expect("container"); + let want: HashSet = 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 = 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)]); + } + } +} diff --git a/crates/sylpheed-formats/src/mesh.rs b/crates/sylpheed-formats/src/mesh.rs index 355e0bf..64ad55e 100644 --- a/crates/sylpheed-formats/src/mesh.rs +++ b/crates/sylpheed-formats/src/mesh.rs @@ -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 } diff --git a/docs/re/structures/xbg7-mesh.md b/docs/re/structures/xbg7-mesh.md index eb0c9d3..855133e 100644 --- a/docs/re/structures/xbg7-mesh.md +++ b/docs/re/structures/xbg7-mesh.md @@ -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 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 `examples/consensus_check.rs` sharpens the cross-container test: with three or