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
|
|
@ -3,7 +3,7 @@ mod tests;
|
|||
|
||||
use crate::asm;
|
||||
use crate::asm::{AddressingMode as AM, Instruction, Opcode::*};
|
||||
use crate::assets::{MusicData, SfxData};
|
||||
use crate::assets::{BackgroundData, MusicData, PaletteData, SfxData};
|
||||
use crate::parser::ast::{Mapper, Mirroring};
|
||||
use crate::rom::RomBuilder;
|
||||
use crate::runtime;
|
||||
|
|
@ -176,21 +176,53 @@ impl Linker {
|
|||
sfx: &[SfxData],
|
||||
music: &[MusicData],
|
||||
switchable_banks: &[PrgBank],
|
||||
) -> Vec<u8> {
|
||||
self.link_banked_with_ppu(user_code, sprites, sfx, music, &[], &[], switchable_banks)
|
||||
}
|
||||
|
||||
/// Link with full asset pipeline including palette and
|
||||
/// background data blobs. Palettes and backgrounds each emit a
|
||||
/// labelled data block inside PRG ROM; the first declared
|
||||
/// palette / background is loaded at reset time before
|
||||
/// rendering is enabled, and any additional ones become
|
||||
/// addressable via `set_palette` / `load_background` (which
|
||||
/// queue a vblank-safe write).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn link_banked_with_ppu(
|
||||
&self,
|
||||
user_code: &[Instruction],
|
||||
sprites: &[SpriteData],
|
||||
sfx: &[SfxData],
|
||||
music: &[MusicData],
|
||||
palettes: &[PaletteData],
|
||||
backgrounds: &[BackgroundData],
|
||||
switchable_banks: &[PrgBank],
|
||||
) -> Vec<u8> {
|
||||
assert!(
|
||||
switchable_banks.is_empty() || self.mapper != Mapper::NROM,
|
||||
"NROM does not support switchable PRG banks (got {} banks)",
|
||||
switchable_banks.len()
|
||||
);
|
||||
self.link_banked_inner(user_code, sprites, sfx, music, switchable_banks)
|
||||
self.link_banked_inner(
|
||||
user_code,
|
||||
sprites,
|
||||
sfx,
|
||||
music,
|
||||
palettes,
|
||||
backgrounds,
|
||||
switchable_banks,
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn link_banked_inner(
|
||||
&self,
|
||||
user_code: &[Instruction],
|
||||
sprites: &[SpriteData],
|
||||
sfx: &[SfxData],
|
||||
music: &[MusicData],
|
||||
palettes: &[PaletteData],
|
||||
backgrounds: &[BackgroundData],
|
||||
switchable_banks: &[PrgBank],
|
||||
) -> Vec<u8> {
|
||||
// ROM layout.
|
||||
|
|
@ -225,8 +257,30 @@ impl Linker {
|
|||
total_banks,
|
||||
));
|
||||
|
||||
// Load default palette
|
||||
all_instructions.extend(self.gen_palette_load());
|
||||
// Load the initial palette. If the program declared any
|
||||
// `palette` blocks, use the first one; otherwise fall back
|
||||
// to the built-in default palette so sprites show up in a
|
||||
// reasonable colour scheme without any user setup.
|
||||
if let Some(first_palette) = palettes.first() {
|
||||
all_instructions.extend(runtime::gen_initial_palette_load(&first_palette.label()));
|
||||
} else {
|
||||
all_instructions.extend(self.gen_palette_load());
|
||||
}
|
||||
|
||||
// Load the initial background if the program declared any.
|
||||
// Most programs don't, so the common case emits nothing
|
||||
// here and leaves nametable 0 zero-filled.
|
||||
if let Some(first_bg) = backgrounds.first() {
|
||||
all_instructions.extend(runtime::gen_initial_background_load(
|
||||
&first_bg.tiles_label(),
|
||||
&first_bg.attrs_label(),
|
||||
));
|
||||
// Enable background rendering. Default init only turns
|
||||
// on sprites (`$10`), so OR in the background bit
|
||||
// (`$08`) when a user background is present.
|
||||
all_instructions.push(Instruction::new(LDA, AM::Immediate(0x1E)));
|
||||
all_instructions.push(Instruction::new(STA, AM::Absolute(0x2001)));
|
||||
}
|
||||
|
||||
// User code (var init + main loop)
|
||||
all_instructions.extend(user_code.iter().cloned());
|
||||
|
|
@ -299,6 +353,40 @@ impl Linker {
|
|||
}
|
||||
}
|
||||
|
||||
// Palette and background data blobs. Each palette is a
|
||||
// 32-byte block labelled `__palette_Name`; backgrounds are
|
||||
// split into two blocks (`__bg_tiles_Name`, `__bg_attrs_Name`)
|
||||
// so the reset loader and the NMI update helper can push
|
||||
// them with independent pointers. We always splice the
|
||||
// blobs whenever the program declares any palette or
|
||||
// background — there's no equivalent of `__audio_used`
|
||||
// because simply *declaring* a palette is enough to need
|
||||
// its bytes in ROM (the reset loader reads them).
|
||||
for pal in palettes {
|
||||
all_instructions.extend(runtime::gen_data_block(&pal.label(), pal.colors.to_vec()));
|
||||
}
|
||||
for bg in backgrounds {
|
||||
all_instructions.extend(runtime::gen_data_block(
|
||||
&bg.tiles_label(),
|
||||
bg.tiles.to_vec(),
|
||||
));
|
||||
all_instructions.extend(runtime::gen_data_block(
|
||||
&bg.attrs_label(),
|
||||
bg.attrs.to_vec(),
|
||||
));
|
||||
}
|
||||
|
||||
// The NMI needs the palette/nametable update helper whenever
|
||||
// the program declared any palette or background, or the
|
||||
// IR codegen emitted the `__ppu_update_used` marker (which
|
||||
// signals that user code contains a `set_palette` or
|
||||
// `load_background` statement). Either condition brings in
|
||||
// the ~70-byte helper; programs that touch neither pay
|
||||
// zero bytes.
|
||||
let has_ppu_updates = !palettes.is_empty()
|
||||
|| !backgrounds.is_empty()
|
||||
|| has_label(user_code, "__ppu_update_used");
|
||||
|
||||
// NMI handler
|
||||
all_instructions.push(Instruction::new(NOP, AM::Label("__nmi".into())));
|
||||
// If user code emits an MMC3 reload hook, splice in a JSR
|
||||
|
|
@ -320,7 +408,7 @@ impl Linker {
|
|||
if has_audio {
|
||||
all_instructions.push(Instruction::new(JSR, AM::Label("__audio_tick".into())));
|
||||
}
|
||||
all_instructions.extend(runtime::gen_nmi());
|
||||
all_instructions.extend(runtime::gen_nmi(has_ppu_updates));
|
||||
|
||||
// IRQ handler
|
||||
all_instructions.push(Instruction::new(NOP, AM::Label("__irq".into())));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue