Files
Going-Commando/crates/gc-formats/src/shrub.rs
T
ReaperOfVeriod 69b81af3b2 Expose the shrub body as 8-byte records, bounded per class
Turns the differential finding into code. Each shrub body record is 6
varying bytes plus 2 constant ones; body_records() hands them back raw and
body_is_well_formed() checks the trailing pair.

ShrubHeader::region now takes an explicit span_end. It has to: without it
the data slice runs to the end of the lump, so the last class in each level
absorbs whatever follows shrub and a per-class check ends up measuring its
neighbours. That is the same unbounded-span mistake as before, caught this
time by a failing assertion rather than by reading the docs afterwards.

The disc test asserts well-formedness only for the 334 of 358 classes whose
span ends at the next class. The last class in each level has no successor,
so its span contains the 690 KB region after shrub and its body is not
wholly records -- that is the open question, and asserting either way would
be wrong. Shrub::span_end_is_a_successor records which case a span is.

Records are summed only across bounded bodies. Summing the last class too
reproduces the "3.5 million records" figure retracted two commits ago, to
within rounding -- which is a useful check on how easy that mistake is to
repeat by accident.

8,938 records across the 334 bounded bodies; 16 denormal reserved words;
358 headers naming their own class.
2026-10-11 03:26:06 +02:00

304 lines
12 KiB
Rust

