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

palette/background: first-class declarations with reset-time load and runtime swaps

Re-adds `palette Name { colors: [...] }` and
`background Name { tiles: [...], attributes: [...] }` as first-class
declarations, plus `set_palette Name` and `load_background Name`
statements for runtime swaps. Unlike the previous iteration that
quietly no-op'd, this one is fully wired through the pipeline and
its behavior is pinned by both unit tests and an emulator golden.

Pipeline:

- Lexer: re-adds `palette`, `background`, `set_palette`,
  `load_background` keywords and tokenizes them.
- AST: `PaletteDecl` (name + 1..=32 colour bytes) and `BackgroundDecl`
  (name + 0..=960 tile bytes + 0..=64 attribute bytes) live in
  `Program`. `Statement::SetPalette` and `Statement::LoadBackground`
  name-reference these declarations.
- Parser: `palette Name { colors: [...] }` / `background Name
  { tiles: [...], attributes: [...] }` blocks and their statement
  forms parse via the existing byte-array helper.
- Analyzer: validates colour indices ($00-$3F), palette length
  (<=32), nametable length (<=960), attribute length (<=64), and
  duplicate decl names. `set_palette` / `load_background` targets
  must reference a declared name (E0502 otherwise). When a program
  declares palette or background, the analyzer bumps the user
  zero-page allocator's starting address from `$10` to `$18` to
  reserve `$11-$17` for the runtime update handshake — programs
  that don't use the feature keep the old layout so their emulator
  goldens stay byte-exact.
- Assets: `PaletteData` and `BackgroundData` resolve declarations
  into zero-padded fixed-size blobs (32 / 960 / 64 bytes) and
  expose `label()` / `tiles_label()` / `attrs_label()` for codegen
  to reference.
- IR: new `IrOp::SetPalette(String)` and
  `IrOp::LoadBackground(String)`; lowering forwards the names
  verbatim.
- Codegen: `gen_set_palette` writes the palette label pointer into
  ZP `$12/$13` and ORs bit 0 into the update flags at `$11`;
  `gen_load_background` does the same for tile/attribute pointers
  at `$14/$15/$16/$17` with bit 1. Both emit a `__ppu_update_used`
  marker so the linker splices in the NMI apply helper only when
  the feature is actually used.
- Runtime: `gen_initial_palette_load` and
  `gen_initial_background_load` write the first declared
  palette/background at reset time (before rendering is enabled,
  where PPU writes are safe). `gen_nmi(has_ppu_updates)` takes a
  new flag; when true it splices `gen_ppu_update_apply` at the top
  of the NMI body, which checks the `$11` flags byte and copies
  pending palette / nametable data to `$3F00` / `$2000` inside
  vblank. All helpers use only ZP $02/$03 as scratch at reset time
  and never clobber ZP slots live across NMI.
- Linker: new `link_banked_with_ppu` takes slice of `PaletteData` /
  `BackgroundData`; splices each blob as a labelled data block in
  PRG ROM, picks the first-declared as the reset-time load target,
  enables background rendering automatically when a background is
  declared, and threads `has_ppu_updates` into `gen_nmi`. Old
  `link_banked` remains as a thin wrapper for callers without
  palette/background data so existing tests don't shift.

Tests:

- Lexer: tokenization of the 4 new keywords (single added test case).
- Parser: 5 new tests for `palette` / `background` decls with and
  without attributes, plus `set_palette` / `load_background`
  statements.
- Analyzer: 9 new tests covering acceptance of declared
  palettes/backgrounds, E0502 for unknown names, E0201 for
  out-of-range NES colors and oversized blobs, E0501 for duplicate
  names, and the zero-page-layout guard (palette/bg decls bump ZP
  start; no decls keeps it at $10).
- Resolver: 3 new tests for zero-padding, truncation of oversized
  decls, and label derivation.
- IR: 2 new lowering tests for `set_palette` and `load_background`.
- Integration: 5 new tests — blob contents spliced verbatim into
  PRG, `STA $12` / `STA $14` emitted by set_palette /
  load_background codegen, and a regression guard that programs
  without palette/background still land user vars at $10.
- Emulator: new `examples/palette_and_background.ne` driven by a
  frame counter that toggles between `CoolBlues` / `WarmReds` and
  `TitleScreen` / `StageOne` every 90 frames. Golden PNG and audio
  hash checked in under `tests/emulator/goldens/` and verified via
  `node run_examples.mjs` — rendered image shows the blue
  `CoolBlues` palette with the nametable populated from
  `TitleScreen`.

Docs:

- `README.md` adds the feature to the headline list and the example
  table.
- `docs/language-guide.md` restores the palette/background sections
  with the full 32-byte layout table and `set_palette` /
  `load_background` statement references.
- `docs/future-work.md` replaces the "removed as dead code" entry
  with the remaining gaps (PNG-sourced palette and nametable
  assets, cross-vblank large background updates, memory-map
  reporting).
- `spec.md` restores the grammar productions and usage examples.
- `examples/README.md` lists the new demo.

All 497 unit + integration tests pass. Clippy clean. All 21
emulator goldens match after the update pass.

https://claude.ai/code/session_012fKB251HvEUQwG3tizFyqt
This commit is contained in:
Claude 2026-04-13 11:11:33 +00:00
parent fdb1ec7c91
commit d98c7f3d82
No known key found for this signature in database
29 changed files with 1757 additions and 46 deletions

View file

