b5edbd181023e6a056d0ada4fd55ae9e1332ee48
57
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3ec548efe8 |
Correct the terrain findings: region layout confirmed, record framing disproven
Two claims in the previous commit were wrong and are retracted here rather than
left standing for the next person to build on.
"382 records of 64 bytes" and "byte +3 is 0x48 in every record" were both
artefacts. The 0x48 at that offset is simply the most common byte in the region --
945 of 2000 sampled slots -- not a marker. Over 33,091 slots at that position
only 4,127 hold 0x48. The pattern holds for the first 382 records and then
breaks, which is exactly how a coincidence of a dense byte value masquerades as
structure.
"The coordinates are fixed-point s16 with a scale of 8" is also not supported.
It rested on one coincidence: 17,496 / 8 = 2,187, the planet radius. No grouping
of the record into coordinate triples produces points on a sphere of that radius.
Tested every stride from 2 to 16 bytes at every starting offset, with three and
four components; the best result is 5.0% of points on-sphere, which is chance.
What is now established, and holds on all 27 levels:
core_data region layout, as strictly increasing offsets
tfrag [0, occlusion)
occlusion [occlusion, sky)
sky [sky, collision)
collision [collision, textures_base)
Ruled out the competing reading where 0x0c/0x10/0x14 are sizes laid end to end:
the cumulative total misses textures_base on every level and the leading bytes
of each supposed region are not plausible structures.
Also new: terrain carries its texels inline. A scan for the PS2 texel pattern
18 18 18 80 finds 6,739 occurrences in 138 clusters starting at 0xd9f8, well
inside the tfrag region, so textures_base names something other than terrain
textures.
The vertex format remains unrecovered and the record says so. The next attempt
should look for a longer run of plausible vertices rather than re-deriving a
framing from one record.
|
||
|
|
84d94337f7 |
Record the terrain record layout: 382 fixed 64-byte records
Continued on the tfrag terrain region and got a firm answer on the framing, which is what the next parser needs before it can read vertices. The region holds exactly 382 records of 64 bytes, from 0x40 to 0x5fc0; the 383rd breaks the pattern. Per-column analysis over 300 records separates marker bytes from data: +3 0x48 in 300/300 -- a marker, the high byte of the s16 at +2 +19,20,21,23,43,45 0x00 in 300/300 +52..55 ff 00 ff ff in 300/300 -- a fixed signature everything else varying Coordinates are fixed-point s16 with a scale of 8. That is not a guess: the value 17,496 appears in the first record and 17496 / 8 = 2187, the radius of a Ratchet & Clank planet to the unit. Read as f32 the data is nonsense, which is consistent with the earlier finding that no plausible float run exists anywhere in the 16.9 MB. Still open, and stated as open: which column is which of x, y and z, and whether 64 bytes is one vertex, two, or a header plus data. The ff 00 ff ff signature is more likely a record delimiter than a GS packet, since a VIF interpretation of the first tag does not chain. The next step is mechanical rather than exploratory: try every grouping of one record into 3-component vertices and keep the one whose points all fall within a sphere of radius ~2,187. Recorded in plan/level-assets.md. No code change. |
||
|
|
85fa9d1432 |
Locate the tfrag terrain region and record what its records look like
The core header field at 0x08 is zero on all 27 levels, and the seven offsets 0x0c, 0x10, 0x14, 0x60, 0x64 and 0x68 are strictly increasing on every level. Reading them as a sequence of region boundaries gives the layout of core_data: tfrag [0, occlusion) occlusion [occlusion, sky) sky [sky, collision) collision [collision, textures_base) So tfrag = 0 is a real value rather than a missing field, and the terrain is the first 2.12 MB of level 0's geometry. The terrain begins with a short header followed by a repeating 64-byte record from 0x40. Established about those records: byte columns 3, 7, 11 and 15 are 0x48 in every record, which are high bytes of four s16 values giving a near-fixed Y on a planet of radius about 2,187; columns 16 and 17 increase steadily across records like a grid walk; and columns 48-51 carry a repeating ffff00ff pattern characteristic of GS packet headers. The record layout itself is not yet recovered, and the write-up says so rather than guessing. Ruled out and recorded: uncompressed f32 vertices (the longest plausible float run in 16.9 MB is a run of zeros in the collision block, so vertices are packed); GS packet tags at every position (0xE0..0xEF is frequent but uniform, which is what packed data looks like); and a 48-byte record stride (the pattern repeats at 64). Also confirmed this session: the core header base is offset 0 with certainty, because the compressed and decompressed sizes of core_data appear at the same offsets (0x88 and 0x8c) in all 27 levels' core_index. And the occlusion block at the header's 0x0c offset reads as an ascending u16 array of 65 values before restarting, consistent with per-cell visibility records. No code change: this is the reverse-engineering record for the next parser. 75 unit tests, clippy clean. |
||
|
|
16c04db559 |
Fix level extents read as bytes, and extract the level geometry
A defect in the Phase 1 extractor, found by reading the data extent's contents: level extents are declared in sector counts, the generic G/ pair table uses byte counts, and level containers were being routed through the generic path. So LEVEL0.WAD's data extent -- 6,484 sectors -- was taken as 6,484 bytes. Every test still passed, because a truncated file is still a file. The extent's own header gave it away: it declares an uncompressed total of 13,277,920 bytes, and 13,277,920 / 6,484 is 2047.7. A ratio landing on 2048 is a unit error, not a compression ratio. Across 27 levels this had been discarding roughly 300 MB of level content. Level containers now go through the typed LevelHeader instead. Level 0 yields: data.bin 13,279,232 the data extent core_data.wad 9,738,678 the geometry, compressed core_data.bin 16,969,296 the geometry, decompressed core_index.bin 31,600 the geometry directory gameplay.bin 659,456 sound_bank.bin 1,503,232 core_data is a WAD LZ77 stream -- the format already implemented for the game -- and it decompresses cleanly. The first real level content this project has recovered. New: LevelDataHeader parses the data extent's own header, a table of byte ranges naming the level overlay executable, the geometry directory, the GS RAM image, the HUD banks and the geometry. Note this is a third meaning for a "size" field on the same disc, after sector counts in the level header and byte counts in the generic table; all three are documented together in level-data.md so the next parser does not rediscover this the hard way. The geometry directory's offsets were checked against the geometry rather than taken on trust: all twelve land inside the 16.9 MB of decompressed core_data, which is what confirms the header is being read correctly. The class tables themselves do not yet decode under the documented layout, and that is recorded as open work in plan/level-assets.md rather than papered over. 75 unit tests, 9 integration tests against the real disc. Hash table regenerated at 5,159 files and 5,671,915,799 bytes -- hashes only, no content. Extraction now writes to ~/Documents/gc-extracted rather than /tmp, which is 16 GB and did not have room for a 5.3 GB output. |
||
|
|
a9e43e39c1 |
Parse the level container header, naming each level's four extents
The first typed structure read from real data. Every one of the 27 level containers begins with the same 0x60-byte header, which turns the generic pair table into named assets: data, sound_bank, gameplay and occlusion, plus three chunk descriptors. Two things about this header are worth stating because both are silent failures rather than errors. Sizes here are sector counts, not byte counts. The generic G/ pair table on the same disc uses byte counts, so both readings type-check and the wrong one is off by a factor of 2048. The tiling check settles it: read as sectors, the four extents tile every one of the 27 containers exactly; read as bytes they overlap each other. Level id is not container order. LEVEL21.WAD reports id 30, so a level was cut from the disc and its number left a gap. Anything keying off file order -- a save slot, the planet select screen -- would mislabel that planet. There is also a second trap in the same area: the ISO filesystem sorts names alphabetically, so LEVEL10 precedes LEVEL2, and any tool needing order has to sort numerically. The header's field at 0x04 is zero on all 27 levels. It is not the container size despite looking like one, so the code reads it as a runtime field and takes the container length from the ISO entry instead. Measured across all 27 levels, three invariants now hold in tests: extents tile in file order; extent coverage plus chunks reaches at least the end of the container (21 levels land exactly on its length, 6 leave 134 to 689 sectors of trailing data); and exactly one level's id differs from its container index. The coverage assertion is >= rather than == on purpose -- 21 levels end one sector past the file end because the header occupies sector 0, and asserting equality would truncate a level's tail. gameplay is left unparsed and that is recorded rather than hidden. It begins "WAD" plus a per-level byte and does not decode with the WAD LZ77 decoder: all 27 levels produce plausible output for the first packet and then fail on a rule the format's own encoder never breaks, which means the input is wrong rather than the decoder. docs/plan/level-assets.md records what is ruled out and what to try next. 66 unit tests, plus 7 integration tests against the real disc. The unpack hash table is unchanged, so this adds parsing without altering the pipeline. |
||
|
|
027373e65b |
Add the disc pipeline: identify, unpack and verify
Phase 1 of the roadmap. The disc is now turned into verified asset files, and
the result is checked against a committed hash table rather than against
whatever happens to be on disk.
gc-extract identify disc.iso SCUS_972.68 v1.01 NTSC, ELF SHA-1 matches
gc-extract unpack disc.iso out/ 5135 files, 4,500,106,960 bytes, 10.8s
gc-extract verify out/ table.tsv OK: 5135 files match
All 92 G/*.WAD containers parse. MPEG1 and MPEG2 are raw video and pass
through whole; the other 90 are indexed containers whose extents are extracted
individually.
Formats recovered and written up in docs/formats/:
- iso9660.rs. R&C2 lists its assets in the filesystem, unlike R&C1, so a
directory walk is the primary asset index. Three details that all fail
quietly rather than loudly: directory records store every 32-bit field twice
(little-endian then big-endian), the CD001 signature is at sector offset 1
rather than 0x8001, and the directory flag at record offset 25 is
authoritative where the identifier's trailing slash is not.
- global_wad.rs. Container headers hold a table of extents. All 92 indexed
containers use {offset in sectors, size in bytes}. Measured, not assumed: the
sector-count reading places every entry of all 27 LEVEL<n> and all 27
AUDIO<n> containers out of range, while the byte reading places all of them
correctly. SCENE<n> is the one exception, a flat sector list whose extents
are sized by the next value.
- wad.rs. Insomniac's LZ77 stream: four flag-selected packet classes, with 0-3
trailing literal bytes per packet carried in unused low bits. This is not a
bit stream, which is why it resists the usual LZ77 reading.
- buf.rs. A bounds-checked reader, so untrusted 2003 disc data produces typed
errors rather than panics.
gc-extract carries the three stages and a SHA-1 implementation with FIPS 180-4
test vectors, so identification works without pulling in a crypto dependency.
Verification: 54 unit tests, plus four integration tests that run only when
GC_DISC names a disc. Those four check identification against the recorded
values, cross-check the Rust identifier against tools/identify-disc.py, parse
all 92 containers, and confirm a full unpack matches the committed table. The
committed table is 316 KB of sizes and SHA-1s for all 5,135 files: hashes only,
no game content, so it is safe to version and it turns "matches retail" into
something a test can assert.
docs/plan/disc-pipeline.md records two wrong turns, because both are easy to
repeat and both failed by producing plausible wrong output rather than an
error: the container table was read backwards, and the LZ77 stream was modelled
as a bit stream. The first survived because it happened to be correct on the
one container used for manual checking.
|
||
|
|
a02f79a6d1 |
Initialise native Rust port of Ratchet & Clank: Going Commando
Sets up the foundation for reimplementing the PS2 game in Rust rather than emulating it. Nothing is implemented yet; this is the scaffold, the verified target and the working agreements that keep a long project coherent. Target disc, verified against a real image: SCUS_972.68 v1.01, USA Black Label, NTSC 60 Hz boot ELF 2,618,684 bytes, SHA-1 bacda860...d098a77, entry 0x00131AE8 The entry point independently matches the value recorded by the rac2-gc-decomp community reference, which cross-confirms the identification. This is the exact build the upstream research pins; Greatest Hits v2.00 is a different binary and is not the target. NTSC matters beyond label compatibility: PAL discs run 50 Hz, which would make chargeboots and other speed-sensitive movement run at 5/6 speed. Verifying the disc before writing movement code avoids discovering that in Phase 5. Contents: - Five-crate workspace (formats, extract, game, video, engine) on Bevy 0.19.1, matching the version ReRAC uses so its Graphics Synthesizer and VU work stays portable. `cargo check --workspace` is clean. - tools/identify-disc.py, which reproduces the identification against any disc image and distinguishes a truncated dump from a non-PS2 image. - .githooks/pre-commit, refusing game data from entering git history: disc image extensions, asset directories, container signatures at offset 0, and files over 50 MiB. Verified against seven cases including a disc image renamed to look like source. Now that this pushes to a remote, this matters more than it would have locally. - AGENTS.md, the operating agreements: no game data ever, reproduce the PS2 float model rather than approximating it, bound every read, verify against the real disc. - PROVENANCE.md, recording that this follows ReRAC (ISC) and learns from rac2-gc-decomp (MIT). Wrench is used as a written specification only and no GPLv3 code is copied, since that would relicense everything derived from it. RAC2Decomp is explicitly not used: its provenance is disputed in the upstream community reference, and it targets the wrong region. - docs/roadmap.md, phased so the riskiest unknowns are retired early. The two that matter: overlay relocation transfers from ReRAC and makes 27 level overlays tractable, while VU1 microcode is binary and is the hardest single item. Phase 5 is a deliberately scoped Veldin vertical slice as the go/no-go. |