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

3 commits

Author SHA1 Message Date
Claude
b6e4c368e1
peephole: step past non-A ops in remove_dead_loads
`remove_dead_loads` now scans past opcodes that touch neither A nor
the flags an LDA sets, so a redundant LDA gets caught by its
successor's overwrite even when an index load or counter bump sits
between them. The extension covers LDX/LDY/INX/INY/DEX/DEY and the
flag ops (CLC/SEC/CLI/SEI/CLD/SED/CLV) alongside the INC/DEC/STX/STY
opcodes the pass already stepped past.

The highest-leverage case is the shape every single-tile `draw`
emits. After copy propagation and dead-store elimination do their
work, the stream reads:

    LDA #<y>      ; stray producer, value never consumed
    LDY oam_cursor
    LDA #<y>      ; real load before STA
    STA $0200,Y

The first LDA was surviving because the pass bailed on the LDY.
With the step-past, it drops. One LDA gone per draw, 2 bytes each.

Measured LDA-count reduction on committed examples:

  platformer  242 → 221   (-21, -8.7 %)
  war         785 → 754   (-31, -4.0 %)
  pong        843 → 827   (-16, -1.9 %)

**Audio goldens.** The cycle savings shift the main-loop/NMI boundary
in audio-emitting programs, which re-times which frame each SFX
trigger lands in. Six audio hashes re-baseline as a result:
audio_demo, friendly_assets, noise_triangle_sfx, platformer, pong,
war. All 50 PNG goldens, the platformer/war/pong demo gifs, and
every non-audio program stay byte-identical. The re-baselined
output is still sample-accurate; what changed is the first-SFX
offset within the captured 132 084-sample window. This is the
audio-shift tradeoff documented in future-work.

Two new peephole unit tests lock in the behaviour:
- `dead_load_elim_steps_past_ldx_ldy` — the DrawSprite shape folds.
- `dead_load_elim_preserves_lda_when_used_by_shift` — a subsequent
  ASL on A keeps the LDA alive across an intervening LDY.

Also updates future-work.md to reflect the shipped change and the
remaining register-allocator wins worth chasing next.
2026-04-19 01:43:58 +00:00
Claude
1c00d20121
runtime: reset PPU scroll after VRAM buffer drain
Each $2006 write inside __vram_buf_drain updates the PPU's `t`
(scroll) register, so leaving it pointing at the last buffer
entry's address shifted the next frame's rendering up/right by
however many cells we wrote past $2000. Reset by writing $00 to
$2006 twice (clears `t` and resets the write-toggle to high)
followed by $00 to $2005 twice (zero X/Y scroll). The HUD demo
golden flips from "smileys offset by ~16px" to the intended
red bar with white hearts and a yellow score digit.

https://claude.ai/code/session_01F7dHsgh7UX7SAK3wZ7JiKc
2026-04-18 22:57:46 +00:00
Claude
807c9c7318
compiler: VRAM update buffer (nt_set / nt_attr / nt_fill_h)
Closes the highest-priority remaining catalogue item (§G). User
code queues PPU writes during `on frame` via three new intrinsics;
the NMI drains the 256-byte ring at `$0400-$04FF` to `$2007`
during vblank. Programs that never touch the buffer pay zero
bytes and zero cycles for the feature — verified by the existing
46 ROMs all matching their goldens with no drift.

Also fixes the failing CI Format check from 7b4570e by running
cargo fmt across the working tree.

**Runtime:**
- New `runtime::gen_vram_buf_drain` emits the drain routine
  (`__vram_buf_drain`). Walks entries `[len][addr_hi][addr_lo]
  [byte_0]...[byte_(len-1)]` and stops at `len == 0`. Uses
  `LDA $0400,X` indexed-absolute so no ZP scratch is needed.
  Drain costs ~12 setup cycles + 8 cycles per data byte; the
  256-byte buffer can hold ~50 single-tile writes that drain
  in roughly 1000 cycles, well inside the ~2273-cycle vblank.
- `NmiOptions` gains `has_vram_buf`. The NMI JSRs the drain
  after the existing palette/background handshake (compiler-
  queued PPU writes win priority for vblank cycles).

**IR + codegen:**
- Three new ops `IrOp::NtSet`, `IrOp::NtAttr`, `IrOp::NtFillH`.
- The codegen helpers compute the PPU address inline:
  `$2000 + y*32 + x` for nametable, `$23C0 + (y/4)*8 + (x/4)`
  for attribute. Each append lays down a fresh `0` sentinel so
  the NMI sees a well-formed buffer regardless of whether more
  entries get appended later in the frame.
- `__vram_buf_used` marker drops on first use; gates the
  runtime splice + NMI JSR.

**Analyzer:**
- AST-walking helper `program_uses_vram_buf` detects intrinsic
  use at analyze-init time so the user-RAM bump pointer can
  start at `$0500` (past the buffer) rather than the legacy
  `$0300`. Programs that don't use the buffer keep the legacy
  start.
- Three intrinsic names registered in `is_intrinsic` /
  `is_void_intrinsic` with arity checks.

**Tests + example:**
- `examples/vram_buffer_demo.ne` exercises all three intrinsics
  on a backgrounded program — three single-tile score writes,
  a 16-tile horizontal fill, and an attribute write that flips
  the top-left metatile group's palette to red. Committed
  golden + audio hash.
- Four new integration tests: byte-level JSR-to-drain
  assertion, drain-omitted-when-unused, RAM-bump assertion for
  programs that DO use the buffer, and arity enforcement for
  `nt_set`.

**CI fix:**
- `cargo fmt` ran across the tree. Picks up a one-line fmt
  diff in `tests/integration_test.rs` that the prior commit
  shipped without running fmt, causing the Format CI job to
  fail on `7b4570e`.

All 758 tests pass. Clippy clean. 47/47 emulator goldens match.
2026-04-18 21:14:31 +00:00