From f18d5919f7571084dcb10d3570cbefcc94e03f35 Mon Sep 17 00:00:00 2001 From: "Claude (auto-RE)" Date: Wed, 12 Aug 2026 00:31:20 +0000 Subject: [PATCH] fix(mesh): anchor XBG7 resources by neighbourhood -- inconsistency 125 -> 63, ships render right 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) --- crates/sylpheed-formats/src/mesh.rs | 144 +++++++++++++++--- .../tests/mesh_consistency_disc.rs | 2 +- docs/re/BACKLOG.md | 21 +++ .../captures/e106-static-assembly-fixed.png | Bin 0 -> 26520 bytes docs/re/structures/xbg7-mesh.md | 34 ++++- 5 files changed, 177 insertions(+), 24 deletions(-) create mode 100644 docs/re/captures/e106-static-assembly-fixed.png diff --git a/crates/sylpheed-formats/src/mesh.rs b/crates/sylpheed-formats/src/mesh.rs index 1eea288..5546d1e 100644 --- a/crates/sylpheed-formats/src/mesh.rs +++ b/crates/sylpheed-formats/src/mesh.rs @@ -487,6 +487,7 @@ impl Xbg7Model { decl: VertexDecl, } let mut resources: Vec = Vec::new(); + let mut asked_for: Vec = Vec::new(); for _ in 0..header.num_resources { let e = match Xpr2ResourceEntry::read(&mut cur) { Ok(e) => e, @@ -516,16 +517,18 @@ 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; - } - } + // Keep NON-wanted resources too: a resource is anchored by where its + // descriptor NEIGHBOURS anchor, so filtering them out here would + // leave a filtered decode with no neighbourhood (and the pre-2026-08 + // first-in-file-order behaviour). Only a ±2 window is actually + // decoded — see `need_pass1` below. + let asked = wanted.map_or(true, |w| w.contains(&name)); resources.push(Res { name, markers, decl, }); + asked_for.push(asked); } if resources.is_empty() { return out; @@ -556,20 +559,30 @@ impl Xbg7Model { // preserves resource order, so the output is identical to the sequential // decode. `should_cancel()` is polled per resource so a superseded load // stops promptly. - let decode_one = |r: &Res| -> Option { + // `near`: prefer candidate anchors close to this offset (see + // `anchor_pool_mesh_near`). Pass 1 runs with `None` to learn where each + // resource lands; pass 2 re-runs with each resource's neighbourhood. + let decode_one_near = |r: &Res, near: Option| -> (Option, Option) { if should_cancel() { - return None; + return (None, None); } let starts = &starts_by_stride[&r.decl.stride]; + let mut anchored_at = None; let meshes = if r.markers.len() == 1 { // Single sub-mesh → the proven per-block adjacency anchor // (index buffer immediately before its vertex buffer). Stages and // simple props take this path; `min_consistency` behaviour is // exactly as before. let (vtx_count, index_count) = r.markers[0]; - anchor_pool_mesh(bytes, starts, index_count, vtx_count, &r.decl, min_consistency) - .into_iter() - .collect() + anchor_pool_mesh_near( + bytes, starts, index_count, vtx_count, &r.decl, min_consistency, near, + ) + .map(|(m, vb)| { + anchored_at = Some(vb); + m + }) + .into_iter() + .collect() } else { // Several sub-meshes sharing grouped index/vertex pools → the // deterministic grouped-pool decode (hero ships et al.). @@ -582,25 +595,88 @@ impl Xbg7Model { // to the original single-block adjacency anchor on the first // marker so coverage is never *below* the pre-grouped decode. let (vtx_count, index_count) = r.markers[0]; - anchor_pool_mesh(bytes, starts, index_count, vtx_count, &r.decl, min_consistency) - .into_iter() - .collect() + anchor_pool_mesh_near( + bytes, starts, index_count, vtx_count, &r.decl, min_consistency, near, + ) + .map(|(m, vb)| { + anchored_at = Some(vb); + m + }) + .into_iter() + .collect() } }; - (!meshes.is_empty()).then(|| Xbg7Model { + let model = (!meshes.is_empty()).then(|| Xbg7Model { name: r.name.clone(), meshes, - }) + }); + (model, anchored_at) }; + /// Median of a resource's neighbours' anchors — the reference a resource + /// should sit near. `None` when too few neighbours anchored to be useful. + fn neighbourhood(vbs: &[Option], i: usize) -> Option { + const SPAN: usize = 2; + let lo = i.saturating_sub(SPAN); + let hi = (i + SPAN + 1).min(vbs.len()); + let mut near: Vec = (lo..hi).filter(|&k| k != i).filter_map(|k| vbs[k]).collect(); + if near.len() < 2 { + return None; + } + near.sort_unstable(); + Some(near[near.len() / 2]) + } + + // Pass 1 — first-match, to learn each resource's neighbourhood. Only the + // asked-for resources and their ±2 neighbours need it, so a filtered + // decode stays proportional to what was asked for. + let need_pass1: Vec = (0..resources.len()) + .map(|i| { + let lo = i.saturating_sub(2); + let hi = (i + 3).min(asked_for.len()); + asked_for[lo..hi].iter().any(|&a| a) + }) + .collect(); + let run1 = |(i, r): (usize, &Res)| -> (Option, Option) { + if need_pass1[i] { + decode_one_near(r, None) + } else { + (None, None) + } + }; + #[cfg(not(target_arch = "wasm32"))] + let pass1: Vec<(Option, Option)> = { + use rayon::prelude::*; + resources.par_iter().enumerate().map(run1).collect() + }; + #[cfg(target_arch = "wasm32")] + let pass1: Vec<(Option, Option)> = + resources.iter().enumerate().map(run1).collect(); + + let vbs: Vec> = pass1.iter().map(|(_, vb)| *vb).collect(); + + // Pass 2 — re-anchor preferring the resource's own neighbourhood, which + // is what separates its data from another resource's identically-shaped + // block. Resources without a usable neighbourhood keep pass 1's result. + let finish = |(i, r): (usize, &Res)| -> Option { + if !asked_for[i] { + return None; + } + match neighbourhood(&vbs, i) { + Some(anchor) => decode_one_near(r, Some(anchor)) + .0 + .or_else(|| pass1[i].0.clone()), + None => pass1[i].0.clone(), + } + }; #[cfg(not(target_arch = "wasm32"))] { use rayon::prelude::*; - out = resources.par_iter().filter_map(decode_one).collect(); + out = resources.par_iter().enumerate().filter_map(finish).collect(); } #[cfg(target_arch = "wasm32")] { - out = resources.iter().filter_map(decode_one).collect(); + out = resources.iter().enumerate().filter_map(finish).collect(); } out } @@ -653,8 +729,35 @@ fn anchor_pool_mesh( decl: &VertexDecl, min_consistency: f32, ) -> Option { + anchor_pool_mesh_near(bytes, starts, index_count, vtx_count, decl, min_consistency, None) + .map(|(m, _)| m) +} + +/// As [`anchor_pool_mesh`], but when `near` is given the candidates are tried in +/// order of distance from it, and the accepted vertex-buffer offset is returned +/// alongside the mesh. +/// +/// Why: the candidate list is one scan of the **whole container** per stride and +/// is shared by every resource of that stride, so first-in-file-order can hand a +/// resource a block belonging to something else that happens to share its vertex +/// and index counts. Both blocks are real geometry and both pass every quality +/// gate, so only *position* separates them — a resource's own data sits near its +/// descriptor neighbours' (`docs/re/structures/xbg7-mesh.md`). +fn anchor_pool_mesh_near( + bytes: &[u8], + starts: &[usize], + index_count: usize, + vtx_count: usize, + decl: &VertexDecl, + min_consistency: f32, + near: Option, +) -> Option<(GameMesh, usize)> { let idx_bytes = index_count * 2; - for &vb in starts { + let mut order: Vec = starts.to_vec(); + if let Some(anchor) = near { + order.sort_by_key(|&vb| vb.abs_diff(anchor)); + } + for &vb in &order { // The index buffer sits just before the vertex buffer, which is 4-byte // aligned — so 0..=3 bytes of padding may separate them (`ib = vb − // idx_bytes − pad`). pad 0 is the immediate-adjacency case (all stages so @@ -674,7 +777,10 @@ fn anchor_pool_mesh( }; if validate_block(bytes, ib, vb, vtx_count, index_count, decl, mc, true) { // ── Accepted: read the full mesh. ── - return Some(read_pool_mesh(bytes, ib, vb, index_count, vtx_count, decl)); + return Some(( + read_pool_mesh(bytes, ib, vb, index_count, vtx_count, decl), + vb, + )); } } } diff --git a/crates/sylpheed-formats/tests/mesh_consistency_disc.rs b/crates/sylpheed-formats/tests/mesh_consistency_disc.rs index a2a0738..69ff646 100644 --- a/crates/sylpheed-formats/tests/mesh_consistency_disc.rs +++ b/crates/sylpheed-formats/tests/mesh_consistency_disc.rs @@ -53,7 +53,7 @@ fn span(m: &Xbg7Model) -> Option<[i64; 3]> { } #[test] -#[ignore = "known-failing: 125 of 681 shared resources decode inconsistently (2026-08-11)"] +#[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)"); diff --git a/docs/re/BACKLOG.md b/docs/re/BACKLOG.md index 6f9a34e..07ea891 100644 --- a/docs/re/BACKLOG.md +++ b/docs/re/BACKLOG.md @@ -153,6 +153,11 @@ report needs re-grounding against a specific ship and a specific expectation. --- +## ✅ FIXED 2026-08-12 — it was a mis-decode, and the anchor now uses locality + +> Resolution at the end of this entry. Kept in full because the two wrong turns +> along the way (a "stray volume", then "monotonic anchoring") are the useful part. + ## ⚠️ The format layer is NOT exonerated — but the cause is a MIS-DECODE, not a stray volume **Found 2026-08-11 by finally doing the visual**, which the notes above kept @@ -255,3 +260,19 @@ worth fixing regardless — it is the same one-way-test shape as the earlier Also unchanged: only **two** cross-id placements exist fleet-wide (`e303_wep_01` on `e101` ×24 and `e106` ×36, across 335 assembled ships), so cross-id mounting is a narrow, real feature rather than a systemic guess. + +--- + +## Resolution (2026-08-12) + +`anchor_pool_mesh` took the **first** candidate in file order from a +container-global scan, so a resource could be handed another resource's block +whenever both shared `(stride, vertex count, index count)`. Fixed by anchoring +each resource near its **descriptor neighbours** (two-pass: learn, then re-anchor). + +- decoded **5 480 / 6 294 unchanged**, inconsistent **125 → 63** +- `e106` renders correctly ([after](captures/e106-static-assembly-fixed.png)) +- the user-reported "capital ships assemble wrong" is **resolved** for this cause + +Still open from this entry: `static_assembly_matches_runtime_capture` walks only +the capture's parts, so **extra** static placements still cannot fail it. diff --git a/docs/re/captures/e106-static-assembly-fixed.png b/docs/re/captures/e106-static-assembly-fixed.png new file mode 100644 index 0000000000000000000000000000000000000000..6b24875e4b6c417567cbc16617b0f9b2d05bbf3b GIT binary patch literal 26520 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 literal 0 HcmV?d00001 diff --git a/docs/re/structures/xbg7-mesh.md b/docs/re/structures/xbg7-mesh.md index de74547..732f397 100644 --- a/docs/re/structures/xbg7-mesh.md +++ b/docs/re/structures/xbg7-mesh.md @@ -393,11 +393,37 @@ and prefer candidates close to the previous resource's anchor), falling back to first-match when there is no neighbour yet. That needs no new format knowledge, and it selects `52 257 440` here. -**Not implemented.** It changes the anchor for every one of the 6 294 resources, -so it needs the before/after measurement — decoded count must not fall from -5 480, and the inconsistency count should fall from 125 — plus the ignored test +### ✅ Implemented (2026-08-12) — inconsistency halved, coverage unchanged + +`anchor_pool_mesh_near` 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, then pass 2 re-anchors each resource preferring its +**neighbourhood** — the median anchor of its ±2 descriptor neighbours. A resource +with too few anchored neighbours keeps pass 1's result, so nothing regresses to +guesswork. + +| | decoded | shared | inconsistent | +|---|---|---|---| +| before | 5 480 / 6 294 | 681 | **125** | +| after | 5 480 / 6 294 | 681 | **63** | + +**Coverage is unchanged and inconsistency halves.** `e303_wep_01` now decodes to +49 × 23 × 42 in *all* containers, and `e106` renders as a destroyer instead of a +slab ([before](../captures/e106-static-assembly-volume-bug.png) · +[after](../captures/e106-static-assembly-fixed.png)) — its two shared turrets sit +symmetrically at X[−203,−154] and X[154,203]. + +**The filtered path needed care.** `models_named` (what the viewer's ship +rendering uses) drops non-wanted resources, which would leave a filtered decode +with no neighbourhood at all — and silently keep 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 for. + +**63 remain.** The ignored test [`mesh_consistency_disc.rs`](../../crates/sylpheed-formats/tests/mesh_consistency_disc.rs) -un-ignored once it passes. +still asserts the target state and now records 63 rather than 125; the remaining +cases are where the neighbourhood is itself wrong or absent. ### Where the mis-decode is *not*: the grouped-pool anchor