visible() documented its argument as `byte << 8 | 1 << bit` -- high byte selects the mask byte, low byte is already the shifted bit -- then shifted the low byte a second time as though it were a bit index. That is wrong twice over. It inverts the meaning of every bit argument, and for any low byte above 8 it panics in debug, because `1u8 << 32` is an overflow. Real data reaches it: the folded bits for the upper bytes of a mask are exactly the values that trip it. Treat the low byte as the bit mask, which is what the doc says and what the renderer needs. Added an assertion for a mask index that does not exist, since that was the case that exposed the mismatch.
268 lines
9.6 KiB
Rust
268 lines
9.6 KiB
Rust
//! Precomputed occlusion: the potentially-visible-set grid.
|
|
//!
|
|
//! Spec: `docs/formats/occlusion.md`.
|
|
//!
|
|
//! The level ships a z → y → x tree of `u16` node offsets over 4x4x4-unit
|
|
//! cells. Populated cells name one 128-byte mask of 1024 visibility bits, which
|
|
//! the renderer fills from the camera's cell each frame and then consults per
|
|
//! object *before* any frustum test.
|
|
//!
|
|
//! Two sentinels carry meaning and are easy to miss. A z or y slot of `0`
|
|
//! means "no subtree here"; an x slot of `0xffff` means "no cell". Reading the
|
|
//! latter as a mask index produces a grid whose highest mask is 65535 on every
|
|
//! level, which is the tell.
|
|
|
|
use crate::buf::{Buf, Error, ErrorKind, Result};
|
|
|
|
/// Bytes per visibility mask (1024 bits).
|
|
pub const MASK_BYTES: usize = 0x80;
|
|
|
|
/// Cell edge in world units. The camera position is scaled by 0.25 and
|
|
/// truncated, so cell 0 spans -4 < p < 4.
|
|
pub const CELL_SIZE: f32 = 4.0;
|
|
|
|
/// An x slot of this value means "no cell".
|
|
const NO_CELL: u16 = 0xffff;
|
|
|
|
/// One populated grid cell, in cell indices.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub struct OcclusionCell {
|
|
pub x: u16,
|
|
pub y: u16,
|
|
pub z: u16,
|
|
/// Index into [`Occlusion::masks`].
|
|
pub mask: u16,
|
|
}
|
|
|
|
/// A parsed occlusion block.
|
|
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
|
pub struct Occlusion {
|
|
/// Byte offset of the mask array from the start of the block.
|
|
pub masks_offset: u32,
|
|
/// Every populated cell, in tree order (z, then y, then x).
|
|
pub cells: Vec<OcclusionCell>,
|
|
/// Masks `0 ..= highest index any cell names`. The game stores no count.
|
|
pub masks: Vec<[u8; MASK_BYTES]>,
|
|
}
|
|
|
|
/// Cell indices for a camera position, as the game computes them: scale by
|
|
/// 0.25 and truncate toward zero.
|
|
pub fn cell_coords(pos: [f32; 3]) -> [i32; 3] {
|
|
pos.map(|p| (p / CELL_SIZE) as i32)
|
|
}
|
|
|
|
impl Occlusion {
|
|
/// The mask index for a cell, or `None` if the grid has no cell there.
|
|
///
|
|
/// This is a linear scan rather than a tree walk. The tree exists so the
|
|
/// *game* can read it directly from the block; by this point every cell has
|
|
/// already been enumerated, and a hash of `(x, y, z)` would be a map to
|
|
/// maintain for a lookup that happens once per object per frame.
|
|
pub fn lookup(&self, x: i32, y: i32, z: i32) -> Option<u16> {
|
|
let (x, y, z) = (x as u16, y as u16, z as u16);
|
|
self.cells
|
|
.iter()
|
|
.find(|c| c.x == x && c.y == y && c.z == z)
|
|
.map(|c| c.mask)
|
|
}
|
|
|
|
/// Is an object with these occlusion bits visible from this mask?
|
|
///
|
|
/// The bits are `byte << 8 | 1 << bit`, folded at level load: the high
|
|
/// byte selects the mask byte and the low byte is *already* the shifted
|
|
/// bit, not an index. Shifting the low byte again is both wrong and a
|
|
/// panic, since any byte above 8 overflows a `u8` shift.
|
|
///
|
|
/// The test is the game's: `mask[byte] & bit == 0` means skip.
|
|
pub fn visible(&self, mask_index: u16, bits: u16) -> bool {
|
|
let byte = (bits >> 8) as usize;
|
|
let bit = (bits & 0xff) as u8;
|
|
self.masks
|
|
.get(mask_index as usize)
|
|
.is_some_and(|m| byte < MASK_BYTES && m[byte] & bit != 0)
|
|
}
|
|
}
|
|
|
|
/// Parse a raw occlusion block.
|
|
///
|
|
/// Every read is bounds-checked and returns an error rather than reading past
|
|
/// the end; the game reads unchecked, and retail data never needs it to.
|
|
pub fn parse_occlusion_block(block: &[u8]) -> Result<Occlusion> {
|
|
let mut b = Buf::new(block);
|
|
let masks_offset = b.i32("occlusion mask offset")?;
|
|
let (z_base, z_count) = (b.u16("occlusion z base")?, b.u16("occlusion z count")?);
|
|
if masks_offset <= 8 || masks_offset as usize > block.len() {
|
|
return Err(Error {
|
|
offset: 0,
|
|
context: "occlusion: mask offset outside the block",
|
|
kind: ErrorKind::Invalid("occlusion mask offset"),
|
|
});
|
|
}
|
|
let at = |o: usize| -> Result<u16> {
|
|
let end = o
|
|
.checked_add(2)
|
|
.filter(|&e| e <= block.len())
|
|
.ok_or(Error {
|
|
offset: o,
|
|
context: "occlusion node offset past the block",
|
|
kind: ErrorKind::OutOfBounds {
|
|
needed: 2,
|
|
available: block.len().saturating_sub(o),
|
|
},
|
|
})?;
|
|
Ok(u16::from_le_bytes([block[end - 2], block[end - 1]]))
|
|
};
|
|
|
|
let mut cells = Vec::new();
|
|
for zi in 0..z_count as usize {
|
|
let zo = at(8 + zi * 2)?;
|
|
if zo == 0 {
|
|
continue;
|
|
}
|
|
let yn = zo as usize * 4;
|
|
let y_base = at(yn)?;
|
|
let y_count = at(yn + 2)?;
|
|
for yi in 0..y_count as usize {
|
|
let yo = at(yn + 4 + yi * 2)?;
|
|
if yo == 0 {
|
|
continue;
|
|
}
|
|
let xn = yo as usize * 4;
|
|
let x_base = at(xn)?;
|
|
let x_count = at(xn + 2)?;
|
|
for xi in 0..x_count as usize {
|
|
let mask = at(xn + 4 + xi * 2)?;
|
|
if mask == NO_CELL {
|
|
continue;
|
|
}
|
|
cells.push(OcclusionCell {
|
|
x: x_base.wrapping_add(xi as u16),
|
|
y: y_base.wrapping_add(yi as u16),
|
|
z: z_base.wrapping_add(zi as u16),
|
|
mask,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
let highest = cells.iter().map(|c| c.mask).max().unwrap_or(0) as usize;
|
|
let masks_start = masks_offset as usize;
|
|
let need = highest
|
|
.checked_add(1)
|
|
.and_then(|n| n.checked_mul(MASK_BYTES))
|
|
.and_then(|n| masks_start.checked_add(n))
|
|
.ok_or(Error {
|
|
offset: masks_start,
|
|
context: "occlusion mask array overflows",
|
|
kind: ErrorKind::InvalidLength("occlusion mask array"),
|
|
})?;
|
|
if need > block.len() {
|
|
return Err(Error {
|
|
offset: masks_start,
|
|
context: "occlusion mask array runs past the block",
|
|
kind: ErrorKind::OutOfBounds {
|
|
needed: need,
|
|
available: block.len(),
|
|
},
|
|
});
|
|
}
|
|
let mut masks = Vec::with_capacity(highest + 1);
|
|
for i in 0..=highest {
|
|
let at = masks_start + i * MASK_BYTES;
|
|
masks.push(block[at..at + MASK_BYTES].try_into().unwrap());
|
|
}
|
|
|
|
Ok(Occlusion {
|
|
masks_offset: masks_offset as u32,
|
|
cells,
|
|
masks,
|
|
})
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
/// A two-cell grid: one populated, one empty via the `0xffff` sentinel.
|
|
fn block() -> Vec<u8> {
|
|
// Masks at 0x40; the tree is tiny so only two masks are needed.
|
|
const MASKS_AT: usize = 0x40;
|
|
let mut b = vec![0u8; MASKS_AT + 2 * MASK_BYTES];
|
|
b[0x00..0x04].copy_from_slice(&(MASKS_AT as i32).to_le_bytes());
|
|
b[0x04..0x06].copy_from_slice(&0u16.to_le_bytes()); // z base
|
|
b[0x06..0x08].copy_from_slice(&1u16.to_le_bytes()); // z count
|
|
b[0x08..0x0a].copy_from_slice(&10u16.to_le_bytes()); // z node at 10*4 = 40
|
|
// y node at 40: base 5, one y slot pointing at x node 12 (48 bytes)
|
|
b[40..42].copy_from_slice(&5u16.to_le_bytes());
|
|
b[42..44].copy_from_slice(&1u16.to_le_bytes());
|
|
b[44..46].copy_from_slice(&12u16.to_le_bytes());
|
|
// x node at 48: base 7, two x slots, the second empty
|
|
b[48..50].copy_from_slice(&7u16.to_le_bytes());
|
|
b[50..52].copy_from_slice(&2u16.to_le_bytes());
|
|
b[52..54].copy_from_slice(&0u16.to_le_bytes()); // mask 0
|
|
b[54..56].copy_from_slice(&NO_CELL.to_le_bytes());
|
|
// Mask 0: visibility bit 5 of byte 0 set.
|
|
b[MASKS_AT] = 1 << 5;
|
|
b
|
|
}
|
|
|
|
#[test]
|
|
fn parses_a_two_cell_grid() {
|
|
let o = parse_occlusion_block(&block()).unwrap();
|
|
assert_eq!(o.masks_offset, 0x40);
|
|
// The empty x slot is dropped, so only one cell survives.
|
|
assert_eq!(o.cells.len(), 1);
|
|
assert_eq!(
|
|
o.cells[0],
|
|
OcclusionCell {
|
|
x: 7,
|
|
y: 5,
|
|
z: 0,
|
|
mask: 0
|
|
}
|
|
);
|
|
assert_eq!(o.masks.len(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn the_empty_sentinel_is_not_a_mask() {
|
|
// Without the sentinel, every level's highest mask would be 65535.
|
|
let o = parse_occlusion_block(&block()).unwrap();
|
|
assert!(o.cells.iter().all(|c| c.mask != NO_CELL));
|
|
}
|
|
|
|
#[test]
|
|
fn lookup_and_visibility() {
|
|
let o = parse_occlusion_block(&block()).unwrap();
|
|
assert_eq!(o.lookup(7, 5, 0), Some(0));
|
|
assert_eq!(o.lookup(8, 5, 0), None);
|
|
// Bits are `byte << 8 | 1 << bit`. Bit 5 of mask 0 is set.
|
|
let bits = |byte: u16, bit: u16| (byte << 8) | (1 << bit);
|
|
assert!(o.visible(0, bits(0, 5)));
|
|
assert!(!o.visible(0, bits(0, 4)));
|
|
assert!(!o.visible(0, bits(1, 5)));
|
|
}
|
|
|
|
#[test]
|
|
fn a_malformed_block_is_an_error_not_a_panic() {
|
|
assert!(parse_occlusion_block(&[]).is_err());
|
|
assert!(parse_occlusion_block(&[0u8; 8]).is_err());
|
|
// A mask offset past the end of the buffer.
|
|
let mut b = block();
|
|
b[0x00..0x04].copy_from_slice(&9_000_000i32.to_le_bytes());
|
|
assert!(parse_occlusion_block(&b).is_err());
|
|
// A node offset pointing outside the block.
|
|
let mut b = block();
|
|
b[0x08..0x0a].copy_from_slice(&9_000u16.to_le_bytes());
|
|
assert!(parse_occlusion_block(&b).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn cells_are_four_units_across() {
|
|
assert_eq!(cell_coords([0.0, 0.0, 0.0]), [0, 0, 0]);
|
|
assert_eq!(cell_coords([5.0, -5.0, 9.0]), [1, -1, 2]);
|
|
// Cell 0 spans -4 < p < 4.
|
|
assert_eq!(cell_coords([-3.9, 3.9, 0.0]), [0, 0, 0]);
|
|
}
|
|
}
|