Read the shrub header floats, including a denormal the PS2 flushes
Extended the shrub header past the two self-identifying words. The first four words are floats -- finite on all 358 spans -- though they do not behave like a min/max bounding box, which orders correctly on only 146 of 358, so they are recorded as bounds rather than named. The word at +0x10 is positive everywhere, 3.0 to 20.0, exactly 5.0 on 281 of them. The word at +0x14 is the interesting one. It is zero on 342 spans and the bit pattern 04000000 on the other 16, which is a denormal of about 2.8e-45. The Emotion Engine has no denormals, so hardware reads all 358 as zero. That is only the second reading. My probe reported the field as 0.000 everywhere because it printed to three decimals, and the test caught it: level 7 class 7 fails a strict zero check. The lesson is that "exact" in a measurement means exact in the measured precision -- rounding in the output made a real distinction invisible. It also matches AGENTS.md on modelling the FPU rather than correcting it: this field looks like noise on disc and is a constant to the machine that reads it, and the parser accepts either rather than picking one. The shrub body past +0x2c is still not decoded.
This commit is contained in:
@@ -1644,6 +1644,8 @@ fn shrub_headers_name_their_own_class_on_every_level() {
|
||||
let mut levels = 0usize;
|
||||
let mut spans = 0usize;
|
||||
let mut absent = 0usize;
|
||||
let (mut min_size, mut max_size) = (f32::MAX, 0.0f32);
|
||||
let mut denormals = 0usize;
|
||||
|
||||
for n in 0..27 {
|
||||
let dir = data.join(format!("level{n}"));
|
||||
@@ -1667,6 +1669,27 @@ fn shrub_headers_name_their_own_class_on_every_level() {
|
||||
"level{n} class {i}"
|
||||
);
|
||||
assert!(!s.data.is_empty(), "level{n} class {i}: empty shrub data");
|
||||
assert!(
|
||||
s.header.bounds.iter().all(|v| v.is_finite()),
|
||||
"level{n} class {i}: non-finite shrub bounds"
|
||||
);
|
||||
assert!(
|
||||
s.header.size.is_finite() && s.header.size > 0.0,
|
||||
"level{n} class {i}: shrub size {}",
|
||||
s.header.size
|
||||
);
|
||||
// The PS2 has no denormals, so a denormal here and a zero
|
||||
// are the same value to the hardware. Both occur on disc.
|
||||
if s.header.reserved != 0.0 {
|
||||
assert!(
|
||||
s.header.reserved.is_subnormal(),
|
||||
"level{n} class {i}: reserved {} is neither zero nor denormal",
|
||||
s.header.reserved
|
||||
);
|
||||
denormals += 1;
|
||||
}
|
||||
min_size = min_size.min(s.header.size);
|
||||
max_size = max_size.max(s.header.size);
|
||||
spans += 1;
|
||||
}
|
||||
Ok(None) => absent += 1,
|
||||
@@ -1684,5 +1707,8 @@ fn shrub_headers_name_their_own_class_on_every_level() {
|
||||
spans + absent
|
||||
);
|
||||
assert!(spans > 340, "only {spans} shrub spans with geometry");
|
||||
eprintln!("shrub: {spans} headers naming their own class, {absent} without geometry");
|
||||
eprintln!(
|
||||
"shrub: {spans} headers naming their own class, {absent} without geometry; \
|
||||
size field {min_size}..{max_size}; {denormals} denormal reserved words"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,9 +16,7 @@
|
||||
//!
|
||||
//! 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`, and the header's leading bytes
|
||||
//! are packed rather than float despite `+0x10` landing exactly on 5.0f on 281
|
||||
//! of 358 spans.
|
||||
//! 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
|
||||
@@ -30,8 +28,22 @@ use crate::buf::{Buf, Error, ErrorKind, Result};
|
||||
pub const SHRUB_CLASS_ENTRY_BYTES: usize = 0x30;
|
||||
|
||||
/// The decoded part of a shrub class header.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[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,
|
||||
@@ -43,7 +55,7 @@ pub struct ShrubHeader {
|
||||
}
|
||||
|
||||
/// A shrub span: its header plus the bytes the header points at.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Shrub<'a> {
|
||||
pub header: ShrubHeader,
|
||||
/// The whole span, starting at `+0x24` with the class number.
|
||||
@@ -72,9 +84,40 @@ impl ShrubHeader {
|
||||
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 {
|
||||
o_class: b.u32("shrub o_class")?,
|
||||
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")?,
|
||||
};
|
||||
@@ -97,7 +140,14 @@ impl ShrubHeader {
|
||||
},
|
||||
})?;
|
||||
Ok(Some(Shrub {
|
||||
header,
|
||||
header: ShrubHeader {
|
||||
bounds,
|
||||
size,
|
||||
reserved,
|
||||
o_class: header.o_class,
|
||||
count: header.count,
|
||||
data_offset: header.data_offset,
|
||||
},
|
||||
raw: &core[start..],
|
||||
data,
|
||||
}))
|
||||
|
||||
Reference in New Issue
Block a user