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

164 lines
6.1 KiB
Text
Raw Normal View History

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
2026-04-13 11:11:33 +00:00
// 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