mirror of
https://github.com/imjasonh/nescript
synced 2026-07-13 02:59:36 +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,6 +3,50 @@ use std::path::Path;
|
|||
use crate::linker::SpriteData;
|
||||
use crate::parser::ast::{AssetSource, Program};
|
||||
|
||||
/// Resolved palette data, ready for the linker to splice into PRG
|
||||
/// ROM as a 32-byte data blob at the label returned by [`Self::label`].
|
||||
/// Declarations shorter than 32 bytes are zero-padded so the runtime
|
||||
/// can always push exactly 32 bytes to `$3F00-$3F1F`.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PaletteData {
|
||||
pub name: String,
|
||||
/// Exactly 32 bytes. Index `i` is the value written to PPU
|
||||
/// address `$3F00 + i`.
|
||||
pub colors: [u8; 32],
|
||||
}
|
||||
|
||||
impl PaletteData {
|
||||
/// The ROM-level label under which the linker emits the 32-byte
|
||||
/// blob. The IR codegen references this label when lowering
|
||||
/// `set_palette Name`.
|
||||
#[must_use]
|
||||
pub fn label(&self) -> String {
|
||||
format!("__palette_{}", self.name)
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolved background data. `tiles` is the 960-byte nametable
|
||||
/// (32 columns × 30 rows) and `attrs` is the 64-byte attribute
|
||||
/// table. Both are zero-padded up from the declared sizes so the
|
||||
/// runtime NMI helper can always push fixed-length data.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BackgroundData {
|
||||
pub name: String,
|
||||
pub tiles: [u8; 960],
|
||||
pub attrs: [u8; 64],
|
||||
}
|
||||
|
||||
impl BackgroundData {
|
||||
#[must_use]
|
||||
pub fn tiles_label(&self) -> String {
|
||||
format!("__bg_tiles_{}", self.name)
|
||||
}
|
||||
#[must_use]
|
||||
pub fn attrs_label(&self) -> String {
|
||||
format!("__bg_attrs_{}", self.name)
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve sprite declarations in a program into concrete CHR byte blobs and
|
||||
/// assign each one a tile index in CHR ROM.
|
||||
///
|
||||
|
|
@ -67,6 +111,53 @@ pub fn resolve_sprites(program: &Program, source_dir: &Path) -> Result<Vec<Sprit
|
|||
Ok(sprites)
|
||||
}
|
||||
|
||||
/// Resolve all `palette Name { ... }` declarations in `program` into
|
||||
/// 32-byte fixed-size blobs suitable for splicing into PRG ROM.
|
||||
/// Declarations with fewer than 32 colors are zero-padded.
|
||||
#[must_use]
|
||||
pub fn resolve_palettes(program: &Program) -> Vec<PaletteData> {
|
||||
program
|
||||
.palettes
|
||||
.iter()
|
||||
.map(|p| {
|
||||
let mut colors = [0u8; 32];
|
||||
for (i, c) in p.colors.iter().enumerate().take(32) {
|
||||
colors[i] = *c;
|
||||
}
|
||||
PaletteData {
|
||||
name: p.name.clone(),
|
||||
colors,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Resolve all `background Name { ... }` declarations in `program`
|
||||
/// into fixed-size 960-byte tile maps and 64-byte attribute tables.
|
||||
/// Declarations shorter than the maximum are zero-padded.
|
||||
#[must_use]
|
||||
pub fn resolve_backgrounds(program: &Program) -> Vec<BackgroundData> {
|
||||
program
|
||||
.backgrounds
|
||||
.iter()
|
||||
.map(|b| {
|
||||
let mut tiles = [0u8; 960];
|
||||
for (i, t) in b.tiles.iter().enumerate().take(960) {
|
||||
tiles[i] = *t;
|
||||
}
|
||||
let mut attrs = [0u8; 64];
|
||||
for (i, a) in b.attributes.iter().enumerate().take(64) {
|
||||
attrs[i] = *a;
|
||||
}
|
||||
BackgroundData {
|
||||
name: b.name.clone(),
|
||||
tiles,
|
||||
attrs,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
@ -88,6 +179,8 @@ mod tests {
|
|||
functions: Vec::new(),
|
||||
states: Vec::new(),
|
||||
sprites: vec![sprite],
|
||||
palettes: Vec::new(),
|
||||
backgrounds: Vec::new(),
|
||||
sfx: Vec::new(),
|
||||
music: Vec::new(),
|
||||
banks: Vec::new(),
|
||||
|
|
@ -145,4 +238,90 @@ mod tests {
|
|||
// Missing binary file → silently skipped
|
||||
assert!(sprites.is_empty());
|
||||
}
|
||||
|
||||
use crate::parser::ast::{BackgroundDecl, PaletteDecl};
|
||||
|
||||
fn blank_program() -> Program {
|
||||
Program {
|
||||
game: GameDecl {
|
||||
name: "Test".to_string(),
|
||||
mapper: Mapper::NROM,
|
||||
mirroring: Mirroring::Horizontal,
|
||||
span: Span::dummy(),
|
||||
},
|
||||
globals: Vec::new(),
|
||||
constants: Vec::new(),
|
||||
enums: Vec::new(),
|
||||
structs: Vec::new(),
|
||||
functions: Vec::new(),
|
||||
states: Vec::new(),
|
||||
sprites: Vec::new(),
|
||||
palettes: Vec::new(),
|
||||
backgrounds: Vec::new(),
|
||||
sfx: Vec::new(),
|
||||
music: Vec::new(),
|
||||
banks: Vec::new(),
|
||||
start_state: "Main".to_string(),
|
||||
span: Span::dummy(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_palette_zero_pads_to_32_bytes() {
|
||||
let mut program = blank_program();
|
||||
program.palettes.push(PaletteDecl {
|
||||
name: "Cool".to_string(),
|
||||
colors: vec![0x0F, 0x01, 0x11, 0x21],
|
||||
span: Span::dummy(),
|
||||
});
|
||||
let resolved = resolve_palettes(&program);
|
||||
assert_eq!(resolved.len(), 1);
|
||||
assert_eq!(resolved[0].name, "Cool");
|
||||
assert_eq!(resolved[0].colors.len(), 32);
|
||||
assert_eq!(&resolved[0].colors[..4], &[0x0F, 0x01, 0x11, 0x21]);
|
||||
// Remainder is zero-padded.
|
||||
assert!(resolved[0].colors[4..].iter().all(|&b| b == 0));
|
||||
assert_eq!(resolved[0].label(), "__palette_Cool");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_palette_truncates_beyond_32_bytes() {
|
||||
// The analyzer rejects >32-byte palettes with E0201; at the
|
||||
// resolve level we defensively truncate so downstream code
|
||||
// always sees exactly 32 bytes. This lets bad input still
|
||||
// produce a valid ROM structure for diagnostic purposes.
|
||||
let mut program = blank_program();
|
||||
program.palettes.push(PaletteDecl {
|
||||
name: "Big".to_string(),
|
||||
colors: (0u8..40).collect(),
|
||||
span: Span::dummy(),
|
||||
});
|
||||
let resolved = resolve_palettes(&program);
|
||||
assert_eq!(resolved[0].colors.len(), 32);
|
||||
assert_eq!(resolved[0].colors[0], 0);
|
||||
assert_eq!(resolved[0].colors[31], 31);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_background_pads_tiles_and_attrs() {
|
||||
let mut program = blank_program();
|
||||
program.backgrounds.push(BackgroundDecl {
|
||||
name: "Stage".to_string(),
|
||||
tiles: vec![1, 2, 3],
|
||||
attributes: vec![0xFF],
|
||||
span: Span::dummy(),
|
||||
});
|
||||
let resolved = resolve_backgrounds(&program);
|
||||
assert_eq!(resolved.len(), 1);
|
||||
assert_eq!(resolved[0].name, "Stage");
|
||||
assert_eq!(resolved[0].tiles.len(), 960);
|
||||
assert_eq!(resolved[0].tiles[0], 1);
|
||||
assert_eq!(resolved[0].tiles[2], 3);
|
||||
assert!(resolved[0].tiles[3..].iter().all(|&b| b == 0));
|
||||
assert_eq!(resolved[0].attrs.len(), 64);
|
||||
assert_eq!(resolved[0].attrs[0], 0xFF);
|
||||
assert!(resolved[0].attrs[1..].iter().all(|&b| b == 0));
|
||||
assert_eq!(resolved[0].tiles_label(), "__bg_tiles_Stage");
|
||||
assert_eq!(resolved[0].attrs_label(), "__bg_attrs_Stage");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue