mirror of
https://github.com/imjasonh/nescript
synced 2026-07-08 08:55:38 +00:00
docs: catalog cc65/nesdoug parity gaps in future-work.md
Enumerates the gaps between NEScript today and what the cc65/nesdoug ecosystem exposes: i16/pointers/bitfields, VRAM update buffer, metatiles, edge-triggered input, PRNG, palette fade, sprite-0 split, additional mappers (AxROM/CNROM/UNROM-512/MMC5), FamiStudio import, SRAM saves, PAL/NTSC abstraction, NSF output, Zapper/Power Pad, configurable debug port, FCEUX .nl labels, and explicit bank hints. Each item has a design sketch and the section ends with a priority ranking. This is the planning doc the follow-up implementation commits will chip away at.
This commit is contained in:
parent
33b5a3958b
commit
c96135fd86
1 changed files with 349 additions and 0 deletions
|
|
@ -238,6 +238,355 @@ semantics come back, add the codes at that point.
|
|||
|
||||
---
|
||||
|
||||
## cc65/nesdoug parity gaps
|
||||
|
||||
The nesdoug tutorial series + neslib expose a broad surface that
|
||||
NEScript can't currently express. This section enumerates the gaps
|
||||
in the order they should probably be tackled (cheapest/highest-
|
||||
leverage first). Anything we finish moves out of this section and
|
||||
into the top of the file with a "ships today" note.
|
||||
|
||||
### A. Numeric types beyond `u8 / i8 / u16 / bool`
|
||||
|
||||
- **`i16`.** The smallest change with the highest blast radius.
|
||||
Negative metasprite offsets, signed velocities, signed scroll
|
||||
deltas, subtraction-of-positions — none of that works today
|
||||
without underflow hazards. Design sketch:
|
||||
- Lexer: no change (type names are already identifiers).
|
||||
- Parser: add `i16` to the primitive-type list.
|
||||
- Analyzer: extend `Type` + coercion table; signed×unsigned
|
||||
mixing should require an explicit cast.
|
||||
- IR: `Add/Sub/Cmp` already carry a signedness flag for `i8`;
|
||||
extend the 16-bit variants the same way.
|
||||
- Codegen: `CMP`/`BCC`/`BCS` for unsigned vs `BMI`/`BPL`/signed
|
||||
compare for signed (XOR the high bits before subtract, or
|
||||
branch on the overflow flag).
|
||||
- **`u32` / `i32`.** Lower priority; realistically needed only for
|
||||
score totals and frame counters. A synthesizable pair of
|
||||
16-bit halves is usually enough.
|
||||
|
||||
### B. Pointers & function pointers
|
||||
|
||||
NEScript has no pointer type. This blocks indirect-dispatch
|
||||
tables (`jmp (vec,x)`), variable-size buffer manipulation, and
|
||||
passing "which thing" to a helper. cc65's `__fastcall__` function
|
||||
pointers via wrapped-call + bank IDs are load-bearing for every
|
||||
game over 32 KB. Design sketch:
|
||||
|
||||
- Introduce `*T` / `fn(T) -> U` type grammar.
|
||||
- Spell a new IR op `CallIndirect` that takes an address in a
|
||||
16-bit temp, plus a `BankHint` so cross-bank pointers trampoline
|
||||
automatically.
|
||||
- For fixed-bank-only code we can lower to a raw `JSR ($vec)`
|
||||
equivalent (`JMP ($vec)` + return stub).
|
||||
|
||||
### C. Bitfields and unions
|
||||
|
||||
OAM attribute bytes, controller masks, collision-flag words, and
|
||||
MMC3 register bits all want bitfield syntax (`struct OamAttr { pal:
|
||||
u2, priority: u1, flip_h: u1, flip_v: u1 }`). Unions show up less
|
||||
often but are useful for reading/writing the same bytes as two
|
||||
different shapes (e.g. a 16-bit counter viewed as `lo: u8, hi: u8`).
|
||||
|
||||
### D. Full inline-assembly escape hatch
|
||||
|
||||
- Today the inline-asm lexer accepts `label:` but not `.label:`
|
||||
(ca65 style). Port the lexer to accept `.`-prefixed local
|
||||
labels and emit them as ca65-compatible locals (`@label`).
|
||||
- Accept cc65's `asm()` format specifiers — `%b` (byte), `%w`
|
||||
(word), `%l` (long), `%v` (var), `%o` (offset), `%g` (global),
|
||||
`%s` (string) — so users can splat a compiler-allocated symbol
|
||||
into a hot-loop fragment.
|
||||
- Extend the directive allow-list: `.byte`, `.word`, `.res`,
|
||||
`.repeat / .endrep`, `.macro / .endmacro`. The assembler can
|
||||
already encode these.
|
||||
|
||||
### E. Dense-`match` jump tables
|
||||
|
||||
`match u8 { 0 => ..., 1 => ..., ... }` desugars to an if/else
|
||||
chain at parse time today, which is `O(n)` compares. For dense
|
||||
(<= 256 entries with <= 4× spread) integer matches, lower to:
|
||||
|
||||
```
|
||||
ASL A ; index *= 2
|
||||
TAX
|
||||
LDA table_lo,X
|
||||
STA $00
|
||||
LDA table_hi,X
|
||||
STA $01
|
||||
JMP ($0000)
|
||||
```
|
||||
|
||||
…with a per-branch `.word` table emitted in the function prologue.
|
||||
This matters most for state dispatch and attack/weapon tables.
|
||||
|
||||
### F. Recursion stance (design constraint, not a bug)
|
||||
|
||||
The analyzer rejects recursive calls with E0402. That's the right
|
||||
call for a compiler targeting a 6502 hardware stack, but it's not
|
||||
documented as a **design choice** anywhere. Add a paragraph to the
|
||||
language guide explaining why, plus a pointer to the hand-rolled
|
||||
explicit-stack pattern (small `u8[N]` stack + `u8` top).
|
||||
|
||||
### G. VRAM update buffer primitive
|
||||
|
||||
The highest-leverage missing runtime feature. Today
|
||||
`load_background` / `set_palette` queue PPU writes under the hood,
|
||||
but there is no user-visible "write these N bytes into nametable
|
||||
slot `(x,y)` next vblank" primitive. That's the idiom behind every
|
||||
scoreboard, dialog box, destroyed-metatile animation, and streaming
|
||||
scroll in the nesdoug chapters. Concrete API sketch:
|
||||
|
||||
```
|
||||
buffer.nametable_write(x, y, [0x20, 0x21, 0x22]) // horizontal
|
||||
buffer.nametable_write_v(x, y, [0x20, 0x21, 0x22]) // vertical
|
||||
buffer.attribute_write(x, y, 0b00011011) // one byte
|
||||
buffer.flush() // force an eof
|
||||
```
|
||||
|
||||
Runtime shape: a fixed ring buffer at a known RAM address
|
||||
(`$0400`?). Each entry is `[header, addr_hi, addr_lo, len, data…]`
|
||||
where `header` carries the `NT_UPD_HORZ` / `NT_UPD_VERT` /
|
||||
`NT_UPD_EOF` bits the neslib engine already uses. The NMI handler
|
||||
drains the buffer every frame and writes `$FF` as the sentinel.
|
||||
|
||||
### H. Metatiles + collision as a first-class construct
|
||||
|
||||
cc65/nesdoug treats 2×2 metatiles + a parallel collision map as
|
||||
the core room format. `docs/future-work.md` mentions "tilemap
|
||||
collision queries"; raise the scope to a single cohesive feature:
|
||||
|
||||
```
|
||||
metatileset DirtWorld {
|
||||
source: @tiles("dirt.chr"),
|
||||
metatiles: [
|
||||
{ id: 0, tiles: [0, 1, 16, 17], collide: false },
|
||||
{ id: 1, tiles: [2, 3, 18, 19], collide: true },
|
||||
...
|
||||
],
|
||||
}
|
||||
|
||||
room Level1 {
|
||||
metatileset: DirtWorld,
|
||||
layout: @room("level1.nxt"), // NEXXT exporter format
|
||||
}
|
||||
|
||||
on_frame {
|
||||
if collides_at(hero.x, hero.y) {
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The compiler would expand each `room` into a packed `[(metatile_id
|
||||
<< 4 | collision_bits), ...]` blob in PRG ROM, emit a
|
||||
`collides_at(x: u16, y: u16) -> bool` helper, and stream the
|
||||
expanded tiles into the VRAM update buffer on a `paint_room()` call.
|
||||
|
||||
### I. RLE + LZ4 nametable decompression
|
||||
|
||||
`vram_unrle` and `vram_unlz4` — for scrolling/multi-room games,
|
||||
packing rooms is mandatory. cc65 ships both in neslib with
|
||||
concrete timing (0.5f RLE vs 2.8f LZ4). The per-state background
|
||||
swapping item in "What ships today" is exactly this problem:
|
||||
without a decompressor that can stream into the VRAM buffer, the
|
||||
NMI-time write budget (~2273 cycles) is too tight for a full
|
||||
nametable. RLE is the smaller first step — emit a `nametable` that
|
||||
can declare `compression: rle` and decompress at swap time.
|
||||
|
||||
### J. Palette brightness / fade
|
||||
|
||||
Neslib's `pal_bright(level: 0..8)` is a one-call fade that flips
|
||||
the PPU mask emphasis bits and optionally darkens the active
|
||||
palette via a brightness LUT. One-call fade-in / fade-out is
|
||||
enormous polish for nearly no runtime cost. API:
|
||||
|
||||
```
|
||||
builtin set_palette_brightness(level: u8) // 0=off, 4=normal, 8=white
|
||||
builtin fade_out(frames: u8) // blocks; drives mask bits
|
||||
builtin fade_in(frames: u8)
|
||||
```
|
||||
|
||||
### K. Edge-triggered input
|
||||
|
||||
Today NEScript exposes level-state buttons (`p1.a` is whatever the
|
||||
hardware reports this frame). Every menu needs a "just pressed
|
||||
this frame" primitive. Extend the button type with `.pressed` and
|
||||
`.released` accessors:
|
||||
|
||||
```
|
||||
if p1.a.pressed { menu_accept() }
|
||||
if p1.start.released { pause_menu() }
|
||||
```
|
||||
|
||||
Implementation is one more ZP byte per controller (`p1_prev`) and
|
||||
an XOR in the input polling stub.
|
||||
|
||||
### L. Sprite 0 hit split-screen
|
||||
|
||||
`split(x, y)` is the neslib primitive for a fixed status bar above
|
||||
a scrolling playfield without MMC3. NEScript only offers
|
||||
`on_scanline(N)` on MMC3. A sprite-0-hit-based split that works on
|
||||
NROM/UxROM/MMC1 unlocks most of the tutorial games. API:
|
||||
|
||||
```
|
||||
sprite_0_split scanline: 32, {
|
||||
scroll_x: 0,
|
||||
scroll_y: 0,
|
||||
}
|
||||
```
|
||||
|
||||
…emits a busy-wait on `$2002` bit 6 followed by the requested
|
||||
scroll write.
|
||||
|
||||
### M. Automatic sprite cycling
|
||||
|
||||
The existing `cycle_sprites` opt-in keyword rotates the DMA offset
|
||||
each frame. A `game { sprite_flicker: true }` attribute that emits
|
||||
the rotation automatically — plus a `draw ... priority: pinned`
|
||||
modifier for HUD sprites that must stay at low OAM slots — is the
|
||||
cleaner user-facing API. Mentioned already under Open Design
|
||||
Questions; bumping it into the active roadmap.
|
||||
|
||||
### N. Runtime PRNG
|
||||
|
||||
`rand8()` / `rand16()` / `set_rand(seed: u16)` are in every
|
||||
nesdoug demo. Implement as a ZP-held 16-bit LFSR (xorshift16 is
|
||||
tiny and good enough). Users shouldn't have to hand-roll this.
|
||||
|
||||
### O. DPCM / DMC sample playback
|
||||
|
||||
Already listed under Audio Pipeline. FamiStudio's DMC support
|
||||
(including bankswitched DMC) is the reference API shape — import
|
||||
`@dpcm("file.dmc")` into a named sample slot and expose
|
||||
`play_dpcm(Slot, pitch: u8, loop: bool)`.
|
||||
|
||||
### P. Expansion audio (VRC6, MMC5, FDS, N163, S5B, VRC7)
|
||||
|
||||
FamiStudio has a single export path with `FAMISTUDIO_CFG_EXTERNAL`
|
||||
and per-chip feature flags. If/when we import a FamiStudio-export
|
||||
format (see Q), the expansion chips come along almost for free
|
||||
— the runtime just has to wire up the extra write ports and the
|
||||
mapper has to expose them (MMC5 for the extra pulse channels,
|
||||
VRC6/VRC7 via their own mappers).
|
||||
|
||||
### Q. FamiStudio text-export import
|
||||
|
||||
`@music("file.famistudio.txt")`. FamiStudio's text export is the
|
||||
pragmatic ingestion path; parsing it gives full tracker semantics
|
||||
(volume/pitch slides, arpeggios, vibrato, release notes) without
|
||||
reinventing the engine. FamiTracker's binary `.ftm` is a worse
|
||||
target — undocumented, version-skewed.
|
||||
|
||||
### R. NEXXT metatile/collision import
|
||||
|
||||
NEXXT is the dominant asset editor in the nesdoug workflow; it
|
||||
emits metatile tables + collision maps as ca65-compatible
|
||||
assembler source. An `@metatiles("room.nxt")` loader (and
|
||||
`@room("level1.nxt")` for layouts — see §H) removes a whole class
|
||||
of hand-typed tile arrays.
|
||||
|
||||
### S. SRAM / battery-backed saves
|
||||
|
||||
Already in the spec as a "reserved for future versions" item. Add
|
||||
a top-level `save { var … }` block that lands its allocations at
|
||||
`$6000+`, flips the iNES battery flag, and exposes the allocations
|
||||
to the rest of the program as if they were ordinary globals (with
|
||||
a compiler-emitted checksum on write to survive cold starts).
|
||||
|
||||
### T. PAL/NTSC region abstraction
|
||||
|
||||
Neslib exposes `ppu_wait_frame` as a virtual-50Hz wait on PAL. Add
|
||||
a `region: ntsc | pal | dual` field on the `game { }` block. For
|
||||
`dual`, the runtime probes `$2002` bit 7 timing at reset and sets a
|
||||
ZP flag; the audio engine's frame tick and any frame-counted timing
|
||||
respects the flag.
|
||||
|
||||
### U. Additional controller types
|
||||
|
||||
Expose Zapper (light-gun) and Power Pad via typed inputs:
|
||||
|
||||
```
|
||||
input gun: zapper on port: 1
|
||||
input mat: power_pad on port: 2
|
||||
```
|
||||
|
||||
`gun.trigger`, `gun.light_detected`, `mat.button(i: u8)` are the
|
||||
three reads every program needs.
|
||||
|
||||
### V. Additional mappers
|
||||
|
||||
In priority order (cheapest × highest demand first):
|
||||
|
||||
1. **AxROM** (mapper 7). Single-screen mirroring, up to 256 KB PRG
|
||||
bankswitched in 32 KB pages. Almost a trivial extension of the
|
||||
UxROM path — one register, different mirroring bit.
|
||||
2. **CNROM** (mapper 3). 8 KB CHR bankswitching, fixed 32 KB PRG.
|
||||
One register, CHR-only. Also trivial.
|
||||
3. **GNROM / MHROM** (mapper 66). Combines AxROM-style PRG with
|
||||
CNROM-style CHR banking. Another single-register mapper.
|
||||
4. **MMC2** (mapper 9, Punch-Out only realistically). Medium.
|
||||
5. **UNROM-512** (mapper 30). The modern homebrew sweet spot —
|
||||
512 KB PRG + CHR-RAM + self-flashing. Mapping is UxROM-like
|
||||
plus a one-screen bit.
|
||||
6. **MMC5** (mapper 5). Big. Driven by FamiStudio's expansion
|
||||
audio more than by the extra PRG/CHR modes. Probably last.
|
||||
|
||||
### W. NSF output target
|
||||
|
||||
The audio engine is already a standalone subsystem. An NSF-output
|
||||
target (`--target nsf`) would wrap the existing music/sfx blocks
|
||||
in the NSF header and expose `init`/`play` entry points. Nearly
|
||||
free, gets the chiptune audience for ~a day of work.
|
||||
|
||||
### X. Configurable / Mesen-native debug output
|
||||
|
||||
Today the debug port is hardcoded to `$4800`. Expose
|
||||
`debug.port: $4800 | mesen` on the `game { }` block. For
|
||||
`mesen`, emit writes to `$4018` (Mesen's documented debug port)
|
||||
and document the trace-log tool invocation in the debug docs.
|
||||
|
||||
### Y. FCEUX `.nl` / `.ld` label file output
|
||||
|
||||
`--dbg` writes ca65-compatible debug info, which Mesen + Mesen2 +
|
||||
FCEUX all consume for source-level stepping. FCEUX also supports
|
||||
its native `.nl` (per-bank label) / `.ld` (line) files, which some
|
||||
users prefer. Cheap addition: `--fceux-labels <prefix>` emits
|
||||
`<prefix>.0.nl`, `<prefix>.1.nl`, …, `<prefix>.ld`.
|
||||
|
||||
### Z. Explicit bank-placement hints on functions and data
|
||||
|
||||
`bank Foo { fun bar() }` already exists; extend the sugar to
|
||||
attributes on individual items so users don't have to restructure
|
||||
their source:
|
||||
|
||||
```
|
||||
@bank(3) fun slow_helper() { ... }
|
||||
@bank(3) const LEVEL_DATA: u8[1024] = [...]
|
||||
```
|
||||
|
||||
This is particularly useful for `const` data, which today lands
|
||||
wherever the analyzer decides; users sometimes need to pin data
|
||||
to a specific bank to avoid bank-switch cost on a hot path.
|
||||
|
||||
### Priority ranking
|
||||
|
||||
In practice the order I'd tackle these for maximum user value:
|
||||
|
||||
1. `i16` (§A) — unblocks signed physics, metasprite offsets.
|
||||
2. VRAM update buffer (§G) — unblocks HUDs, dialog, streaming.
|
||||
3. Edge-triggered input (§K) + PRNG (§N) — one-line demo wins.
|
||||
4. Palette fade (§J) + sprite-0 split (§L) — cheap polish wins.
|
||||
5. Register allocator (existing section) — compounding size win.
|
||||
6. Metatiles + collision (§H) — closes several items at once.
|
||||
7. Inline-asm completeness (§D) — escape hatch for power users.
|
||||
8. Arrays-of-structs + bitfields (§C) + fn pointers (§B) —
|
||||
turns NEScript into a general-purpose NES language.
|
||||
9. SRAM (§S) + AxROM/CNROM/UNROM-512 (§V) — ecosystem fit.
|
||||
10. FamiStudio import (§Q) + DPCM (§O) + expansion audio (§P).
|
||||
|
||||
---
|
||||
|
||||
## Open design questions
|
||||
|
||||
1. **Inline asm label syntax.** `.label:` (ca65 style) vs `label:` (generic)?
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue