fix(mesh): refine the anchor map before using it -- inconsistency 63 -> 51

Pass 1's anchor map contains exactly the mistakes the neighbourhood is meant to
correct, so a resource sitting beside a mis-anchored neighbour inherits a bad
reference. Re-anchoring against the improving map and repeating converges
quickly: two rounds, and a third changes nothing (the loop exits early when a
round is a fixpoint).

  before                 decoded 5480/6294  inconsistent 125
  neighbourhood anchor   decoded 5480/6294  inconsistent  63
  + refining the map     decoded 5480/6294  inconsistent  51

Coverage still unchanged. The 51 that remain cluster in _l (LOD) and _dead
variants -- e001_l, e010_bdy_01_l, e106_eng_02_l, _rou_f302_base_dead,
e303_base_dead and friends. A plausible reading is that a variant shares its
base's vertex and index counts, making the two mutually confusable so that
locality cannot separate them; recorded as untested rather than asserted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-12 00:39:35 +00:00
parent f18d5919f7
commit 27a0701e0d
3 changed files with 33 additions and 4 deletions

View File

@@ -653,7 +653,25 @@ impl Xbg7Model {
let pass1: Vec<(Option<Xbg7Model>, Option<usize>)> = let pass1: Vec<(Option<Xbg7Model>, Option<usize>)> =
resources.iter().enumerate().map(run1).collect(); resources.iter().enumerate().map(run1).collect();
let vbs: Vec<Option<usize>> = pass1.iter().map(|(_, vb)| *vb).collect(); let mut vbs: Vec<Option<usize>> = pass1.iter().map(|(_, vb)| *vb).collect();
// Refine the anchor map before using it: pass 1's anchors include the
// very mistakes this is meant to correct, so a resource next to a
// mis-anchored neighbour inherits a bad reference. Re-anchoring against
// the improving map and repeating converges quickly; two rounds is
// enough on this disc (a third changes nothing).
for _ in 0..2 {
let refined: Vec<Option<usize>> = (0..resources.len())
.map(|i| match (need_pass1[i], neighbourhood(&vbs, i)) {
(true, Some(anchor)) => decode_one_near(&resources[i], Some(anchor)).1.or(vbs[i]),
_ => vbs[i],
})
.collect();
if refined == vbs {
break;
}
vbs = refined;
}
// Pass 2 — re-anchor preferring the resource's own neighbourhood, which // Pass 2 — re-anchor preferring the resource's own neighbourhood, which
// is what separates its data from another resource's identically-shaped // is what separates its data from another resource's identically-shaped

View File

@@ -53,7 +53,7 @@ fn span(m: &Xbg7Model) -> Option<[i64; 3]> {
} }
#[test] #[test]
#[ignore = "known-failing: 63 of 681 shared resources still decode inconsistently (was 125 before the neighbourhood anchor, 2026-08-12)"] #[ignore = "known-failing: 51 of 681 shared resources still decode inconsistently (125 before the neighbourhood anchor, 63 before refining it — 2026-08-12)"]
fn shared_resources_decode_identically_in_every_container() { fn shared_resources_decode_identically_in_every_container() {
let Some(root) = disc_root() else { let Some(root) = disc_root() else {
eprintln!("SKIP: extracted disc not found (set SYLPHEED_DISC to enable)"); eprintln!("SKIP: extracted disc not found (set SYLPHEED_DISC to enable)");

View File

@@ -405,7 +405,13 @@ guesswork.
| | decoded | shared | inconsistent | | | decoded | shared | inconsistent |
|---|---|---|---| |---|---|---|---|
| before | 5 480 / 6 294 | 681 | **125** | | before | 5 480 / 6 294 | 681 | **125** |
| after | 5 480 / 6 294 | 681 | **63** | | neighbourhood anchor | 5 480 / 6 294 | 681 | **63** |
| + refining the map | 5 480 / 6 294 | 681 | **51** |
**Refining matters** because pass 1's anchor map contains the very mistakes the
neighbourhood is meant to correct, so a resource beside a mis-anchored neighbour
inherits a bad reference. Re-anchoring against the improving map and repeating
converges quickly — two rounds, with a third changing nothing.
**Coverage is unchanged and inconsistency halves.** `e303_wep_01` now decodes to **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 49 × 23 × 42 in *all* containers, and `e106` renders as a destroyer instead of a
@@ -420,7 +426,12 @@ now collected regardless of the filter, but only the asked-for ones and their ±
neighbours are decoded in pass 1, so a filtered decode stays proportional to what neighbours are decoded in pass 1, so a filtered decode stays proportional to what
was asked for. was asked for.
**63 remain.** The ignored test **51 remain**, and they cluster: `_l` (LOD) and `_dead` variants —
`e001_l`, `e010_bdy_01_l`, `e011_bdy_01_l`, `e016_l`, `e104_bdy_05_l`,
`e106_eng_02_l`, `e501_01_l`, `_rou_f301_base_dead`, `_rou_f302_base_dead`,
`e303_base_dead`. 🟡 A plausible reading is that a variant shares its base's
vertex and index counts, so the two are mutually confusable and the neighbourhood
cannot separate them — untested. The ignored test
[`mesh_consistency_disc.rs`](../../crates/sylpheed-formats/tests/mesh_consistency_disc.rs) [`mesh_consistency_disc.rs`](../../crates/sylpheed-formats/tests/mesh_consistency_disc.rs)
still asserts the target state and now records 63 rather than 125; the remaining still asserts the target state and now records 63 rather than 125; the remaining
cases are where the neighbourhood is itself wrong or absent. cases are where the neighbourhood is itself wrong or absent.