//! Shrub geometry: the class header.
//!
//! Spec: `docs/plan/level-assets.md`.
//!
//! Shrub is the third class-geometry family, and it is not the moby one. All
//! 358 shrub spans on the disc have a leading word of 0 or one too large to
//! index a table into, so none of them contain a moby-style part table -- the
//! rule `vertex_offset + 16 * vertex_count == next_part`, which holds for
//! 43,467 of 43,467 moby parts, holds for **zero** of them.
//!
//! What the shrub header *does* have is two fields that self-identify exactly,
//! on all 358 spans:
//!
//! - `+0x24` equals the owning class entry's own `o_class`.
//! - `+0x2c` is a data offset that lands inside the span.
//!
//! Those two are what this parses. The body past the data offset is not
//! decoded: the run of bytes there does not tile at any stride that divides the
//! tail consistently with the count at `+0x28`.
//!
//! Note the class entry size differs here too: shrub entries are 0x30 bytes,
//! not 0x20, because they carry billboard texture fields the other two tables
//! do not have.
use crate::buf::{Buf, Error, ErrorKind, Result};
/// The measured size of a shrub class entry.
pub const SHRUB_CLASS_ENTRY_BYTES: usize = 0x30;
/// The decoded part of a shrub class header.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ShrubHeader {
/// Four floats at `+0x00..+0x10`. All finite on all 358 spans, but they do
/// not behave like a `min.xyz / max.xyz` bounding box -- that ordering only
/// holds on 146 of 358. Not named because it is not established.
pub bounds: [f32; 4],
/// A positive float at `+0x10`, 3.0 to 20.0 across the disc.
pub size: f32,
/// A float at `+0x14`. Exactly 0.0 on 342 of the disc's 358 spans; the
/// other 16 hold the bit pattern `04000000`, a denormal of about 2.8e-45.
///
/// The Emotion Engine has no denormals, so hardware reads all 358 of these
/// as zero. That is the only reading the game itself ever sees, and it is
/// why this is safe to treat as a flag rather than a magnitude -- see
/// `AGENTS.md` on reproducing the FPU model rather than correcting it.
pub reserved: f32,
/// The class this geometry belongs to; equals the class entry's
/// `o_class` on all 358 spans on the disc.
pub o_class: u32,
/// A count. Meaning not established.
pub count: u32,
/// Offset of the geometry data from the start of the span. Lands inside
/// the span on all 358 spans on the disc.
pub data_offset: u32,
}
/// Bytes per body record.
///
/// Each record is 6 varying bytes followed by 2 constant ones. Found by
/// grouping sibling spans and byte-diffing them rather than by stride-fitting,
/// which failed three times: the varying payload is **not** sized by the
/// `count` field, and was a constant 144 bytes per group whether `count` was
/// 1, 2, 3, 5 or 6.
pub const SHRUB_BODY_RECORD_BYTES: usize = 8;
/// The two constant bytes closing each body record.
pub const SHRUB_RECORD_TAIL: [u8; 2] = [0, 0];
/// A shrub span: its header plus the bytes the header points at.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Shrub<'a> {
pub header: ShrubHeader,
/// The whole span, starting at `+0x24` with the class number.
pub raw: &'a [u8],
/// The bytes at `data_offset`, running to `span_end`. Not yet decoded.
pub data: &'a [u8],
/// Whether `span_end` came from the next class rather than from the end of
/// the lump. False for the last class in each level, whose span contains
/// whatever follows shrub.
pub span_end_is_a_successor: bool,
}
impl<'a> Shrub<'a> {
/// The body's 8-byte records.
///
/// The payload of each record is six bytes whose encoding is **not**
/// established. It is not half-floats -- decoding as a `half3` gives
/// values like 245.25 and -168.375 -- and a VIF reading of it was
/// retracted after a control put it on the random baseline. Handed back
/// raw rather than guessed at.
pub fn body_records(&self) -> impl Iterator<Item = &'a [u8]> {
self.data.chunks_exact(SHRUB_BODY_RECORD_BYTES)
}
/// How many whole records the body holds.
pub fn body_record_count(&self) -> usize {
self.data.len() / SHRUB_BODY_RECORD_BYTES
}
/// Whether every whole record ends in the constant pair.
///
/// This is how a class's body is delimited: on all 24 levels that have
/// shrub geometry, the last shrub class's body ends exactly where its
/// trailing `00 00` pair stops, always on a record boundary.
pub fn body_is_well_formed(&self) -> bool {
let records: Vec<&[u8]> = self.body_records().collect();
!records.is_empty() && records.iter().all(|r| r[6..8] == SHRUB_RECORD_TAIL)
}
}
impl ShrubHeader {
/// Parse a shrub span starting at `offset` within `core_data`.
///
/// `o_class` is passed in so the self-identifying field can be checked
/// rather than merely reported: a span whose `+0x24` disagrees with its
/// own class entry is a different thing entirely, and quietly returning
/// the mismatched value would hide that.
/// `span_end` bounds the class span within `core`. It matters: without it
/// `data` runs to the end of the lump and swallows whatever follows, which
/// is how a per-class check ends up measuring its neighbours.
pub fn region<'a>(
core: &'a [u8],
offset: i32,
o_class: i32,
span_end: usize,
) -> Result<Option<Shrub<'a>>> {
if offset <= 0 {
return Ok(None);
}
let start = offset as usize;
let head = core.get(start..).filter(|s| s.len() >= 0x30).ok_or(Error {
offset: start,
context: "shrub span is too short to hold a header",
kind: ErrorKind::OutOfBounds {
needed: 0x30,
available: core.len().saturating_sub(start),
},
})?;
let mut fb = Buf::new(&head[..16]);
let bounds = [
fb.f32("shrub bounds 0")?,
fb.f32("shrub bounds 1")?,
fb.f32("shrub bounds 2")?,
fb.f32("shrub bounds 3")?,
];
let mut fs = Buf::new(&head[0x10..0x18]);
let size = fs.f32("shrub size")?;
let reserved = fs.f32("shrub reserved")?;
if !bounds.iter().all(|v| v.is_finite()) || !size.is_finite() || !reserved.is_finite() {
return Err(Error {
offset: start,
context: "shrub header holds a non-finite float",
kind: ErrorKind::Invalid("shrub header float"),
});
}
// Zero, or the denormal the PS2 would flush to zero anyway. Anything
// else means the header is not laid out the way this thinks it is.
if reserved != 0.0 && !reserved.is_subnormal() {
return Err(Error {
offset: start + 0x14,
context: "shrub reserved word is neither zero nor denormal",
kind: ErrorKind::Invalid("shrub reserved"),
});
}
let mut b = Buf::new(&head[0x24..]);
let span_class = b.u32("shrub o_class")?;
let header = ShrubHeader {
bounds: [0.0; 4],
size: 0.0,
reserved: 0.0,
o_class: span_class,
count: b.u32("shrub count")?,
data_offset: b.u32("shrub data offset")?,
};
if header.o_class != o_class.max(0) as u32 {
return Err(Error {
offset: start + 0x24,
context: "shrub span does not name its own class",
kind: ErrorKind::Invalid("shrub o_class disagrees with the class entry"),
});
}
let at = header.data_offset as usize;
let stop = span_end.clamp(start, core.len());
let data = core.get(start + at..stop).ok_or(Error {
offset: start + at,
context: "shrub data offset runs past the lump",
kind: ErrorKind::OutOfBounds {
needed: at,
available: core.len().saturating_sub(start),
},
})?;
Ok(Some(Shrub {
span_end_is_a_successor: true,
header: ShrubHeader {
bounds,
size,
reserved,
o_class: header.o_class,
count: header.count,
data_offset: header.data_offset,
},
raw: &core[start..],
data,
}))
}
}
#[cfg(test)]
mod tests {
use super::*;
/// Where the span sits in the fake lump. Offset 0 means "absent", so the
/// span cannot start there.
const BASE: usize = 0x10;
/// A span whose `+0x24` names class 1072 and whose data starts at 0x30.
fn span() -> Vec<u8> {
let mut v = vec![0u8; BASE + 0x40];
v[BASE + 0x24..BASE + 0x28].copy_from_slice(&1072u32.to_le_bytes());
v[BASE + 0x28..BASE + 0x2c].copy_from_slice(&5u32.to_le_bytes());
v[BASE + 0x2c..BASE + 0x30].copy_from_slice(&0x30u32.to_le_bytes());
v[BASE + 0x30..].fill(0xab);
v
}
#[test]
fn reads_the_self_identifying_header() {
let c = span();
let s = ShrubHeader::region(&c, BASE as i32, 1072, c.len())
.unwrap()
.expect("present");
assert_eq!(s.header.o_class, 1072);
assert_eq!(s.header.count, 5);
assert_eq!(s.header.data_offset, 0x30);
assert!(s.data.iter().all(|&b| b == 0xab));
}
#[test]
fn a_span_naming_the_wrong_class_is_rejected() {
let c = span();
let err = ShrubHeader::region(&c, BASE as i32, 999, c.len()).unwrap_err();
assert!(format!("{err:?}").contains("disagrees"));
}
#[test]
fn a_zero_offset_means_absent_not_present() {
let c = span();
assert!(ShrubHeader::region(&c, 0, 1072, c.len()).unwrap().is_none());
assert!(ShrubHeader::region(&c, -1, 1072, c.len())
.unwrap()
.is_none());
}
#[test]
fn body_records_split_into_eights_and_check_their_tail() {
let c = span();
let s = ShrubHeader::region(&c, BASE as i32, 1072, c.len())
.unwrap()
.expect("present");
// The fixture fills 0xab, so every record's tail is wrong.
assert!(!s.body_is_well_formed());
assert_eq!(s.body_record_count(), (0x40 - 0x30) / 8);
// Fix the tails and it is well formed.
let mut c2 = span();
for i in 0..((0x40 - 0x30) / 8) {
c2[BASE + 0x30 + i * 8 + 6] = 0;
c2[BASE + 0x30 + i * 8 + 7] = 0;
}
let s2 = ShrubHeader::region(&c2, BASE as i32, 1072, c2.len())
.unwrap()
.expect("present");
assert!(s2.body_is_well_formed());
assert_eq!(s2.body_records().count(), s2.body_record_count());
}
#[test]
fn a_body_shorter_than_one_record_is_not_well_formed() {
let mut c = span();
// Point the data at the last four bytes of the span.
c[BASE + 0x2c..BASE + 0x30].copy_from_slice(&(0x3cu32).to_le_bytes());
let s = ShrubHeader::region(&c, BASE as i32, 1072, c.len())
.unwrap()
.expect("present");
assert_eq!(s.body_record_count(), 0);
assert!(!s.body_is_well_formed());
}
#[test]
fn out_of_range_is_an_error_not_a_panic() {
// Offset 0 is absent, not an error -- an empty lump is just empty.
assert!(ShrubHeader::region(&[], 0, 1, 0).unwrap().is_none());
assert!(ShrubHeader::region(&[], 4, 1, 8).is_err());
// Data offset past the end of the lump.
let mut c = span();
c[BASE + 0x2c..BASE + 0x30].copy_from_slice(&9_000u32.to_le_bytes());
assert!(ShrubHeader::region(&c, BASE as i32, 1072, c.len()).is_err());
// Span starting near the end, with no room for a header.
assert!(ShrubHeader::region(&c, (c.len() - 4) as i32, 1072, c.len()).is_err());
}
}