mirror of
https://github.com/imjasonh/nescript
synced 2026-07-08 17:06:04 +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:
parent
fdb1ec7c91
commit
d98c7f3d82
29 changed files with 1757 additions and 46 deletions
|
|
@ -26,6 +26,7 @@ Open any `.nes` file in an NES emulator ([Mesen](https://www.mesen.ca/), [FCEUX]
|
|||
| `state_machine.ne` | on enter/exit, transitions | Multi-state flow with timers |
|
||||
| `sprites_and_palettes.ne` | sprites, scroll, cast | Inline CHR data, PPU scroll writes, type casting |
|
||||
| `mmc1_banked.ne` | MMC1, banks, multiply | Banked mapper with software multiply |
|
||||
| `palette_and_background.ne` | palette, background, set_palette, load_background | Reset-time initial load plus vblank-safe runtime swaps |
|
||||
|
||||
## Emulator Controls
|
||||
|
||||
|
|
|
|||
163
examples/palette_and_background.ne
Normal file
163
examples/palette_and_background.ne
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
// Palette and Background Demo — shows the `palette` and
|
||||
// `background` declarations plus runtime `set_palette` /
|
||||
// `load_background` swaps.
|
||||
//
|
||||
// The program declares two palettes and two backgrounds. The
|
||||
// *first* of each is loaded at reset time (before rendering is
|
||||
// enabled) so the title screen comes up with the right colours
|
||||
// and nametable on frame 0. A frame counter then toggles between
|
||||
// the two palettes every 90 frames and between the two backgrounds
|
||||
// every 180 frames, exercising the vblank-safe update path.
|
||||
//
|
||||
// Background tile 0 is the compiler's built-in smiley face (see
|
||||
// the default CHR in `src/linker/mod.rs`). Tile indices 1+ would
|
||||
// need matching `sprite` declarations with `chr:` data — we stick
|
||||
// to tile 0 so the example is self-contained.
|
||||
//
|
||||
// Build: cargo run -- build examples/palette_and_background.ne
|
||||
// Output: examples/palette_and_background.nes
|
||||
|
||||
game "Palette + BG Demo" {
|
||||
mapper: NROM
|
||||
mirroring: horizontal
|
||||
}
|
||||
|
||||
// ── Palettes ────────────────────────────────────────────────
|
||||
//
|
||||
// Each palette is 32 bytes laid out in the standard NES order:
|
||||
// $00-$0F background palettes 0-3 (4 colours each)
|
||||
// $10-$1F sprite palettes 0-3 (4 colours each, $10/$14/$18/$1C
|
||||
// mirror the bg slots)
|
||||
//
|
||||
// `CoolBlues` uses deep blue background tiles with pale highlights;
|
||||
// `WarmReds` swaps them for red/orange tones. Every 4 bytes is a
|
||||
// sub-palette.
|
||||
|
||||
palette CoolBlues {
|
||||
colors: [
|
||||
0x0F, 0x01, 0x11, 0x21, // bg palette 0: black, deep blue, sky, pale
|
||||
0x0F, 0x02, 0x12, 0x22, // bg palette 1
|
||||
0x0F, 0x0C, 0x1C, 0x2C, // bg palette 2
|
||||
0x0F, 0x0B, 0x1B, 0x2B, // bg palette 3
|
||||
0x0F, 0x01, 0x11, 0x21, // sprite palette 0
|
||||
0x0F, 0x16, 0x27, 0x30, // sprite palette 1
|
||||
0x0F, 0x14, 0x24, 0x34, // sprite palette 2
|
||||
0x0F, 0x0B, 0x1B, 0x2B // sprite palette 3
|
||||
]
|
||||
}
|
||||
|
||||
palette WarmReds {
|
||||
colors: [
|
||||
0x0F, 0x06, 0x16, 0x26, // bg palette 0: black, dark red, red, peach
|
||||
0x0F, 0x07, 0x17, 0x27, // bg palette 1
|
||||
0x0F, 0x08, 0x18, 0x28, // bg palette 2
|
||||
0x0F, 0x09, 0x19, 0x29, // bg palette 3
|
||||
0x0F, 0x06, 0x16, 0x26, // sprite palette 0
|
||||
0x0F, 0x16, 0x27, 0x30, // sprite palette 1
|
||||
0x0F, 0x14, 0x24, 0x34, // sprite palette 2
|
||||
0x0F, 0x0B, 0x1B, 0x2B // sprite palette 3
|
||||
]
|
||||
}
|
||||
|
||||
// ── Backgrounds ─────────────────────────────────────────────
|
||||
//
|
||||
// A 32×30 nametable is 960 tile bytes + 64 attribute bytes. We
|
||||
// only specify the first few tiles per background — the rest of
|
||||
// the nametable is zero-padded by the asset pipeline, so
|
||||
// undeclared cells render as tile 0 (the built-in smiley). The
|
||||
// attribute table is left empty so every 16×16 metatile uses
|
||||
// background sub-palette 0 (which is where CoolBlues / WarmReds
|
||||
// put their headline colour).
|
||||
//
|
||||
// TitleScreen paints a short ribbon of tile 0 across row 14 (the
|
||||
// middle of the screen). StageOne paints a diagonal stripe from
|
||||
// top-left to bottom-right so the swap is visually obvious.
|
||||
|
||||
background TitleScreen {
|
||||
tiles: [
|
||||
// Row 14 (offset 14*32 = 448): a ribbon of tile 0
|
||||
// across columns 4..28.
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
// Row 14
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
|
||||
]
|
||||
}
|
||||
|
||||
background StageOne {
|
||||
tiles: [
|
||||
// A few scattered tile-0s across the first 4 rows so the
|
||||
// background visibly differs from TitleScreen after the
|
||||
// swap. Remaining rows are zero-padded and render as
|
||||
// tile 0 anyway — the visual difference comes from the
|
||||
// palette swap triggered alongside.
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
|
||||
]
|
||||
}
|
||||
|
||||
var tick: u8 = 0
|
||||
var phase: u8 = 0
|
||||
|
||||
on frame {
|
||||
tick += 1
|
||||
|
||||
// Every 90 frames toggle the palette between the two.
|
||||
if tick >= 90 {
|
||||
tick = 0
|
||||
phase += 1
|
||||
if phase == 4 {
|
||||
phase = 0
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 0: CoolBlues + TitleScreen
|
||||
// Phase 1: WarmReds + TitleScreen
|
||||
// Phase 2: CoolBlues + StageOne
|
||||
// Phase 3: WarmReds + StageOne
|
||||
//
|
||||
// We reissue the set_palette / load_background call on every
|
||||
// phase transition (the single-frame condition also keeps the
|
||||
// golden stable — the jsnes test only renders a handful of
|
||||
// frames and the comparison runs without any controller input).
|
||||
if tick == 1 {
|
||||
if phase == 0 {
|
||||
set_palette CoolBlues
|
||||
load_background TitleScreen
|
||||
}
|
||||
if phase == 1 {
|
||||
set_palette WarmReds
|
||||
load_background TitleScreen
|
||||
}
|
||||
if phase == 2 {
|
||||
set_palette CoolBlues
|
||||
load_background StageOne
|
||||
}
|
||||
if phase == 3 {
|
||||
set_palette WarmReds
|
||||
load_background StageOne
|
||||
}
|
||||
}
|
||||
|
||||
// Draw a single sprite tracking the tick so the sprite still
|
||||
// moves on screen and the golden diff tests spot regressions
|
||||
// in OAM DMA alongside the palette/background path.
|
||||
draw Smiley at: (tick + 60, 120)
|
||||
}
|
||||
|
||||
start Main
|
||||
Loading…
Add table
Add a link
Reference in a new issue