Decode tfrag texture bindings, vertex colours and light records
Adds level_core.rs (the uncompressed core index header and texture table) and extends tfrag.rs with the data a draw call needs beyond geometry. An ad-gif is five A+D quadwords holding packed fields rather than GS registers; the level load rewrites them in place before drawing. On disc TEX0_1.data_lo indexes the level's texture table, TEX1_1.data_lo is the LOD bias K and data_hi the minification filter, and CLAMP_1 holds both wrap modes. Three things are easy to get wrong here: * K is a signed 12-bit value in 1/16 units, sign-extended by shifting the field up 4 and back down. Retail values are -137..-89 mip-scaled; reading it unsigned gives 1911 instead of -137. * TEX1_1.data_hi is MMIN, and 4 decodes as LinearMipmapNearest, not LinearMipmapLinear as the raw bit layout suggests. * RGBA and the light/normal records are not in the VIF lists at all. They sit at fixed offsets in the patch data block: rgba_size*4 colours at rgba_ofs, vert_count light records at light_ofs+0x10. The tfrag origin lives there too -- if no STROW row supplied one, the quadword at light_ofs is it. The texture table checks out on its own terms: 93 entries at index offset 0x2900 on level 0, and 0x2900 + 93*16 lands exactly on the next range. Verification: an ad-gif index outside the level's table would mean the geometry and the textures were misread independently, so every one is range-checked against real entries. All 27 levels: 47,950 ad-gifs over 18,102 patches, all in range, referencing 1429 of 1595 level texture entries (90%). Each patch's light array is exactly vert_count records and its RGBA array exactly rgba_size*4. Still open: LOD selection and morphing, and the texel data itself.
This commit is contained in:
@@ -564,7 +564,7 @@ fn tfrag_topology_reproduces_the_header_triangle_count() {
|
||||
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);
|
||||
assert!((t.ad_gif as usize) < g.ad_gifs.len());
|
||||
}
|
||||
patches += 1;
|
||||
triangles += tris.len();
|
||||
@@ -580,3 +580,127 @@ fn tfrag_topology_reproduces_the_header_triangle_count() {
|
||||
);
|
||||
eprintln!("reproduced {triangles} triangles across {patches} patches in {levels} levels");
|
||||
}
|
||||
|
||||
/// Texture bindings: every ad-gif a patch uses must index an entry that
|
||||
/// actually exists in the level's texture table, and the light array must be
|
||||
/// one record per vertex.
|
||||
///
|
||||
/// The texture table is the join between tfrags and the texel data, so an
|
||||
/// out-of-range index here means the level's geometry and its textures have
|
||||
/// been misread independently of each other.
|
||||
#[test]
|
||||
fn tfrag_ad_gifs_index_the_level_texture_table() {
|
||||
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 ad_gifs = 0usize;
|
||||
let mut textures_seen = 0usize;
|
||||
let mut textures_total = 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 ch = gc_formats::level_core::LevelCoreHeader::parse(&index).expect("core header");
|
||||
let textures = gc_formats::level_core::texture_table(&index, ch.tfrag_textures)
|
||||
.expect("texture table");
|
||||
assert!(!textures.is_empty(), "level{n}: no tfrag textures");
|
||||
// Indices are per-level, so the pair is what identifies a texture.
|
||||
let mut level_seen = std::collections::BTreeSet::new();
|
||||
|
||||
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}"));
|
||||
|
||||
assert_eq!(
|
||||
g.lights.len(),
|
||||
th.vert_count as usize,
|
||||
"level{n} patch {i}: {} light records for {} vertices",
|
||||
g.lights.len(),
|
||||
th.vert_count
|
||||
);
|
||||
assert_eq!(
|
||||
g.rgba.len(),
|
||||
th.rgba_size as usize * 4,
|
||||
"level{n} patch {i}: rgba"
|
||||
);
|
||||
|
||||
for a in &g.ad_gifs {
|
||||
let t = a.texture_index();
|
||||
assert!(
|
||||
t < textures.len(),
|
||||
"level{n} patch {i}: texture {} outside the {} entry table",
|
||||
t,
|
||||
textures.len()
|
||||
);
|
||||
let tex = &textures[t];
|
||||
assert!(
|
||||
tex.width > 0 && tex.height > 0 && tex.width <= 256 && tex.height <= 256,
|
||||
"level{n} patch {i}: texture {t} is {}x{}",
|
||||
tex.width,
|
||||
tex.height
|
||||
);
|
||||
level_seen.insert(t);
|
||||
ad_gifs += 1;
|
||||
}
|
||||
patches += 1;
|
||||
}
|
||||
textures_seen += level_seen.len();
|
||||
textures_total += textures.len();
|
||||
levels += 1;
|
||||
}
|
||||
|
||||
assert!(levels > 0, "no unpacked levels were found");
|
||||
assert!(patches > 18_000, "expected 18k+ patches, got {patches}");
|
||||
// Most of each level's texture table should be referenced by some tfrag. A
|
||||
// handful genuinely go unused; if that share grows, the ad-gif walk has
|
||||
// started missing primitives rather than the data having spare entries.
|
||||
let covered = textures_seen as f64 / textures_total as f64;
|
||||
assert!(
|
||||
covered > 0.85,
|
||||
"only {textures_seen} of {textures_total} level textures referenced ({:.1}%)",
|
||||
covered * 100.0
|
||||
);
|
||||
eprintln!(
|
||||
"{ad_gifs} ad-gifs over {patches} patches reference {textures_seen}/{textures_total} level textures"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user