a991088103bc533e8bd366f06cd74f7b04809736
217
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
c2af0528e3 |
Correct the chunk loader finding and decode a record field
The loader note said every record gains a 02 00 prefix in RAM. It does not: the word is the record's own leading field, already on the disc at every record. What the loader changes is the pointer -- it addresses the word, two bytes earlier than the disc's table -- and that pointer shift is what made the bytes look inserted. Measured, in order: - the two bytes before a payload are identical in core_data and in RAM, record for record: 02 00 on level 0's 90-record chunk at 0x100b302, 08 00 on the level's first chunk. Nothing is inserted; - the RAM table's low 24 bits are the payload's minus 2, consistently, so the record is [leading u16][two index bytes][payload] and the loaded table addresses the leading word. That is also why the index bytes read as +2 from the pointer -- the same +2 the earlier RAM result quotes; - the word is a field, not a pad: uniform across a chunk's records on 1,439 of 1,509 chunks (None on the 70 that vary, reported rather than averaged), fixed within all 71 chunk types, and only ever 1, 2, 4, 6, 8 or 10 -- 554 chunks carry 2, 520 carry 4. Two readings of the value were measured and disproved, so they are recorded as disproved rather than left as candidates: it is not an entry count (the payload divided by it leaves 57 distinct fractional "sizes", and 162 chunks do not divide at all), and it is not the number of distinct leading bytes (agrees on 0 of 1,506 chunks). Chunk gains `prefix: Option<u16>`, and the disc test asserts the uniformity, the per-type fixity and the value set. The scratch example that produced the per-type class-ID table is dropped now that the same scan is a committed test, which also lands here: moby class IDs sit in one body slot per chunk type, with the slot varying by type and the distinct >= 4 filter load-bearing -- without it the top slots are constants that read as 305x because any value below the moby count counts as a hit. |
||
|
|
8147110377 |
Bring the roadmap's chunk section up to date
Four commits of chunk work -- uniform record length, the leading-byte phase counter, the two-byte record index, and the PCSX2 RAM findings -- were written to docs/formats/core-data-regions.md and docs/plan/pcsx2-verification.md but never reached the roadmap, because the scripted replacements kept missing their target text and I did not check that they had applied. That is the staleness complained about earlier recurring through a different route: not a missing edit, but a silently-failed one. The tie, collision and level_overlay entries had landed; only the chunk ones had not. Rewritten from the commits rather than from memory, and the numbers are the ones the tests assert: 1,509 chunks, 34,491 records, uniform length 1,509/1,509, phase counter 1,262/1,509, index rule 1,235/1,509, RAM prefix 02 00 on 90/90. |
||
|
|
2c4bbdc600 |
Use PCSX2 RAM to recover the chunk loader's transformation
This is what RAM was needed for: nothing about the loader's transformation is recoverable from the disc. Anchor: a 32-byte probe from inside a chunk record in level 0's core_data, searched in the level-0 savestate. It hits at 0x18538a8 and the record's first bytes at 0x185387e -- exactly chunk_base + record_offset, no rescaling. So the chunk payload is copied VERBATIM; unlike collision and the shrub packet tables it is not expanded or repacked. Result 1: the offset table is rewritten to absolute EE addresses. disc: f000057c f00006b0 f00007e4 f0000914 RAM: 0xf185387c 0xf18539b0 0xf1853ae4 0xf1853c14 The 0xf0000000|low24 tag becomes 0xf1850000|low24 -- tag replaced by the chunk's load base, low 24 bits untouched. Consecutive deltas agree exactly (0x134, 0x134, 0x130), which is why the disc-side stride is approximate rather than constant. Result 2: every record gains a constant 2-byte prefix 02 00, and the rewritten table points at THAT rather than at the record data. Walking all 90 records from the RAM dump: prefix 02 00 on 90 of 90; the disc's index bytes preserved unchanged at +2/+3 on 90 of 90. Result 3, the useful one: the byte at +2 takes exactly 0, 16, 32, ... 240 -- the 16 values index_rule predicts for step = 16, phase = 0. The mixed-radix record index derived from the disc is corroborated on the running game's own copy of the chunk. That is a disc-derived claim validated against the game, which is the strongest form of check available here. On disc a chunk is [16-byte header][10 zero bytes][tagged offset table][records]. In RAM it is [16-byte header][10 zero bytes][absolute-pointer table][records each prefixed with 02 00]. The 02 00 is unexplained -- possibly a record type or an alignment the loader needs -- but it is constant, so like the two index bytes it carries no per-record information. |
||
|
|
e2b12a2e52 |
Rule out a count field in chunk record bodies; correct a six-record impression
With the record framing settled (leading two bytes = record index), the body is bytes 2..len. Four probes, mostly negative, recorded so the next attempt does not repeat them. No count field. For every chunk type, every u16 and u32 position in the first 64 body bytes, at element strides 1,2,3,4,6,8,12,16: does the stored value equal (record_len - offset) / stride on 90%+ of records? No type produced a hit. The record does not carry an element count near its head. The body is dense, not float-dominated. For the largest type (2430 records, 326-byte bodies): f32 reads within +/-5000 of origin 74%, uniform-random control 49%, zero-byte fraction 13%. Above baseline but far short of the tie blocks (53-64% against 16%, with 43% zero bytes). There is an index-like region near the tail. In the largest type, 12 of 163 u16 slots have a maximum below 0x1000 -- too small for float or half-float, so those look like indices or small counts -- clustered at slots 111-121 and 130-131. A sample-size correction. Dumping six records of one chunk showed slots 112-119 zero on every record plus a long run of other constants, which reads as "the record is mostly a per-chunk template". Measured across the 1,171 chunks with at least 8 records: 15,422 of 164,390 u16 slots are constant across their chunk, 9.4%, and 0 of 1,171 chunks are entirely constant. So 9.4%, and no chunk is a template. Six records looked like a majority because that chunk's tail carries shared constants. Per type the figure ranges from 9.4% overall to 55.6% on one type, so it is reported per type as well as pooled -- the pooled figure alone would hide that spread, which is the same trap rule 3a describes. |
||
|
|
28680b4be3 |
Show the first two chunk-record bytes encode the record's own index
Continuing from the phase counter. Within a chunk: i : 0 1 2 3 4 5 6 7 8 9 10 11 ... 15 16 17 ... byte0: 0 16 32 48 64 80 96 112 128 144 160 176 ... 240 0 16 32 byte1: 0 0 0 0 0 0 0 0 0 0 0 0 ... 0 1 1 1 byte0 is step*(j mod g) and byte1 is (j / g), where g = 256/step and j = index + phase. The pair is the record's index in mixed radix: byte0 carries the low digits scaled by step, byte1 the quotient. Measured over all 27 levels: 1,235 of 1,509 chunks fit this on both bytes. Byte 0 alone fits 1,262, so the 27-chunk difference are chunks whose first byte follows the rule and whose second does not. On 1,235 chunks a record knows its own position and the two leading bytes hold no per-record information. ResolvedChunk::index_rule returns the (step, phase) only when the whole chunk fits on BOTH bytes, so the 274 that do not stay distinguishable and nothing skips a leading byte on a rule that does not apply to it. A number that moved when the implementation changed: a first probe reported 1,181; the committed method reports 1,235. The method is right. The probe accumulated ok0/ok1 flags across candidate phases with an early-exit that could skip a phase which would have matched both bytes, and it also disagreed with bucket_rule on the byte-0 count (1,208 against 1,262), which is what gave it away. Recorded because both numbers came from the same run and only the discrepancy exposed the probe as wrong. The lesson is narrow: when a one-off probe and a committed helper disagree, check the probe first. |
||
|
|
79914c420e |
Show the chunk record's leading byte is a per-chunk phase counter; every chunk
has a uniform record length Follows the alignment result and settles what the leading byte is. Within one chunk the leading byte steps by a constant and wraps: offsets: 1420 1748 2072 2392 2716 3044 3372 3700 4028 4352 ... byte0: 0 16 32 48 64 80 96 112 128 144 ... Sweeping every (step, phase) over all 27 levels: fully explained: 1,262 of 1,509 chunks by step: 8 -> 51 chunks, 16 -> 554, 32 -> 520, 64 -> 137 unexplained: 247 chunks across 11 chunk types So on 1,262 chunks the leading byte carries no per-record information: it is a deterministic function of the record's index and the chunk's phase, which is a position marker rather than data. That is why the earlier "16 discrete values, uniform counts" pattern appeared at all. ResolvedChunk::bucket_rule returns the (step, phase) when the whole chunk fits and None otherwise, so the 247 unexplained chunks stay distinguishable and nothing skips a leading byte on the strength of a rule that does not apply to it. Every chunk has a uniform record length: 1,509 of 1,509. Length varies BETWEEN chunks of a type -- one type runs 76 to 104 bytes -- but never within one. This withdraws the previous commit's "variable-length type", which came from pooling minlen/maxlen across all of a type's records at once. That is the pooled-claim error rule 3a exists to prevent, and it is the third time this session a pooled measurement produced a claim that partitioning reversed. The type is not variable-length; the chunk is fixed-length and different chunks differ. |
||
|
|
a9bf74fc10 |
Characterise all 77 chunk types: record byte 0 is a bucket index, not data
Characterised every chunk type -- 77 of them, 1,509 chunks, 34,491 records, all 27 levels. Byte 0 of every record is a small aligned index, not record content. It takes 16 discrete values per chunk type (0x00, 0x10, ... 0xf0) with near-uniform counts WITHIN each type. Uniform weighting is the tell: a running byte offset would weight the buckets unevenly, because records differ in size. type_id recs len byte0 0x007d7ec438 2430 328 0x00:162 0x10:162 0x20:162 ... 0xf0:135 0x00429ec43d 2079 368 0x00:162 0x10:135 0x20:135 ... 0xf0:108 0x00d76cc453 810 108 0x00:27 0x08:27 0x10:27 ... 0xe8:27 <- steps by 8 The alignment step is per type, not global. The leading byte is a multiple of 8 on 34,432 of 34,491 records but a multiple of 16 on only 33,349 -- the 1,142-record difference falls in seven types, four of which step by 8. 59 records in three types break even the 8-byte rule, and that is where the variable-length type lives: 0x00f2ef4275, records 76 to 104 bytes, no single stride to align to. What the bucket indexes is NOT named. It is reported as CHUNK_RECORD_ALIGNMENT and record_is_aligned rather than as a parsed field, because naming it would be a guess while the invariant is solid. The test pins the exact exception set -- 59 records across 3 types -- so a change there fails loudly. A measurement I initially got wrong: the first version of the constant claimed a 16-byte multiple on 34,432 records. 34,432 is the multiple-of-8 count; the multiple-of-16 count is 33,349. Right number, wrong label, and the test caught it. That is the second time this session a correct number carried a wrong label, after the tie four-value retraction. Both times the assertion did, which is the argument for putting these numbers in tests rather than only in prose. Still open: what the bucket indexes, and the remaining record fields. |
||
|
|
291153a0d9 |
Find the tie LOD block terminator; packets == 0 means no block at that LOD
parse_tie_block walks to a 0xf6 terminator that every real block carries, on
2177 of 2177 across all 27 levels, followed by zero padding. This is what makes
the blocks parseable: packets[1] bounds one on only 461 of 1864 classes, so the
terminator is the only general way to find one.
The terminator was worth a control, because 0xf6 could have been byte-frequency
coincidence. It occurs in payloads at 0.83x the uniform rate -- slightly LESS
often than chance -- while the last non-zero byte is 0xf6 on 909 of 922 blocks in
the first subset, against 17 of 922 for the same blocks cut at a random point. So
it is a terminator, not a common byte.
packets == 0 means the class has NO block at that LOD: of 3,728 (class, LOD)
slots, 2177 point at a block and 1551 are exactly zero.
Corrected header, over all 2177:
+0x00 count multiple of 16 on all; 42 distinct; never zero
+0x02 zero on all
+0x04 exactly four values: 0x501 (1069), 0xa02 (720), 0xf03 (350),
0x1404 (38)
+0x06 16 distinct, never zero
+0x08 180 distinct, never zero
+0x0a 61 distinct, never zero
+0x0c 125 distinct, never zero, clustered 0x043f..0x0443
+0x0e 11 distinct, zero on 2162 -- the only non-universal field
+0x0c clusters around 1087-1091, close enough to a block length to be worth
stating, but it is NOT the length: the difference against the true payload length
scatters from -12 to -172.
A correction to the previous commit. It reported 3,728 blocks all terminating and
packed_texture taking 842 distinct values, and on that basis retracted the
four-value claim. Both numbers were wrong the same way: the scan did not skip
packets == 0, so it computed class_base + 0 and walked forward from the CLASS
BODY, finding a terminator there by accident. That contaminated 1551 of the 3728.
With the zeros excluded the slot takes exactly four values on all 2177 blocks.
The four-value claim was right; my retraction of it was wrong; and the retraction
was made on a number from the same contaminated set. A retraction is not
automatically better than the claim it retracts -- it needs its own bounds.
Still open: the payload's records. Established this round: blocks are
self-delimiting, absent LODs are marked by a zero packets word, and the header is
fully characterised. Not established: any record boundary inside the payload,
which stays variable-length.
|
||
|
|
02342de31b |
Decode the tie LOD block header; establish the payload is variable-length
TieBlockHeader parses the block's own 16 bytes, measured over the 922 blocks where "the next packets offset ends this block" holds, across all 27 levels. A real-disc test asserts every figure. +0x00 count multiple of 16 on 922/922; 16..2096; 72 distinct disc-wide +0x02 zero on 922/922 +0x04 ONE OF FOUR VALUES DISC-WIDE: 0x501, 0xa02, 0xf03, 0x1404 +0x06 15 distinct, 0x0206..0x0512 +0x08 135 distinct, 0x0b05..0x4ee4 +0x0a 45 distinct +0x0c 94 distinct, 0x010e..0x0553 +0x0e zero on 914/922 +0x04 is packed rather than a magnitude: four values across the whole disc with the two bytes moving independently, low 1-4 and high 5/10/15/20 in lockstep. That it is a texture index paired with a scale is a guess; that it is two fields is not. The count is not a record count -- block_size minus the header is divisible by it on 0 of 922, for any record size and any header up to 512 bytes. On the payload, a control was needed and changed the answer. Reading blocks as 3 x f32 triples, 53-64% of reads land within +/-5000 of origin, against 16% for a uniform-random control of identical length. So the content is float-shaped and genuinely not random. But the rate is flat across every start offset and every stride tried, 53-65% throughout. A statistic that cannot separate the right stride from the wrong one cannot identify a stride -- rule 4's exact case. Flatness plus a count that does not tile means the records are variable length. That killed two readings. A VIF1 command walk consumes 0 of 88 blocks exactly, so it is not a VIF1 CPU-format stream. wad::decompress rejects the block at every start offset, so it is not WAD LZ77. Scanning every (start, stride) pair from 0x10 to 0x7f for byte positions with a small distinct-value vocabulary finds none: every position takes all 256 values at every alignment. For float payload that is expected, so the scan found nothing rather than confirming anything -- worth saying, because "no fields found" here is uninformative, not evidence against fields. Still open: what the payload holds. Established: variable-length, float-shaped, non-random, not LZ77, not VIF1. Not established: any record boundary. |
||
|
|
881fd48e9a |
Withdraw the 16-byte tie record claim: block size is a multiple of 16, not records
The previous commit inferred a 16-byte record from "922 of 922 consecutive block sizes are a multiple of 16". That inference does not hold, and the claim is withdrawn rather than softened. What survives: - Every one of those 922 blocks opens with a u32 that is positive and a multiple of 4, on 922 of 922, ranging 48 to 1744 with 17 distinct values across level 0's LOD0 blocks. The block opens with a count, not a size and not a pointer. - Reading that u32 as a byte size and chaining to the next block lands exactly on the next block start on 0 of 1383 blocks. Not a size chain. - Assuming [header][count][count uniform records] and solving for the record size gives no answer: no record size tiles the block for any header size up to 512 bytes. Not 16, not 12, not 8. - Reading the block as uniform 16-byte units gives fields spanning their entire legal range -- u32@0 from -2139029504 to 2139029587, u16@8 and u16@10 both 0..65535, and 8969 distinct values in i16@0 on level 0. Uniform units would not produce that. So the 16 is a property of the block size, not of a record. The earlier commit also claimed a record size "on the classes where it can be measured"; those classes are still correctly identified, but what they measure is the block size. The strict-increase disproof from two commits ago stands untouched and is what should have prevented this: the word at +0 was already known not to behave, so treating 16-byte units as records contradicted a measurement sitting in the same file. Divisibility is arithmetic; it is not evidence of a record boundary. Roadmap updated to say block sizes are established and the record layout is not, with the three measurements that rule the 16-byte reading out. |
||
|
|
855e640f39 |
Establish the 16-byte tie record on measurable classes; packets[0] is a constant
The previous commit reported "3580 non-empty LOD blocks compute as divisible by 16" and then said the figure should not be trusted, because it came from the "next packets offset ends this block" rule. That rule is unsound: packets is strictly increasing on only 461 of 1864 classes. On the other 1403 the two offsets are unordered and cannot both be block starts into one ascending region. Restricting to the 461 classes where the rule does hold, 922 of 922 consecutive block sizes are a multiple of 16. That is the evidence for a 16-byte record, and it is now scoped to the subset where it can be checked rather than claimed over all 1864. The other 1403 are unmeasured, not measured-and-different. packets[0] is a constant, not an offset: it reads 256 on 1864 of 1864 classes, while packets[1] and packets[2] vary widely and land at different addresses in core_data on every class. Read as a block start, 256 points just past the 0x48-byte header -- which is how it was being used, and exactly how a field gets mistaken for a pointer. It is now TIE_PACKETS0 and nothing depends on it. Still not decoded: what the 16 bytes hold. i16[0] at +4 takes 8969 distinct values on level 0 and is not the two-valued field a 12-unit sample suggested; the word at +0 is not a run offset. The test pins all four numbers -- packets[0] constant, the 922/922 divisibility, the 461/1864 ordering, and the 0/1864 monotonic disproof -- and asserts that the ordering is *not* universal, so a future change there fails loudly instead of quietly invalidating the record size. |
||
|
|
60e3f129d8 |
Resolve both tie indirections on all 1,864 classes, and disprove a sample artifact
resolve_tie_class walks the chain, which is two steps and both are easy to get
wrong:
1. The tie class table lives in core_index.bin, not core_data.bin. On level 0
the tables chain as moby at 192, 238 x 32 bytes later tie at 7808, 74 x 32
later shrub at 10176, which closes. Reading the same table out of
core_data.bin produces plausible garbage rather than an error.
2. ClassEntry::offset_in_asset_wad addresses core_data.bin, despite the field
name and despite core_data.wad sitting alongside it.
Verified: all 1864 tie classes resolve, o_class agreeing on every one, and all
5592 LOD block offsets land inside core_data.
Disproved, and this is the part worth reading: the word at +0 of a 16-byte unit
inside an LOD block. On the first twelve units of level 0's class 0 it rises
strictly -- 288, 1520, 2848, 3968, 5088, 6096, 7312, 8528, 9792, 10976, 12016,
13072 -- which reads exactly like a linked list, and the accompanying i16s looked
quantised, two distinct values in i16[0] and three in i16[1].
Across the population it is strictly increasing on 0 of 1864 blocks, and i16[0]
takes 8969 distinct values on level 0 alone rather than two. The sample was
twelve units out of 1,724,334. Both the monotonic reading and the quantisation
were artifacts of that sample.
That is the pooled-claim error AGENTS.md rule 3a exists to prevent, committed
almost verbatim: the first twelve units looked clean, and I was one step from
writing "u32[0] is a linked list of runs" into a format spec. The test now
asserts the disproof, so it cannot creep back.
Not established: the block's record size. 3580 non-empty LOD blocks compute as
divisible by 16, but some packets offsets run backwards, so the "next offset
delimits this block" end is wrong for them and that figure should not be trusted.
The tie layout stays partial: indirections and header done, records not.
|
||
|
|
e5d267a810 |
Reconcile roadmap.md with the parsers that actually exist
Findings were going to docs/plan/level-assets.md and docs/formats/*.md while roadmap.md kept listing work as open that has been done for some time. The worst case was occlusion appearing twice: open at one line with "2 to 25 sectors, small enough to read whole", and done further down with 412521 cells over 140939 masks. The open line is deleted. Also corrected: - tfrag was listed flatly open while six tfrag entries beneath it were checked. Now partial: format, topology, textures, LOD, normals and colour are done; morph targets and strip/chain bookkeeping are not. - sky was lumped in with volumes as open. Sky is done -- 149 sub-objects across 27 levels -- so it has its own entry, keeping the word at +0x0c documented as not a count, since it disagrees with the table on most levels and reads 196617 on level 2 against 4 sub-objects. - level_overlay was open. It now has a parser and a 27/27 test, so it is partial rather than done: header, constant block and level data parse, +0x10..+0x1220 is identical on every level, and the word at +0x0c falls below the file length on 17 levels and above on 10. The header words are still unnamed and the Phase 3 seven-section parser is untouched. - sound_bank was flatly open. It is partial: container framing parses and bounds are verified on 27/27, but the SBlk record fields are not decoded and DAW shows no stride, consistent with ADPCM. - tie now records what is actually known, including the two indirection chains that have not been walked: class table entries point into core_data.wad, and the header's packets[0..3] resolve against the lump base. Level 0 has 74 tie, 238 moby and 7 shrub classes. - collision now separates the header, which is decoded, from the cell layout, which is not -- including the 17-entry table at +5932 against a 75-cell grid and the disproved 64-byte RAM stride. - moby_anim records the negative result: per-chunk-type record counts are level-independent, so nothing in the chunk set is keyed by moby class. The "Where we are" section now states plainly what has parsers and that what remains open is naming rather than locating, and names the five types with no decoded fields. Reading that list is how you would have known to ask. Phase 2 remains in progress. Volumes and moby_anim have no located structure and SBlk records, tie vertices and collision cells have located structure without decoded fields. |
||
|
|
d8b8ccc6d4 |
Follow the gameplay.bin pointers: they address position and direction arrays
Where the pointers go. Following them shows what gameplay.bin indexes. The leading table is 39 words, all of which land inside the file, and three clusters dominate: near 150000, near 800000 and near 1050000. The region near 797808 is position data -- 31 consecutive plausible floats that group cleanly into xyz triples: 246.948 201.015 73.867 | 88.240 186.000 302.598 | 107.712 12.723 183.125 353.591 126.365 19.962 | 182.068 374.001 122.567 | 116.961 265.001 437.866 and the region near 797456 is direction data -- unit-length vectors: 0.969 0.945 0.733 | -0.120 0.433 0.250 | -0.866 0.000 0.267 So the file's leading words are a table of pointers into float arrays, not a flat list of small integers. The earlier reading of it as "u32 pointers holding nothing" was too quick: it is a pointer table into data that is present. Volumes remain unlocated. The position arrays are the right shape to bound a volume and nothing yet shows the volume records themselves. |
||
|
|
b17ec17127 |
Rule out the moby_anim instance-table reading of the chunk types
moby_anim was the one post-shrub chunk type with a concrete reason to exist: an instance table would have to scale with the level's moby class count, so comparing per-type record counts against that count was the obvious test. It fails, and the way it fails is itself the result: per-type record counts are level-independent. The same types report the same counts on every level tried: type_id level0 level1 level2 level3 0x007d7ec438 90 90 90 90 0x00d3d8c4a9 90 90 90 90 0x00429ec43d 77 77 77 77 0x0053c8c4aa 77 77 77 77 0x007626c4a4 50 50 50 50 0x00fb88c462 47 47 47 47 Across all 27 levels there are 77 distinct chunk types, the largest at 2430 records total -- exactly 90 x 27. What that rules out: nothing in this chunk set is a per-moby-class instance table, because those counts would vary by level the way the moby class counts do. So whatever holds moby animation data is either keyed by something other than the class count, or it is not among these chunk types. Also visible: the types pair up. 0x007d7ec438 and 0x00d3d8c4a9 both report 90 on every level; 0x00429ec43d and 0x0053c8c4aa both report 77. Two chunk types carrying identical, constant record counts on every level is a signature of two views of the same fixed table rather than two independent tables that happen to agree. Still open: the pairings are described but not explained, and no type has been named. The Moby class-ID enrichment from earlier stands separately -- 9493 hits against 246.1 expected is real, and the class arrays are contiguous per type, but that identifies where classes are referenced, not which chunk is moby_anim. |
||
|
|
02ced5ee83 |
Parse the per-level overlay: header, constant block, level data
level_overlay was the least-characterised file in the level set -- no parser, no located structure. It now has gc_formats::overlay and a real-disc test over all 27 levels: overlay: 27 levels; field_00 1735808, field_08 1; field_04 37540..=45308, field_0c 2763912..=3027336 (17 below the file length, 10 above); +0x10..0x1220 identical on all The header is four words. +0x0c is not the file length: it was recorded as one on the strength of the first levels measured, where it sat near the file end, but across all 27 it falls on both sides -- below on 17, above on 10 -- so it is an ordinary level-specific word. The test asserts this, being the claim most worth pinning. The common prefix is 4 bytes, not the whole header: +0x00 is constant at 1735808 while +0x04 and +0x0c vary. Everything from +0x10 to +0x1220 -- 4624 bytes -- is byte-identical on all 27 levels. Still not named: what any of the four header words means, and how the level-specific tail is structured. The module reports them as fields. Also records in docs/plan/pcsx2-verification.md that the 64-byte RAM stride from the previous commit is disproved. A 415 KB dump of level 1 finds the longest such run at 27 records, not 384, and those are identity matrices -- 27 of them, one per level. It is a per-level transform table, not the collision grid, and the 20.0 reported there was a coincidence. |
||
|
|
bd012efe71 |
Find a 64-byte collision record stride in RAM, and record a savestate search pitfall
Searching the savestates for the collision grid dimensions as a u32 pair -- level
0's 11 x 33 and level 1's 24 x 16 -- puts level 1's at several RAM addresses and
level 0's nowhere. So the grid dimensions are not held in RAM as adjacent u32s,
and the collision payload is reshaped on load, as the shrub packet tables are.
A nearby region of level 1's RAM does show structure. At 0x12e048c and
0x12e04cc, 64 bytes apart:
28 01 00 00 | 92 ff 00 00 | 04 00 a0 41 | ...
296 -110 20.0f
Searching for that shared 12-byte tag finds a consecutive run at a 64-byte
stride in slot 2:
0x12df390
0x12df3d0 +0x40
0x12df410 +0x40
plus scattered singles elsewhere. Slot 1 has the tag at four addresses not on a
64-byte stride, so the two levels lay it out differently.
What this is: a 64-byte record carrying two small signed values and a float
reading exactly 20.0 -- the shape of a per-cell entry with coordinates and a
radius or half-extent, which is what the collision grid would hold.
What this is not: it is not decoded. Three consecutive records is not enough to
establish a stride, the tag matches only a few records rather than a whole grid,
and 20.0 may be coincidence. Level 1's grid is 384 cells; nothing here has been
shown to count to 384.
Method note: savestates are compressed on disk, so a byte scan of the file finds
nothing. All RAM search must go through gc-trace find --state, which decompresses
first -- scanning the file directly reports zero hits for a sequence that
demonstrably exists in memory.
|
||
|
|
d3e1475f64 |
Characterise gameplay.bin as a flat pointer table, and record volumes as unlocated
Range measured: the first 40 u32 and the whole file, on levels 0 and 1 --
1872256 and 2006400 bytes.
Result: the file is a flat array of u32, opening with a small count then a run of
ascending offsets, terminating on a zero:
level 0 336 797472 798832 799136 464 150672 150688 314800 479920 ...
... 1847600 1845456 797808 1829056 160 1849088 1249808 1847616 0
level 1 496 799904 800128 800432 624 151152 151168 315680 481456 ...
... 1984496 1982400 800112 1966000 160 1988160 1303456 1984512 0
Both levels carry the same shape. The sentinel 160 appears at u32 index 34 on
both and the zero terminator at index 38 on both, with counts 336 against 496
and 464 against 624 at the head. The leading ~38 words are a structured header
whose layout is level-independent apart from its sizes.
The offsets cluster into groups -- near 800000, 150000, 1050000, 1800000 --
which is what a table of pointers into several buffers looks like.
Not decoded: what the pointers point into, what 160 is, and why the zero
terminator sits at the same index on both levels. No gc_formats module exists for
this yet; it is characterised, not parsed.
On volumes: nothing resembling volumes was found here. gameplay.bin is u32
pointers throughout and holds no float data in its first 64 words read as f32 --
59 of 64 are denormals, i.e. small integers reinterpreted. Volumes remain
unlocated on the disc; nothing in the regions examined so far presents itself as
one.
|
||
|
|
575d1a5bdd |
Correct the overlay strings from a table to a pool
The previous commit described a "debug string table on 16-byte alignment". It is not a table. Dumping the 32 bytes before each run shows most are the tail of the previous string: +3104 "host0:r:/rc2/shock/wavetable.bin" 32 chars +3120 "ck/wavetable.bin" its own tail +3152 "/i5/shock/wavetable1.bin" +3168 "ble1.bin" its own tail and the 32-byte prefixes are 327 distinct out of 332 -- essentially all different, because they are just the preceding string's bytes. There is no per-record header here. It is one contiguous pool of debug text, and the 16-byte alignment is a property of how the pool was laid out, which makes continuations land on boundaries and read as separate entries. The count of 332 "strings" was therefore inflated by the same effect: it counts continuations as entries. The substance is unchanged and still useful -- the pool carries the game's own debug text, including "Camera_CollPrimTest WARNING! - grid out of bounds! (RAR)", which is direct evidence for reading the collision payload's two leading u16 as grid dimensions, and the wavetable.bin paths, which bear on the sound bank framing. Only the framing was wrong. The pool is located and counted but not parsed, and there is no gc_formats module for it. The +0x18/+0x20 names are section names but their structure is not established, and it is not shown that they index anything. |
||
|
|
a5a33b9bc0 |
Locate level_overlay's sections and a 16-aligned debug string table
level_overlay was on the open list with no parser and no located structure. It now has a header naming two sections and a string table. Range measured: overlay.bin on levels 0, 1 and 2 -- 2751268, 2763668 and 2928424 bytes. Result 1: the file opens with section names. +000: 80 7c 1a 00 | dc 99 00 00 | 01 00 00 00 | 88 3b 2a 00 +010: 00 00 00 00 | 00 00 00 00 | "crap\n" | 00 00 00 00 00 +020: "system misc" 00 00 00 00 00 00 00 00 00 +030: 20 80 ff 40 | 00 60 cf 00 | 64 00 | 46 00 | 19 00 00 00 u32@+0x00 is 1735808 on all three levels, +0x08 is 1, and the bytes at +0x18 and +0x20 spell "crap" and "system misc". Result 2: a debug string table on 16-byte alignment. 332 such strings on level 0, 362 on level 1, 351 on level 2, first at +0x20 and last between 1.3 MB and 2.5 MB. They are the game's own debug text: host0:r:/rc2/shock/wavetable.bin host0:z:/i5/shock/wavetable1.bin unloaded sound bank %#010x Camera_CollPrimTest WARNING! - grid out of bounds! (RAR) render setup sky draw pre effects pre 2 effects Camera_CollPrimTest WARNING! - grid out of bounds! (RAR) is direct evidence for the collision reading. It is the game's own string, naming a grid and a collision test, and it is why the two leading counts in the collision payload are worth continuing to treat as grid dimensions despite not sizing the payload. The wavetable.bin paths also confirm the sound bank is loaded from named files, which bears on the SBlk/DAW framing. Not done: the string table is located and counted but not parsed -- the records preceding each string have not been identified, so there is no gc_formats module for it yet. The +0x18/+0x20 names are section names but their structure is not established, and it is not yet shown that they index anything. |
||
|
|
e58ab0e815 |
Correct the collision table's extent: 17 entries, deltas rise then fall
+5932 is the table's start, not a point inside it: walking backwards, no predecessor satisfies "ascending with a delta under 4096". The run is 17 entries, not 19 -- the earlier 19 counted two entries past the jump. The deltas are 204 212 216 224 224 228 228 228 224 220 216 200 96 96 100 96 then a jump of 41484 to 190508. The deltas rise and then fall -- a symmetric profile, not a monotone one. That weakens the prefix-sum reading: a cumulative offset table has no reason to come back down, whereas cell contents that grow and shrink as the table walks across a level would. 17 against a 75-cell grid, and 75 - 17 = 58. No arithmetic relating the two has been found. The earlier guess that 19 related to 3x25 did not survive re-measurement and is not carried forward. Still open. 17 entries may not correspond to the grid at all -- the table may cover something else, or a subset of cells. Resolving it needs either the same offset found in other levels at a position the leading counts predict, or a reason for 17. Neither exists yet, and this is where the measurement trail goes cold on collision. |
||
|
|
b66e77855c |
Locate a per-cell offset table in collision, on level 4
Range measured: longest strictly-ascending u32 run (delta under 65536) in each
level's collision payload, levels 0 to 5.
Level 4 is the clean case. Grid 3x25, payload 2660608, and at payload +5932 a
run of 19 entries whose values are all inside the payload, starting at 146012:
deltas 204, 212, 216, 224, 224, 228, 228
The deltas grow monotonically, which is the signature of a prefix sum over
records of increasing size -- what a per-cell offset table looks like when cell
contents vary.
The others are not:
level 0 11x33 +21232 17 8193, 24575, 8192, 8192 multiples of 8192,
coincidental on
dense data
level 1 24x16 +41452 29 32771, 57339, 24575 wild, no trend
level 2 1x53 +13752 49 24, 64, 396, 104 in-region but
irregular
level 3 19x31 +667152 18 769, 257, 257, 257 values ~5.2e8, far
outside the payload
level 4 3x25 +5932 19 204, 212, 216, 224 growing, in-region
Only level 4's run has both properties a prefix sum needs: values inside the
payload, and deltas that grow.
What this does not give: level 4's run is 19 entries against 75 grid cells, so
it is not one offset per cell -- either most cells share an offset, are empty,
or the table covers something other than the grid. Nothing measured picks between
those.
Next to check: whether 19 relates to 3x25 by any arithmetic, and whether the
same offset at +5932 exists in other levels at a position given by their leading
counts. Neither has been tried.
|
||
|
|
2569cc06ab |
Decode the collision payload's leading two counts
Range measured: the first four bytes of the payload on all 19 levels that carry a non-empty one. Result: two u16 at payload +0 and +2, both non-zero on all 19, ranging 1..74 and 13..112, products 53 to 3256. level 0 11 x 33 = 363 payload 903248 level 1 24 x 16 = 384 payload 2800496 level 2 1 x 53 = 53 payload 2315296 level 7 40 x 35 = 1400 payload 4049232 level 9 74 x 44 = 3256 payload 2518352 level 13 2 x 64 = 128 payload 3333520 level 26 27 x 112 = 3024 payload 652176 Reported as two counts, not as a named grid. They have the shape of collision grid dimensions -- bounded, both non-zero, products in the hundreds to thousands -- but nothing measured ties the product to the payload size or to any per-cell stride. Level 2's 1 x 53 giving a 2.3 MB payload and level 26's 27 x 112 giving 652 KB shows the product does not size the payload, so either cells vary wildly in size or these are not grid dimensions. Naming them width and depth would be a reading, not a measurement. After the two counts comes a run of u16 that stays below 8192 for 30 to 58 entries depending on level, always an even count, and is not ascending. That is where the per-cell table most likely starts, and it is the next thing to resolve. Exposed as CollisionHeader::first and ::second. They live in the payload rather than the header, so CollisionHeader::region fills them in and the bare CollisionHeader::parse leaves them zero -- documented at the call site so the difference is not mistaken for an absence. |
||
|
|
72bbd5da38 |
Rule out GS packet framing in tie, with a control that nearly fooled me
Range measured: level 0, the 73 tie span bodies (bytes +0x40 onward), 2585856 bytes pooled. Per-position distinct counts, an autocorrelation scan against a shuffled control, and a GS GIF tag test. Result 1: the bodies are dense. Every one of the 16 slot positions takes 245 to 256 distinct values, byte 15 zero on 81.3% and byte 7 zero on 78.8%. The best stride peak is 48 at +0.097, barely above 16 at +0.095 -- no period. Result 2: no GS GIF framing at 16-byte alignment. Testing whether byte 15's low three bits fall in 0..4, the GS tag range, gives 97.0%. That looked like a strong hit until the control was done properly. A uniform shuffle gives 62.6%, exactly 5/8, what a random byte predicts -- so the naive comparison reads as a 34-point excess. The distribution-matched null is 97.0%, identical to the observation. The whole excess was that byte 15 is 81% zero and (0 & 7) <= 4 is always true. Recomputed per alignment the null tracks the observation everywhere: align 0 97.0% align 4 75.7% align 8 96.7% align 2 75.5% align 12 89.3% This is the control rule earning its place a second time. A uniform shuffle is not a matched control when the data is skewed, and AGENTS.md already says so; the near-miss was reading 62.6% as a baseline without checking what the data's own distribution predicts. The tie bytes concentrate on 0, 1, 2, 3 and 63 -- suggestive in isolation -- and still carry no framing. What this rules out: the tie bodies are not GS GIF packet streams at 16-byte alignment and carry no measurable periodicity. With every slot position taking ~256 distinct values they are dense data -- fat vertices, as the roadmap expects -- and locating the vertex layout needs a reference for this game's instanced geometry format, not another stride scan. |
||
|
|
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.
|
||
|
|
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. |