capture_ib_truth now proposes an identity for each drawn buffer our decoder cannot place, by matching (vertex, index) counts against declared-but-not-decoded resources. That identified n201_01/_02/_03 in Stage_S02.xpr — three of the 63 resources that decode in no container — and the capture pins all four sub-meshes: #0 vb 0x32BA71C ib 0x32B39FC 4464 idx 777 v stride 24 #1 vb 0x32BEFF4 ib 0x32B5CDC 4464 idx 869 v stride 24 #2 vb 0x32C416C ib 0x32B7FBC 576 idx 192 v stride 24 #3 vb 0x32C536C ib 0x32B843C 4464 idx 869 v stride 28 <- different The layout matches our assumptions exactly (tight index packing, last buffer flush against vb0 so pad 0, span 27936 == align4-summed markers, contiguous vertex buffers, max index == verts-1 everywhere). The defect is that sub-mesh #3 has a different stride AND its own vertex shader: anchor_grouped_meshes parses one declaration per resource and applies its stride to every sub-mesh, so it reads #3 out of phase (372 non-finite position components of 2607), and since the pivot is the largest index count with ties going to the last marker, the pivot IS that sub-mesh — so the whole resource is declined. Also adds examples/miss_targets.rs (which container to aim a capture at): 63 resources decode nowhere, 58 of them in exactly one container, clustering as Stage_S16 21 (e901_wing_05_*), ptc_pack 12, Base 6, then per-stage n2xx groups. Flying stage 16 did not draw the e901 wings — the container is resident but the unit must also be on screen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NptfmpjdpNCKEez6d2xvA9
87 lines
4.0 KiB
Rust
87 lines
4.0 KiB
Rust
//! Which container should the next runtime capture aim at?
|
|
//!
|
|
//! The 2026-08-13 stage-05 capture showed that a mission keeps several containers
|
|
//! resident and that its OWN `Stage_SNN.xpr` is among them (the f101 ACROPOLIS was
|
|
//! drawn from `Stage_S05.xpr`), so a capture can be aimed by choosing the mission.
|
|
//! This ranks the targets: for every XBG7 resource on the disc it records the
|
|
//! containers where it decodes and the ones where it does not, then reports
|
|
//!
|
|
//! * resources that decode **nowhere** — the real gaps, where a capture is the
|
|
//! only ground truth left;
|
|
//! * per container, how many of those it holds (fly that mission);
|
|
//! * resources that miss in one container but decode in another — a defect worth
|
|
//! fixing, but the geometry is already recoverable, so a capture is not needed.
|
|
//!
|
|
//! Usage: miss_targets <resource3d_dir>
|
|
use sylpheed_formats::mesh::{xbg7_resource_names, Xbg7Model};
|
|
use std::collections::{BTreeMap, BTreeSet};
|
|
|
|
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();
|
|
|
|
// resource -> (containers where it decodes, containers where it misses)
|
|
let mut ok: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
|
|
let mut miss: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
|
|
for f in &files {
|
|
let Ok(bytes) = std::fs::read(f) else { continue };
|
|
let where_ = f.file_name().unwrap().to_string_lossy().to_string();
|
|
let declared: BTreeSet<String> = xbg7_resource_names(&bytes).into_iter().collect();
|
|
if declared.is_empty() {
|
|
continue;
|
|
}
|
|
let decoded: BTreeSet<String> =
|
|
Xbg7Model::stage_models(&bytes).into_iter().map(|m| m.name).collect();
|
|
for n in &declared {
|
|
if decoded.contains(n) {
|
|
ok.entry(n.clone()).or_default().insert(where_.clone());
|
|
} else {
|
|
miss.entry(n.clone()).or_default().insert(where_.clone());
|
|
}
|
|
}
|
|
}
|
|
|
|
let never: BTreeMap<&String, &BTreeSet<String>> =
|
|
miss.iter().filter(|(n, _)| !ok.contains_key(*n)).collect();
|
|
let recoverable: Vec<&String> = miss.keys().filter(|n| ok.contains_key(*n)).collect();
|
|
|
|
println!("resources that decode NOWHERE: {}", never.len());
|
|
println!("resources that miss somewhere but decode elsewhere: {}\n", recoverable.len());
|
|
|
|
// Rank containers by how many never-decoding resources they hold.
|
|
let mut per_container: BTreeMap<&String, Vec<&String>> = BTreeMap::new();
|
|
for (n, wheres) in &never {
|
|
for w in wheres.iter() {
|
|
per_container.entry(w).or_default().push(n);
|
|
}
|
|
}
|
|
let mut ranked: Vec<_> = per_container.iter().collect();
|
|
ranked.sort_by_key(|(_, v)| std::cmp::Reverse(v.len()));
|
|
println!("container never-decoding resources it holds");
|
|
for (w, v) in ranked.iter().take(14) {
|
|
let sample: Vec<&str> = v.iter().take(6).map(|s| s.as_str()).collect();
|
|
println!("{:<26} {:>3} {}", w, v.len(), sample.join(", "));
|
|
}
|
|
|
|
// Exclusives: a never-decoding resource that only ONE container holds is only
|
|
// reachable through whatever loads that container.
|
|
let excl: Vec<(&&String, &&BTreeSet<String>)> =
|
|
never.iter().filter(|(_, w)| w.len() == 1).collect();
|
|
println!("\nof the never-decoding, {} live in exactly one container", excl.len());
|
|
let mut by_one: BTreeMap<&String, Vec<&String>> = BTreeMap::new();
|
|
for (n, w) in &excl {
|
|
by_one.entry(w.iter().next().unwrap()).or_default().push(n);
|
|
}
|
|
let mut b: Vec<_> = by_one.iter().collect();
|
|
b.sort_by_key(|(_, v)| std::cmp::Reverse(v.len()));
|
|
for (w, v) in b.iter().take(10) {
|
|
println!(" {:<24} {:>3} {}", w, v.len(), v.iter().take(5).map(|s| s.as_str()).collect::<Vec<_>>().join(", "));
|
|
}
|
|
}
|