fix(mesh): distinct assignment now covers grouped pools too
anchor_grouped_meshes takes the same taken set; a colliding grouped model is re-placed whole past everything claimed, or keeps what it had. Runtime oracle unchanged (45/46 exact, 0 unclaimed), coverage unchanged (6069), cross-container inconsistency 62 -> 56, suite green. It does not clear the n206 twin collapse: no alternative pool validates for the loser, so that pair is a validator case (like eng_02_l before the cap move), not a selection one. Also fixes a stale '0.28 (default)' label in edge_cap_sweep. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -75,6 +75,6 @@ fn main() {
|
|||||||
}
|
}
|
||||||
println!(
|
println!(
|
||||||
"cap={} models={models} verts={verts} shared={shared} inconsistent={inconsistent}",
|
"cap={} models={models} verts={verts} shared={shared} inconsistent={inconsistent}",
|
||||||
std::env::var("XBG7_EDGE_CAP").unwrap_or_else(|_| "0.28 (default)".into())
|
std::env::var("XBG7_EDGE_CAP").unwrap_or_else(|_| "0.42 (default)".into())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -589,7 +589,8 @@ impl Xbg7Model {
|
|||||||
} else {
|
} else {
|
||||||
// Several sub-meshes sharing grouped index/vertex pools → the
|
// Several sub-meshes sharing grouped index/vertex pools → the
|
||||||
// deterministic grouped-pool decode (hero ships et al.).
|
// deterministic grouped-pool decode (hero ships et al.).
|
||||||
let grouped = anchor_grouped_meshes(bytes, data_base, starts, &r.markers, &r.decl);
|
let grouped =
|
||||||
|
anchor_grouped_meshes(bytes, data_base, starts, &r.markers, &r.decl, &empty_taken);
|
||||||
if !grouped.is_empty() {
|
if !grouped.is_empty() {
|
||||||
grouped
|
grouped
|
||||||
} else {
|
} else {
|
||||||
@@ -652,10 +653,10 @@ impl Xbg7Model {
|
|||||||
let mut models: Vec<Xbg7Model> = Vec::with_capacity(decoded.len());
|
let mut models: Vec<Xbg7Model> = Vec::with_capacity(decoded.len());
|
||||||
for (i, mut m) in decoded {
|
for (i, mut m) in decoded {
|
||||||
let r = &resources[i];
|
let r = &resources[i];
|
||||||
if m.meshes.len() == 1 && r.markers.len() == 1 {
|
let starts = &starts_by_stride[&r.decl.stride];
|
||||||
if let Some(vb) = m.meshes[0].vbuf_offset {
|
if let Some(vb) = m.meshes[0].vbuf_offset {
|
||||||
if taken.contains(&vb) {
|
if taken.contains(&vb) {
|
||||||
let starts = &starts_by_stride[&r.decl.stride];
|
if m.meshes.len() == 1 && r.markers.len() == 1 {
|
||||||
let (vtx_count, index_count) = r.markers[0];
|
let (vtx_count, index_count) = r.markers[0];
|
||||||
if let Some(alt) = anchor_pool_mesh(
|
if let Some(alt) = anchor_pool_mesh(
|
||||||
bytes,
|
bytes,
|
||||||
@@ -668,10 +669,21 @@ impl Xbg7Model {
|
|||||||
) {
|
) {
|
||||||
m.meshes[0] = alt;
|
m.meshes[0] = alt;
|
||||||
}
|
}
|
||||||
// No free candidate → keep the collided decode rather
|
} else {
|
||||||
// than drop the resource; coverage never regresses.
|
// Grouped pool: re-place the WHOLE pool past everything
|
||||||
|
// claimed, or keep what we had.
|
||||||
|
let alt = anchor_grouped_meshes(
|
||||||
|
bytes, data_base, starts, &r.markers, &r.decl, &taken,
|
||||||
|
);
|
||||||
|
if !alt.is_empty() {
|
||||||
|
m.meshes = alt;
|
||||||
}
|
}
|
||||||
if let Some(vb2) = m.meshes[0].vbuf_offset {
|
}
|
||||||
|
// No free candidate → keep the collided decode rather than
|
||||||
|
// drop the resource; coverage never regresses.
|
||||||
|
}
|
||||||
|
for sub in &m.meshes {
|
||||||
|
if let Some(vb2) = sub.vbuf_offset {
|
||||||
taken.insert(vb2);
|
taken.insert(vb2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1147,6 +1159,7 @@ fn anchor_grouped_meshes(
|
|||||||
starts: &[usize],
|
starts: &[usize],
|
||||||
markers: &[(usize, usize)], // (vtx_count, idx_count) in descriptor/file order
|
markers: &[(usize, usize)], // (vtx_count, idx_count) in descriptor/file order
|
||||||
decl: &VertexDecl,
|
decl: &VertexDecl,
|
||||||
|
taken: &std::collections::HashSet<usize>,
|
||||||
) -> Vec<GameMesh> {
|
) -> Vec<GameMesh> {
|
||||||
let n = markers.len();
|
let n = markers.len();
|
||||||
if n == 0 {
|
if n == 0 {
|
||||||
@@ -1182,6 +1195,11 @@ fn anchor_grouped_meshes(
|
|||||||
let (vck, ick) = markers[kmax];
|
let (vck, ick) = markers[kmax];
|
||||||
|
|
||||||
for &vb0 in starts {
|
for &vb0 in starts {
|
||||||
|
// Distinct assignment: a pool another resource already claimed is not a
|
||||||
|
// candidate (see the collision resolution in `anchor_models_filtered`).
|
||||||
|
if taken.contains(&vb0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
for pad in 0..=3usize {
|
for pad in 0..=3usize {
|
||||||
if vb0 < span + pad {
|
if vb0 < span + pad {
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -930,6 +930,29 @@ of one buffer, a resource landing on a **mirrored** copy would be a real defect
|
|||||||
and would look identical to a correct decode in every count-based metric. Only a
|
and would look identical to a correct decode in every count-based metric. Only a
|
||||||
capture (or the twin-pair invariant) can catch it.
|
capture (or the twin-pair invariant) can catch it.
|
||||||
|
|
||||||
|
### Distinct assignment extended to grouped pools (2026-08-12)
|
||||||
|
|
||||||
|
`anchor_grouped_meshes` now takes the same `taken` set: a pool whose start another
|
||||||
|
resource already claimed is skipped, and a grouped model that collides is
|
||||||
|
**re-placed whole** past everything claimed (or keeps what it had, so coverage
|
||||||
|
cannot regress).
|
||||||
|
|
||||||
|
| | oracle exact / 46 | unclaimed | resources decoded | shared inconsistent |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| single-mesh distinctness only | 45 | 0 | 6 069 | 62 |
|
||||||
|
| **+ grouped pools (now)** | 45 | 0 | 6 069 | **56** |
|
||||||
|
|
||||||
|
The runtime oracle is unchanged and cross-container inconsistency falls another
|
||||||
|
10 %. The suite, including the disc- and ISO-gated tests, stays green.
|
||||||
|
|
||||||
|
**It does *not* clear the `n206` collapse**, and that is informative: both twins
|
||||||
|
are grouped, the loser is re-placed past the taken pool, and no alternative pool
|
||||||
|
**validates** — so it keeps the collided decode. `n206_02` is therefore the same
|
||||||
|
class as `e106_eng_02_l` was before the cap moved: the correct block is rejected
|
||||||
|
by the validator, not lost to selection. Its correct pool is one of
|
||||||
|
`0x342d984` (direct) or `0x33b7754` / `0x342e284` (mirrored); a capture of a
|
||||||
|
stage containing `n206` would say which, and is the cheapest way to settle it.
|
||||||
|
|
||||||
### The twin invariant, checked disc-wide (2026-08-12)
|
### The twin invariant, checked disc-wide (2026-08-12)
|
||||||
|
|
||||||
The capture gave a rule that needs no capture to apply: a `…_01`/`…_02` pair of
|
The capture gave a rule that needs no capture to apply: a `…_01`/`…_02` pair of
|
||||||
|
|||||||
Reference in New Issue
Block a user