Record a 6-second gif of examples/pong.nes running in jsnes and
embed it alongside docs/platformer.gif and docs/war.gif as the
third project demo. The gif opens on Pong's title menu (CPU VS
CPU / 1 PLAYER / 2 PLAYERS) — warmup = 4 frames keeps the menu
as the thumbnail the way war's recording does, and then the
headless autopilot advances to gameplay partway through the
clip.
- docs/pong.gif committed (128 KB)
- README.md links it under the war demo
- scripts/pre-commit rebuilds it when examples/pong* or the
recorder/harness change
- .github/workflows/ci.yml fails if the committed copy is stale
- CLAUDE.md and tests/emulator/record_gif.mjs reference the new
gif in the "how to regenerate" sections
https://claude.ai/code/session_0134F5uwDEVTes2Ee9S7JeXy
Captures the first ~6 s of examples/war.ne via the same
puppeteer + jsnes + gifenc pipeline that powers
docs/platformer.gif: title menu thumbnail, 52-card deal
animation, and a few rounds of CPU vs CPU play. Embedded
in the top-level README right under the platformer demo.
record_gif.mjs gains a 6th positional arg for the warmup
override (defaulting to the existing WARMUP env / 30) so
the war command can keep its title menu as the first frame
while platformer keeps skipping past its own title. The
CI emulator job and the pre-commit hook both rebuild the
gif into a tmp path and fail-with-fix-command if the
committed copy is stale; the war trigger covers war.ne,
war.nes, any examples/war/*.ne include, plus the recorder
and harness.
The compile bench had a hand-maintained parallel copy of
`src/main.rs::compile`, and that copy went out of sync after
bank switching landed — the bench kept handing the linker
`PrgBank::empty(...)` slots even though the CLI started
populating per-bank instruction streams + trampoline requests.
The assembler then panicked with `unresolved label:
'__tramp_step_animation'` on `uxrom_user_banked.ne` under
`cargo test --all-targets`, which is what CI runs. A plain
`cargo test --release` (what CLAUDE.md used to document) never
builds the bench so the bug slipped through local validation.
Fix:
- New `nescript::pipeline` module with `compile_source(source,
source_dir, &CompileOptions)` that owns the full
`parse → analyze → lower → optimize → codegen → peephole →
link` pipeline including the per-bank stream + trampoline
reconstruction. Returns a `CompileOutput` carrying the ROM,
the linker result, analysis, IR, assets, instructions, and
source-loc markers so downstream tools have one place to
pull metadata from.
- `src/main.rs::compile` reduces to file I/O + preprocessing +
a single `compile_source` call + CLI-only side effects
(`--dump-ir`, `--call-graph`, `--asm-dump`, `--memory-map`,
`--symbols`, `--source-map`).
- `benches/compile.rs::compile_pipeline` becomes a one-line
`compile_source` call. It is now structurally impossible for
the bench to drift from the CLI path.
- `tests/integration_test.rs::compile_with_debug_artifacts`
likewise delegates to `compile_source`. This also fixes a
latent bug in the helper where it used `Linker::with_mapper`
without `.with_header(...)` — programs opting into
`header: nes2` would have quietly got an iNES 1.0 header
through this path.
- `CLAUDE.md`: updated the "Running the basics" section to
specify `cargo test --all-targets` (plain `cargo test` skips
benches) and to point at `scripts/pre-commit` with the exact
install command. Also installed the hook in this worktree.
All 24 existing `examples/*.nes` rebuild byte-identical through
the new pipeline. 624 tests + all 25 emulator goldens pass.
https://claude.ai/code/session_01MaNVcDmK9gsspRkdxowQAM
The optimizer fix in the previous commit changes the observable
gameplay of `examples/platformer.ne` — pre-fix the player got
spurious enemy-1 stomp bounces every time coin 2 drifted into its
pickup window, so the README demo gif showed the player bouncing
mid-air around emu frames 85-125 instead of walking through the
coin at ground level. Regenerate `docs/platformer.gif` from the
fixed compiler so the README matches reality.
To stop this from drifting again, treat the gif the same way the
repo already treats `examples/*.nes`:
- `gifenc` + `jsnes` + the harness are deterministic, so a fresh
recording byte-matches a valid commit. Verified across two
back-to-back runs (identical md5).
- `.github/workflows/ci.yml`'s `emulator` job now renders the gif
into `/tmp/platformer.gif` and `cmp`s it against `docs/platformer.gif`,
emitting a `::error` annotation pointing at the exact rerun
command if the committed copy is stale. This piggybacks on the
existing puppeteer + node setup, adding ~20s to the job.
- `scripts/pre-commit` runs the same check locally, but only when
`examples/platformer.{ne,nes}`, `tests/emulator/record_gif.mjs`,
or `tests/emulator/harness.html` is staged, and only if
`tests/emulator/node_modules` is already installed. Cold-start
puppeteer is ~20s — too slow to pay on every commit, but cheap
enough to pay when something gif-relevant changed.
- The header of `tests/emulator/record_gif.mjs` and the project
conventions section of `CLAUDE.md` both spell out the rerun
command and the invariant, so the next agent doesn't have to
re-derive any of this.
https://claude.ai/code/session_013Bi4H4YQ5or5HtMB4doUFi
The previous commit wired the emulator job to read the committed
examples/*.nes directly, with the rationale that the `examples`
job's reproducibility diff would catch "stale committed ROMs"
at PR time. That reasoning is wrong and defeats the point of
the emulator harness.
Failure mode: a compiler change lands with correctly-rebuilt
ROMs (passing the examples job) but introduces a rendering
regression that flips a golden. The emulator job would catch
this today because it runs the harness against freshly-compiled
ROMs. With the previous commit's shortcut, the harness would
boot the committed ROMs — which the PR author just rebuilt to
match — so the mismatch wouldn't show up until a second commit
touched anything else. That's exactly the kind of silent-failure
hole the golden harness exists to plug.
Fix: emulator job rebuilds the compiler (toolchain + cache +
`cargo build --release`) and compiles every .ne into the
workspace before running the harness, same as the pre-ROMs-
committed era. The committed ROMs keep their review/demo role
(clone-and-play, diff visibility in PRs) but the test job
always validates the working compiler, not a frozen snapshot.
CLAUDE.md updated to match: the harness runs against whatever
sits in examples/*.nes, so iterating locally still means
rebuilding the ROM(s) you care about first.
https://claude.ai/code/session_01BcCcHi6FUmTh8jC7UgkA3A
The compiler is deterministic: rebuilding any example produces
a byte-identical ROM, verified across all 22 examples and all
four mappers (NROM, MMC1, UxROM, MMC3). That means the .nes
files are reproducible artefacts and can live next to their
sources without drift.
Benefits:
- Users can clone the repo and open any example in an emulator
without installing a Rust toolchain or running the compiler.
- The emulator harness can trust examples/*.nes directly, so its
CI job no longer needs a compiler build or a "compile all
examples" loop — it just boots jsnes against the committed
ROMs and diffs each against its golden.
- ROM diffs in PRs are now meaningful: "this compiler change
flipped 17 bytes in hello_sprite.nes" is visible review
signal, not hidden behind the emulator golden.
Guard rails so the ROMs don't drift from their sources:
- .gitignore no longer excludes *.nes.
- The `examples` CI job rebuilds every .ne into /tmp and fails
loudly (with a GitHub error annotation pointing at the exact
cargo command to rerun) if any committed ROM differs.
- scripts/pre-commit does the same check locally.
- CLAUDE.md now states that editing a .ne file requires
rebuilding its .nes in the same commit, so future agents
won't miss the invariant.
Total footprint: 22 ROMs, 624 KB (avg 28 KB each — most are
NROM 24 KB; two banked examples are larger).
https://claude.ai/code/session_01BcCcHi6FUmTh8jC7UgkA3A
New top-level `CLAUDE.md` so future AI agents (and humans) don't have
to rederive the project conventions from the existing docs. Covers:
- The phase layout under `src/` and the expectation that every
module has a co-located `tests.rs`.
- The core `cargo` commands and the fact that `fmt` and `clippy`
are mandatory before committing.
- The jsnes emulator harness end-to-end:
- File layout (`harness.html`, `run_examples.mjs`, `goldens/`,
`actual/`).
- How to run it locally (build release compiler, compile every
example, `npm install`, `node run_examples.mjs`).
- How to update goldens with `UPDATE_GOLDENS=1` and the rules for
when that's allowed.
- The procedure for adding a new example (build, golden, verify,
README).
- What the harness catches (codegen / runtime / PPU / APU / asset
regressions) vs. what it doesn't (input, beyond frame 180).
- How the CI job in `.github/workflows/ci.yml` wires everything up
and uploads `actual/` on failure.
- Zero-page allocation rules (runtime reserves `$00-$0F`,
palette/bg reserves `$11-$17` conditionally, user vars `$10+` or
`$18+`, IR temps `$80+`) so the next agent doesn't accidentally
collide with the runtime.
- "Things to avoid" list: no backwards-compat shims, no silencing
goldens without understanding the diff, no committing the
gitignored harness directories.
https://claude.ai/code/session_012fKB251HvEUQwG3tizFyqt