1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-08 08:55:38 +00:00
Commit graph

4 commits

Author SHA1 Message Date
Claude
0600f5b872
codegen: fuse compare-then-branch to drop boolean materialization
Every NEScript condition (`if x < N`, `while i < end`, etc.)
lowers in two IR ops: `CmpX(d, a, b)` materializes a 0/1
boolean into temp `d`, and the block's terminator
`Branch(d, t, f)` reads `d` and branches on it. The codegen
faithfully emitted both halves — `LDA / CMP / branch-to-true /
LDA #0 / JMP done / true: LDA #1 / done:`, then later
`LDA d_slot / BNE branch_t / JMP branch_f` — about 14 cycles +
13 bytes per condition.

The 6502's natural pattern is one `CMP` + one branch on the
flags it just set: 8 cycles, no register-clobber, no temp slot.
Detect the canonical pattern in `gen_block` (last op is an 8-bit
`CmpX` whose dest temp is what the terminator branches on, with
no other uses) and emit the fused form directly via a new
`gen_cmp_branch` helper. The temp's allocation, store, load, and
the terminator's branch fall away.

Bookkeeping subtlety: the source temps `a`/`b` must be retired
*after* the fused emit, not before — the original `gen_op` order
is "emit body of op, then `retire_op_sources`". Decrementing
their use counts before the CMP would free their slots while
they were still live; `load_temp(a)` would then re-allocate `a`
to whatever stale slot the free list popped next. Got hit by
this on the first attempt — the SHA-256 example dutifully
returned all-zero hashes until the order was fixed.

Updated `ir_codegen_local_label_suffix_is_bank_namespaced`: the
test was relying on `if x == 0` to emit `__ir_cmp_*` labels for
its bank-namespacing check, which the fusion now collapses into
direct branches. Switched the test source to a shift-by-variable
pattern (`x = x << n`), which always emits `__ir_shift_loop_*`
labels regardless of future cmp/branch optimizations.

Cycle savings: ~6 cycles per condition. The SHA-256 rotate
loops alone account for ~9K cycles per block. Across all
examples the cycle drift shows up as audio-tick phase shifts
in five timing-sensitive ROMs (`audio_demo`, `friendly_assets`,
`noise_triangle_sfx`, `platformer`, `sfx_pitch_envelope`); the
goldens for those are refreshed in this commit, plus
`platformer.gif` (the only demo gif whose bytes actually moved).

Verified: cargo test/clippy/fmt clean on rustc 1.95.0;
emulator harness 34/34; reproducibility diff clean; SHA-256 of
"NES" still computes to AE9145DB…4E0D.

https://claude.ai/code/session_01FRmSBruVWCufm3LsUVMs8v
2026-04-16 17:10:02 +00:00
Claude
0b5470b054
codegen: skip parameter-spill prologue for leaf functions
Leaf functions — those that never JSR another routine from inside
their body — don't need to spill the `$04..$07` parameter
transport slots into per-function RAM, because nothing inside the
body clobbers those slots. Detect them in `IrCodeGen::new` via a
linear scan over each function's IR ops, point their parameters
at `$04..$07` directly in `var_addrs` (and in a parallel
`leaf_param_overrides` map for inline-asm `{name}` substitution),
and have `gen_function` skip the spill prologue.

The "leaf" predicate is conservative: any of `IrOp::Call`, `Mul`,
`Div`, `Mod`, `Transition`, or an inline-asm body containing a
`JSR` token disqualifies the function. SetPalette /
LoadBackground / PlaySfx / StartMusic / DebugLog / DebugAssert
were verified by inspection to not emit JSRs.

Per call to a leaf primitive: `LDA $04 / STA <local> / LDA $05 /
STA <local+1>` is now omitted — saves 12 cycles and 12 bytes of
code per call. Across the SHA-256 example's ~5500 leaf-primitive
calls per block, that's ~66K cycles saved per compression — about
2.2 frames at NTSC.

The fix also touches every committed `examples/*.nes` (the leaf
prologue was emitted by every fun with params, not just the SHA
ones), so 9 ROMs and the same three timing-sensitive goldens
(war.png + platformer/pong/war audio hashes) get refreshed; the
two committed gifs that drifted do too.

Verified: cargo test/clippy/fmt clean on rustc 1.95.0; emulator
harness 34/34; reproducibility diff clean; SHA-256 of "NES" still
computes to AE9145DB…4E0D.

