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

@ -0,0 +1 @@
a82b6ff5 132084

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View file

@ -828,6 +828,126 @@ fn program_with_inline_sprite_chr() {
rom::validate_ines(&rom_data).expect("should be valid iNES");
}
#[test]
fn program_with_palette_compiles_and_blob_is_in_prg() {
let source = r#"
game "PalTest" { mapper: NROM }
palette Cool {
colors: [0x0F, 0x01, 0x11, 0x21,
0x0F, 0x02, 0x12, 0x22,
0x0F, 0x0C, 0x1C, 0x2C,
0x0F, 0x0B, 0x1B, 0x2B,
0x0F, 0x01, 0x11, 0x21,
0x0F, 0x16, 0x27, 0x30,
0x0F, 0x14, 0x24, 0x34,
0x0F, 0x0B, 0x1B, 0x2B]
}
on frame { wait_frame }
start Main
"#;
let rom_data = compile_banked(source);
rom::validate_ines(&rom_data).expect("should be valid iNES");
// The 32-byte palette blob lands verbatim inside PRG ROM.
// Search for a distinctive 8-byte subsequence from sub-palette 3
// that doesn't collide with any of the other blobs or init
// sequences the linker emits.
let needle = [0x0F, 0x16, 0x27, 0x30, 0x0F, 0x14, 0x24, 0x34];
let found = rom_data.windows(needle.len()).any(|w| w == needle);
assert!(
found,
"palette bytes should be spliced into PRG ROM verbatim"
);
}
#[test]
fn program_with_set_palette_queues_update_at_runtime() {
// A program with a `set_palette Name` statement should emit
// the `__ppu_update_used` marker (so the linker pulls in the
// NMI helper) and must contain the zero-page write sequence
// that stores the palette label pointer into $12/$13.
let source = r#"
game "PalRuntime" { mapper: NROM }
palette Swap { colors: [0x0F, 0x01, 0x11, 0x21] }
on frame { set_palette Swap }
start Main
"#;
let rom_data = compile_banked(source);
rom::validate_ines(&rom_data).expect("should be valid iNES");
// $12 == ZP_PENDING_PALETTE_LO, so the code will contain
// `STA $12` (opcode 85 12) somewhere in PRG.
let sta_12 = [0x85u8, 0x12];
let found = rom_data.windows(sta_12.len()).any(|w| w == sta_12);
assert!(
found,
"set_palette codegen should emit `STA $12` (ZP_PENDING_PALETTE_LO)"
);
}
#[test]
fn program_with_background_compiles_and_tiles_spliced() {
let source = r#"
game "BgTest" { mapper: NROM }
background Stage {
tiles: [0xAA, 0xBB, 0xCC, 0xDD, 0xEE]
}
on frame { wait_frame }
start Main
"#;
let rom_data = compile_banked(source);
rom::validate_ines(&rom_data).expect("should be valid iNES");
// The distinctive 5-byte prefix of the tiles blob should be in
// PRG verbatim (the resolver zero-pads to 960 bytes so the tail
// is mostly zero).
let needle = [0xAA, 0xBB, 0xCC, 0xDD, 0xEE];
let found = rom_data.windows(needle.len()).any(|w| w == needle);
assert!(
found,
"background tile bytes should be spliced into PRG ROM verbatim"
);
}
#[test]
fn program_with_load_background_queues_update() {
let source = r#"
game "BgRuntime" { mapper: NROM }
background Stage { tiles: [1, 2, 3] }
on frame { load_background Stage }
start Main
"#;
let rom_data = compile_banked(source);
rom::validate_ines(&rom_data).expect("should be valid iNES");
// $14 == ZP_PENDING_BG_TILES_LO.
let sta_14 = [0x85u8, 0x14];
let found = rom_data.windows(sta_14.len()).any(|w| w == sta_14);
assert!(
found,
"load_background codegen should emit `STA $14` (ZP_PENDING_BG_TILES_LO)"
);
}
#[test]
fn program_without_palette_does_not_reserve_ppu_zero_page() {
// Regression guard: programs that don't declare palette or
// background should keep user vars starting at $10, same as
// they always did, so existing emulator goldens don't shift.
let source = r#"
game "NoPal" { mapper: NROM }
var x: u8 = 42
on frame { x += 1 }
start Main
"#;
let rom_data = compile_banked(source);
rom::validate_ines(&rom_data).expect("should be valid iNES");
// `STA $10` (85 10) corresponds to writing to the first user
// var slot. Guarantees `x` is still allocated at $10.
let sta_10 = [0x85u8, 0x10];
let found = rom_data.windows(sta_10.len()).any(|w| w == sta_10);
assert!(
found,
"user var should still land at $10 when no palette/bg declared"
);
}
// ── M5 Tests ──
/// Compile a source string using the mapper-aware linker.
@ -860,6 +980,8 @@ fn compile_banked(source: &str) -> Vec<u8> {
.expect("sprite resolution should succeed");
let sfx = assets::resolve_sfx(&program).expect("sfx resolution should succeed");
let music = assets::resolve_music(&program).expect("music resolution should succeed");
let palettes = assets::resolve_palettes(&program);
let backgrounds = assets::resolve_backgrounds(&program);
let codegen = IrCodeGen::new(&analysis.var_allocations, &ir_program)
.with_sprites(&sprites)
@ -874,7 +996,15 @@ fn compile_banked(source: &str) -> Vec<u8> {
.filter(|b| b.bank_type == BankType::Prg)
.map(|b| PrgBank::empty(&b.name))
.collect();
linker.link_banked(&instructions, &sprites, &sfx, &music, &switchable_banks)
linker.link_banked_with_ppu(
&instructions,
&sprites,
&sfx,
&music,
&palettes,
&backgrounds,
&switchable_banks,
)
}
#[test]