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

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.
This commit is contained in:
Claude 2026-04-18 21:14:31 +00:00
parent 7b4570eee5
commit 807c9c7318
No known key found for this signature in database
15 changed files with 699 additions and 27 deletions

View file

@ -336,27 +336,31 @@ documented as a **design choice** anywhere. Add a paragraph to the
language guide explaining why, plus a pointer to the hand-rolled
explicit-stack pattern (small `u8[N]` stack + `u8` top).
### G. VRAM update buffer primitive
### G. VRAM update buffer follow-ups
The highest-leverage missing runtime feature. Today
`load_background` / `set_palette` queue PPU writes under the hood,
but there is no user-visible "write these N bytes into nametable
slot `(x,y)` next vblank" primitive. That's the idiom behind every
scoreboard, dialog box, destroyed-metatile animation, and streaming
scroll in the nesdoug chapters. Concrete API sketch:
`nt_set(x, y, tile)`, `nt_attr(x, y, value)`, and
`nt_fill_h(x, y, len, tile)` ship today — see
`examples/vram_buffer_demo.ne`. The runtime ring lives at
`$0400-$04FF` (gated on the `__vram_buf_used` marker; the analyzer
bumps the user-RAM bump pointer to `$0500` when the buffer is in
use). Each append lays down `[len][addr_hi][addr_lo][data…]` and
writes a fresh `0` sentinel; the NMI drains the buffer at vblank
via `LDA $0400,X / STA $2007` indexed-absolute (4 cycles per data
byte, no ZP cost).
```
buffer.nametable_write(x, y, [0x20, 0x21, 0x22]) // horizontal
buffer.nametable_write_v(x, y, [0x20, 0x21, 0x22]) // vertical
buffer.attribute_write(x, y, 0b00011011) // one byte
buffer.flush() // force an eof
```
Still TODO:
Runtime shape: a fixed ring buffer at a known RAM address
(`$0400`?). Each entry is `[header, addr_hi, addr_lo, len, data…]`
where `header` carries the `NT_UPD_HORZ` / `NT_UPD_VERT` /
`NT_UPD_EOF` bits the neslib engine already uses. The NMI handler
drains the buffer every frame and writes `$FF` as the sentinel.
- **Vertical (column) writes**`nt_write_v(x, y, ...)` would set
`$2000` bit 2 (auto-increment 32) before the data writes and
clear it after. Useful for tilemap-driven scrolling.
- **Variable-length writes from a u8 array global** — today
`nt_fill_h` repeats one tile; a `nt_copy_h(x, y, src_var, len)`
variant that copies from a declared `u8[N]` global removes the
fill-only restriction.
- **Buffer-overflow detection** — the runtime drain assumes the
256-byte buffer never overflows. A debug-mode check that traps
when `head` would advance past `$04FF` would catch the worst
failure mode (writes wrapping into adjacent RAM).
### H. Metatiles + collision as a first-class construct
@ -559,12 +563,13 @@ to a specific bank to avoid bank-switch cost on a hot path.
Remaining gap items in order of user value:
1. VRAM update buffer (§G) — unblocks HUDs, dialog, streaming.
2. Register allocator (existing section) — compounding size win.
3. Signedness on Cmp16/Cmp ops (§A follow-up) — closes the i16
1. Register allocator (existing section) — compounding size win.
2. Signedness on Cmp16/Cmp ops (§A follow-up) — closes the i16
correctness gap.
4. Metatiles + collision (§H) — closes several items at once.
5. Inline-asm format specifiers + directive list (§D follow-ups).
3. Metatiles + collision (§H) — closes several items at once.
4. Inline-asm format specifiers + directive list (§D follow-ups).
5. VRAM buffer follow-ups (§G) — vertical writes, array copy,
overflow detection.
6. Arrays-of-structs + bitfields (§C) + fn pointers (§B) —
turns NEScript into a general-purpose NES language.
7. UNROM-512 + MMC5 (§V) — ecosystem fit.