Parse the shrub packet header, a fixed signature on all 2056 disc packets
The packets found by the +0x40 table have a header the whole disc agrees on. Range measured: every packet on the disc -- 2056 of them, across the 334 successor-bounded spans -- read at each packet's span-relative offset. bytes 0-3 04 04 00 01 2056 / 2056 bytes 4-7 zero 2056 / 2056 byte 8 zero 2056 / 2056 byte 11 0x05 2056 / 2056 size % 16 zero 2056 / 2056 Zero exceptions on any of the five. And the loader fills the rest in. Read back out of the running game the same records carry byte 8 as a count (0x0b to 0x29 observed), byte 11 as one of four tag values, and bytes 12-15 as an absolute EE RAM pointer where the disc has a span-relative offset. Bytes 8-15 are rewritten wholesale on load. That settles what the disc header is for. It carries no count of its own, and the count the RAM copy has is the +0x40 table's count arriving by another route. The disc header is a signature; the RAM header is populated state. ShrubPacketHeader::parse and Shrub::packet_headers() expose this. parse returns None unless all the invariants hold, rather than a default, so a packet that does not match is reported as not-understood instead of half-interpreted. The gated test asserts all four header invariants and the size alignment on every packet of every bounded span. This is the first piece of the mesh packet format represented in gc-formats; everything known about the packet payloads so far is RAM measurement, since the disc form of bytes 8-15 is rewritten at load.
This commit is contained in:
@@ -1664,6 +1664,7 @@ fn shrub_headers_name_their_own_class_on_every_level() {
|
||||
let mut billboards = 0usize;
|
||||
let mut billboard_entries = 0usize;
|
||||
let mut packets_total = 0usize;
|
||||
let mut packet_headers_total = 0usize;
|
||||
|
||||
for n in 0..27 {
|
||||
let dir = data.join(format!("level{n}"));
|
||||
@@ -1803,6 +1804,30 @@ fn shrub_headers_name_their_own_class_on_every_level() {
|
||||
);
|
||||
packets_total += packets.len();
|
||||
|
||||
// The packet headers themselves. The disc form is a fixed
|
||||
// signature -- magic, four zero bytes, tag 0x05 -- and the
|
||||
// loader rewrites bytes 8-15 on load, so the count and the
|
||||
// pointer live in RAM, not here.
|
||||
let hdrs: Vec<_> = s.packet_headers().collect();
|
||||
assert_eq!(
|
||||
hdrs.len(),
|
||||
packets.len(),
|
||||
"level{n} class {i}: header count disagrees with packet count"
|
||||
);
|
||||
for (pi, h) in hdrs.iter().enumerate() {
|
||||
assert!(
|
||||
h.is_some(),
|
||||
"level{n} class {i}: packet {pi} header does not match the \
|
||||
disc invariants (magic 04 04 00 01, bytes 4-8 zero, tag 0x05)"
|
||||
);
|
||||
assert_eq!(
|
||||
packets[pi].size % 16,
|
||||
0,
|
||||
"level{n} class {i}: packet {pi} size is not a multiple of 16"
|
||||
);
|
||||
}
|
||||
packet_headers_total += hdrs.len();
|
||||
|
||||
// The billboard: present exactly when the class entry
|
||||
// carries billboard texture fields.
|
||||
let entry_has_billboard = e.billboard_width != 0 || e.billboard_height != 0;
|
||||
@@ -1856,7 +1881,7 @@ fn shrub_headers_name_their_own_class_on_every_level() {
|
||||
all {bounded_bodies} bodies bounded, {bounded_records} records, \
|
||||
{palette_records} of them palette normals and {padding_records} zero padding; \
|
||||
{billboards} billboards, all with texture fields; \
|
||||
{packets_total} packets across all blobs; \
|
||||
{packets_total} packets across all blobs, {packet_headers_total} headers; \
|
||||
mode_bits {mode_hist:?}, radius {radius_range:?}, scale {scale_range:?}, \
|
||||
largest palette deviation from unit length {norm2_deviation} of a possible 65535"
|
||||
);
|
||||
|
||||
@@ -202,6 +202,70 @@ pub struct Shrub<'a> {
|
||||
pub span_end_is_a_successor: bool,
|
||||
}
|
||||
|
||||
/// The 16-byte header at the front of a mesh packet.
|
||||
///
|
||||
/// Measured on **every packet on the disc** -- 2,056 of them, across the 334
|
||||
/// successor-bounded spans -- the header is almost entirely constant:
|
||||
///
|
||||
/// | bytes | value | packets |
|
||||
/// |---|---|---|
|
||||
/// | 0-3 | `04 04 00 01` | 2056 / 2056 |
|
||||
/// | 4-7 | zero | 2056 / 2056 |
|
||||
/// | 8 | zero | 2056 / 2056 |
|
||||
/// | 11 | `0x05` | 2056 / 2056 |
|
||||
///
|
||||
/// and every packet's size is a multiple of 16 (2056 / 2056).
|
||||
///
|
||||
/// **What the loader does to it.** Read back out of the running game's memory,
|
||||
/// the same records have byte 8 holding a **count** (0x0b to 0x29 observed),
|
||||
/// byte 11 holding one of four tag values, and bytes 12-15 holding an absolute
|
||||
/// EE RAM pointer where the disc has a span-relative offset. So bytes 8-15 are
|
||||
/// rewritten wholesale on load, and the count this header does *not* carry on
|
||||
/// disc is the one the `+0x40` table already supplies.
|
||||
///
|
||||
/// That is why the disc header is worth asserting and the RAM one is not: the
|
||||
/// disc form is a fixed signature, the RAM form is populated state.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct ShrubPacketHeader {
|
||||
/// The constant leading magic, `04 04 00 01`.
|
||||
pub magic: [u8; 4],
|
||||
/// The tag byte at 11. `0x05` on every packet on the disc; rewritten on
|
||||
/// load to one of `0x00`, `0x20`, `0x30`, `0xbc`.
|
||||
pub tag: u8,
|
||||
}
|
||||
|
||||
/// The magic every mesh packet opens with, on every span on the disc.
|
||||
pub const SHRUB_PACKET_MAGIC: [u8; 4] = [0x04, 0x04, 0x00, 0x01];
|
||||
|
||||
/// The tag byte every mesh packet carries on the disc, before the loader
|
||||
/// replaces it.
|
||||
pub const SHRUB_PACKET_TAG: u8 = 0x05;
|
||||
|
||||
impl ShrubPacketHeader {
|
||||
/// Parse a packet header, bounds-checked against `bytes`.
|
||||
///
|
||||
/// Returns `None` unless the magic matches, bytes 4-8 are zero and the tag
|
||||
/// is [`SHRUB_PACKET_TAG`] -- the three invariants that hold on 2,056 of
|
||||
/// 2,056 packets. Rejecting anything else is deliberate: a packet that does
|
||||
/// not match is not one this parser understands, and passing it back
|
||||
/// half-understood is how a wrong reading gets treated as a finding.
|
||||
pub fn parse(bytes: &[u8]) -> Option<ShrubPacketHeader> {
|
||||
if bytes.len() < 16 || bytes[..4] != SHRUB_PACKET_MAGIC {
|
||||
return None;
|
||||
}
|
||||
if bytes[4..9].iter().any(|&b| b != 0) {
|
||||
return None;
|
||||
}
|
||||
if bytes[11] != SHRUB_PACKET_TAG {
|
||||
return None;
|
||||
}
|
||||
Some(ShrubPacketHeader {
|
||||
magic: SHRUB_PACKET_MAGIC,
|
||||
tag: bytes[11],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// One mesh packet: where it sits in the span and how long it is.
|
||||
///
|
||||
/// The offsets are relative to the start of the span, like every other
|
||||
@@ -268,6 +332,24 @@ impl<'a> Shrub<'a> {
|
||||
(cursor == expected_end).then_some(out)
|
||||
}
|
||||
|
||||
/// The mesh packets' headers, parsed.
|
||||
///
|
||||
/// Yields `None` for any packet whose header does not match the disc
|
||||
/// invariants, so a caller can tell a bad packet from a good one rather
|
||||
/// than receiving a default.
|
||||
pub fn packet_headers(&self) -> impl Iterator<Item = Option<ShrubPacketHeader>> + 'a {
|
||||
self.packets()
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(move |p| {
|
||||
self.raw
|
||||
.get(p.offset..p.offset + 16)
|
||||
.and_then(ShrubPacketHeader::parse)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.into_iter()
|
||||
}
|
||||
|
||||
/// The palette: the first [`SHRUB_NORMAL_COUNT`] records, parsed.
|
||||
///
|
||||
/// Returns fewer only if the body is shorter than the palette, which does
|
||||
|
||||
@@ -1263,3 +1263,36 @@ PS2 primitive lists, which is the obvious next reference to bring in.
|
||||
VIF stream: the 16-byte tiling is uniform across all 29 packets with no
|
||||
command-shaped tail, and a VIF packet would show a changing command byte in its
|
||||
last position rather than the constant `6c` at header byte 15.
|
||||
|
||||
## Shrub packet headers: a fixed disc signature, rewritten on load
|
||||
|
||||
The packets found by the `+0x40` table have a header the whole disc agrees on.
|
||||
|
||||
**Range measured:** every packet on the disc -- **2,056 of them**, across the
|
||||
334 successor-bounded spans -- read at each packet's span-relative offset.
|
||||
|
||||
**Result:**
|
||||
|
||||
| bytes | value | packets |
|
||||
|---|---|---|
|
||||
| 0-3 | `04 04 00 01` | **2056 / 2056** |
|
||||
| 4-7 | zero | **2056 / 2056** |
|
||||
| 8 | zero | **2056 / 2056** |
|
||||
| 11 | `0x05` | **2056 / 2056** |
|
||||
|
||||
Every packet's size is also a multiple of 16 (2056 / 2056). Zero exceptions on
|
||||
any of the four.
|
||||
|
||||
**And the loader fills the rest in.** Read back out of the running game, the
|
||||
same records carry byte 8 as a **count** (0x0b to 0x29 observed), byte 11 as
|
||||
one of four tag values, and bytes 12-15 as an absolute EE RAM pointer where the
|
||||
disc has a span-relative offset.
|
||||
|
||||
So **bytes 8-15 are rewritten wholesale on load.** What that means for reading
|
||||
the disc: the header carries no count of its own, and the one the RAM copy has
|
||||
is the `+0x40` table's `count` arriving by another route. The disc header is a
|
||||
signature; the RAM header is populated state.
|
||||
|
||||
`ShrubPacketHeader::parse` and `Shrub::packet_headers()` expose this, and the
|
||||
gated test asserts all four invariants plus the size alignment on every packet
|
||||
of every bounded span.
|
||||
|
||||
Reference in New Issue
Block a user