@ -6,31 +6,29 @@ tested is omitted — `git log` is the authoritative record of what shipped.
---
## Runtime palette / nametable updates
## PNG-sourced palette and nametable assets
**Status.** Parsed away. Previously the compiler accepted `palette Name
{ colors: [...] }`, `background Name { chr: @chr(...) }`, `load_background
Name`, and `set_palette Name` statements, but the lowering silently dropped
them: the declarations were never resolved into CHR or palette blobs and the
statements emitted zero instructions. They were removed during cleanup to
avoid quietly misleading users.
**What ships today.** `palette Name { colors: [...] }` and
`background Name { tiles: [...], attributes: [...] }` declarations with
inline byte arrays. The first declared palette / background is loaded
at reset time (before rendering enables), and both blocks get named
data blobs in PRG ROM so `set_palette Name` / `load_background Name`
can queue a vblank-safe swap via the NMI handler. See
`examples/palette_and_background.ne`.
**What a proper implementation needs.**
- New AST declarations for palette and background/nametable data, with
analyzer validation for color indices and nametable dimensions.
- An asset pipeline pass that compiles PNG or inline byte data into a
ROM-resident blob with a known label.
- Runtime helpers that write to PPU `$2006/$2007` during vblank from a
source pointer.
- IR ops `LoadBackground(BlobId)` and `SetPalette(BlobId)` that the
codegen emits inside an NMI-safe window.
- A `--memory-map` breakdown that shows palette and nametable budgets,
since they eat into the PRG-ROM cost model that `memory_map` reports
for CHR bytes.
Until this exists, programs should use `poke(0x2006, ...)` / `poke(0x2007, ...)`
directly inside an `on frame` handler (runs immediately after vblank) to push
palette or nametable updates.
**Still TODO.**
- `@palette("file.png")` — analyze image colours and map to nearest
NES master-palette indices. `nearest_nes_color()` already lives in
`src/assets/palette.rs` but is not wired through the resolver.
- `@nametable("file.png")` — convert a 256×240 image into a 960-byte
nametable plus 64-byte attribute table with automatic tile
deduplication (max 256 unique tiles per pattern table).
- Per-state background rendering control — programs currently get a
single fixed nametable at reset; per-state swaps work but are
limited by the NMI-time write budget (~2273 cycles, enough for a
palette but not a full 1024-byte nametable).
- `--memory-map` should report palette and background PRG ROM usage
alongside the variable layout.
---

View file

@ -641,6 +641,27 @@ Set the PPU scroll position:
scroll(scroll_x, scroll_y)
```
### Set Palette
```
set_palette NightPalette
```
Queues the named palette for a vblank-safe copy into PPU palette
RAM (`$3F00-$3F1F`). The write is applied by the NMI handler on the
next vblank. See `palette` declarations below.
### Load Background
```
load_background Level1
```
Queues the named background (a full-screen 32×30 nametable + 64-byte
attribute table) for a vblank-safe copy into nametable 0
(`$2000-$23FF`). Applied by the NMI handler at the next vblank. See
`background` declarations below.
### Function Calls as Statements
```
@ -664,6 +685,72 @@ sprite Coin {
}
```
### Palette Declarations
```
palette MainPalette {
colors: [0x0F, 0x01, 0x11, 0x21,
0x0F, 0x06, 0x16, 0x26,
0x0F, 0x09, 0x19, 0x29,
0x0F, 0x0B, 0x1B, 0x2B,
0x0F, 0x01, 0x11, 0x21,
0x0F, 0x16, 0x27, 0x30,
0x0F, 0x14, 0x24, 0x34,
0x0F, 0x0B, 0x1B, 0x2B]
}
```
Each byte is an NES master-palette index (`$00-$3F`). The 32 bytes
map directly to PPU palette RAM `$3F00-$3F1F` in the canonical NES
layout:
| Offset | Contents |
|-----------------|-------------------------------------|
| `$00`-`$03` | Background sub-palette 0 (4 colours) |
| `$04`-`$07` | Background sub-palette 1 |
| `$08`-`$0B` | Background sub-palette 2 |
| `$0C`-`$0F` | Background sub-palette 3 |
| `$10`-`$13` | Sprite sub-palette 0 |
| `$14`-`$17` | Sprite sub-palette 1 |
| `$18`-`$1B` | Sprite sub-palette 2 |
| `$1C`-`$1F` | Sprite sub-palette 3 |
The first byte of each sub-palette is typically `$0F` (black); it
serves as the shared background colour. Lists shorter than 32 bytes
are zero-padded; lists longer than 32 are a compile error.
The *first* `palette` declared in a program is loaded into VRAM at
reset time, before rendering is enabled, so the title screen boots
with the right colours on frame 0. Additional declarations sit in
PRG ROM as named data blobs and become active via `set_palette Name`,
which queues the write for the next vblank.
### Background Declarations
```
background TitleScreen {
tiles: [0x00, 0x01, 0x01, 0x00, /* ... up to 960 bytes ... */]
attributes: [0xFF, 0x55, /* ... up to 64 bytes ... */]
}
```
A background is a 32×30 nametable. `tiles` is the nametable itself
— 960 bytes of CHR tile indices, one per 8×8 cell, in row-major
order (left-to-right, top-to-bottom). `attributes` is the 8×8
attribute table (64 bytes) that controls which sub-palette each
16×16 metatile uses.
Both lists are zero-padded up to their fixed sizes; `attributes`
may be omitted entirely, in which case every cell uses background
sub-palette 0.
The *first* `background` declared is loaded into nametable 0 at
reset time and background rendering is enabled automatically.
Additional backgrounds can be swapped in via `load_background Name`,
which queues the update for the next vblank. Full-nametable updates
do not fit inside a single vblank, so large background swaps may
require the program to disable rendering temporarily.
### Asset Sources
Three ways to provide asset data: