Parse the occlusion PVS grid
A z -> y -> x tree of u16 node offsets over 4x4x4-unit cells, each populated cell naming one 128-byte mask of 1024 visibility bits that the renderer fills from the camera's cell each frame. Two sentinels carry meaning and one of them is easy to miss: a z or y slot of 0 means "no subtree", and an x slot of 0xffff means "no cell". Reading the latter as a mask index gives a grid whose highest mask is 65535 on every single level, which is the tell. The check that makes this real is the mask array. It starts at masks_offset and runs to (highest_mask + 1) * 0x80, and on every level with a grid that ends at the block boundary exactly -- widest unexplained tail across the disc is 0 bytes. A wrong tree shape or cell size would leave the highest mask index elsewhere entirely. 412,521 populated cells over 140,939 masks, across the 24 levels that have one. An offset of 0 means the level has no occlusion data at all, which is not the same as "at the start of the file" -- that is where the tfrags block lives, and parsing it as a grid produces a mask array that fits anyway, with 1600 bytes to spare. That is what made level 10 look plausible on the first pass; three levels (10, 24, 25) carry none.
This commit is contained in:
@@ -1284,3 +1284,112 @@ fn tie_class_headers_match_their_index_entry() {
|
||||
{ordered} have ordered LOD distances, {unordered} do not"
|
||||
);
|
||||
}
|
||||
|
||||
/// The occlusion PVS grid.
|
||||
///
|
||||
/// The check that makes this real is the mask array: it starts at
|
||||
/// `masks_offset` and runs to `(highest_mask + 1) * 0x80`, and on every level
|
||||
/// that ends within a few bytes of the block's own length. A wrong tree shape
|
||||
/// or a wrong cell size would leave the highest mask index somewhere else
|
||||
/// entirely and the array would not fit.
|
||||
#[test]
|
||||
fn occlusion_grids_parse_on_every_level() {
|
||||
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;
|
||||
}
|
||||
|
||||
use gc_formats::level_core::LevelCoreHeader;
|
||||
use gc_formats::occlusion::parse_occlusion_block;
|
||||
let mut levels = 0usize;
|
||||
let mut absent = 0usize;
|
||||
let mut widest_slack = 0usize;
|
||||
let (mut cells, mut masks) = (0usize, 0usize);
|
||||
|
||||
for n in 0..27 {
|
||||
let dir = data.join(format!("level{n}"));
|
||||
let (Ok(core), Ok(index)) = (
|
||||
std::fs::read(dir.join("core_data.bin")),
|
||||
std::fs::read(dir.join("core_index.bin")),
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
let h = LevelCoreHeader::parse(&index).expect("core header");
|
||||
// An offset of 0 means the level has no occlusion data at all, which is
|
||||
// not the same as "at the start of the file" -- that is where the
|
||||
// tfrags block lives, and parsing it as a grid produces a mask array
|
||||
// that fits anyway, which is how this nearly went wrong.
|
||||
if h.occlusion <= 0 {
|
||||
absent += 1;
|
||||
continue;
|
||||
}
|
||||
let start = h.occlusion as usize;
|
||||
// The block runs to the sky region, or to the end when there is none.
|
||||
let end = if h.sky > 0 {
|
||||
h.sky as usize
|
||||
} else {
|
||||
core.len()
|
||||
};
|
||||
assert!(
|
||||
start < end && end <= core.len(),
|
||||
"level{n}: bad occlusion range"
|
||||
);
|
||||
let block = &core[start..end];
|
||||
|
||||
let o = parse_occlusion_block(block)
|
||||
.unwrap_or_else(|e| panic!("level{n}: occlusion block: {e}"));
|
||||
// Every mask a cell names exists.
|
||||
for c in &o.cells {
|
||||
assert!(
|
||||
(c.mask as usize) < o.masks.len(),
|
||||
"level{n}: cell names a missing mask"
|
||||
);
|
||||
}
|
||||
// The array ends at or just before the block's end. Retail levels sit
|
||||
// within a few bytes; a gross error would leave a large gap.
|
||||
let need = o.masks_offset as usize + o.masks.len() * gc_formats::occlusion::MASK_BYTES;
|
||||
assert!(need <= block.len(), "level{n}: masks run past the block");
|
||||
let slack = block.len() - need;
|
||||
// Levels whose grid is empty are mostly header and padding, so this is
|
||||
// proportional and only applies where there are cells at all.
|
||||
if !o.cells.is_empty() {
|
||||
assert!(
|
||||
slack * 8 < block.len(),
|
||||
"level{n}: {slack} of {} bytes unexplained after the mask array",
|
||||
block.len()
|
||||
);
|
||||
}
|
||||
widest_slack = widest_slack.max(slack);
|
||||
|
||||
cells += o.cells.len();
|
||||
masks += o.masks.len();
|
||||
levels += 1;
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
levels + absent,
|
||||
27,
|
||||
"expected all 27 levels, got {}",
|
||||
levels + absent
|
||||
);
|
||||
assert_eq!(
|
||||
absent, 3,
|
||||
"expected 3 levels without occlusion data, got {absent}"
|
||||
);
|
||||
assert!(cells > 400_000, "only {cells} occlusion cells");
|
||||
eprintln!(
|
||||
"occlusion: {cells} populated cells over {masks} masks across {levels} levels; \
|
||||
widest unexplained tail {widest_slack} bytes"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user