mirror of
https://github.com/imjasonh/nescript
synced 2026-07-08 00:45: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 from7b4570eby 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:
parent
7b4570eee5
commit
807c9c7318
15 changed files with 699 additions and 27 deletions
|
|
@ -52,6 +52,7 @@ Open any `.nes` file in an NES emulator ([Mesen](https://www.mesen.ca/), [FCEUX]
|
|||
| `sprite_0_split_demo.ne` | `sprite_0_split(x, y)` | Mid-frame scroll change driven by the PPU's sprite-0 hit flag (`$2002` bit 6), so the effect works on any mapper — NROM, UxROM, MMC1 — not just MMC3 via `on_scanline(N)`. Two-phase busy-wait (wait for clear, then wait for set) guarantees the hit we're responding to came from the current frame. Requires a sprite in OAM slot 0 that overlaps opaque background pixels; this demo uses a full smiley background so every frame's sprite-0 hit fires deterministically. |
|
||||
| `i16_demo.ne` | `i16` signed 16-bit type | Negative literals fold to wide two's complement (`-10` → `$FFF6`), so `var vy: i16 = -10` stores the right bytes instead of the zero-extended `$00F6`. Comparisons currently use the unsigned 16-bit compare path (matching existing `i8` behaviour) — fine for positive ranges, wrong for negative compares. The companion `i16_negative_literal_sign_extends_to_wide_store` integration test guards the literal-fold path. |
|
||||
| `sram_demo.ne` | `save { var ... }` | Battery-backed save block. The analyzer allocates `high_score` and `coins` at `$6000+` (cartridge SRAM window) instead of main RAM, and the linker flips iNES header byte-6 bit-1 so emulators (FCEUX, Mesen, Nestopia) load and persist the region from a `.sav` file alongside the ROM. SRAM is uninitialized at first power-on; production games should reserve a magic-byte sentinel and validate it before trusting the rest of the data — the compiler doesn't auto-initialize and emits W0111 if you try. |
|
||||
| `vram_buffer_demo.ne` | `nt_set`, `nt_attr`, `nt_fill_h` | VRAM update buffer. User code queues PPU writes during `on frame` via three intrinsics; the NMI drains the 256-byte ring at `$0400-$04FF` to `$2007` during vblank. Each entry is `[len][addr_hi][addr_lo][data...]` with a zero-byte sentinel; the codegen lays down a fresh sentinel after every append. The runtime drain uses `LDA $0400,X` indexed-absolute (4 cycles per data byte, no ZP cost). Gated on the `__vram_buf_used` marker — programs that never call any of the three intrinsics keep the 256 bytes free for analyzer allocation and the NMI never JSRs the drain. |
|
||||
|
||||
## Emulator Controls
|
||||
|
||||
|
|
|
|||
57
examples/vram_buffer_demo.ne
Normal file
57
examples/vram_buffer_demo.ne
Normal 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
|
||||
BIN
examples/vram_buffer_demo.nes
Normal file
BIN
examples/vram_buffer_demo.nes
Normal file
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue