An addition to AGENTS 3b, from nearly repeating the failure it describes.
Checking the count in 25d73b4, the suite came back 278 passing -- the same 278 as
always. But GC_DISC was not set for that run, so all 58 disc tests hit their
`require_disc!` early return and were counted as passed. The number was right and
the coverage behind it was not.
What distinguishes the two runs is the wall clock and nothing else:
without GC_DISC 58 tests, 4.10s
with GC_DISC 58 tests, 36.14s
Same test count, same "ok", same zero failures. A reader -- including me -- has no
way to tell those two lines apart. That is a sharper version of the 3b failure
than the filtered sum was, because no amount of reading the output carefully
distinguishes them. The duration does, and it is in the line already.
So 3b now says: if a run finishes suspiciously fast, the disc tests did not run.
And it keeps the standing instruction to set GC_DISC when touching disc-decoding
code, since 25d73b4 changed texture-adjacent code and the verification genuinely
needed the disc.
Re-verified properly: 278 passed across 23 + 19 + 58 + 156 + 22, disc present,
36.14s on the disc binary. Clippy clean.
Rule 3b, requested after a commit went out with a failing test in it.
cargo test -q --workspace | grep "^test result: ok" | awk '{s += $4}'
That sums the green test binaries and ignores the red one. A broken build still
prints a number, and the number looks entirely reasonable -- six healthy counts
added together is not a number anyone questions. I used that filtered sum as the
evidence of health in commit messages for the better part of a session, and it
was reporting green on a build that was not.
What actually happened is the part worth keeping. The count came back lower than
the previous run -- 100 instead of 278. That is a genuine signal and I explained
it away twice as an environment difference, the disc not being set, before
checking. Both explanations were plausible. Neither was tested. Only the
discrepancy was noticed, and noticing it is what surfaced the failure.
So the rule covers both halves: do not commit or push with a failing test, and do
not derive the number you report with a filter that drops the failing binaries.
The second half is the one that is easy to get wrong again, because
`cargo test -q` is quiet, the grep looks like a reasonable way to total up, and
the total is the only part anyone reads.
Also records the mistake as a mechanism rather than a symptom. Same defect as
rules 3 and 3a, one level up: a quantity that should have had one definition had
two, and nothing compared them.
Added a line to "Working rhythm" pointing at it, since that is the list someone
reads before committing.
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.
Rule 3 required bounds and a method for every number, but not that the
measurement be partitioned. Three errors in one stretch were all of that kind,
and all three passed the control check rule 3 asks for -- because a matched
control bounds significance while saying nothing about scope:
- a 368-byte period attributed to a whole region, when it was one chunk's
stride among ~15. Averaging the strides put the peak at the noise floor,
which read as "no structure" rather than "wrong scope".
- a ten-entry table disconfirmed, when it existed at a fixed stride among
several.
- a moby class array reported at u16 slots 129-140, which was the
largest-stride chunk type dominating a pooled scan. Per type, all 26 place
their array elsewhere.
In each case the aggregate was arithmetically correct and the conclusion was
wrong. The re-run that caught all three was identical: group by the thing that
varies, then look.
Rule 3a now requires stating the partition before the number, reporting per
partition or saying explicitly that a claim is pooled and unverified, treating
a structure found in one partition as unproven until every partition agrees,
and retracting rather than softening a result that changes under partitioning.
Renumbered the following rules.
Four numbers in one session looked load-bearing and turned out to be
artifacts: a format identified as VIF-packed on the strength of recurring
byte patterns where a control put it on the random baseline, a body size
computed over wrongly bounded spans, a "6.2 MB of unidentified region" that
was texture data, and "3 unexplained blocks" that were a sampling artifact.
All four shared one cause -- measuring before bounding, naming bytes before
looking at them -- and none errored, which is why they survived long enough
to be believed. Two of them needed follow-up commits to undo.
The rule: any size, count or distribution comes with the byte range it
describes and how it was measured. Ranges come from core_layout rather than
from running to core_data.len(). A statistic that cannot separate your format
from uniform random cannot tell you anything. And counting distinct byte
values only tells you which blocks to look at, not what they are.
Worth having in the working agreements rather than in anyone's memory.
No behaviour change. This is the pass over the last stretch of work to
make sure the accumulated result hangs together rather than only each
commit in isolation.
Code. lit_color had grown a `let _ = (depth, lod_distances)` and a dead
branch from repeated edits. The directional pass genuinely does not
depend on distance, so the parameters are gone rather than discarded,
with a note that the point lights -- not implemented -- will need them.
lit_at folded into lit_color, which had become a wrapper around it.
Checked for leftover scaffolding: only two env vars remain, both real
(GC_ENGINE_SHOT and GC_ENGINE_DEBUG_BOUNDS); GC_ENGINE_UNLIT,
GC_ENGINE_DEBUG_COLOR and GC_ENGINE_ABL were never actually written,
because those edits silently failed to apply against rustfmt'd source.
AGENTS.md. The crate table did not say that gc-engine owns hardware
models. ps2.rs is the Emotion Engine's FPU and the lighting pass runs
on it, so the boundary is now stated: gc-formats decodes inputs,
gc-engine reproduces how the hardware computes with them. That is the
distinction rule 2 depends on, and it was implicit.
Docs. Three claims had been overtaken by what we learned and were
actively misleading:
* docs/formats/level-wad.md still said gameplay "does not decode with
the WAD decoder". It does; it is a WAD stream.
* docs/plan/level-assets.md carried a whole section, "a compression we
have not identified", with a ruled-out list including "bit order",
"header length" and "the WAD format at all". All three were ruled out
against the wrong input. Replaced, with the failure mode kept: a
uniform failure across 27 inputs was read as "the decoder is wrong"
when it meant "the data was handed over uncompressed", and the
positive control -- decompress and look -- was never run.
* The same file still said the tfrag "record framing is not recovered".
True, but it was a false lead that has since been superseded by the
VIF1 framing. Kept as a recorded wrong turn rather than deleted.
Also refreshed "Still open" to name what is actually outstanding:
point lights, the GS register set, the guard-band clip test, the
unread core_index class tables, and -- stated plainly -- that lighting
magnitudes are unverified because there is still nothing to compare
against.
Verified after the changes: 148 tests pass, clippy and fmt clean, all
5,213 extracted files match the hash table, and level 0 still loads at
26,758 triangles with the same LOD split and fully lit.
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.