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.