1
0
Fork 0
mirror of https://github.com/imjasonh/nescript synced 2026-07-08 17:06:04 +00:00
nescript/docs/nes-reference.md
Claude 7b4570eee5
compiler: i16 / SRAM saves / inline-asm dot labels / docs
Another batch from the cc65/nesdoug catalogue. All gated on
parser-level opt-in or default-false attributes so existing
programs produce byte-identical ROMs (no committed .nes file
changed).

**§A — `i16` signed 16-bit type:**
- New `KwI16` lexer token, `NesType::I16` AST variant, parser
  case in `parse_type`. Type-size and integer-type tables
  treat `i16` like `u16` (2 bytes, integer).
- IR lowering accepts `i16` everywhere it accepts `u16` for
  wide-load / wide-store / widen-narrow paths.
- New constant fold for `UnaryOp::Negate(IntLiteral(v))` that
  emits the wide two's-complement form. Without it, `var vy:
  i16 = -10` would zero-extend to `$00F6` (= 246) instead of
  sign-extending to `$FFF6` (= -10). Negative literals now
  store the right bytes.
- Comparisons reuse the existing unsigned 16-bit compare ops
  (matching the existing `i8` behaviour). Documented in the
  `NesType::I16` doc comment and in `future-work.md` §A.
- Example `examples/i16_demo.ne` with committed golden.
- Tests cover the literal-fold sign-extension and end-to-end
  compile of the example.

**§S — SRAM / battery-backed saves:**
- New `save { var ... }` top-level block. Lexer + parser opt
  into a dedicated `KwSave` token. Analyzer allocates save
  vars from a separate `next_sram_addr` bump pointer starting
  at `$6000`, capped at `$8000` (8 KB cartridge SRAM window).
- Linker reads `analysis.has_battery_saves` and flips iNES
  byte-6 bit-1 via the new `RomBuilder::set_battery` /
  `Linker::with_battery` chain.
- New `W0111` warning for save-var initializers — SRAM is
  preserved across power cycles, so an init expression would
  either silently never run or clobber persisted data on
  every boot. The warning teaches the user about the
  magic-byte sentinel pattern.
- Struct fields in save blocks are explicitly rejected for now
  (the field-flattening path uses the main-RAM allocator).
- Example `examples/sram_demo.ne` with committed golden, plus
  4 integration tests.

**§D (partial) — inline-asm `.label:` syntax:**
- Codegen-side mangler rewrites `.IDENT` → `__ilab_<N>_IDENT`
  per inline-asm block, where `<N>` is the call site's
  monotonic suffix. Two `asm { .loop: ... }` blocks in the
  same function now coexist without colliding in the linker's
  label table.
- Bounds checks on `.` placement: `$2002` and `name.field`
  are unaffected; only `.IDENT` in label / branch context
  triggers the rewrite. Two integration tests pin the
  uniqueness and dollar-vs-dot disambiguation.

**§X follow-up — Mesen trace-log docs:**
- New "Debugger-assisted workflows" section in
  `docs/nes-reference.md` walking through the Mesen / FCEUX
  log workflows alongside the new `debug_port:` attribute.

**Misc:**
- `future-work.md` updated to mark the shipped items out of
  the catalogue and reshuffle the priority ranking. Remaining
  niche follow-ups (signedness on Cmp16, struct save fields,
  inline-asm format specifiers) documented inline so future
  passes know the design.

All 757 tests pass. Clippy clean. 46/46 emulator goldens match.
2026-04-18 20:49:06 +00:00

248 lines
12 KiB
Markdown

# NES Hardware Quick Reference
A concise reference to the NES hardware for NEScript contributors. Understanding these constraints explains many of the compiler's design decisions.
---
## CPU: Ricoh 2A03 (MOS 6502 variant)
- **Clock speed:** 1.79 MHz (NTSC), 1.66 MHz (PAL)
- **Architecture:** 8-bit data bus, 16-bit address bus
- **Registers:**
- `A` (Accumulator) -- 8-bit, used for arithmetic and logic
- `X` (Index X) -- 8-bit, used for indexing and counting
- `Y` (Index Y) -- 8-bit, used for indexing and counting
- `SP` (Stack Pointer) -- 8-bit, points into the $0100-$01FF range
- `P` (Status) -- 8-bit flags: N(egative), V(overflow), B(reak), D(ecimal), I(nterrupt), Z(ero), C(arry)
- `PC` (Program Counter) -- 16-bit
- **No multiply, divide, or floating point instructions**
- **Instruction set:** 56 official opcodes with multiple addressing modes (implied, immediate, zero page, zero page X/Y, absolute, absolute X/Y, indirect, indexed indirect, indirect indexed)
### Why This Matters for NEScript
The 6502's 3-register architecture drives the compiler's register allocator. The lack of multiply/divide means the compiler emits software routines for `*`, `/`, and `%`. Zero-page addressing is 1 byte shorter and 1 cycle faster than absolute addressing, which is why `fast` variable placement matters.
---
## Memory Map
| Address Range | Size | Description |
|-------------------|------------|-------------------------------------|
| `$0000`-`$00FF` | 256 bytes | Zero page -- fast access |
| `$0100`-`$01FF` | 256 bytes | Stack (grows downward) |
| `$0200`-`$02FF` | 256 bytes | OAM shadow buffer (convention) |
| `$0300`-`$07FF` | 1280 bytes | General purpose RAM |
| `$0800`-`$1FFF` | -- | Mirrors of $0000-$07FF |
| `$2000`-`$2007` | 8 bytes | PPU registers |
| `$2008`-`$3FFF` | -- | Mirrors of PPU registers |
| `$4000`-`$4017` | 24 bytes | APU and I/O registers |
| `$4018`-`$401F` | 8 bytes | CPU test mode (normally disabled) |
| `$4020`-`$5FFF` | -- | Expansion ROM (mapper-dependent) |
| `$6000`-`$7FFF` | 8 KB | SRAM / PRG RAM (if present) |
| `$8000`-`$BFFF` | 16 KB | PRG ROM lower bank |
| `$C000`-`$FFFF` | 16 KB | PRG ROM upper bank (or fixed bank) |
### Interrupt Vectors
| Address | Vector | Description |
|----------|---------|------------------------------------------|
| `$FFFA` | NMI | Non-Maskable Interrupt (vertical blank) |
| `$FFFC` | RESET | Power-on / reset entry point |
| `$FFFE` | IRQ | Interrupt Request (mapper-dependent) |
### NEScript Memory Usage
The compiler reserves the following:
- `$00`-`$0F`: System use (frame counter, input, OAM cursor, SFX/music pointers, mul/div scratch)
- `$10`: `ZP_BANK_CURRENT` (current switchable PRG bank index, only in banked programs)
- `$11`-`$17`: PPU update slots (palette/nametable flags and pending pointers, only when the program declares palette or background blocks)
- `$18`-`$7F`: Available for `fast` variables (user zero-page)
- `$80`-`$FF`: IR codegen temp slots (scratch for expression evaluation)
- `$0200`-`$02FF`: OAM shadow buffer (DMA'd to PPU each frame)
- `$0300`-`$07EE`: General variables, per-function RAM (parameter spill + locals), state-local storage
- `$07EF`: `SPRITE_CYCLE_ADDR` — rotating offset byte used by `cycle_sprites` (only when the program emits a `cycle_sprites` statement)
- `$07F0`-`$07F7`: Audio channel state (noise/triangle/sfx-pitch pointers; only when the program declares a matching sfx)
- `$07FC`: `DEBUG_SPRITE_OVERFLOW_FLAG_ADDR` — per-frame sticky bit (debug builds only)
- `$07FD`: `DEBUG_SPRITE_OVERFLOW_COUNT_ADDR` — cumulative PPU sprite overflow counter (debug builds only)
- `$07FE`: `DEBUG_FRAME_OVERRUN_FLAG_ADDR` — per-frame sticky bit (debug builds only)
- `$07FF`: `DEBUG_FRAME_OVERRUN_ADDR` — cumulative frame overrun counter (debug builds only)
Release-mode programs that don't opt into audio, banking, debug, or
sprite cycling leave the corresponding slots untouched and the
analyzer is free to allocate user globals over them.
---
## PPU (Picture Processing Unit)
- **Resolution:** 256 x 240 pixels
- **Refresh rate:** 60 Hz (NTSC), 50 Hz (PAL)
- **VRAM:** 2 KB internal (2 nametables), expandable by mapper
- **Pattern tables:** 2 tables of 256 tiles each (one for backgrounds, one for sprites), stored in CHR ROM/RAM
- **Tile size:** 8x8 pixels (or 8x16 for tall sprites)
- **Color depth:** 2 bits per pixel (4 colors per tile, selected from a sub-palette)
### Nametables
- 4 logical nametables, each 960 bytes of tile indices + 64 bytes of attribute data
- With 2 KB VRAM, 2 physical nametables exist; the other 2 are mirrors
- Mirroring arrangement (horizontal or vertical) is set by the cartridge/mapper
### Sprites (OAM)
- **64 sprites** total, each defined by 4 bytes:
- Byte 0: Y position
- Byte 1: Tile index
- Byte 2: Attributes (palette, priority, flip H/V)
- Byte 3: X position
- **8 sprites per scanline** maximum (excess sprites are dropped)
- OAM is 256 bytes, typically shadow-buffered at CPU `$0200` and DMA'd via `$4014`
### Palettes
- **Background:** 4 sub-palettes, each with 4 colors (first color shared across all)
- **Sprite:** 4 sub-palettes, each with 4 colors (first color is transparent)
- Colors are indices into the NES master palette (64 entries, `$00`-`$3F`)
- Palette RAM is 32 bytes total at PPU `$3F00`-`$3F1F`
### PPU Registers
| Address | Name | Description |
|---------|----------|--------------------------------------|
| `$2000` | PPUCTRL | NMI enable, sprite size, pattern table select, nametable select |
| `$2001` | PPUMASK | Color emphasis, sprite/background enable, clipping |
| `$2002` | PPUSTATUS| Vblank flag, sprite 0 hit, sprite overflow |
| `$2003` | OAMADDR | OAM address for writes |
| `$2004` | OAMDATA | OAM data read/write |
| `$2005` | PPUSCROLL| Scroll position (write twice: X, Y) |
| `$2006` | PPUADDR | VRAM address (write twice: high, low)|
| `$2007` | PPUDATA | VRAM data read/write |
### Rendering Timing
- **Vblank** starts at scanline 241 and lasts ~20 scanlines (~2,273 CPU cycles)
- PPU updates (palette, nametable, scroll) must happen during vblank
- `on frame` code runs during the visible frame; the implicit `wait_frame()` yields until the next vblank
- Approximately **29,780 CPU cycles per frame** (NTSC)
---
## APU (Audio Processing Unit)
| Channel | Type | Description |
|-----------|------------|------------------------------------|
| Pulse 1 | Square wave| Variable duty cycle (12.5/25/50/75%) |
| Pulse 2 | Square wave| Same as Pulse 1 |
| Triangle | Triangle | Fixed volume, good for bass |
| Noise | Noise | Pseudo-random, for percussion |
| DMC | Sample | 1-bit delta-modulated samples |
APU registers span `$4000`-`$4017`. The audio driver (included automatically when `sfx` or `music` declarations exist) runs during NMI.
---
## iNES ROM Format
The standard ROM file format for NES emulators:
```
Offset Size Description
------ ------- -----------
0 4 bytes Magic number: "NES" followed by $1A
4 1 byte PRG ROM size in 16 KB units
5 1 byte CHR ROM size in 8 KB units
6 1 byte Flags 6: mapper (low nibble), mirroring, battery, trainer
7 1 byte Flags 7: mapper (high nibble), VS/Playchoice, NES 2.0
8-15 8 bytes Padding (zeros)
16+ varies PRG ROM data (N x 16384 bytes)
after varies CHR ROM data (N x 8192 bytes)
```
### Common Mapper Numbers
| Number | Name | PRG ROM | CHR | Notes |
|--------|--------|-------------|-------------|----------------------------|
| 0 | NROM | 16/32 KB | 8 KB ROM | No bank switching |
| 1 | MMC1 | Up to 256 KB| Up to 128 KB| Switchable 16 KB PRG banks |
| 2 | UxROM | Up to 256 KB| 8 KB RAM | Switchable 16 KB PRG banks |
| 4 | MMC3 | Up to 512 KB| Up to 256 KB| Scanline counter, 8 KB banks|
### Mirroring
- **Horizontal:** nametables A-A-B-B (vertical scrolling games)
- **Vertical:** nametables A-B-A-B (horizontal scrolling games)
- Set in byte 6, bit 0 of the iNES header (0 = horizontal, 1 = vertical)
---
## Controller
Each controller has 8 buttons read as a serial shift register via `$4016` (port 1) and `$4017` (port 2).
Read sequence:
1. Write `$01` then `$00` to `$4016` to strobe (latch button states)
2. Read `$4016` (or `$4017`) 8 times; bit 0 of each read is one button
Button order: A, B, Select, Start, Up, Down, Left, Right.
The NEScript runtime handles this automatically. The programmer reads button state via `button.a`, `button.up`, etc.
---
## Key Constraints for NEScript
| Constraint | Limit | NEScript Response |
|-------------------------|----------------|------------------------------------------|
| Total RAM | 2 KB | Static allocation, no heap |
| Zero page | 256 bytes | `fast`/`slow` hints, compiler promotion |
| Stack | 256 bytes | Call depth limit, no recursion |
| Sprites per frame | 64 | Compiler manages OAM buffer |
| Sprites per scanline | 8 | Hardware limit, no workaround |
| Vblank time | ~2,273 cycles | PPU updates must be fast |
| Frame budget | ~29,780 cycles | Frame overrun detection in debug mode |
| No multiply/divide HW | -- | Software routines, power-of-2 optimization|
---
## Debugger-assisted workflows
NEScript compiles three debugger-friendly sidecar files that Mesen,
Mesen2, and FCEUX can load alongside the `.nes`. All three are
off by default (release ROMs should be as small as possible) and
enabled per-run via CLI flags:
| Flag | Format | Consumers |
|---------------------------------|----------------------------------|-----------------|
| `--symbols <path.mlb>` | Mesen labels (`P:` / `R:` lines) | Mesen, Mesen2 |
| `--dbg <path.dbg>` | ca65 debug-info format | Mesen, Mesen2, FCEUX |
| `--fceux-labels <prefix>` | `<prefix>.<bank>.nl` + `.ram.nl` | FCEUX |
### Mesen trace-log
Mesen supports address-based execution tracing via its `log` /
`save-log` scripting APIs. Combined with NEScript's `debug.log`
builtin, the standard workflow is:
1. Build with `--debug` and `--symbols out.mlb`. `debug.log(value)`
writes `value` to the emulator debug port every time it executes.
2. In the `game { }` block, set `debug_port: mesen` so writes land
at `$4018` (Mesen's documented tracing port):
```
game "MyGame" {
mapper: NROM
debug_port: mesen
}
```
3. In Mesen, enable the trace log (Debug → Trace Logger), load
the `.mlb`, and filter by memory operation on `$4018`. Each
`debug.log(x)` call appears in the trace with the source
function + line resolved from the label table.
For FCEUX on Linux, keep the default `debug_port: fceux` (writes
to `$4800`) and pass `--fceux-labels out` — FCEUX reads
`out.<bank>.nl` automatically when it opens the ROM from the same
directory. FCEUX's conditional-breakpoint syntax can match writes
to `$4800` directly (`A(4800)!=0`) so you can break when any log
fires.