https://claude.ai/code/session_01FRmSBruVWCufm3LsUVMs8v
2026-04-16 16:47:07 +00:00
Claude
20a244b9e7
examples: regenerate ROMs, gifs, and goldens after codegen local fix
Commit 76d0fd0 moved function-locals from a codegen-minted
`$0300+` absolute range into the analyzer's zero-page
allocations so inline-asm `{param}` substitutions resolve
correctly (compiler-bugs.md #1). Observable semantics are
preserved — the analyzer + codegen now agree, and every
primitive that used to work still does — but the emitted ROM
bytes change whenever a function reads or writes a local,
because zero-page addressing uses a 2-byte instruction and
absolute addressing uses 3.

Consequences that need regenerated artifacts:

- **Twelve committed `.nes` files are stale.** Same source, new
  compiler, different bytes. The `Build Examples` CI job
  rebuilds each example into a tmp path and diffs against the
  committed ROM, so any drift is a hard failure. Rebuilt all
  twelve (arrays_and_functions, bitwise_ops, coin_cavern,
  function_chain, loop_break_continue, mmc1_banked, platformer,
  pong, sprites_and_palettes, state_machine, structs_enums_for,
  war).

- **Three goldens drift by one animation frame.** Zero-page
  addressing shaves a cycle per local access, which over a full
  frame handler shifts timing-sensitive sequences by a cycle or
  two. war's dealing animation and platformer + pong's audio
  tick stream catch the shift at frame 180 — war's card under
  player A's deck is now one frame earlier in its slide, and all
  three programs' captured audio buffers start from slightly
  different envelope positions. The new goldens (`war.png` + the
  three `.audio.hash` files) reflect the same code compiled with
  the cycle-count-corrected primitives.

- **`platformer.gif` and `war.gif` rebuild.** Same one-frame
  timing drift, integrated across 360 frames of captured
  gameplay — the emulator job's gif-reproducibility check
  wouldn't pass without the refresh. `pong.gif` happened to
  byte-match the old capture after rebuild.

All verified:
  - `cargo clippy --all-targets -- -D warnings` clean on both
    rustc 1.94.1 and 1.95.0.
  - `cargo test --all-targets` — 616 + 3 + 75 tests pass.
  - Full emulator harness — 34/34 ROMs match goldens.
  - Committed-ROM reproducibility diff clean — every
    `examples/*.ne` compiles byte-identical to its committed
    `.nes`.
  - `docs/{platformer,war,pong}.gif` byte-match fresh captures.
  - SHA-256 of "NES" still computes to `AE9145DB…4E0D`.

https://claude.ai/code/session_01FRmSBruVWCufm3LsUVMs8v
2026-04-16 16:12:46 +00:00
Claude
21b91f6398
examples/pong: production-quality Pong game with powerups and multi-ball
A complete, playable Pong game split across examples/pong/*.ne files
and pulled in from a top-level examples/pong.ne. Features:

- **Title screen** with a 3-option menu (CPU VS CPU / 1 PLAYER /
  2 PLAYERS), a cursor sprite, blinking "PRESS A" prompt, brisk
  title march on pulse 2, and autopilot that auto-confirms CPU VS
  CPU after 45 frames of no input so the headless jsnes golden
  harness reaches gameplay by frame 180.

- **Ball physics** with signed-magnitude velocity (u8 magnitude +
  sign bit per axis), wall bounce at top/bottom, paddle AABB
  collision with push-out, and score-out detection at left/right
  exits.

- **Multi-ball** via parallel ball_* arrays (MAX_BALLS = 3). Each
  ball scores a point independently; the round continues until the
  last ball exits the playfield.

- **CPU AI** that tracks the nearest active ball heading toward its
  side with a per-frame step, 4 px dead zone, and CPU_SPEED = 1 so
  rallies can end naturally.

- **Three powerup types** that spawn every ~4 seconds, bounce off
  all four walls, and are caught by paddle AABB overlap:
  1. LONG — extends the catching paddle from 24 → 40 px for 5 hits
  2. FAST — doubles ball x-velocity on the catcher's next hit
  3. MULTI — spawns two extra balls on the catcher's next hit

- **Victory** at first-to-7 with a "PLAYER N WINS" banner and the
  builtin fanfare, auto-returning to Title.

- **Audio**: 5 user-declared sfx (WallBounce, PaddleHit, Score,
  PowerSpawn, PowerCatch) plus a title march and the builtin
  fanfare for victory.

Source layout mirrors examples/war:

    examples/pong.ne               top-level game shell
    examples/pong/PLAN.md          living design doc
    examples/pong/constants.ne     layout + gameplay constants
    examples/pong/assets.ne        45-tile Tileset (paddles, ball, alphabet,
                                   digits, cursor, center-line, powerup icons)
    examples/pong/audio.ne         sfx + music declarations
    examples/pong/state.ne         all mutable globals
    examples/pong/rng.ne           8-bit Galois LFSR
    examples/pong/render.ne        draw helpers
    examples/pong/input.ne         paddle step (human + CPU AI)
    examples/pong/ball.ne          multi-ball physics + paddle collision
    examples/pong/powerup.ne       powerup entity (spawn, bounce, catch, apply)
    examples/pong/title_state.ne   state Title + menu
    examples/pong/play_state.ne    state Playing (P_SERVE/P_PLAY/P_POINT)
    examples/pong/victory_state.ne state Victory

Verification:
- 616 compiler unit tests pass (cargo test --all-targets)
- cargo fmt / cargo clippy --all-targets -- -D warnings clean
- 33/33 emulator harness goldens match
- examples/pong.nes builds byte-identically from source

https://claude.ai/code/session_0134F5uwDEVTes2Ee9S7JeXy
2026-04-16 01:25:29 +00:00