re: n206 was fixed after all -- the audit was classifying by geometry, not buffer
debug_grouped_report (why_rejected) shows n206_02's alternative pool is ACCEPTED at pad 0 under production gates and is in the candidate list -- and the decoder does take it: n206_02 now anchors at 0x342d984. The 'still collapsed' reading came from the audit comparing decoded geometry, and that offset holds a direct (unmirrored) copy, so a separated pair still looked identical. The audit now requires a SHARED BUFFER to call it a collapse: disc-wide 18 exact mirrors, 16 related, 0 collapses, 0 unrelated -- and the regression test drops its exception. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -755,6 +755,60 @@ pub fn debug_try_anchor(
|
||||
None
|
||||
}
|
||||
|
||||
/// Diagnostic: why does the decoder refuse a grouped-pool resource at a given
|
||||
/// pool start? Recomputes the pool layout exactly as [`anchor_grouped_meshes`]
|
||||
/// does and reports the pivot sub-mesh's verdict for each index/vertex pad —
|
||||
/// so a capture-proven pool that the decoder rejects names the gate to fix.
|
||||
pub fn debug_grouped_report(bytes: &[u8], name: &str, vb0: usize) -> Vec<String> {
|
||||
let Some((decl, markers)) = decl_of(bytes, name) else {
|
||||
return vec!["no such XBG7 resource".into()];
|
||||
};
|
||||
let n = markers.len();
|
||||
if n == 0 {
|
||||
return vec!["no index markers".into()];
|
||||
}
|
||||
let (mut rel_ib, mut acc_i) = (Vec::with_capacity(n), 0usize);
|
||||
for &(_, ic) in &markers {
|
||||
rel_ib.push(acc_i);
|
||||
acc_i = align4(acc_i + ic * 2);
|
||||
}
|
||||
let span = rel_ib[n - 1] + markers[n - 1].1 * 2;
|
||||
let kmax = (0..n).max_by_key(|&i| markers[i].1).unwrap_or(0);
|
||||
let (vck, ick) = markers[kmax];
|
||||
let mut off_v = 0usize;
|
||||
for &(vc, _) in markers.iter().take(kmax) {
|
||||
off_v += vc * decl.stride;
|
||||
}
|
||||
let mut out = vec![format!(
|
||||
"{name}: {n} sub-meshes, pivot #{kmax} ({vck} verts, {ick} idx), pool span {span}"
|
||||
)];
|
||||
for pad in 0..=3usize {
|
||||
if vb0 < span + pad {
|
||||
out.push(format!(" pad {pad}: pool start is before the index pool"));
|
||||
continue;
|
||||
}
|
||||
let ib0 = vb0 - span - pad;
|
||||
// Same gates the production pivot test uses: strict winding (0.85)
|
||||
// AND connectivity. Reporting at 0.0 would accept blocks the decoder
|
||||
// rejects and send the reader chasing the wrong gate.
|
||||
let verdict = validate_block_report(
|
||||
bytes,
|
||||
ib0 + rel_ib[kmax],
|
||||
vb0 + off_v,
|
||||
vck,
|
||||
ick,
|
||||
&decl,
|
||||
0.85,
|
||||
true,
|
||||
);
|
||||
out.push(match verdict {
|
||||
Ok(()) => format!(" pad {pad}: ACCEPTED"),
|
||||
Err(why) => format!(" pad {pad}: {why}"),
|
||||
});
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Diagnostic: for a resource whose vertex buffer is *known* (a runtime capture
|
||||
/// names it), where could its index buffer be? The anchor scan assumes the index
|
||||
/// buffer sits immediately before the vertex buffer; this scans the whole
|
||||
@@ -998,17 +1052,43 @@ fn validate_block(
|
||||
min_consistency: f32,
|
||||
strict_connectivity: bool,
|
||||
) -> bool {
|
||||
validate_block_report(
|
||||
bytes,
|
||||
ib,
|
||||
vb,
|
||||
vtx_count,
|
||||
index_count,
|
||||
decl,
|
||||
min_consistency,
|
||||
strict_connectivity,
|
||||
)
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
/// [`validate_block`], but naming the gate that rejected a block. A runtime
|
||||
/// capture can prove a block is real; when the decoder still refuses it, this
|
||||
/// says which test is wrong rather than leaving a threshold to be guessed at.
|
||||
fn validate_block_report(
|
||||
bytes: &[u8],
|
||||
ib: usize,
|
||||
vb: usize,
|
||||
vtx_count: usize,
|
||||
index_count: usize,
|
||||
decl: &VertexDecl,
|
||||
min_consistency: f32,
|
||||
strict_connectivity: bool,
|
||||
) -> Result<(), String> {
|
||||
let stride = decl.stride;
|
||||
let idx_bytes = index_count * 2;
|
||||
if ib + idx_bytes > bytes.len() {
|
||||
return false;
|
||||
return Err("index buffer runs past the container".into());
|
||||
}
|
||||
let vtx_bytes = match vtx_count.checked_mul(stride) {
|
||||
Some(v) => v,
|
||||
None => return false,
|
||||
None => return Err("vertex byte count overflows".into()),
|
||||
};
|
||||
if vb + vtx_bytes > bytes.len() {
|
||||
return false;
|
||||
return Err("vertex buffer runs past the container".into());
|
||||
}
|
||||
|
||||
// ── Full index validation: every index in range, uses ~all vertices. ──
|
||||
@@ -1016,12 +1096,14 @@ fn validate_block(
|
||||
for k in 0..index_count {
|
||||
let i = be16(bytes, ib + k * 2) as u32;
|
||||
if i >= vtx_count as u32 {
|
||||
return false;
|
||||
return Err(format!("index {i} out of range (vtx_count {vtx_count})"));
|
||||
}
|
||||
max_idx = max_idx.max(i);
|
||||
}
|
||||
if (max_idx as usize) + 4 < vtx_count {
|
||||
return false;
|
||||
return Err(format!(
|
||||
"indices reach only {max_idx} of {vtx_count} vertices (buffer not covered)"
|
||||
));
|
||||
}
|
||||
|
||||
// ── Triangle quality: finite, non-degenerate, real spatial extent. ──
|
||||
@@ -1046,7 +1128,7 @@ fn validate_block(
|
||||
for (a, slot) in pc.iter_mut().enumerate() {
|
||||
let x = bef(bytes, base + a * 4);
|
||||
if !x.is_finite() || x.abs() > 1.0e6 {
|
||||
return false;
|
||||
return Err(format!("position component {x} is not finite/plausible"));
|
||||
}
|
||||
*slot = x;
|
||||
lo[a] = lo[a].min(x);
|
||||
@@ -1088,7 +1170,9 @@ fn validate_block(
|
||||
}
|
||||
let extent = (hi[0] - lo[0]).max(hi[1] - lo[1]).max(hi[2] - lo[2]);
|
||||
if extent < 0.5 || sampled == 0 || degenerate * 10 > sampled * 3 {
|
||||
return false; // too flat, or >30% degenerate → not this block
|
||||
return Err(format!(
|
||||
"extent {extent:.3} (min 0.5), {degenerate}/{sampled} degenerate (max 30%)"
|
||||
));
|
||||
}
|
||||
// Connectivity check: a correctly-anchored mesh has triangle edges that
|
||||
// are SMALL relative to its overall size (~0.05–0.15 of the bbox
|
||||
@@ -1111,7 +1195,10 @@ fn validate_block(
|
||||
// `XBG7_EDGE_CAP_SMALL` instead — a targeted relaxation, off by default.
|
||||
let cap = if tris < small_tris() { small_cap() } else { edge_cap() };
|
||||
if mean_edge / diag > cap {
|
||||
return false;
|
||||
return Err(format!(
|
||||
"connectivity: mean_edge/diag {:.3} > cap {cap:.2}",
|
||||
mean_edge / diag
|
||||
));
|
||||
}
|
||||
}
|
||||
// Winding-consistency gate (same as `from_xpr2`): a correctly-anchored
|
||||
@@ -1123,10 +1210,13 @@ fn validate_block(
|
||||
if nrm_counted > 0 && min_consistency > 0.0 {
|
||||
let na = nrm_agree as f32 / nrm_counted as f32;
|
||||
if na.max(1.0 - na) < min_consistency {
|
||||
return false;
|
||||
return Err(format!(
|
||||
"winding consistency {:.3} < {min_consistency:.2}",
|
||||
na.max(1.0 - na)
|
||||
));
|
||||
}
|
||||
}
|
||||
true
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Decode a **grouped-pool** XBG7 resource: one whose descriptor holds *several*
|
||||
|
||||
Reference in New Issue
Block a user