diff --git a/crates/sylpheed-formats/examples/submesh_dump.rs b/crates/sylpheed-formats/examples/submesh_dump.rs index cb8497a..a904e1f 100644 --- a/crates/sylpheed-formats/examples/submesh_dump.rs +++ b/crates/sylpheed-formats/examples/submesh_dump.rs @@ -11,13 +11,23 @@ fn main() { println!("{} — {} sub-meshes decoded, {} markers declared", m.name, m.meshes.len(), markers.len()); for (i, s) in m.meshes.iter().enumerate() { let max_idx = s.indices.iter().max().copied().unwrap_or(0) as usize; + let (mut lo, mut hi) = ([f32::MAX; 3], [f32::MIN; 3]); + for q in &s.positions { + for k in 0..3 { + lo[k] = lo[k].min(q[k]); + hi[k] = hi[k].max(q[k]); + } + } println!( - " #{i:<2} at 0x{:<9x} verts {:<6} idx {:<6} max_idx {:<6} slack {}", + " #{i:<2} at 0x{:<9x} verts {:<6} idx {:<6} max_idx {:<6} slack {} span [{:.0} {:.0} {:.0}]", s.vbuf_offset.unwrap_or(0), s.positions.len(), s.indices.len(), max_idx, - s.positions.len() as i64 - 1 - max_idx as i64 + s.positions.len() as i64 - 1 - max_idx as i64, + hi[0] - lo[0], + hi[1] - lo[1], + hi[2] - lo[2] ); } } diff --git a/crates/sylpheed-formats/src/mesh.rs b/crates/sylpheed-formats/src/mesh.rs index 7fcf46f..876ce50 100644 --- a/crates/sylpheed-formats/src/mesh.rs +++ b/crates/sylpheed-formats/src/mesh.rs @@ -976,6 +976,21 @@ fn grouped_consistency() -> f32 { std::env::var("XBG7_GROUPED_CONSISTENCY").ok().and_then(|v| v.parse().ok()).unwrap_or(0.85) } +/// Coverage requirement, as `max_index + N >= vtx_count`. **`1` since +/// 2026-08-12** — i.e. the indices must reach the pool's last vertex exactly. +/// +/// The old `4` tolerated three unreferenced tail vertices, and that slack was a +/// mis-anchor tell rather than a real variation: 8 580 of 8 629 decoded +/// sub-meshes cover their pool exactly, and `e106_bdy_03` in `Stage_S02` was one +/// of the few that did not — slack 3, decoding to a 600×1600×998 slab visible in +/// a render, where three other containers give 276×236×941. Requiring exact +/// coverage moves it onto the block those containers agree on. Costs 3 resources +/// disc-wide; capture oracle unchanged at 46/46. `XBG7_COVER_SLACK` overrides +/// (note `0` rejects everything — the comparison is `max_idx + N >= vtx_count`). +fn cover_slack() -> usize { + std::env::var("XBG7_COVER_SLACK").ok().and_then(|v| v.parse().ok()).unwrap_or(1) +} + /// Smallest bounding-box extent a block may have (default `0.5`). An absolute /// floor on a format with no unit convention is a scale assumption, so it is a /// knob: `XBG7_MIN_EXTENT`. @@ -1262,7 +1277,7 @@ fn validate_block_report( } max_idx = max_idx.max(i); } - if (max_idx as usize) + 4 < vtx_count { + if (max_idx as usize) + cover_slack() < vtx_count { return Err(format!( "indices reach only {max_idx} of {vtx_count} vertices (buffer not covered)" )); @@ -1525,7 +1540,7 @@ fn anchor_grouped_meshes( max_idx = max_idx.max(i); i < vc }); - if in_range && max_idx + 4 >= vc { + if in_range && max_idx + cover_slack() >= vc { meshes.push(read_pool_mesh(bytes, ib, vb, ic, vc, decl)); } vb += vc * stride; diff --git a/docs/re/captures/e106-cover-slack-before-after.png b/docs/re/captures/e106-cover-slack-before-after.png new file mode 100644 index 0000000..550cf4e Binary files /dev/null and b/docs/re/captures/e106-cover-slack-before-after.png differ diff --git a/docs/re/structures/xbg7-mesh.md b/docs/re/structures/xbg7-mesh.md index 1226160..f513d7c 100644 --- a/docs/re/structures/xbg7-mesh.md +++ b/docs/re/structures/xbg7-mesh.md @@ -1042,6 +1042,35 @@ not obviously right, and both points are one env var apart (`XBG7_PAD0_CONSISTENCY`, `XBG7_EDGE_CAP`) for anyone who wants the conservative end. +### ✅ Fixed by *looking* at the output: exact pool coverage (2026-08-12) + +Rendering the assembled `e106` from `Stage_S02` — something no metric had done — +showed the old slab back: `e106_bdy_03` spanning **600×1600×998**, a blocky mass +beside the hull. The same resource decodes to **276×236×941** in `Stage_S01`, +`Stage_S03` and `Stage_S04`. The tell was already in the data: + +| container | anchor | slack | span | +|---|---|---|---| +| `Stage_S01` | `0x3c9b7c` | 0 | 276×236×941 | +| **`Stage_S02`** | `0x12d1d44` | **3** | **600×1600×998** | +| `Stage_S03` | `0x1e07b7c` | 0 | 276×236×941 | +| `Stage_S04` | `0x164337c` | 0 | 276×236×941 | +| `Stage_S06` | `0x1f05298` | 0 | 414×636×1121 | + +The coverage gate tolerated up to **three** unreferenced tail vertices, and that +tolerance was hiding a mis-anchor: real blocks reach their pool's last vertex +exactly (8 580 of 8 629). Requiring exact coverage moves `Stage_S02`'s +`e106_bdy_03` to `0x2d35b7c`, slack 0, **276×236×941** — the block three other +containers agree on — and the slab disappears from the render +([`captures/e106-cover-slack-before-after.png`](../captures/e106-cover-slack-before-after.png)). + +Cost: **3** resources disc-wide (6 212 → 6 209); cross-container inconsistency +39 → 38; capture oracle unchanged at 46/46 claimed, 0 unclaimed; suite green. + +(`Stage_S06` still gives a third answer at slack 0, so `e106_bdy_03` is not fully +settled — but it is now consistent across four of the six containers that carry +it instead of three.) + ### The anchor work has plateaued at 98.7 % — state and what is left Four evidence-driven changes took the decoder from 5 480 to **6 212 of 6 294**