1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-11 10:10:49 +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

@ -0,0 +1,57 @@
// VRAM update buffer demo — `nt_set`, `nt_attr`, and `nt_fill_h`
// append entries to a 256-byte ring at `$0400-$04FF` during
// `on frame`; the NMI handler drains them to PPU `$2007` during
// vblank. This is the idiom every nesdoug HUD / dialog box / score
// counter is built on — the user code never touches `$2006` or
// `$2007` directly, just appends record after record.
//
// This demo paints a "scoreboard" of three tiles in the top row
// each frame, then fills a 16-tile horizontal stripe a few rows
// down with a single tile pattern. Frame 180 captures the scene
// after the buffer has drained the same set of writes for ~3
// seconds — the visible output is stable.
game "VRAM Buffer Demo" {
mapper: NROM
mirroring: horizontal
}
palette Default {
universal: black
bg0: [dk_blue, blue, sky_blue]
bg1: [dk_red, red, lt_red]
bg2: [dk_green, green, lt_green]
bg3: [black, lt_gray, white]
sp0: [dk_blue, blue, sky_blue]
sp1: [red, orange, white]
sp2: [dk_teal, teal, lt_teal]
sp3: [dk_olive, olive, yellow]
}
background Empty {
legend { ".": 0 }
map: [
"................................"
]
}
on frame {
// Three single-tile writes for a tiny "score" digit row.
// Each call appends a 4-byte buffer entry: [len=1][hi][lo][tile].
nt_set(2, 1, 1)
nt_set(3, 1, 2)
nt_set(4, 1, 3)
// A horizontal fill: 16 copies of the smiley starting at (8, 4).
// Buffer entry is [len=16][hi][lo][tile × 16] = 19 bytes.
nt_fill_h(8, 4, 16, 0)
// Update the attribute byte for the metatile that contains the
// score row so it picks up sub-palette 1 (red gradient) for
// visual contrast against the rest of the screen. (x, y) here
// are nametable cell coordinates; the codegen translates to
// the attribute table address $23C0+.
nt_attr(0, 0, 0b01010101)
}
start Main