main
252
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
86ff2e6052 |
Measure tie spans: a constant first word and three quantized floats
Range measured: all 1864 tie spans across 27 levels, read at each class entry's
geometry offset.
Method: read +0x00, +0x0c and the three f32 at +0x10, +0x14, +0x18, and count
distinct values per position.
Result:
u32@+0x00 takes exactly one value across all 1864 spans: 0x00000100, 256
+0x0c takes 83 distinct values, so it is not constant
the three floats take 103 distinct triples built from 42 distinct values,
and those values are round -- 4, 8, 9, 10, 12, 15, 16, 18, 20, 24, 25, 30
with only 10 of the 42 exact powers of two
Two level 0 spans open:
+000 00000100 00005070 000065c0 07050612
+010 41400000 41c80000 42200000 00009460
+000 00000100 00007ca0 0000a040 0605091e
+010 41400000 41c80000 42200000 0000ddf0
This rules out the bounding-box reading. A box would take continuous values;
these come from a 42-value set of round numbers, so they are authored or
quantised quantities -- a grid, a size class, or LOD thresholds -- and nothing
measured separates those.
Correction: these floats were previously read as a bounding-box minimum, from
level 0's first two spans happening to share (12.0, 25.0, 40.0). That triple
holds on 266 of 1864 spans, not all of them, and the value set is quantised.
Not decoded: what the three floats are, what 0x100 at +0x00 counts, and what
the 83 values at +0x0c are.
|
||
|
|
32a24574be |
Identify data.bin as the level file descriptor, not geometry
data.bin is the largest per-level file -- 13.3 MB on level 0, 4.2 to 21.9 MB across the disc -- and had been characterised only as "4-byte structured". Range measured: the first 64 u32 of data.bin on levels 0, 1, 2 and 5, compared against the actual byte lengths of the other extracted level files. Method: read the first 64 u32 and look for any equal to the size of core_index.bin, gs_ram.bin, overlay.bin, gameplay.bin, sound_bank.bin or core_data.bin. Result: three exact matches, at the same indices on every level. u32 index 1 overlay.bin 2751268 2763668 2928424 2863676 u32 index 3 core_index.bin 31600 34016 33808 23536 u32 index 5 gs_ram.bin 518656 569344 513024 434176 Each is the exact byte length of the named file and each lands on the same index on all four levels. Three coincidences at fixed positions is not a coincidence. So data.bin is a descriptor for the level's other files, not geometry in its own right. The roadmap's "the large one: 4.2 to 21.9 MB of geometry per level" entry was wrong and is corrected. The intervening indices 0, 2 and 4 hold values that are not file sizes and their meaning is not established; they are plausibly offsets, but no cumulative-sum or offset+size relation has been checked yet. What follows the descriptor is still unknown, and the file is large enough that the descriptor is unlikely to be all of it. |
||
|
|
5610d173b0 |
Narrow the collision header to two fields plus a third used by five levels
Range measured: all 27 levels that carry collision, reading the 0x40-byte
header each.
Method: read +0x00, +0x04 and +0x08, then check +0x08..+0x40 for zero.
Result:
u32@+0x00 is 0x40 on 27 of 27 -- the header is self-sizing
u32@+0x04 is the payload size, 20 distinct values across the disc
bytes +0x08..+0x40 zero on 22 of 27
Five levels carry a large non-zero word at +0x08:
level 7 1248048 level 12 2541616
level 9 1766832 level 13 3376592
level 14 4060640
So the header is not two fields and 56 bytes of padding: there is a third field
most levels leave at zero and five use. Whether it is a size, an offset or a
second block's length is not established, and nothing measured here separates
those readings. Exposed as CollisionHeader::field_08.
Correction: the collision header was previously described as self-describing
across its full 0x40, which is true for 22 levels and overstates the case for
the other five.
Still not decoded: the payload. It contains ascending u32 runs on levels 0 and
2 -- level 0 at +280 with deltas 12, 12, 12 then a jump, level 2 at +13868 with
deltas 204, 204, 200, 196 -- but irregular deltas fit neither a flat table nor a
simple stride, and level 1's apparent run is a byte-shift false positive.
Nothing about the payload is asserted.
|
||
|
|
51348b214d |
Report the sky reserved-word count in the test output
20 of 149 sky sub-objects carry a non-zero word where the other 129 are zero, so the reserved area is not uniformly padding. Reporting it rather than asserting it, and clearing three clippy warnings from the unused counter. |
||
|
|
681e7e62ef |
Parse the sky block: 149 sub-objects with a bounding sphere, and correct the count field
Range measured: all 27 levels, each sky region from the core index's sky offset to the next region. Method: read the 32-bit offset table at +0x20, bounded by staying inside the region, and parse each sub-object it names. Result: 149 sub-objects across 27 levels. Each opens with a count, then a bounding sphere of four floats at +0x10, then a texture offset at +0x20 and dimensions at +0x24/+0x26: +000: 26 00 00 00 | 00 00 00 00 00 00 00 00 00 00 00 00 +010: e0 8d b2 c1 | 9f d6 f6 3d | ef fc fb be | 1c 61 4e 41 +020: 90 99 05 00 | 1d 00 | 1d 00 | 00 00 e8 00 ... Count 1 to 38, width 3 to 81, radius 1.95 to 14.56, and the radius is positive on 149 of 149 -- the same sphere shape and the same reasoning as the shrub class header's. Correction to an earlier claim: the word at +0x0c is not the sub-object count. It reads 5 on level 0 where the table holds 6 entries, 3 on level 1 against 4, 7 on level 3 against 8, and 196617 on level 2 against 4. Across the disc offsets.len() == count+1 holds on 10 of 27 levels. The field is reported as SkyHeader::field_0c and nothing depends on it; the offset table enumerates the sub-objects and is self-limiting. That earlier claim came from level 0 alone, where 5 against 6 looked like a consistent off-by-one. It is not an off-by-one -- it is a field that happens to be small on most levels and is not a count at all. gc_formats::sky parses the header and the sub-objects. The gated test asserts the sphere is finite, the radius positive where positive at all, the dimensions non-zero, and the sub-object list non-empty on every level. |
||
|
|
bbce492c91 |
Establish that the loader produces the RAM packet forms, not the disc
Everything previously reported about five-block sub-lists and fixed-point
payloads was read out of the running game. Checking the same packets on disc
settles what the disc actually stores.
Range measured: level 0, all 29 packets named by the +0x40 tables of the 6
bounded spans, read at their span-relative offsets; 1636 whole 16-byte records
after the 16-byte header.
Neither RAM form exists on disc:
disc RAM
00 00 00 30 per exactly 1 exactly 5
sub-list packet
blocks at 0/32/64 0 of 10 11 of 11
fixed-point byte3 0 of 19 100%
== 0x80
The 29 packets split 10 sub-list / 19 fixed-point by whether they contain
00 00 00 30 at all, and not one of the ten has the block structure, and not one
of the nineteen has the fixed-point marker.
The disc body is a compact 16-byte record stream:
+000: 04 04 00 01 | 00 00 00 00 | 00 00 00 05 | 00 80 0e 6c
+010: 02 00 00 00 | 05 00 00 00 | 32 00 00 00 | 0e 00 00 00
+020: 09 00 00 00 | 00 40 3e 30 | 12 04 00 00 | 05 00 00 00
+030: 09 00 00 00 | 00 40 3e 30 | 12 04 00 00 | 21 00 00 00
Small u32 fields and a repeating 00 40 3e 30 12 04 00 00. Per position over all
1636 records the same two outliers hold: byte 7 takes 4 distinct values and byte
15 takes 2, against 101 to 153 everywhere else.
Byte 7 is per-record, not per-packet: the first record of all 29 packets has
byte 7 = 0x00. The four-value vocabulary varies within a packet body rather than
tagging the packet, which is why grouping packets by it never separated the two
kinds.
Consequence for Phase 2: the payload is encoded on disc and expanded at load, so
a disc parser can bound it and check its signature but cannot read it as either
RAM form. Decoding it means reversing the expansion, not re-describing what the
loader produced. That is the honest boundary of what is known.
|
||
|
|
5ade83cf0a |
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. |
||
|
|
af0b1aa949 |
Repeat the packet measurements per level, correcting two over-general claims
The five-block result was measured on level 0 only. Rule 3a asks for the same measurement per partition, so it was repeated on level 1's savestate before being written up as a property. Range measured: savestate slot 2 (level 1), RAM 0x22a000..0x28f52f, 415024 bytes, then six of its pointer-bearing records read to count*16. The six sampled pointers split 3/3 between the two payload kinds: 00949430 41 00 00 00 30 sub-list 009496c0 13 69 b0 bf 80 fixed-point 00957970 36 00 00 00 30 sub-list 00957bb0 10 58 7e 8d 80 fixed-point 0095b740 12 37 49 41 80 fixed-point 0095d6d0 57 00 00 00 30 sub-list Taking all six as sub-list gives 3/6 with five blocks. Taking only the three that actually open 00 00 00 30 gives 3/3, with the first three blocks at 0, 32, 64 in all three. The three fixed-point payloads have zero such blocks, as expected. The earlier 3/6 was my sampling, not a counterexample -- the six pointers were never all the same kind of object. Combined: 11 of 11 sub-list payloads across both levels have exactly five blocks, the first three at offsets 0, 32, 64. The fixed-point payloads have an exact signature: byte 3 of every 4-byte group is 0x80 in 100% of cases -- 52/52, 40/40, 48/48 over the first 512 bytes of the three level 1 samples, and the same across all nine level 0 payloads. That separates the two payload kinds without the record tag, which does not select them. Correction: level 1's record tags are not level 0's. The same scan finds 18 04 04 00 01 headers against 19, plus 6 02 01 00 01 against 10, and byte 11 is no longer constant: 0x30 on 10, 0x00 on 5, 0x20 on 2, 0xbc on 1, where level 0 had 0x30 on all 19. "Byte 11 is 0x30, constant" was a level-0 partition result written up as universal; across both levels it takes four values. Fixed-point payload entropy over all nine level 0 payloads is 5.51 to 5.89 bits per byte against 8.0 for uniform, and the mean of the s16 reading varies per payload from -82 to -907 -- structured, and consistent with vertex positions relative to a per-payload origin. Not established: what the five blocks are, and how the fixed-point data decodes to vertices. Its entropy is far below random, so it is compressed or permuted rather than stored plainly, and the permutation table seen once in 0x009376f0 remains unexplained. |
||
|
|
186a700f32 |
Establish the sub-list payload as a fixed five-block shape
All eight sub-list payloads from level 0 were read to their exact extent (count * 16) and compared block by block. Range measured: savestate slot 1, the packets at 0x009376f0, 0x00939c50, 0x0093cc70, 0x00998880, 0x009c19c0, 0x009cf370, 0x009d2da0 and 0x009f5aa0, counts 21 to 41, each truncated to exactly count*16 bytes. Every payload contains exactly five blocks tagged 00 00 00 30, and the first three are always at offsets 0, 32 and 64: 009376f0 22 0, 32, 64, 184, 304 00939c50 36 0, 32, 64, 268, 400 0093cc70 23 0, 32, 64, 200, 288 00998880 36 0, 32, 64, 272, 404 009c19c0 25 0, 32, 64, 196, 300 009cf370 24 0, 32, 64, 204, 316 009d2da0 41 0, 32, 64, 308, 448 009f5aa0 21 0, 32, 64, 176, 280 Blocks 4 and 5 move with the payload; the first three do not. Eight payloads, eight different block-4/5 positions, and 0/32/64 in every case. Per-block fields constant across all eight: block 0 byte 4 varies 0x78..0xcd block 1 byte 4 = 0 in 8/8, trailer 00000000 in 8/8 block 2 byte 4 varies independently, bytes 16-20 zero in 8/8 block 3 byte 4 EQUALS BLOCK 0 in 8/8 block 4 byte 4 = 0 in 8/8 Block 2's word is unrelated to block 0's in every case, so the equality is not every block reading one shared field. Correction: the "byte permutation table" reported in the previous commit does not generalise. It appears in 0x009376f0 at offset 212, but a scan across all eight payloads finds no such run outside that one. It was one payload's block content, not a format feature. The five-block shape, the fixed 0/32/64 offsets and the block-0/block-3 equality are what survive eight-way comparison. Not established: what the five blocks are, why blocks 0 and 3 share a word, and what moves blocks 4 and 5 -- their offsets are not monotone in count (21 gives 280, 36 gives 400 and 404), so something inside the earlier blocks sets them. |
||
|
|
3fa64b7003 |
Read a full sub-list payload: tagged blocks and a byte permutation table
With the extent known from the previous entry (count * 16), a whole sub-list
payload can be read. This is the packet at 0x009376f0, count 22, 352 bytes.
Range measured: savestate slot 1, 0x009376f0..0x0093784f.
The payload is a sequence of blocks, each opening 00 00 00 30, carrying a
repeating word and a short trailer:
00 00 00 30 | cd 00 00 00 cd 00 00 00 cd 00 00 00 cd 00 00 00
01 00 00 05 | 22 c1 01 6e | 00 00 00 00
00 00 00 30 | 00 00 00 45 00 00 00 45 1d 00 00 00 1d 00 00 00 1f
81 01 6d 00 | 00 00 00 00 | 00 00 a4 00
00 00 00 30 | bc c0 03 00 23 17 03 00 e5 dd 00 00 00 00 00 00
02 01 00 01 | c1 80 01 69 | 00 00 00 00
The repeating word differs per block (0xcd, 0x45, 0x03) and the trailer's
second word varies in its high bytes (22 c1 01 6e, 81 01 6d 00, c1 80 01 69).
Every block ends 6e, 69 or 6d at byte 23 -- the same 6e the 02 01 00 01 records
carried at bytes 8-9.
The payload contains a byte permutation table at 0x009377c4:
00 01 02 03 3e 3f 53 54 40 41 42 43 4e 4f 50 51 04 05 06 07 08 ...
Ascending byte values with a handful (3e 3f, 53 54, 40 41 42 43, 4e 4f 50 51)
lifted out of order ahead of the sequential run. Read as a table it maps each
of 0x00..0x39 to a slot -- the shape of a code-length or symbol-ordering table,
and the first thing in this format that is self-evidently a table rather than
data to be interpreted.
Not said: whether it is a Huffman-style code, an index reorder for the vertex
data that follows, or a palette permutation. The sequence continues to 39 then
23 c1 01 6e, after which the next block opens.
Corroboration: the fixed-point payload 0x00937850 begins exactly where this one
ends. The 22-record identity made the boundary exact and the next packet starts
on a payload boundary with nothing in between, so the two payload kinds sit
adjacent in RAM -- consistent with them being two records of the same blob.
|
||
|
|
f1cf6cd164 |
Show the byte-8 count sizes sub-list payloads at exactly 16 bytes each
The previous entry left one test: compare the count at byte 8 against the payload extent. Pointer spacing gives the extent, since the packets lie in ascending order in EE RAM. Range measured: savestate slot 1, the 17 packets in RAM 0x22a000..0x24037f, each with its record address, byte-8 count and pointer, sorted by pointer. Result: eight packets sit exactly count*16 bytes after their predecessor -- a ratio of 16.00 in every one, no rounding. 0x22a6e8 count 22 ptr 009376f0 gap 352 16.00 0x22a728 count 36 ptr 00939c50 gap 576 16.00 0x22a768 count 23 ptr 0093cc70 gap 368 16.00 0x22a7a8 count 36 ptr 00998880 gap 576 16.00 0x22a7e8 count 25 ptr 009c19c0 gap 400 16.00 0x22a828 count 24 ptr 009cf370 gap 384 16.00 0x22a868 count 41 ptr 009d2da0 gap 656 16.00 0x22a8a8 count 21 ptr 009f5aa0 gap 336 16.00 Eight exact hits at a ratio that must be an integer is not luck. The count at byte 8 is the record count, and these payloads are 16 bytes per record. These are exactly the sub-list payloads -- 009376f0, 00939c50, 0093cc70, 00998880 and 009d2da0 were read directly in earlier passes and all open 00 00 00 30 followed by a repeated word. The other nine have ratios of 168, 419, 559, 31288, 7985, 3458, 764 and 6452 -- no shared factor, far too large to be a record count times anything. Either their payload is not followed immediately by the next packet in pointer order, or they are not 16 bytes per record. Ruled out either way: they do not follow the same rule. This resolves byte 8, measured two entries ago as "a count, 0x0b to 0x29" and now shown to size a payload exactly in eight independent cases within one level. Whether it also sizes the fixed-point payloads is the open half. Still open: why the fixed-point packets differ, and what the 16 bytes per sub-list record are -- the repeated word (0xcd, 0xc0, 0xc8, 0x78) differs per packet, so the records are not identical to each other. |
||
|
|
db76319600 |
Find a second packet record magic interleaved with the first
Range measured: savestate slot 1, RAM 0x22a000..0x24037f. Result: a second four-byte record magic appears at comparable density beside 04 04 00 01 -- 02 01 00 01, ten occurrences against nineteen. The two interleave: every 02 01 00 01 sits between two 04 04 00 01 records. 0x22a6b8 04 04 00 01 13 00 00 30 70 6a 93 00 0x22a6c8 02 01 00 01 19 c0 4c 6e 00 00 10 00 00 00 00 00 0x22a6e8 04 04 00 01 16 00 00 30 f0 76 93 00 0x22a6f8 04 04 00 01 16 00 00 30 50 78 93 00 0x22a708 02 01 00 01 1e c0 58 6e 00 00 10 00 00 00 00 00 0x22a728 04 04 00 01 24 00 00 30 50 9c 93 00 The 02 01 00 01 records are 16 bytes like the others but carry no pointer -- bytes 12-15 are zero in all ten. Their fields are a count at 4-5 (0x14, 0x19, 0x1e), a constant 0xc0 at 6, a varying byte at 7 (0x30, 0x4c, 0x54, 0x58), 0x6e at 8-9, and 10 00 at 10-11. The 0x6e at bytes 8-9 matches the ".. c1 01 6e" the sub-list payloads carry at offset 20-23. That is a second correspondence, and unlike the one retracted in the previous commit it is a shared value rather than a shared position. Not settled: this still does not say what discriminates the two payload kinds among the 04 04 00 01 records, since those share the same magic and the same 0x30 at byte 11 and differ only in the count at byte 8. The 02 01 00 01 records sit between them rather than replacing them, so they are a companion structure rather than the missing discriminator. Next: compare the count at byte 8 against the payload's own length. |
||
|
|
2d7de9f13e |
Correct the packet tag mapping: the tag does not select the payload
The previous commit mapped the disc tag vocabulary onto the three payload classes. A wider scan says that is wrong. Range measured: savestate slot 1, RAM 0x22a000..0x24037f -- 91008 bytes, 20 times the span scanned before. Result: 19 packet headers, and all 19 carry byte 11 = 0x30. There are no 0x6d or 0x80 tags anywhere in this range. Reading six payloads: rec 0022a6b8 tag 30 -> 8d 8c 80 80 89 87 7c 80 ... fixed point rec 0022a6e8 tag 30 -> 00 00 00 30 cd 00 00 00 ... sub-list rec 0022a6f8 tag 30 -> 92 90 84 80 94 94 86 80 ... fixed point rec 0022a728 tag 30 -> 00 00 00 30 c0 00 00 00 ... sub-list rec 0022a738 tag 30 -> 4c 4c 4b 80 4c 4c 4b 80 ... fixed point rec 0022a768 tag 30 -> 00 00 00 30 78 00 00 00 ... sub-list Records of identical tag alternate between two payload kinds. The tag does not select the payload. What went wrong: the previous entry took the 0x30 and 0x6d that head the payloads to be the disc-side tag vocabulary, and matched them to payload classes by that correspondence. The disc vocabulary at record byte 7 (0x00, 0x30, 0x6d, 0x80) and RAM record byte 11 are different fields that merely overlap in two values. Three payloads each made the coincidence look like a rule. What is actually established about the RAM header: 0-3 04 04 00 01, constant 4-7 zero, constant 8 a count, 0x0b to 0x29 across the 19 records 9-10 zero, constant 11 0x30, constant on all 19 in this range 12-15 an EE RAM pointer Byte 8 is the finding that survives: a small count varying per record, and the one field here doing arithmetic rather than tagging. Still open: what 0x30 at byte 11 means, what discriminates payload kinds given the tag does not, and 0x80, which has no sample in 91 KB of RAM. |
||
|
|
0b1793d6af |
Map shrub packet tags onto three payload kinds
Dereferenced all nine pointers in the level 0 table that carry the 0x30 tag at byte 11 and an EE pointer at 12..15, reading 32 bytes each. They fall into exactly three classes, three payloads each, with no overlap. A, Q16.15 fixed-point vertex data (3 of 9): every high byte 0x80 or 0x7c, repeating on an 8-byte boundary, values differing per payload while the 4e 4e 4c 80 tail is shared by two. 00936a70 8d 8c 80 80 89 87 7c 80 ... 4e 4e 4c 80 0093cde0 8a 8a 7f 80 92 90 84 80 ... 4e 4e 4c 80 009cf4f0 6e 6f 68 80 6e 6f 68 80 ... 54 54 51 80 B, a 0x30 sub-list with a repeated word (3 of 9), structurally identical apart from that word (0xcd, 0xc0, 0xc8), all carrying 01 00 00 05 at offset 16 and a ".. c1 01 6e" at 20. 009376f0 00 00 00 30 cd 00 00 00 cd 00 00 00 ... 00998880 00 00 00 30 c0 00 00 00 c0 00 00 00 ... 009d2da0 00 00 00 30 c8 00 00 00 c8 00 00 00 ... C, an index/attribute list (3 of 9), both opening 00 c0 05 6d, then a count, then a run of small ascending u16. 00939700 00 c0 05 6d 3e 00 00 00 01 00 00 00 ... 009c1540 00 c0 05 6d 36 00 00 00 01 00 00 00 ... So the tag vocabulary measured on disc at record byte 7 -- 0x00, 0x30, 0x6d, 0x80 -- selects the payload: 0x30 class B, the 00 00 00 30 sub-list 0x6d class C, the 00 c0 05 6d attribute list 0x00 class A, the fixed-point vertex data 0x30 heads the class B payloads and 0x6d heads the class C payloads. That was flagged as a single coincidence two commits back; three payloads each makes it a rule. 0x80 has no sample among these nine and its meaning stays open. Correction: the record spacing in this table is not uniformly 0x30. The find output shows 0x30, 0x10, 0x30 between consecutive packet headers, and only 9 of the 5632 bytes scanned carry both the 0x30 tag and an EE pointer. The earlier "0x30 spacing" was inferred from a truncated hit list. |
||
|
|
ad70e4c4be |
Follow shrub packet pointers: adjacent records hold unrelated payloads
The header pointers found in the previous commit are live, so they can be dereferenced. Two records 0x30 apart in the level 0 table at 0x22a6b8 were followed. Range measured: savestate slot 1, reading 0x00936a70 (from the record at 0x22a6b8) and 0x009376f0 (from the record at 0x22a6e8), 64 bytes each. The two payloads have nothing in common. 0x00936a70 is fixed-point vertex data: 8d 8c 80 80 89 87 7c 80 8d 8c 80 80 89 87 7c 80 4e 4e 4c 80 4e 4e 4c 80 4e 4e 4c 80 4e 4e 4c 80 repeating Every high byte is 0x80 or 0x7c and the pattern repeats on an 8-byte boundary. As s16 these are large negatives in a narrow band around -32500, consistent with Q16.15 fixed point rather than f32, and with a stride of 4 s16 per vertex. 0x009376f0 is a tagged sub-list, not vertex data: 00 00 00 30 cd 00 00 00 cd 00 00 00 cd 00 00 00 cd 00 00 00 01 00 00 05 22 c1 01 6e 00 00 00 00 Structurally different: small integers, a repeating 0xcd, 0x30 tags in the high byte. Nothing here is position data. This supports the type reading of the header: the table is a tagged collection where the tag selects the payload kind, not a uniform array. It also rules out the alternative that the 16-byte tiling was one uniform record format with varying fields -- two adjacent records with unrelated payloads is not that. Still open: the mapping from tag value to payload kind. The 0x30 appearing in record bytes 8-11 in RAM also heads the second payload, which is suggestive but is one coincidence so far and is deliberately not written up as a link. |
||
|
|
3854e52019 |
Show the loader rewrites shrub packet headers into RAM pointers
Answering the "what is the tag byte" question needed an anchor. Searching live RAM from scratch was the wrong way to get one: 04 04 00 01 matches unrelated data at 0x1ac988 and there is no way to tell a real hit from a coincidental one. The savestates are the anchor -- slot 1 is level 0, and searching one gives candidates that can be checked rather than guessed at. Range measured: savestate slot 1, ~/.config/PCSX2/sstates/SCUS-97268 (38996035).01.p2s, records at 0x22a6b8 onward, read with gc-trace read --state. Result: 40+ records beginning 04 04 00 01 00 00 00 00 at 0x30 spacing -- the packet framing identified on disc -- with the second half of each header rewritten: disc: 04 04 00 01 | 00 00 00 00 | 00 00 00 05 | 00 80 0e 6c RAM: 04 04 00 01 | 00 00 00 00 | 13 00 00 30 | 70 6a 93 00 Bytes 0-7 unchanged; bytes 8-15 are not. On disc bytes 12-13 are the constant 00 80 and byte 15 the constant 6c; in RAM bytes 12-15 hold a pointer -- 0x00936a70, 0x009376f0, 0x00937850, 0x00939c50 in consecutive records, all inside EE RAM and ascending across the table as a walk of consecutive objects would be. Byte 11 becomes 0x30 in every record seen, against a constant 0x05 on disc, and bytes 8-9 hold a varying small value (0x13, 0x16, 0x24). So the packet table at +0x40 is what the loader walks: span-relative on disc, absolute in RAM. This was inferred from one class before; these 40+ records show it directly. Consequence for the tag bytes: the vocabularies at record bytes 7 and 15 are real, but byte 15 sits in a field the loader overwrites with a pointer. Its disc values are read as data by something rather than stored as a tag. Not settled: whether byte 7's vocabulary is also rewritten, and whether 0x6d marks payload the loader expands. That needs these records compared one by one against their disc spans, which is cheap now that the anchor exists. |
||
|
|
d1aec9f866 |
Show shrub packet records are two tagged 8-byte halves, fixed length
Range measured: level 0, the 1636 sixteen-byte records following the header across all 29 packets. Per-byte distinct counts, not pooled. Method: count distinct values and zero frequency at each of the 16 positions, then tabulate the joint distribution of the two low-entropy positions. Result: fourteen positions are uniformly varied, 101 to 153 distinct values, zero in 13% to 58% of records. Two are not: byte 7 4 distinct: 0 (1432), 0x30 (135), 0x6d (40), 0x80 (29) byte 15 2 distinct: 0 (1618), 0x6d (18) Both sit in the last byte of an 8-byte half, eight bytes apart, and both take values from a small closed vocabulary rather than the full byte range. The joint distribution shows no record where both are non-zero, and 0x6d appears in either position but never both: (0,0) 1414 (0,0x6d) 18 (0x30,0) 135 (0x6d,0) 40 (0x80,0) 29 Each 8-byte half carries a tag or subtype byte in its last position, from a small closed set. The shared 0x6d is what suggests one vocabulary used twice rather than two unrelated fields. This answers the question the previous commit left open. There is no per-record length field: the best candidate offsets hold the value 16 on at most 13% of records, which is the chance rate for small integers, not a self-describing record. The records are therefore fixed 16 bytes, and the tiling is not a coincidence of variable-length records that happen to be multiples of 16. Still open: what the four and two tag values mean, and what the 14 varied bytes encode. The tag vocabulary is small enough to enumerate against known PS2 primitive lists, which is the reference to bring in next. |
||
|
|
e6dd60ec3a |
Characterise shrub mesh packets: 16-byte header, 16-byte tiling
The packet table decode left the packet contents open, with a warning that a VIF reading had already been retracted once and needed a control rather than a pattern match. This is that measurement. Range measured: level 0, the 29 packets of the 6 successor-bounded shrub spans, 26640 bytes. Per-packet; nothing pooled. Method: autocorrelation within the largest packet against a distribution matched shuffled control, then per-byte distinct counts across all 29 packets. Result 1: all 29 packet sizes are exact multiples of 16, and the internal stride scan peaks at 16 (+0.283) with 32, 48, 64, 80 and 128 all elevated behind it -- the signature of a record array, not a coincidental spacing. Result 2: record 0 is a 16-byte header. Constant across all 29 packets at bytes 0-3 (04 04 00 01), 4-11 (zero), 12-13 (00 80) and 15 (6c). Bytes 14 and 16-23 vary. The 04 04 00 01 prefix looks like a magic or format version. Result 3: header byte 14 is echoed in the body. It takes values 06, 07, 0e and 11 across packets, and the same value appears at offset 12 of record 1 in every packet sampled. A real repeated field, not two small values coinciding. Not established: no header field matches the record count. The packet holds 1056/16 = 66 records and none of the candidate u32 or u16 header offsets at 8, 10, 12, 14, 16, 18 or 20 equals 66, 65 or 67 on more than half the packets. So the count is either computed from a size field elsewhere in the span, or the records are variable length and merely 16-byte aligned. That distinction is unresolved and matters. Fixed 16-byte records make the payload a flat table; variable records make the 16-byte alignment a coincidence of every record being a multiple of 16 long. The stride scan cannot separate the two, because both peak at 16. The retracted VIF reading is not revived: the tiling is uniform across all 29 packets with no command-shaped tail, and a VIF packet would show a changing command byte last rather than the constant 6c at header byte 15. Next: read the record count from the packet table in the span rather than from the packet header, which is what would separate the two readings. |
||
|
|
aa2b14ad66 |
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. |
||
|
|
c83f5104d2 |
Document the PCSX2 test setup
PCSX2 was set up and working but existed only in this machine's filesystem and in people's heads, so every comparison looked like a one-off observation rather than something re-runnable. The setup section in docs/plan/pcsx2-verification.md now records where the emulator, the BIOS, the disc, the savestates and the PINE socket are, how a state is captured, and which command the disc integration tests need -- and AGENTS.md and README.md point at it from their orientation sections. One operational detail worth spelling out: the emulator's directory is lowercase (pcsx2), which is not what people type. |
||
|
|
bbdb51e2f9 |
Verify level 1's tfrag lighting against the game
Savestate slot 2 is the second planet, so the comparison runs on a level with a different geometry extent, light bank and runtime-set population: level1: 401/401 headers, 401/401 pointers, bank byte-identical vertices compared: 26470 (105880 colour bytes) -- equal: 100.00% The live emulator agrees with the savestate byte for byte. Level 1 is a different planet, so the two fixes in the previous commit are not level-0 coincidences: the normal's z sign and the unlit-record base colour both hold across 268,280 colour bytes on two levels. |
||
|
|
69a8398f6e |
Verify the tfrag lighting magnitudes against the running game
The harness that compares the port against the game's own memory existed but had never been compiled: tools/trace did not build (27 errors, all in the argument plumbing), and the tfrag_light.rs light-bank correction it motivated was unverified. This finishes it and runs it, and the run finds two real bugs. Result, on level 0 against three inputs -- the live emulator over PINE and two PCSX2 savestates from the same level, agreeing byte for byte: headers matching the disc on invariant bytes: 833/833 data pointers relocated as table + disc offset: 833/833 light bank in RAM at 0x50ab20, byte-identical to the disc's vertices compared: 40600 (162400 colour bytes) equal: 162400 (100.00%) The locator finds the level's tfrags block in RAM by tfrag 0's bounding sphere and accepts a candidate only when every header matches the disc on the bytes the game leaves alone and every data pointer is the relocated form, so "the block was found" is itself a verification rather than an assumption. The bank is searched in RAM by its exact section bytes, which is what settles its framing: offset at +0x04, an i32 set count, twelve bytes of pad, then the sets. The previous reading (offset at +0x00, count at +0x04) was wrong. Two bugs the comparison found, both of which produced plausible output: 1. vertex_normal_bits negated x and y and left z positive, while NormalTable::normal negates all three. dA came out -0.707 where the game had +0.707, so max(dA, dA*wA) took the wrong branch and every vertex lit near its base colour: dark, but not wrong-dark. 95.88% agreement with the bug, 100.00% without. 2. A record whose select names a set the bank lacks drew transparent black. The game writes the record's base colour; 3,130 of level 0's 40,600 vertices do this (select 0x000f, set 15 -- a runtime slot the level's directional bank does not have). The vertex is unlit, not invisible. Both are now regression tests in the engine. The comparison prints the pass's per-vertex terms (light_vertex_terms) for the first mismatches, because a colour mismatch alone cannot be attributed and the comparison is worthless without attribution -- that is how the second bug was identified from a three-line read rather than guessed at. Docs: docs/plan/pcsx2-verification.md records the harness, the result, the two bugs and what to compare next; roadmap and level-assets.md stop carrying lighting magnitudes as an open item. Findings reach tests only as numbers -- no test reads a savestate, a dump or a live emulator. |
||
|
|
2c7555fe16 |
Decode the shrub class blob: a 24-normal palette and a billboard record
The shrub body's payload encoding was the open question after the span-bound work, and it answered itself once the R&C1 reference was in view: R&C1's shrub class header keeps the field offsets R&C2 shows (o_class at 0x24, a count at 0x28, the data offset at 0x2c), which says which bytes to look at, and the disc says what they hold. Range measured: all 358 shrub class spans over the 24 levels that have them, each bounded by its successor class or the located chunk container. Nothing outside the spans is read. Result. The body is a palette of 24 unit-length s16 normals -- s16 x, y, z and a zero pad word per 8-byte record -- followed by all-zero padding records. Three independent measurements, each of which fails for any other reading: - all 8,592 live entries have squared length within 65,535 of 32767^2, and the bound is tight (the largest observed deviation is exactly 65,535). That is 32768^2 - 32767^2, which is what a unit vector scaled by 32768 produces when an axis-aligned normal quantises to -32768 and the positive axis clamps to 32767; components run the full s16 range and 1,765 entries are axis-aligned. A random s16 triple is off by about a billion, so one entry in fifteen thousand would pass by chance: all 8,592 pass. - the padding records are always at index 24 or later and always all zero, never a live entry. That is what separates the palette from geometry, and it is the observation the three earlier stride-fitting attempts were missing. - 17 classes carry a palette of 24 identical (0, 0, 1) entries. A position or index table cannot produce that; a normal palette for a flat-facing class must. The header decodes as: sphere at +0x00 (centre + radius, radius positive on all 358 spans, 1.72..41.57), size at +0x10, mode_bits and scratch at +0x14, a zero word at +0x18, the billboard offset at +0x1c, and scale at +0x20. The parser asserts the two invariants that hold without exception (scratch and the reserved word are zero on all 358) plus a positive radius and scale, and reports mode_bits rather than asserting it: it is 4 on 16 of the 358 spans. The billboard offset is the strongest self-identification after o_class because it is confirmed from a second file: it is non-zero on exactly the 25 classes whose core-index entry carries billboard texture fields, pairing correctly both ways. At the offset sits R&C1's ShrubBillboard shape, register block and all -- fade_distance, then TEX1_1 (K -233..-168, MMIN 4, address 0x14), TEX0_1 (index 1..4, address 0x06) and MIPTBP1_1 (address 0x34), all three register addresses checked in the disc test. Two corrections this produces, both recorded in docs/plan/level-assets.md because the old readings were confidently wrong rather than merely unverified: - "reserved is 0 or a denormal" had the two u16 halves the wrong way round: the 16 non-zero spans are mode_bits = 4 with scratch = 0. As an f32 the word is still a denormal, so the PS2 reads zero either way -- but the field is a flag word, not a magnitude, which is exactly the FPU point worth making. - "bounds" was tested as a min.xyz/max.xyz box, which holds on only 146 of 358 spans, and the field was left unnamed as a result. It is a bounding sphere: the fourth float is positive on all 358 spans and the centres cluster near the class origin. The 146/358 figure was a property of the wrong question. Still open: the three floats in the billboard record (they repeat byte-identically across levels -- 16 distinct triples among 25 records, one on four classes across levels 1, 4, 12 and 21 -- so they are shared art data rather than dimensions), size, mode_bits, count, and where the shrub mesh geometry lives. R&C1 puts the mesh packets in this blob; R&C2's blob holds only the palette, so the 333 classes without a billboard must draw meshes from somewhere else. |
||
|
|
ccea53807a |
Resolve the shrub region bound: the chunk container closes it
The shrub region's end was the last unlocated bound in core_data. It was not in the core index, and unlike moby->tie it had no successor boundary to chain against, so the last shrub class in every level ran out to the lump's end and the aggregates over it (the "3.5M records / 28MB of shrub body") were artifacts of that oversized span. Range measured: for each level, from the last shrub class's geometry offset to the constant trailer. Nothing outside that range is read. Method: scan forward from the last shrub class for the pair (a) the constant two bytes f6 7c sitting immediately in front of (b) a chunk signature -- a 16-byte header ending 0xff, ten zero bytes, then four or more 0xf0-tagged offsets. The two halves are each weak alone, and the halves are measured separately: the marker pair occurs 25 to 103 times in a 5 to 17 MB lump, so matching it alone picks dozens of wrong offsets; the chunk shape matches random data often enough to need four tagged offsets before it is believed. Result: on all 27 levels the marker sits exactly two bytes before the first chunk header, and the two parts of the signature are never found apart. No chunk signature occurs inside any last shrub body, on any level. The three levels without shrub (10, 25, 26) carry the same marker before the container that closes tie instead, which is what makes the locator a property of the region rather than a shrub rule. Consequences: - All 358 shrub class bodies are bounded and are whole 8-byte records. That was not assertable while the last class in each level ran to the lump's end, and it is the check the boundary earns its keep for. - 9,600 records in total, against the 8,938 previously measurable in successor-bounded spans; 662 of them sit in the 24 last-class bodies. The shrub region totals 2,033,776 bytes across the 24 levels that have one. - The chunk container is 25,775,850 bytes across 27 levels (1,236 chunks, 22,562 records). The container measurements were previously taken over "the 900,000 bytes before the trailer", a window that did not align to its real extent: on level 0 it is 690,302 bytes, and 27 of 1,123 chunks were truncated at that invented edge. chunk_containers_resolve_on_every_level now reads the region from core_layout instead of measuring backwards from the end of the lump. core_layout takes core_data now: the container's start is the one bound that cannot be derived from the index. It gains the chunks region, so every byte of level-specific core_data belongs to a named region. Where the signature is not found the end stays None -- the region is left visible rather than clamped to the tail. Still open: the marker's meaning (constant across 27 levels, at exactly this position, unnamed) and the shrub body payload's encoding. |
||
|
|
4d1e277a15 |
Exclude f32 and u16 readings of the shrub body record
Range measured: level 0 shrub bodies, 164 records of 8 bytes, following the
previous commit.
The six varying bytes are not an f32 pair:
f32 @0 158/164 finite, range up to ~1e35. Not positions or normals. The 6
non-finite values (3.7%) are far above the 0.05% random bytes would
give, so the field is structured but is not an f32.
f32 @4 164/164 finite, range 0 to 9.18e-5. Looks promising until you notice
bytes 6 and 7 are always zero, which pins it to a coarse grid and
explains the tiny range. Not independent evidence.
u16 @0 85 distinct, range 0..65534
u16 @2 90 distinct, range 0..65534
u16 @4 80 distinct, range 0..65512
The u16 fields are not small counts or indices; they span essentially the whole
range.
Excluded: two packed u32, an f32 pair, and narrow u16 fields. The six bytes
remain uninterpreted.
Worth noting the f32 @4 case as a trap: a plausible-looking finite non-negative
float range that was entirely an artifact of the two constant zero bytes
pinning it. Checking the constant bytes before believing a float reading is now
the order to work in.
|
||
|
|
7c0df0f893 |
Characterise shrub body records on level 0 as a per-level partition
Range measured: level 0, shrub class table, 7 entries at core index offset 10176. Six classes are bounded by a successor; the seventh runs to the end of core_data and is excluded because its span contains the whole post-shrub region. Method: bound each span by the next class geometry offset, take the body from data_offset to the span end, split into 8-byte records. Result: 164 records across 6 bounded bodies, lengths 192 twice, 208 twice, 224, 240. Per byte over those 164 records: byte 0: 74 distinct, 60 zero byte 4: 72 distinct, 35 zero byte 1: 58 distinct, 65 zero byte 5: 59 distinct, 38 zero byte 2: 76 distinct, 43 zero byte 6: 1 distinct, 164 zero byte 3: 62 distinct, 48 zero byte 7: 1 distinct, 164 zero Bytes 6 and 7 are zero in every record; bytes 0 to 5 take 58 to 76 distinct values with only 21 to 39% zero, so uniformly varied rather than sparse. Read as u32 the fields are not plausible: u32[0] spans 0 to 4294871316 and no field is monotone across the body. The six bytes are not two packed 32-bit values. This settles the six-varying-plus-two-constant shape on level 0 as a per-level partition, rather than only in aggregate over the disc's 8938 records, which is what rule 3a asks for. Two trailing zero bytes are padding or a flag pair. Unresolved: whether the six bytes are six u8, an f32 plus two u16, or something else. The u32 reading is excluded but no positive reading is established. Process note: the first attempt used ClassEntry and a hand-computed o_class instead of ShrubClassEntry, and every ShrubHeader::region call failed. The parser was fine; the caller was not. |
||
|
|
efe4c06be6 |
Show the chunk header does not encode the record stride
All 90 two-byte windows of header[0..14] were tested: none maps to a unique stride across every group. There is no simple two-byte field to read the record size from. The stride must therefore come from the offset table, by differencing the first two tagged offsets, which is what the parser already does. This validates that choice as more than convenience. Given three chunks sharing one header with three different strides, a dispatch that assumed "header implies size" would be wrong for the whole type-ID coverage on at least one layout. Reading the stride from the offsets is the only interpretation consistent with every measurement so far. Negative result recorded deliberately: it rules out a whole family of hypotheses (stride encoded in the header) rather than narrowing one. |
||
|
|
1d84562e88 |
Resolve the colliding header: three types share 16 bytes
Header 7542eff271b522a79e45348cca4509ff was reported with three strides, 76, 92 and 104. It is not one type with a variable stride, and not a misparse. It appears three times per level, always with record count 9, always in the same order at the same offsets within the region: offset stride count first last 863842 92 9 928 1640 867554 76 9 912 1520 872866 104 9 1000 1832 The offsets are 3712 apart each time and the strides differ, so these are three separate chunks sharing 16 header bytes. Consequence for the type ID: 64 distinct headers for 66 distinct record layouts, not 64 for 64. The header is nearly a type ID but does not uniquely determine the layout. Not excluded: a distinguishing byte sits outside the 16 captured, or the layout varies within a type. This does not change "63 of 64 headers imply one stride", which remains true, but the mapping is not a bijection and should not be used as one. Worth checking before the parser is extended to dispatch on header alone. |
||
|
|
4c4175de7f |
Add a rule requiring measurements to be partitioned before reporting
Rule 3 required bounds and a method for every number, but not that the measurement be partitioned. Three errors in one stretch were all of that kind, and all three passed the control check rule 3 asks for -- because a matched control bounds significance while saying nothing about scope: - a 368-byte period attributed to a whole region, when it was one chunk's stride among ~15. Averaging the strides put the peak at the noise floor, which read as "no structure" rather than "wrong scope". - a ten-entry table disconfirmed, when it existed at a fixed stride among several. - a moby class array reported at u16 slots 129-140, which was the largest-stride chunk type dominating a pooled scan. Per type, all 26 place their array elsewhere. In each case the aggregate was arithmetically correct and the conclusion was wrong. The re-run that caught all three was identical: group by the thing that varies, then look. Rule 3a now requires stating the partition before the number, reporting per partition or saying explicitly that a claim is pooled and unverified, treating a structure found in one partition as unproven until every partition agrees, and retracting rather than softening a result that changes under partitioning. Renumbered the following rules. |
||
|
|
b13901a7e7 |
Correct the moby class array offset: it is per chunk type
The previous commit reported moby class IDs at u16 slots 129 through 140. That was a pooling artifact. Repeating the scan per chunk type shows every type places its class array at a different offset: aac4 129-134 82c4 104-109 4ec4 106-111 b8c4 120-125 9943 82-87 79c4 111-115 3245 24-29 and so on across all 26 types Slots 129-140 were simply the type with the largest stride dominating a scan that pooled every type together. The previous commit flagged this as unverified; the check confirmed the flag. What survives is stronger than what it replaced. In every one of the 26 types the top hits form a contiguous run of consecutive u16 slots. The real invariant is therefore per-type: each chunk type has its own class array, at its own offset, of its own length. That fits the header-implies-one-stride result -- each type is a distinct record layout -- but the class array is a per-type field, not a common one at a fixed offset. The moby reference itself is unaffected. The 38.6x excess was measured across all records and does not depend on which slots were counted. |
||
|
|
ddfbc6eb33 |
Find moby class references in chunk records at 38x chance
Range measured: level 0, the 900000 bytes before the trailer; 28 chunks, 475 records, 67780 u16 slots. Reference set: the 238 distinct o_class values from level 0's moby class table, read through LevelCoreHeader. Method: scan every u16 in every record, count membership in the moby class set, and compare against the chance rate of 238/65536 per slot. Result: 9493 hits against 246.1 expected, a 38.6x excess. The hits are concentrated, not spread. Slots 129 through 140 -- twelve consecutive u16, bytes 258 to 280 of the record -- carry 105 to 188 hits each. Nothing else comes close except a separate cluster at slot 38. Every chunk contributes in proportion to its record count. A contiguous twelve-value array that is mostly moby class identifiers is what a model or instance definition listing referenced mobies would look like. Enrichment is not identification. A 38x excess over chance is strong evidence these are moby class IDs rather than incidental small integers, but it does not establish that the chunk is moby_anim: an instance table, a model list and an animation table would all reference class IDs. The format and the meaning of slots 129-140 are both still unnamed. Also unverified: whether the twelve slots are always the same fields. The offsets come from the largest records, and this scan pooled all chunk types together, so a type with a different stride could place them elsewhere. Process note: the first version of this test extracted the moby class table by hand and got 67 entries instead of 238, which made every chunk look like a hit and would have supported any conclusion. Reading it through LevelCoreHeader was what made the measurement meaningful. |
||
|
|
379c12f8ec |
Identify the chunk header as a type ID that determines the record stride
Range measured: all 27 levels, 1123 chunks. Method: group chunks by their 16-byte header; for each distinct header, collect the set of strides seen across every level it appears on. Result: only 64 distinct headers across 1123 chunks. The header is a type identifier, not a per-chunk descriptor -- the same 16 bytes recur on every level. And 63 of those 64 imply exactly one stride, so header to record layout is very nearly a function. The exception is header 7542eff271b522a79e45348cca4509ff, seen with strides 76, 92 and 104. Either three types share a header or one is a misparse. This also settles the previous entry's open question about D/E/F being incidental. Grouping by header bytes 9 and 13: DE 26 chunks, 2 strides (184, 344) EE 1050 chunks, 46 strides EF 34 chunks, 2 strides (268, 640) FF 13 chunks, 1 stride (520) Rare tags are tightly coupled to their layouts -- FF is a single stride across all 13 chunks -- while EE is heterogeneous. That is what a type tag looks like. Consequence: a chunk can be dispatched on its header to a record layout, and 63 of 64 headers give that layout's size without inspecting a record. First point in this container where a field does real work rather than correlating. Still open: what the 64 types mean, which record fields are index/length/offset, and whether any type is moby_anim. |
||
|
|
10188416e2 |
Decode the chunk count field: header byte 14 is the record count
Range measured: all 27 levels, 1123 chunks, over the 900000 bytes before the constant trailer. Method: compare header byte 14 against the number of offsets actually resolved for that chunk. Result: byte 14 equals the record count on 1123 of 1123 chunks, zero mismatches. This is the first genuinely decoded field in this container rather than a measured correlation. The parser now truncates the offset table to header byte 14, so a stray tagged word in a chunk's record data cannot silently extend the table and put the count at odds with its own header. Two more header bytes are near-constant and read as ASCII: byte 13 takes only 0x45 (1076 chunks) and 0x46 (47); byte 9 takes 0x44 (26), 0x45 (1084) and 0x46 (13). Those are D, E and F -- a three-letter tag at two byte positions with different distributions. Recorded as suggestive, not established: the other 14 header bytes are high-entropy and read as ASCII at no shift tried, so the D/E/F may be incidental. Worth testing against a known type table. Still open: the 14 opaque header bytes, whether bytes 9 and 13 are type tags, which record fields are index/length/offset, and whether a chunk is moby_anim. |
||
|
|
681333535a |
Add a bounds-checked parser for the post-shrub chunk container
gc_formats::chunks parses the container that fills the region after the last shrub class body and before the constant trailer: +0x00 16 bytes, last byte 0xff (opaque, differs per chunk) +0x10 10 zero bytes +0x1a N tagged offsets, top byte 0xf0 Each tagged offset is 0xF0000000 | low24, and a record is read at chunk_start + low. Resolving that way gives records sharing a field layout, which is what distinguishes the container from a coincidental byte pattern. Fields are not named. The 16-byte header is opaque, the 0xf0 tag is unexplained, and it is not established that a chunk is moby_anim -- a 49 x 368-byte table inside this region is byte-identical on all 27 levels, which is evidence against that identification. What the parser provides is chunk and record extraction, which is what the analysis established. The gated test measures, over all 27 levels: 1123 chunks, 19546 records, 50 distinct strides, 12.4% of byte positions constant across records (per-chunk range 2.8% to 75%). Chance gives a constant position probability of 1/256^n. Three things the test got wrong first, recorded because they are the traps: - A per-chunk threshold of 20% constant positions. Individual chunks run from 2.8% to 75%; the aggregate is the invariant, not the chunk. Asserting 20% per chunk was asserting a property the format does not have. - Requiring every offset to resolve. 27 chunks truncate at the window edge, because the 900000-byte window does not align to chunk boundaries. Counted now rather than asserted away. - read_u32 in the chunk signature check was big-endian while Buf::u32 is little-endian. The tag lives in the high byte of a little-endian word, so the mismatch made every level report zero chunks while parse_chunk -- which goes through Buf -- was correct. Worth remembering that the two paths can disagree on a format this young. Tests: 190 -> 191. |
||
|
|
8b0a2545a3 |
Confirm chunk offsets address real records on all 27 levels
Range measured: each level's post-shrub region, 900000 bytes before the constant trailer. Method: per chunk, read tagged offsets as 0xF0000000 | low24, take stride as the first delta, load one record per offset at chunk_start + low, and count byte positions constant across that chunk's records. Result: 1096 chunks and roughly 13000 records resolve on all 27 levels. Every level shows 17-21% of byte positions constant across its own records. Level 0: 27 chunks, 466 records, 20.8% constant. The stride-368 chunk (77 records) has 26 constant positions of 368; the stride-340 chunk has 25 of 340. Constant positions cluster at the record end -- 267-276 for stride 368, 258-268 for stride 340 -- so a footer, not scattered padding. The tag is uniform: every tagged offset has top byte 0xF0 on all levels. This settles the previous entry's framing. Resolving at chunk_start + low yields records with ~20% constant positions, which does not happen by chance, so the offsets genuinely address records and those records share a layout. Still not decoded: the 16-byte header, the 0xF0 tag, which fields are index/length/offset, and whether a chunk is moby_anim. A parser can extract chunks and records bounds-checked today; naming the fields needs a domain argument this project has not found. |
||
|
|
1d6cddf3bb |
Identify the post-shrub region as a chunk container
Range measured: level 0 post-shrub, 16047040..16737344, 690304 bytes. Method: scan for a 16-byte header ending 0xff, then 10 zero bytes, then a run of u32 whose top nibble is 0xF. Result: 28 chunk headers on level 0, present on all 27 levels. Each chunk is +0x00 16 bytes, last byte 0xff (opaque, differs per chunk) +0x10 10 zero bytes +0x1a N tagged offsets, top nibble 0xF Within a chunk the offsets step by a chunk-specific stride. On level 0: 148, 340, 368, 288, 320, 236, 272, 384, 116, 160, 250, 580, 576. Near-constant within a chunk, different between chunks. This resolves two earlier results, both of which were half-right. The 368-byte period is one chunk's stride, not a region property. That is why the region-wide autocorrelation is flat: ~15 strides averaging to noise. The 15360-byte window where 368 scored +0.315 is the third chunk at region+30066, whose stride is 368. The "ten-entry table" earlier disconfirmed is real. Chunks carry 5 to 30 tagged offsets, 10 most common on level 0. It was disconfirmed because I tested for a fixed 148 stride across a whole region; the table exists, but 148 is one chunk's stride among many. Both errors were the same one: reporting a local measurement as a regional property. Twice now. The control methodology caught neither, because a matched control bounds significance but says nothing about scope. Not decoded: the 16-byte header, the 0xF tag, where chunk data lives, and what any chunk means. Framing established, contents not. |
||
|
|
c60340b1de |
Correct the post-shrub 368 stride: real, but in one 15 KB window
The claim that the post-shrub region has a 368-byte period throughout is wrong. Re-measured over the whole region the curve is flat and near noise: stride 8 +0.027 stride 12 +0.023 stride 148 +0.009 stride 340 +0.018 stride 368 +0.018 stride 632 +0.020 Nothing peaks at 368, and the 0.1110 recorded earlier does not reproduce under any sampling convention tried. Where the structure is: scanning 8 KB windows at 1 KB steps for a 368-excess above 0.15 finds one substantial region, region+41984..+57344, 15360 bytes, plus a 4 KB patch at region+650240. Within that window 368 is unambiguous -- +0.315 against +0.284 at 736, with 364, 372 and 732 all at noise. 41 whole records, mean 16 distinct values per position, and nine positions exactly constant and zero: 329-336 and 340. Twenty-five positions have 3 or fewer distinct values, clustered in 325-349, which is a tail rather than scattered padding. Corrected statement: a 15360-byte window of the post-shrub region is a table of 41 records at stride 368, each with a roughly 43-byte tail whose fields are mostly zero. The surrounding 675 KB is not that table. What went wrong before: the original measurement was taken over a window where 368 held, then reported as a property of the region. That window holds 41 of 1841 records, and averaging it over the region flattens the peak to the noise floor. The re-verification checked the stride but not whether it applied uniformly, so it confirmed the number without testing the claim attached to it. |
||
|
|
7992e9d8f3 |
Note that the SBlk period is not a record boundary
Range measured: level 0 SBlk, 24..19508, read at 4-byte alignment. The most common u32 values are 0x0 (2261), 0x1 (369), 0x7f (168), 0x5a000000 (125), 0x1eff7f7f (125), 0x1dfcc (124), 0x19fc0 (93), 0x7c34240d (91), 0x7c3821c8 (91). That does not look like a table of records with typed fields. A 40-byte layout of index, length and offset would not produce 0x1eff7f7f and 0x7c34240d a hundred times each, nor would those values survive a byte shift. So either the 40-byte period is not a record boundary, or the region is an encoded stream that happens to have a 40-byte statistical period. The 0x7f values point at the latter: in a varint encoding 0x7f is the largest value representable in seven bits, so it appears as a frequent saturated byte -- consistent with a coded stream, inconsistent with fixed-layout records. Left open. The 40-byte period is solid -- 27 of 27 levels, sharply isolated, multiples confirming -- and so is the alignment with four constant zero positions. But those two are in tension in a way nothing else in this project has been: a confirmed period that is not a record boundary. Someone attacking this format should not assume the record framing that the autocorrelation suggests. |
||
|
|
e9c927994c |
Establish the SBlk 40-byte record layout
Range measured: level 0 SBlk, 24..19508, aligned at +0 -- 487 whole records. Method: for each of 40 candidate alignments, count byte positions that are near-constant across records, and keep the best. Alignment +0 wins with 10. Four positions are exactly constant and all zero: 7, 11, 31 and 35. Distinct values per position run from 1 to 155, with position 29 the most variable by a wide margin -- 155 against 21 to 48 elsewhere -- and positions 2, 10, 27, 34 and 39 low at 2 to 8. The first record carries the SBlk magic and the DAW tag rather than descriptor data, so it is a header rather than an entry. That matters before reading any field at a fixed offset. Not decoded: the layout is established and the constant positions are known, but the fields are not named and which of the 40 bytes are an index, a length and an offset is still open. |
||
|
|
0f1a7f27c7 |
Resolve the SBlk discrepancy: 40 is the record, markers are nested
A fine scan settles it in favour of 40: stride 40 +0.454 stride 39 +0.266 stride 41 +0.215 stride 38 +0.214 The peak is sharp and isolated, nearly twice its immediate neighbours, and the multiples confirm it: 80 scores +0.376, 120 +0.394, 160 +0.340, while the multiples of 12 (24, 48, 96, 144) all sit at 0.21 to 0.24. So the record is 40 bytes and the 0x7f/0x47f markers at 12-byte spacing are a nested structure inside those records, not the record boundary. Their spacing is irregular -- 12, 24 and larger -- which is what one would expect of a field present in some records and absent in others. The previous commit's "they disagree" framing was itself wrong in one respect: the marker spacing never claimed to be the record size. Read as a claim about markers inside records rather than about the record itself, the two measurements were never in conflict, and I had set up a false dichotomy between them. |
||
|
|
5df3914160 |
Record an unresolved discrepancy in the SBlk record size
Range measured: level 0 SBlk, 24..19508, 19484 bytes. The 0x7f/0x47f marker words sit at 12-byte spacing on 144 of roughly 180 occurrences, with 24-byte and larger gaps accounting for the rest. So there is a 12-byte structure. That contradicts the autocorrelation. If records were 12 bytes, stride 12 should peak, and it does not: stride 12 +0.237 stride 20 +0.175 stride 24 +0.237 stride 40 +0.454 stride 48 +0.239 40 is not a multiple of 12, so the measurements disagree rather than one explaining the other. Recorded as open rather than resolved in favour of either: the marker spacing says 12, the period says 40, and 40 being twice as strong is the more surprising of the two. Worth resolving before attempting a record layout, because a layout built on the wrong one will parse plausibly and be wrong. |
||
|
|
d3373042b9 |
Confirm moby's 16-byte vertex by two independent methods
The part table established vertex_offset + 16 * vertex_count == next_part on 43467 of 43467 parts, which fixes the vertex stride at 16. Autocorrelation over the spans now reaches the same number without using that result. Range measured: moby class spans of 16 KB or more on level 0 -- 60 spans, bounded by consecutive class offsets, over their first 32768 bytes. Method: peak-stride autocorrelation against a matched control, strides 4 to 64. stride 16 43 spans stride 8 8 stride 48 4 stride 32 3 stride 24 2 16 dominates and it is the same 16 the part table produces from a completely different argument -- a tiling identity over part records rather than a periodicity measurement over span contents. Two unrelated methods agreeing is worth more than either alone. The autocorrelation would be vulnerable to the exact artifacts this project has already been bitten by: a dominant byte value inflates it, and a period that happens to divide the measurement window looks identical to a real one. Neither applies here, and a tiling identity cannot be inflated at all. This is the strongest form of evidence produced so far in this project -- a structural invariant confirmed by two methods that do not share a failure mode. |
||
|
|
332dc63600 |
Measure tie class spans: 16-byte unit, 48-byte period, dominant not universal
Range measured: every tie class span of 16 KB or more on levels 0, 3, 6 and every third level after -- 386 spans, bounded by consecutive class offsets. Method: peak-stride autocorrelation with a matched control over the first 32768 bytes of each span. stride 48 285 spans stride 16 58 stride 64 29 stride 96 11 stride 24 2 stride 32 1 48 bytes dominates but is not universal: 58 spans peak at 16 against 285 at 48, so this is a majority rather than a constant. Every observed peak is a multiple of 16, suggesting a 16-byte base unit with 48 arising from how those units group rather than 48 being the record. This is weaker than the sound bank's 40-byte result and is recorded at the confidence it deserves. That one was sharply isolated -- 0.454 excess at 40 against 0.21 to 0.28 across strides 36 to 44 -- and identical on all 27 levels. Here the peak is 0.09 to 0.22 excess and moves between levels and spans, so the two should not be treated as the same kind of claim. |
||
|
|
d1190a3343 |
Find a 40-byte period in the sound bank SBlk, on all 27 levels
Range measured: each level's SBlk region -- the descriptor table between sblk_offset and daw_offset, from the sound bank header rather than from core_data. 17-20 KB per level. Method: peak-stride autocorrelation with a matched control, the same bytes shuffled with the same seed on every level. The peak is at exactly 40 bytes on all 27 levels, every one with excess above 0.2. On level 0 the peak is sharply isolated: 0.454 excess at stride 40 against 0.207 to 0.278 for strides 36 through 44, so it is a period rather than a broad tendency. This is the first structural result about the sound bank beyond the container framing already committed. Record counts are 487, 443, 428, 494 and 447 whole records on levels 0, 1, 2, 13 and 26, and the region is not an exact multiple of 40 on any level -- 19484 leaves 4 bytes over -- so 40 is a period rather than a record size with no remainder. Unexplained: which byte positions are constant varies by level. Level 0 has 7, 11, 31 and 35 constant; level 1 has 15, 19, 23, 35 and 39; level 2 has 7, 23, 27 and 31. A fixed 40-byte layout would share its constant positions, so either the records have a variable tail or 40 is a period in something the records sit inside. Also measured: DAW shows no stride above threshold against its control, consistent with ADPCM nibble-packed audio and with the container framing being the only part worth decoding. gameplay.bin peaks at 16 bytes. gs_ram.bin has almost no stride structure but a lag-8 correlation of +0.503, so it is smooth rather than tabular. |
||
|
|
51e81b497e |
Show the sky sub-object shape holds across levels
Range measured: sub-objects of the sky region on levels 0, 1, 3, 7, 13 and 26 -- 25 sub-objects, each over its whole length. Method: correlation at lag against the byte sequence, as before. lag 8 +0.53 .. +0.79 positive on all 25 lag 64 +0.16 .. +0.63 positive on all 25 lag 512 -0.13 .. +0.42 not universal lag 1024 -0.05 .. +0.37 not universal Lag 8 and lag 64 hold on every sub-object of every level tested. The longer lags do not, so the second scale seen in level 0's first sub-object is a property of that data rather than of the format. The sky sub-objects are all the same kind of thing: 8-byte units strongly related to their neighbours, with a further relation at 64 bytes. That is the first result about sky that holds everywhere rather than on one level. The 8-byte unit is still unnamed and the values still unidentified. |
||
|
|
b5edbd1810 |
Show sky sub-object 0 is a slowly-varying sequence at two scales
Range measured: level 0, sky region +365760..+388880, 23120 bytes. Method: Pearson correlation between the byte sequence and itself at increasing lag, against a shuffled control. This asks whether consecutive records are related, which the position-wise distinct-value counts could not show. lag 1 +0.050 lag 256 -0.044 lag 4 +0.560 lag 512 +0.304 lag 8 +0.746 lag 1024 +0.247 lag 16 +0.666 lag 2048 +0.079 lag 32 +0.602 shuffled +0.003 lag 64 +0.432 lag 128 +0.140 The profile peaks at lag 8, decays to a zero crossing near 256, then rises again to a second smaller peak at 512 before decaying. So records are related within themselves and repeat every 64 records. This settles the earlier "no constant substructure" reading. The records share no fixed fields, but they are not independent either, and that was the thing the distinct-value counts were failing to capture. Slowly varying values, related within a record, repeating every 64 records, is the shape of a keyframe or lookup table -- which is what sky data should be, where a colour or fog parameter is sampled rather than computed per vertex. Still not decoded: nothing names the values or the sampling. But the shape is now known well enough for a decode attempt to aim at, rather than to hunt. |
||
|
|
0ba790df1f |
Correct the sky sub-object 0 live-byte figure; record the byte 7 anomaly
The previous commit said "a mean of 6.99 of every 8 bytes are zero". That is inverted. 6.99 is the mean count of live bytes, and the records are nearly full rather than sparse. The live-byte histogram is 0:15, 2:1, 4:31, 5:238, 6:461, 7:1030, 8:1114. That error also invalidated the reasoning built on it in this session's next message, where the high zero-count was used to explain why the differential analysis found no constant positions. There was no zero-density to explain. The differential result stands -- all 8 positions vary -- but for the plain reason that the records are dense, not because of any artifact. New finding from re-examining it: byte 7 takes only 14 distinct values where bytes 0-6 take 142-204. Its common values are 0x00 (1378), 0xff (806) and 0x80 (592), which are the high bytes of signed 16-bit values. That is consistent with byte 7 being an MSB rather than ordinary data. Unexplained. Also tried and recorded as not helping: a signed 16-bit reading spans -32743..32633, near the full range, with no per-position structure (533, 371, 401 and 187 distinct at positions 0-3). |
||
|
|
3855169d48 |
Characterise sky sub-object 0: sparse, with duplicated entries
Range measured: level 0, sky region +365760..+388880, 23120 bytes, read as 2890 records of 8. The matched-control autocorrelation puts the peak at stride 8 (0.217 real, 0.000 shuffled), so 8 is the unit to describe. Composition: 1185 distinct u32 values across 5780 words; a mean of 6.99 of every 8 bytes are zero, so most records are sparse; and 549 of 2890 adjacent pairs, 19%, are exact duplicates of each other. The most common 8-byte record appears 283 times and is d7d7d780 d7d7d780 -- a 4-byte value stored twice. Others follow the same shape: 00051980 00051980, 00082580 00082580. Not decoded. Sparsity with repeated entries is what a keyframe or index table looks like, but nothing here establishes that, and the duplicated-4-byte shape may just as easily be two adjacent records sharing a value. Recorded as a characterisation with its range rather than a finding, because that is what it is. The useful part is that the next attempt does not have to re-derive the composition to know whether a decode is plausible. |
||
|
|
a82e4a711f |
Measure sky sub-objects across levels; the stride table was over-fitted
Range measured: every level's sky region, from the count field at +0x0c through the sub-object offsets at +0x20, each sub-object over its first 16384 bytes. Method: peak-stride autocorrelation per sub-object with a matched control -- the same bytes shuffled, seeded per level and index. Field note: the sub-object count is a u32 at +0x0c, not +0x0a. Reading +0x0a gives 327,680 on level 0, which is not a count. A brief probe used the wrong offset and read as though it were a count of 5; it got the right offsets by accident because the offset table starts at +0x20 either way. Level 0 has five sub-objects at 365760, 388880, 396432, 406784 and 414032, of lengths 23120, 7552, 10352, 7248 and 1536, each with structure well clear of its control. But the strides do not repeat across levels: obj 0: 4 (x6), 8 (x13), 32, 288 obj 1: 16 (x3), 32 (x6), 288 obj 2: 8, 16 (x5), 256, 288 obj 3: 8, 16 (x6), 216 obj 4: 8, 16 (x3), 512 (x4) So the per-index stride table read off level 0 alone was over-fitted to one level, and is withdrawn. What is consistent is weaker and more honest: every sub-object sits above its matched control, and most peak at 4, 8, 16 or 32, suggesting 16- or 32-bit records whose stride varies with content. Level 2's header is still genuinely different: its count field reads 196,617 and is unusable while every other level yields a plausible count. |
||
|
|
98c0d1ab3c |
Re-verify the 368 stride with a control matched to its data
The sky autocorrelation turned out to be inflated by a control that did not match the data, so the 368 result was re-checked the same way rather than being left on the evidence I now know to be weak. Range measured: each level's post-shrub region start, first 65536 bytes. Method: as before, but the control is the same bytes shuffled rather than uniform random. Shuffling preserves the value distribution and destroys only the ordering, which is what a control for this statistic needs to do. Level 0's region is evenly distributed -- 256 distinct byte values, most common only 15.6% -- so uniform random was the appropriate control here, and the shuffled control independently confirms it, reading 0.0000 at strides 340, 368 and 4 against the real data's 0.1110 at 368. Recomputed across all levels with matched controls: 24 of 24 peak at exactly 368, every one with excess above 0.05. So the 368 period stands, now with a control appropriate to its data. The contrast is the useful part: the same statistic, asked the same question of two regions, gave a real answer in one and an inflated one in the other, and the only thing that separated them was whether the control matched the byte distribution. |