The shrub mesh geometry is in the class blob after all
The open question from the shrub decode was where the mesh lives: the blob looked like header, palette and optional billboard, with the 333 classes without a billboard having nothing to draw. Answering it took the running game, not the disc. Diffing a loaded shrub class against its disc bytes in the live PCSX2 (level 1, class 1) shows what the loader rewrites: a table at +0x40 of (offset, size) pairs, whose offsets are span-relative on disc and absolute in RAM with the sizes unchanged -- all 21 entries matching offset for offset and size for size -- plus a relocated palette pointer at +0x2c and a pointer at +0x18, zero on disc, that asserts the parser enforces. So the blob is header, packet table, packets, [billboard], palette: the mesh was here the whole time. Why it was missed is the part worth recording -- data_offset points at the END of the span, so reading the body as data_offset..span_end accounts for the last few hundred bytes and leaves the 20 KB in front of it unexamined. A correct bound and a body small enough to look plausible are together enough to hide a region. Range measured: every shrub span on the disc, 358 over 24 levels, each bounded by its successor class or the located chunk container. Method: read count at +0x28, then the entries at +0x40, and require them to tile -- the first packet at align16(0x40 + count*8), every entry exactly size past the previous, and the last ending at the palette, or 0x40 short of it on the 25 spans that carry a billboard. Result: 358 of 358 tile exactly, holding 2,239 packets. That is what pins the field meanings: count is the packet count (1..55 on the disc), data_offset is the palette's offset, and the earlier "the body is not sized by count" note was true for a reason nobody had understood -- the count sizes the packets. Shrub::packets() exposes them and returns None rather than a partial list when a table does not tile. Still open: the packet contents (a VIF reading was already retracted once, so this needs a control rather than a pattern match), and the 0x20-byte records the loader points at from +0x18.
This commit is contained in:
@@ -1663,6 +1663,7 @@ fn shrub_headers_name_their_own_class_on_every_level() {
|
||||
let mut norm2_deviation = 0i64;
|
||||
let mut billboards = 0usize;
|
||||
let mut billboard_entries = 0usize;
|
||||
let mut packets_total = 0usize;
|
||||
|
||||
for n in 0..27 {
|
||||
let dir = data.join(format!("level{n}"));
|
||||
@@ -1787,6 +1788,21 @@ fn shrub_headers_name_their_own_class_on_every_level() {
|
||||
}
|
||||
padding_records += s.padding_record_count();
|
||||
|
||||
// The packets: the table at +0x40 tiles the span from
|
||||
// align16 of its own end to the palette, or to the
|
||||
// billboard when the class has one. This is what puts the
|
||||
// mesh geometry in the blob, and it is the identity that
|
||||
// makes `count` the packet count rather than a guess.
|
||||
let packets = s.packets().unwrap_or_else(|| {
|
||||
panic!("level{n} class {i}: packet table does not tile")
|
||||
});
|
||||
assert_eq!(
|
||||
packets.len(),
|
||||
s.header.count as usize,
|
||||
"level{n} class {i}: packet count disagrees with the header"
|
||||
);
|
||||
packets_total += packets.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;
|
||||
@@ -1840,6 +1856,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; \
|
||||
mode_bits {mode_hist:?}, radius {radius_range:?}, scale {scale_range:?}, \
|
||||
largest palette deviation from unit length {norm2_deviation} of a possible 65535"
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user