Parse tfrag topology: vertex info, strips, indices and triangles
Turns tfrag positions into drawable geometry. Every unpack in a patch's VIF lists is now classified by format *and* destination VU address, using the VU header at the head of the common list. That table is what tells the strips from the indices from the parent arrays: they are all V4_8 and only the address distinguishes them. Strip indices address the vertex-info array, not the position array. Several vertex-info entries can share a position, so an index resolves through vertex_info[i].vertex / 2 to reach a position. Vertex-info also carries UVs and the LOD parent link. A strip run of n vertices is a GS triangle strip of n - 2 triangles with alternating winding. The vertex count is biased by -128 when negative, and the first record of a list is always biased even when positive -- real patches open with -116 or -124, never a small positive number. tri_count lives in the patch header and cannot be derived from the lists without walking them, so reproducing it is a strong check: all 27 levels, 18,102 patches, 809,879 triangles, every patch matching its header, every triangle index inside its LOD's vertex-info range. Four wrong turns got there, each of which failed quietly rather than loudly: reading the element size from the immediate instead of the command, 2-byte payload padding instead of 4, ignoring the inline payload of STROW/STCOL/STMASK/MPG/DIRECT, and banking strips by arrival order instead of by LOD. Every one produced parseable, plausible geometry. Only the header's own triangle count caught them. Still open: ad-gif texture bindings, per-vertex RGBA and light records, and the LOD switch and morph rules.
This commit is contained in:
@@ -487,3 +487,96 @@ fn tfrag_vertices_decode_for_every_level() {
|
||||
);
|
||||
eprintln!("decoded {total_verts} vertices from {total_tfrags} tfrags across {levels} levels");
|
||||
}
|
||||
|
||||
/// Terrain topology: strips and index arrays must expand to exactly the
|
||||
/// triangle count each patch's header claims.
|
||||
///
|
||||
/// `tri_count` is stored in the patch header but is not derivable from the
|
||||
/// lists directly, so reproducing it on all 18,102 patches is a strong check
|
||||
/// that the strip walk, the index bank and the LOD assignment are all right.
|
||||
/// Triangle indices must also stay inside the vertex-info range for the LOD.
|
||||
#[test]
|
||||
fn tfrag_topology_reproduces_the_header_triangle_count() {
|
||||
require_disc!();
|
||||
gc_formats::iso9660::IsoImage::open(std::path::Path::new(
|
||||
&std::env::var("GC_DISC").expect("GC_DISC is set by require_disc!"),
|
||||
))
|
||||
.expect("ISO should open");
|
||||
let data = std::env::var_os("GC_EXTRACTED")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|| {
|
||||
let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
|
||||
PathBuf::from(home).join("Documents/gc-extracted")
|
||||
});
|
||||
if !data.exists() {
|
||||
eprintln!("skipped: unpack the disc first");
|
||||
return;
|
||||
}
|
||||
|
||||
let mut patches = 0usize;
|
||||
let mut triangles = 0usize;
|
||||
let mut levels = 0usize;
|
||||
|
||||
for n in 0..27 {
|
||||
let dir = data.join(format!("level{n}"));
|
||||
let core = match std::fs::read(dir.join("core_data.bin")) {
|
||||
Ok(c) => c,
|
||||
Err(_) => continue,
|
||||
};
|
||||
let index = match std::fs::read(dir.join("core_index.bin")) {
|
||||
Ok(i) => i,
|
||||
Err(_) => continue,
|
||||
};
|
||||
let region =
|
||||
|off: usize| i32::from_le_bytes(index[off..off + 4].try_into().unwrap()) as usize;
|
||||
let start = region(0x08);
|
||||
let end = [0x0c_usize, 0x10, 0x14]
|
||||
.iter()
|
||||
.map(|&o| region(o))
|
||||
.filter(|&v| v > 0)
|
||||
.min()
|
||||
.unwrap_or(core.len());
|
||||
let block = &core[start..end];
|
||||
|
||||
let header = gc_formats::tfrag::TfragsBlock::parse(block).expect("tfrags block header");
|
||||
let table = header.table_offset as usize;
|
||||
for i in 0..header.tfrag_count as usize {
|
||||
let th = header.header(block, i).expect("tfrag header");
|
||||
let data_base = table.wrapping_add(th.data as usize);
|
||||
if data_base >= block.len() {
|
||||
continue;
|
||||
}
|
||||
let g = gc_formats::tfrag::parse_tfrag(&block[data_base..], &th)
|
||||
.unwrap_or_else(|e| panic!("level{n} patch {i}: {e}"));
|
||||
let tris = gc_formats::tfrag::tfrag_triangles(&g, 0)
|
||||
.unwrap_or_else(|e| panic!("level{n} patch {i}: {e}"));
|
||||
assert_eq!(
|
||||
tris.len(),
|
||||
th.tri_count as usize,
|
||||
"level{n} patch {i}: {} triangles, header says {}",
|
||||
tris.len(),
|
||||
th.tri_count
|
||||
);
|
||||
let vinfo = g.lod_vinfo_count(0);
|
||||
for t in &tris {
|
||||
let idx = [t.a as usize, t.b as usize, t.c as usize];
|
||||
assert!(
|
||||
idx.iter().all(|&v| v < vinfo),
|
||||
"level{n} patch {i}: triangle index outside {vinfo} vertex-info entries"
|
||||
);
|
||||
assert!((t.ad_gif as usize) < g.ad_gif_count);
|
||||
}
|
||||
patches += 1;
|
||||
triangles += tris.len();
|
||||
}
|
||||
levels += 1;
|
||||
}
|
||||
|
||||
assert!(levels > 0, "no unpacked levels were found");
|
||||
assert!(patches > 18_000, "expected 18k+ patches, got {patches}");
|
||||
assert!(
|
||||
triangles > 700_000,
|
||||
"expected 700k+ triangles, got {triangles}"
|
||||
);
|
||||
eprintln!("reproduced {triangles} triangles across {patches} patches in {levels} levels");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user