`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.
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
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.