test: lock in the twin invariant -- 0 unrelated pairs disc-wide, 1 known collapse
twin_mirror_audit applies the capture-derived rule to all 166 containers: of 34 equal-count twin pairs, 18 are exact X-mirrors, 15 related another way, 1 identical, 0 unrelated. Two calibration fixes were needed first (authored halves need a tolerance, and a mirrored pair may be stored in another vertex order). The one collapse, n206_01/_02, is a grouped-pool pair -- the path distinct assignment excludes -- so it names the next target. Added a disc-gated regression test; refreshed the stale ignore message on the consistency test. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
44
crates/sylpheed-formats/examples/find_mirror.rs
Normal file
44
crates/sylpheed-formats/examples/find_mirror.rs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
//! Does the container hold an X-mirrored copy of a resource's decoded buffer?
|
||||||
|
//!
|
||||||
|
//! The twin invariant (see `twin_mirror_audit`) flags `…_01`/`…_02` pairs that
|
||||||
|
//! decode to unrelated geometry. If the mirror of one twin's buffer exists
|
||||||
|
//! somewhere else in the container, that offset is where the other twin belongs
|
||||||
|
//! and the pair is a mis-anchor; if it does not exist, the pair is simply not a
|
||||||
|
//! mirrored pair.
|
||||||
|
//!
|
||||||
|
//! Usage: find_mirror <container.xpr> <resource>...
|
||||||
|
use sylpheed_formats::mesh::Xbg7Model;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let a: Vec<String> = std::env::args().collect();
|
||||||
|
let bytes = std::fs::read(&a[1]).expect("container");
|
||||||
|
let want: HashSet<String> = a[2..].iter().cloned().collect();
|
||||||
|
let models = Xbg7Model::models_named(&bytes, &want, &|| false);
|
||||||
|
let be = |at: usize| f32::from_be_bytes(bytes[at..at + 4].try_into().unwrap());
|
||||||
|
|
||||||
|
for m in &models {
|
||||||
|
let pos: Vec<[f32; 3]> = m.meshes.iter().flat_map(|s| s.positions.clone()).take(8).collect();
|
||||||
|
if pos.len() < 8 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let anchored = m.meshes[0].vbuf_offset.unwrap_or(0);
|
||||||
|
let (mut direct, mut mirror) = (Vec::new(), Vec::new());
|
||||||
|
for o in (0..bytes.len().saturating_sub(12 + 8 * 24)).step_by(4) {
|
||||||
|
for (flip, out) in [(1.0f32, &mut direct), (-1.0f32, &mut mirror)] {
|
||||||
|
if (0..8).all(|k| {
|
||||||
|
let at = o + k * 24;
|
||||||
|
(be(at) - flip * pos[k][0]).abs() <= 1e-4
|
||||||
|
&& (be(at + 4) - pos[k][1]).abs() <= 1e-4
|
||||||
|
&& (be(at + 8) - pos[k][2]).abs() <= 1e-4
|
||||||
|
}) {
|
||||||
|
out.push(o);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!(
|
||||||
|
"{:<20} anchored 0x{anchored:x} direct copies {:x?} mirrored copies {:x?}",
|
||||||
|
m.name, direct, mirror
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
110
crates/sylpheed-formats/examples/twin_mirror_audit.rs
Normal file
110
crates/sylpheed-formats/examples/twin_mirror_audit.rs
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
//! Do port/starboard twins decode to mirror images of each other?
|
||||||
|
//!
|
||||||
|
//! A runtime capture proved the container stores both halves of the `e106` hull
|
||||||
|
//! as separate, X-reflected buffers. That gives a **capture-free invariant**:
|
||||||
|
//! a `…_01`/`…_02` pair of equal vertex count should decode to geometry that is
|
||||||
|
//! an exact X-mirror — never to identical geometry (that is the collapse the
|
||||||
|
//! distinct-assignment fix targets), and never to something unrelated (that is a
|
||||||
|
//! mis-anchor no count-based metric can see).
|
||||||
|
//!
|
||||||
|
//! Usage: twin_mirror_audit <resource3d_dir>
|
||||||
|
use sylpheed_formats::mesh::Xbg7Model;
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
fn key(p: [f32; 3]) -> (i64, i64, i64) {
|
||||||
|
(
|
||||||
|
(p[0] * 1e3).round() as i64,
|
||||||
|
(p[1] * 1e3).round() as i64,
|
||||||
|
(p[2] * 1e3).round() as i64,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Elementwise equality with a tolerance. Truncating keys is too strict for a
|
||||||
|
/// mirrored pair: the halves are authored, not bit-negated, so they differ in
|
||||||
|
/// the last digits and an exact key test reports them as unrelated.
|
||||||
|
fn near(p: [f32; 3], q: [f32; 3]) -> bool {
|
||||||
|
(0..3).all(|c| (p[c] - q[c]).abs() <= 1e-3 * (1.0 + q[c].abs()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let dir = std::env::args().nth(1).expect("resource3d dir");
|
||||||
|
let mut files: Vec<_> = std::fs::read_dir(&dir)
|
||||||
|
.unwrap()
|
||||||
|
.flatten()
|
||||||
|
.map(|e| e.path())
|
||||||
|
.filter(|p| p.extension().and_then(|s| s.to_str()) == Some("xpr"))
|
||||||
|
.collect();
|
||||||
|
files.sort();
|
||||||
|
|
||||||
|
let (mut same, mut mirrored, mut unrelated, mut related) = (0usize, 0usize, 0usize, 0usize);
|
||||||
|
let mut examples: Vec<String> = Vec::new();
|
||||||
|
for f in &files {
|
||||||
|
let Ok(bytes) = std::fs::read(f) else { continue };
|
||||||
|
let models = Xbg7Model::anchor_models_cancellable(&bytes, 0.0, &|| false);
|
||||||
|
let by_name: BTreeMap<&str, &Xbg7Model> =
|
||||||
|
models.iter().map(|m| (m.name.as_str(), m)).collect();
|
||||||
|
for m in &models {
|
||||||
|
let Some(stem) = m.name.strip_suffix("_01") else { continue };
|
||||||
|
let Some(t) = by_name.get(format!("{stem}_02").as_str()) else { continue };
|
||||||
|
let a: Vec<[f32; 3]> = m.meshes.iter().flat_map(|s| s.positions.clone()).collect();
|
||||||
|
let b: Vec<[f32; 3]> = t.meshes.iter().flat_map(|s| s.positions.clone()).collect();
|
||||||
|
if a.len() != b.len() || a.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let ident = a.iter().zip(&b).all(|(p, q)| near(*p, *q));
|
||||||
|
let mirr = a.iter().zip(&b).all(|(p, q)| near([-p[0], p[1], p[2]], *q));
|
||||||
|
// A pair that is neither may still be RELATED: mirrored on another
|
||||||
|
// axis, or the same point cloud in a different vertex order. Only a
|
||||||
|
// pair that is none of these is evidence of a mis-anchor.
|
||||||
|
let mirr_y = a.iter().zip(&b).all(|(p, q)| near([p[0], -p[1], p[2]], *q));
|
||||||
|
let mirr_z = a.iter().zip(&b).all(|(p, q)| near([p[0], p[1], -p[2]], *q));
|
||||||
|
let set = |v: &Vec<[f32; 3]>| {
|
||||||
|
let mut s: Vec<_> = v.iter().map(|p| key(*p)).collect();
|
||||||
|
s.sort_unstable();
|
||||||
|
s
|
||||||
|
};
|
||||||
|
let same_cloud = set(&a) == set(&b);
|
||||||
|
// …and the same point cloud after mirroring, for a pair whose
|
||||||
|
// halves are authored in different vertex order.
|
||||||
|
let mirrored_cloud = {
|
||||||
|
let am: Vec<[f32; 3]> = a.iter().map(|p| [-p[0], p[1], p[2]]).collect();
|
||||||
|
set(&am) == set(&b)
|
||||||
|
};
|
||||||
|
if !ident && !mirr && (mirr_y || mirr_z || same_cloud || mirrored_cloud) {
|
||||||
|
related += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ident {
|
||||||
|
same += 1;
|
||||||
|
if examples.len() < 6 {
|
||||||
|
examples.push(format!(
|
||||||
|
"identical: {} / {}_02 in {}",
|
||||||
|
m.name,
|
||||||
|
stem,
|
||||||
|
f.file_name().unwrap().to_string_lossy()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} else if mirr {
|
||||||
|
mirrored += 1;
|
||||||
|
} else {
|
||||||
|
unrelated += 1;
|
||||||
|
if examples.len() < 6 {
|
||||||
|
examples.push(format!(
|
||||||
|
"unrelated: {} / {}_02 in {}",
|
||||||
|
m.name,
|
||||||
|
stem,
|
||||||
|
f.file_name().unwrap().to_string_lossy()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!("twin pairs of equal vertex count: {}", same + mirrored + unrelated + related);
|
||||||
|
println!(" exact X-mirror (expected) : {mirrored}");
|
||||||
|
println!(" IDENTICAL (collapse) : {same}");
|
||||||
|
println!(" related other way (Y/Z mirror, reordered): {related}");
|
||||||
|
println!(" unrelated (mis-anchor?) : {unrelated}");
|
||||||
|
for e in examples {
|
||||||
|
println!(" e.g. {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -53,7 +53,7 @@ fn span(m: &Xbg7Model) -> Option<[i64; 3]> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[ignore = "known-failing: 125 of 681 shared resources decode inconsistently. A neighbourhood anchor took this to 51 but regressed the e106 twin-mirror decision and was withdrawn — see docs/re/structures/xbg7-mesh.md"]
|
#[ignore = "known-failing: 62 of 714 shared resources decode inconsistently (was 125 of 681; distinct anchor assignment + the 0.42 connectivity cap fixed the rest). Note this metric is the WEAKER witness — a systematic mis-anchor is consistent — see docs/re/structures/xbg7-mesh.md"]
|
||||||
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)");
|
||||||
@@ -102,3 +102,45 @@ fn shared_resources_decode_identically_in_every_container() {
|
|||||||
bad.len()
|
bad.len()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Port/starboard twins must not decode to the *same* buffer.
|
||||||
|
///
|
||||||
|
/// A runtime capture showed the container stores both halves of the `e106` hull
|
||||||
|
/// as separate X-reflected buffers, so a `…_01`/`…_02` pair of equal vertex
|
||||||
|
/// count should come out mirrored (or related by another axis / vertex order) —
|
||||||
|
/// never identical, which is the collapse distinct assignment fixes. Disc-wide
|
||||||
|
/// this holds for every such pair except `n206`, whose twins are grouped-pool
|
||||||
|
/// resources (4 sub-meshes) and so are excluded from distinct assignment; that
|
||||||
|
/// one is the remaining known case, asserted explicitly so it cannot grow.
|
||||||
|
#[test]
|
||||||
|
fn twin_pairs_do_not_share_a_buffer() {
|
||||||
|
let Some(root) = disc_root() else {
|
||||||
|
eprintln!("SKIP: extracted disc not found (set SYLPHEED_DISC to enable)");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let mut files: Vec<PathBuf> = std::fs::read_dir(root.join("hidden/resource3d"))
|
||||||
|
.expect("resource3d/")
|
||||||
|
.flatten()
|
||||||
|
.map(|e| e.path())
|
||||||
|
.filter(|p| p.extension().and_then(|s| s.to_str()) == Some("xpr"))
|
||||||
|
.collect();
|
||||||
|
files.sort();
|
||||||
|
|
||||||
|
let mut collapsed: Vec<String> = Vec::new();
|
||||||
|
for f in &files {
|
||||||
|
let Ok(bytes) = std::fs::read(f) else { continue };
|
||||||
|
let models = Xbg7Model::anchor_models_cancellable(&bytes, 0.0, &|| false);
|
||||||
|
let by_name: BTreeMap<&str, &Xbg7Model> =
|
||||||
|
models.iter().map(|m| (m.name.as_str(), m)).collect();
|
||||||
|
for m in &models {
|
||||||
|
let Some(stem) = m.name.strip_suffix("_01") else { continue };
|
||||||
|
let Some(t) = by_name.get(format!("{stem}_02").as_str()) else { continue };
|
||||||
|
let (a, b) = (m.meshes[0].vbuf_offset, t.meshes[0].vbuf_offset);
|
||||||
|
if a.is_some() && a == b {
|
||||||
|
collapsed.push(format!("{}/{stem}_02 in {}", m.name, f.file_name().unwrap().to_string_lossy()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
collapsed.retain(|c| !c.starts_with("n206_01"));
|
||||||
|
assert!(collapsed.is_empty(), "twin pairs sharing one buffer: {collapsed:?}");
|
||||||
|
}
|
||||||
|
|||||||
@@ -930,6 +930,36 @@ 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.
|
||||||
|
|
||||||
|
### 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
|
||||||
|
equal vertex count should decode to **mirrored** geometry, never to the same
|
||||||
|
buffer. `examples/twin_mirror_audit.rs` applies it to all 166 containers:
|
||||||
|
|
||||||
|
| twin pairs of equal vertex count | 34 |
|
||||||
|
|---|---|
|
||||||
|
| exact X-mirror | **18** |
|
||||||
|
| related another way (Y/Z mirror, or the same cloud in another vertex order) | 15 |
|
||||||
|
| identical — a collapse | **1** |
|
||||||
|
| unrelated — no relation at all | **0** |
|
||||||
|
|
||||||
|
Two calibration notes, because the first run of this audit got both wrong.
|
||||||
|
Comparing quantised keys **exactly** reported four false "unrelated" pairs
|
||||||
|
(`e101_eng_01/_02`): the halves are authored, not bit-negated, so they differ in
|
||||||
|
the last digits — a tolerance is required. And a mirrored pair may be stored in a
|
||||||
|
**different vertex order**, so the multiset has to be compared mirrored as well
|
||||||
|
as directly. With both fixed, nothing on the disc is unrelated.
|
||||||
|
|
||||||
|
The single collapse is `n206_01`/`n206_02` (`Stage_S08`), both anchored at
|
||||||
|
`0x33b6e54` while the container holds a second direct copy at `0x342d984` and
|
||||||
|
mirrors at `0x33b7754`/`0x342e284`. It survives because both twins are
|
||||||
|
**grouped-pool** resources (4 sub-meshes), and distinct assignment excludes that
|
||||||
|
path — so this is the concrete next target, and the fix direction is to extend
|
||||||
|
distinctness across grouped models.
|
||||||
|
|
||||||
|
`tests/mesh_consistency_disc.rs::twin_pairs_do_not_share_a_buffer` locks this in:
|
||||||
|
no twin pair may share a buffer, with `n206` the one asserted exception.
|
||||||
|
|
||||||
Not settled: `e106_brg_01_b_02` ≡ `e106_brg_01_l` (51 verts). A second 51-vertex
|
Not settled: `e106_brg_01_b_02` ≡ `e106_brg_01_l` (51 verts). A second 51-vertex
|
||||||
`vbase` exists in the logs but is **not** from this container, and the container
|
`vbase` exists in the logs but is **not** from this container, and the container
|
||||||
holds three near-identical 51-vertex runs, so the pair has no oracle yet.
|
holds three near-identical 51-vertex runs, so the pair has no oracle yet.
|
||||||
|
|||||||
Reference in New Issue
Block a user