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:
@@ -1123,12 +1123,58 @@ three classes on different levels carry identical `fade`, floats, K and scale
|
||||
blobs themselves are shared between levels, which is the same behaviour the
|
||||
chunk container showed with its byte-identical tables.
|
||||
|
||||
### The packets: the mesh geometry is in the blob (2026-10-11, later)
|
||||
|
||||
The open question above -- where the mesh geometry lives -- answered itself
|
||||
once the running game was consulted. The running PCSX2 loads shrub classes
|
||||
into RAM, and diffing a loaded blob against its disc bytes shows what the
|
||||
loader rewrites:
|
||||
|
||||
- **`+0x40` holds a table of `count` entries of `(offset, size)`**, both
|
||||
relative to the span start on disc, rewritten to absolute addresses in RAM
|
||||
with the sizes unchanged. Verified on level 1 class 1: all 21 entries match
|
||||
the disc offset for offset and the size for size after adding the blob's
|
||||
RAM base.
|
||||
- **`+0x2c` (the palette offset) is relocated** the same way, to an absolute
|
||||
pointer.
|
||||
- **`+0x18` -- zero on disc, asserted by the parser -- is written with a
|
||||
pointer at load**, to a table of 0x20-byte records that look instance-shaped
|
||||
(a position, a size, a count, and a `0xffff` light slot). Not established;
|
||||
recorded because it is where the next question starts.
|
||||
|
||||
So the blob is header, packet table, packets, [billboard], palette -- the same
|
||||
shape as R&C1's, and the mesh was here the whole time. The reason it was
|
||||
missed is worth writing down: **`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 bound that
|
||||
is correct and a body that is small enough to look plausible are together
|
||||
enough to hide a region.
|
||||
|
||||
**Range measured:** every shrub class span on the disc -- 358 spans over 24
|
||||
levels, each bounded by its successor class or the located chunk container.
|
||||
|
||||
**Method:** for each span, read `count` from `+0x28`, then the entries at
|
||||
`+0x40`, and require them to tile: the first packet starts at
|
||||
`align16(0x40 + count * 8)` -- the table, rounded up to 16 -- every entry is
|
||||
exactly `size` past the previous one, and the last ends at the palette, or
|
||||
exactly 0x40 short of it on the spans that carry a billboard record in
|
||||
between.
|
||||
|
||||
**Result: 358 of 358 tile exactly, holding 2,239 packets.** No span's
|
||||
deviations: the first-packet offsets, the sizes and the ends all agree with
|
||||
the rule. That is what pins the field meanings -- `count` is the **packet
|
||||
count** (1..55 on the disc), and `data_offset` is the palette's offset -- and
|
||||
it is why the earlier "the body is not sized by count" observation was true
|
||||
for a reason that had not been understood: the body is not sized by the count
|
||||
because the count sizes the packets.
|
||||
|
||||
`gc_formats::shrub::Shrub::packets()` exposes them, and returns `None` rather
|
||||
than a partial list when a table does not tile.
|
||||
|
||||
### Still open
|
||||
|
||||
- The three billboard floats, `size`, `mode_bits`, and `count`.
|
||||
- **Where shrub mesh geometry lives.** R&C1 keeps the class's mesh packets in
|
||||
the blob between the header and the palette; R&C2's blob is header, palette
|
||||
and optional billboard only -- 24 normals and nothing else. The 333 classes
|
||||
without a billboard still have to draw a mesh, and it is not here. The
|
||||
candidates are the chunk container that follows shrub and the gameplay
|
||||
extent, and neither is decoded.
|
||||
- The three billboard floats, `size`, and `mode_bits` (4 on 16 spans).
|
||||
- The packet contents. They are VIF command lists in R&C1; the R&C2 packets
|
||||
have not been decoded, and the VIF reading of a shrub body was already
|
||||
retracted once, so this needs a control, not a pattern match.
|
||||
- The 0x20-byte records the loader points at from `+0x18.
|
||||
|
||||
Reference in New Issue
Block a user