romdevtools 0.28.0 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +53 -43
- package/CHANGELOG.md +91 -0
- package/README.md +3 -3
- package/examples/README.md +7 -7
- package/examples/atari2600/templates/platformer.asm +1225 -332
- package/examples/atari2600/templates/puzzle.asm +1056 -0
- package/examples/atari2600/templates/racing.asm +906 -275
- package/examples/atari2600/templates/shmup.asm +1031 -239
- package/examples/atari2600/templates/sports.asm +1135 -253
- package/examples/atari7800/templates/platformer.c +991 -156
- package/examples/atari7800/templates/puzzle.c +1091 -148
- package/examples/atari7800/templates/racing.c +952 -124
- package/examples/atari7800/templates/shmup.c +812 -134
- package/examples/atari7800/templates/sports.c +820 -184
- package/examples/c64/templates/platformer.c +879 -164
- package/examples/c64/templates/puzzle.c +855 -178
- package/examples/c64/templates/racing.c +873 -97
- package/examples/c64/templates/shmup.c +757 -161
- package/examples/c64/templates/sports.c +755 -100
- package/examples/gb/templates/platformer.c +841 -179
- package/examples/gb/templates/puzzle.c +986 -246
- package/examples/gb/templates/racing.c +754 -174
- package/examples/gb/templates/shmup.c +673 -175
- package/examples/gb/templates/sports.c +790 -159
- package/examples/gba/templates/platformer.c +626 -165
- package/examples/gba/templates/puzzle.c +519 -269
- package/examples/gba/templates/racing.c +511 -206
- package/examples/gba/templates/shmup.c +564 -179
- package/examples/gba/templates/sports.c +454 -174
- package/examples/gbc/templates/platformer.c +944 -180
- package/examples/gbc/templates/puzzle.c +363 -109
- package/examples/gbc/templates/racing.c +884 -180
- package/examples/gbc/templates/shmup.c +821 -185
- package/examples/gbc/templates/sports.c +870 -162
- package/examples/genesis/templates/platformer.c +747 -129
- package/examples/genesis/templates/puzzle.c +694 -261
- package/examples/genesis/templates/racing.c +726 -203
- package/examples/genesis/templates/shmup.c +535 -142
- package/examples/genesis/templates/sports.c +495 -158
- package/examples/gg/templates/platformer.c +880 -215
- package/examples/gg/templates/puzzle.c +875 -216
- package/examples/gg/templates/racing.c +915 -172
- package/examples/gg/templates/shmup.c +714 -191
- package/examples/gg/templates/sports.c +732 -129
- package/examples/lynx/templates/platformer.c +604 -69
- package/examples/lynx/templates/puzzle.c +498 -158
- package/examples/lynx/templates/racing.c +538 -102
- package/examples/lynx/templates/shmup.c +458 -131
- package/examples/lynx/templates/sports.c +496 -72
- package/examples/msx/platformer/main.c +649 -162
- package/examples/msx/puzzle/main.c +742 -240
- package/examples/msx/racing/main.c +669 -178
- package/examples/msx/shmup/main.c +460 -178
- package/examples/msx/sports/main.c +592 -126
- package/examples/nes/templates/platformer.c +589 -171
- package/examples/nes/templates/puzzle.c +563 -242
- package/examples/nes/templates/racing.c +502 -208
- package/examples/nes/templates/shmup.c +339 -145
- package/examples/nes/templates/sports.c +341 -183
- package/examples/pce/platformer/main.c +874 -205
- package/examples/pce/puzzle/main.c +802 -287
- package/examples/pce/racing/main.c +783 -208
- package/examples/pce/shmup/main.c +638 -212
- package/examples/pce/sports/main.c +586 -169
- package/examples/porting-across-platforms/README.md +1 -1
- package/examples/sms/templates/platformer.c +762 -177
- package/examples/sms/templates/puzzle.c +752 -212
- package/examples/sms/templates/racing.c +808 -145
- package/examples/sms/templates/shmup.c +599 -162
- package/examples/sms/templates/sports.c +630 -122
- package/examples/snes/templates/music_demo.c +7 -0
- package/examples/snes/templates/platformer-data.asm +123 -24
- package/examples/snes/templates/platformer-hdr.asm +57 -0
- package/examples/snes/templates/platformer.c +586 -165
- package/examples/snes/templates/puzzle-data.asm +116 -21
- package/examples/snes/templates/puzzle-hdr.asm +57 -0
- package/examples/snes/templates/puzzle.c +614 -235
- package/examples/snes/templates/racing-data.asm +390 -32
- package/examples/snes/templates/racing-hdr.asm +57 -0
- package/examples/snes/templates/racing.c +807 -196
- package/examples/snes/templates/shmup-data.asm +87 -29
- package/examples/snes/templates/shmup-hdr.asm +57 -0
- package/examples/snes/templates/shmup.c +459 -198
- package/examples/snes/templates/sports-data.asm +48 -2
- package/examples/snes/templates/sports-hdr.asm +57 -0
- package/examples/snes/templates/sports.c +414 -163
- package/package.json +12 -12
- package/src/cores/wasm/bluemsx_libretro.js +1 -1
- package/src/cores/wasm/bluemsx_libretro.wasm +0 -0
- package/src/cores/wasm/fceumm_libretro.js +1 -1
- package/src/cores/wasm/fceumm_libretro.wasm +0 -0
- package/src/cores/wasm/gambatte_libretro.js +1 -1
- package/src/cores/wasm/gambatte_libretro.wasm +0 -0
- package/src/cores/wasm/geargrafx_libretro.js +1 -1
- package/src/cores/wasm/geargrafx_libretro.wasm +0 -0
- package/src/cores/wasm/genesis_plus_gx_libretro.js +1 -1
- package/src/cores/wasm/genesis_plus_gx_libretro.wasm +0 -0
- package/src/cores/wasm/handy_libretro.js +1 -1
- package/src/cores/wasm/handy_libretro.wasm +0 -0
- package/src/cores/wasm/mgba_libretro.js +1 -1
- package/src/cores/wasm/mgba_libretro.wasm +0 -0
- package/src/cores/wasm/prosystem_libretro.js +1 -1
- package/src/cores/wasm/prosystem_libretro.wasm +0 -0
- package/src/cores/wasm/snes9x_libretro.js +1 -1
- package/src/cores/wasm/snes9x_libretro.wasm +0 -0
- package/src/cores/wasm/stella2014_libretro.js +1 -1
- package/src/cores/wasm/stella2014_libretro.wasm +0 -0
- package/src/cores/wasm/vice_x64_libretro.js +1 -1
- package/src/cores/wasm/vice_x64_libretro.wasm +0 -0
- package/src/host/LibretroHost.js +84 -8
- package/src/http/tool-registry.js +11 -11
- package/src/mcp/tools/cheats.js +2 -1
- package/src/mcp/tools/frame.js +3 -2
- package/src/mcp/tools/index.js +3 -3
- package/src/mcp/tools/input.js +5 -4
- package/src/mcp/tools/lifecycle.js +6 -4
- package/src/mcp/tools/memory.js +131 -24
- package/src/mcp/tools/platform-docs.js +1 -1
- package/src/mcp/tools/preview-tile.js +6 -2
- package/src/mcp/tools/project.js +1098 -130
- package/src/mcp/tools/record.js +6 -7
- package/src/mcp/tools/rom-id.js +5 -1
- package/src/mcp/tools/run-until.js +12 -4
- package/src/mcp/tools/snippets.js +6 -6
- package/src/mcp/tools/sprite-pipeline.js +14 -2
- package/src/mcp/tools/state.js +2 -1
- package/src/mcp/tools/tile-inspect.js +8 -1
- package/src/mcp/tools/toolchain.js +12 -1
- package/src/mcp/tools/watch-memory.js +53 -10
- package/src/observer/bus.js +73 -0
- package/src/observer/livestream.html +4 -2
- package/src/observer/tool-wrap.js +17 -14
- package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +32 -3
- package/src/platforms/atari7800/MENTAL_MODEL.md +5 -5
- package/src/platforms/atari7800/TROUBLESHOOTING.md +5 -5
- package/src/platforms/c64/MENTAL_MODEL.md +11 -4
- package/src/platforms/c64/TROUBLESHOOTING.md +13 -0
- package/src/platforms/gb/MENTAL_MODEL.md +3 -3
- package/src/platforms/gb/TROUBLESHOOTING.md +61 -8
- package/src/platforms/gb/lib/c/README.md +10 -11
- package/src/platforms/gb/lib/c/gb_crt0.s +27 -3
- package/src/platforms/gb/lib/c/patch-header.js +13 -3
- package/src/platforms/gba/MENTAL_MODEL.md +4 -4
- package/src/platforms/gba/TROUBLESHOOTING.md +3 -3
- package/src/platforms/gba/lib/c/gba_sfx.c +40 -0
- package/src/platforms/gba/lib/c/gba_sfx.h +10 -0
- package/src/platforms/gbc/MENTAL_MODEL.md +4 -4
- package/src/platforms/gbc/TROUBLESHOOTING.md +4 -4
- package/src/platforms/gbc/UPSTREAM_SOURCES.md +1 -1
- package/src/platforms/gbc/lib/c/README.md +10 -11
- package/src/platforms/gbc/lib/c/gb_crt0.s +26 -3
- package/src/platforms/gbc/lib/c/patch-header.js +13 -3
- package/src/platforms/genesis/MENTAL_MODEL.md +3 -3
- package/src/platforms/genesis/TROUBLESHOOTING.md +2 -2
- package/src/platforms/gg/MENTAL_MODEL.md +4 -4
- package/src/platforms/gg/TROUBLESHOOTING.md +3 -3
- package/src/platforms/gg/UPSTREAM_SOURCES.md +1 -1
- package/src/platforms/gg/lib/c/joypad_read.c +29 -0
- package/src/platforms/lynx/MENTAL_MODEL.md +1 -1
- package/src/platforms/lynx/TROUBLESHOOTING.md +3 -3
- package/src/platforms/msx/MENTAL_MODEL.md +5 -5
- package/src/platforms/msx/TROUBLESHOOTING.md +2 -2
- package/src/platforms/msx/lib/c/msx_hw.h +1 -0
- package/src/platforms/msx/lib/c/msx_vdp.c +25 -0
- package/src/platforms/nes/MENTAL_MODEL.md +2 -2
- package/src/platforms/nes/lib/c/nes_runtime.c +149 -34
- package/src/platforms/nes/lib/c/nes_runtime.h +34 -1
- package/src/platforms/pce/MENTAL_MODEL.md +5 -5
- package/src/platforms/pce/TROUBLESHOOTING.md +1 -1
- package/src/platforms/pce/lib/c/pce_hw.h +11 -0
- package/src/platforms/pce/lib/c/pce_video.c +32 -0
- package/src/platforms/sms/MENTAL_MODEL.md +6 -6
- package/src/platforms/snes/MENTAL_MODEL.md +2 -2
- package/src/platforms/snes/TROUBLESHOOTING.md +40 -1
- package/src/toolchains/cc65/presets/nes/chr-ram-runtime.cfg +13 -8
- package/src/toolchains/cc65/presets/nes/chr-ram-runtime.crt0.s +58 -5
- package/src/toolchains/cc65/presets/nes/chr-rom.crt0.s +52 -3
- package/src/toolchains/cc65/presets/pce/rom32k.cfg +52 -0
- package/src/toolchains/index.js +27 -11
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# GB / GBC C runtime + headers
|
|
2
2
|
|
|
3
3
|
These are the source files that back the GB/GBC C templates. They're
|
|
4
|
-
**not** auto-injected at build time — `
|
|
5
|
-
|
|
4
|
+
**not** auto-injected at build time — `examples({op:'fork', example:"gb/<name>" or
|
|
5
|
+
"gbc/<name>", name, path})` copies them into your project directory so the
|
|
6
6
|
project is self-describing. Build calls then point at your project's
|
|
7
7
|
copy of these files via `sourcesPaths` / `includePaths` / `crt0Path`.
|
|
8
8
|
|
|
@@ -32,7 +32,7 @@ didn't produce, or to override a field:
|
|
|
32
32
|
Fixes up / overrides the header of an existing ROM on disk (title, cart
|
|
33
33
|
type, ROM/RAM size, CGB flag, etc.).
|
|
34
34
|
- `node patch-header.js out.gb` — standalone Node script, copied into
|
|
35
|
-
every GB project by `
|
|
35
|
+
every GB project by `examples({op:'fork'})`. Same logic, no MCP needed.
|
|
36
36
|
- `rgbfix -v -p 0 out.gb` — what the build pipeline runs under the hood;
|
|
37
37
|
RGBDS asm projects can invoke it directly.
|
|
38
38
|
|
|
@@ -46,17 +46,16 @@ didn't produce, or to override a field:
|
|
|
46
46
|
hardware, OAM DMA timing, joypad layout. Read this before your first
|
|
47
47
|
GB/GBC project.
|
|
48
48
|
|
|
49
|
-
##
|
|
49
|
+
## Forking an example
|
|
50
50
|
|
|
51
|
-
Bootstrap a working game-loop skeleton with `
|
|
51
|
+
Bootstrap a working game-loop skeleton by forking an example with `examples({op:'fork'})`:
|
|
52
52
|
|
|
53
53
|
```js
|
|
54
|
-
|
|
55
|
-
op:
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
path: "/abs/path",
|
|
54
|
+
examples({
|
|
55
|
+
op: 'fork',
|
|
56
|
+
example: "gbc/tile_engine", // or "gbc/hello_sprite", or "gbc/default"
|
|
57
|
+
name: "mygame",
|
|
58
|
+
path: "/abs/path",
|
|
60
59
|
})
|
|
61
60
|
```
|
|
62
61
|
|
|
@@ -86,11 +86,34 @@
|
|
|
86
86
|
nop
|
|
87
87
|
jp init
|
|
88
88
|
|
|
89
|
-
;; ─── Header bytes at $0104-$014F — host pipeline fills these
|
|
89
|
+
;; ─── Header bytes at $0104-$014F — host pipeline fills most of these ──
|
|
90
90
|
.area _HEADERe (ABS)
|
|
91
91
|
.org 0x0104
|
|
92
|
-
;;
|
|
93
|
-
|
|
92
|
+
;; Nintendo logo ($0104-$0133, 48 bytes) + title/manufacturer/CGB
|
|
93
|
+
;; flag ($0134-$0143) + licensee/SGB ($0144-$0146): left as a gap —
|
|
94
|
+
;; the post-link header fix (bundled rgbfix, or patch-header.js when
|
|
95
|
+
;; rebuilding outside romdev) writes the canonical logo, the CGB
|
|
96
|
+
;; flag, and both checksums.
|
|
97
|
+
.ds 0x43
|
|
98
|
+
;; Cartridge TYPE + RAM size ($0147-$0149) are emitted EXPLICITLY so
|
|
99
|
+
;; the post-link fix can preserve them (it reads these bytes and
|
|
100
|
+
;; passes them through; unknown/garbage values fall back to ROM-only).
|
|
101
|
+
;; This is the GB equivalent of the NES crt0's iNES BATTERY bit:
|
|
102
|
+
;; declaring the cart in the boot file makes battery saves part of
|
|
103
|
+
;; the project source, not a build flag someone has to remember.
|
|
104
|
+
;;
|
|
105
|
+
;; $0147 = $03 MBC1 + RAM + BATTERY → the core exposes the 8KB
|
|
106
|
+
;; at $A000-$BFFF as persistent SAVE_RAM (.srm).
|
|
107
|
+
;; Writes only stick after the $0A enable sequence —
|
|
108
|
+
;; see the SRAM HARDWARE IDIOM in the puzzle example.
|
|
109
|
+
;; $0148 = $00 32 KB ROM (2 banks — no banking needed)
|
|
110
|
+
;; $0149 = $02 8 KB cart RAM (one bank at $A000)
|
|
111
|
+
.org 0x0147
|
|
112
|
+
.db 0x03 ; cart type: MBC1+RAM+BATTERY
|
|
113
|
+
.db 0x00 ; ROM size: 32 KB
|
|
114
|
+
.db 0x02 ; RAM size: 8 KB
|
|
115
|
+
;; $014A-$014F (destination/licensee/version/checksums): host fills.
|
|
116
|
+
.ds 0x06
|
|
94
117
|
|
|
95
118
|
;; ─── init: real boot code, lives in _CODE starting at $0150 ────────
|
|
96
119
|
.area _CODE
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// runs a bundled rgbfix after every gb/gbc link — see the
|
|
14
14
|
// "rgbfix (auto header fix)" line in build logs), so you only need this
|
|
15
15
|
// script when rebuilding the project OUTSIDE romdev with stock SDCC and
|
|
16
|
-
// no RGBDS installed. It's what keeps the
|
|
16
|
+
// no RGBDS installed. It's what keeps the forked project self-contained.
|
|
17
17
|
//
|
|
18
18
|
// The bundled gb_crt0.s reserves $0100-$014F for the header window,
|
|
19
19
|
// so the bytes patched in here land on actual cartridge-header
|
|
@@ -151,7 +151,17 @@ if (isCli) {
|
|
|
151
151
|
const rom = new Uint8Array(readFileSync(inPath));
|
|
152
152
|
// Auto-detect CGB based on file extension.
|
|
153
153
|
const cgb = /\.gbc$/i.test(inPath) || /\.gbc$/i.test(outPath);
|
|
154
|
-
|
|
154
|
+
// Battery-cart passthrough: the bundled gb_crt0.s emits the cart-type and
|
|
155
|
+
// RAM-size bytes EXPLICITLY ($0147=$03 MBC1+RAM+BATTERY, $0149=$02 8KB) so
|
|
156
|
+
// battery hi-score saves work. Preserve a known battery-MBC declaration
|
|
157
|
+
// instead of stomping it back to ROM-only; unknown/garbage bytes (a crt0
|
|
158
|
+
// that left the header window as a .ds gap) still get the safe defaults.
|
|
159
|
+
const BATTERY_TYPES = new Set([0x03, 0x06, 0x0F, 0x10, 0x13, 0x1B, 0x1E]);
|
|
160
|
+
const declType = rom.length > 0x149 ? rom[0x147] : 0x00;
|
|
161
|
+
const declRam = rom.length > 0x149 ? rom[0x149] : 0x00;
|
|
162
|
+
const cartType = BATTERY_TYPES.has(declType) ? declType : 0x00;
|
|
163
|
+
const ramSize = cartType !== 0x00 && declRam >= 0x01 && declRam <= 0x05 ? declRam : 0x00;
|
|
164
|
+
patchGbHeader(rom, { cgb, cartType, ramSize });
|
|
155
165
|
writeFileSync(outPath, rom);
|
|
156
|
-
console.log(`patched ${inPath}${outPath !== inPath ? ` → ${outPath}` : ""} (${rom.length} bytes${cgb ? ", CGB" : ""})`);
|
|
166
|
+
console.log(`patched ${inPath}${outPath !== inPath ? ` → ${outPath}` : ""} (${rom.length} bytes${cgb ? ", CGB" : ""}${cartType ? `, cart $${cartType.toString(16)}+RAM` : ""})`);
|
|
157
167
|
}
|
|
@@ -178,7 +178,7 @@ software mistake, not a hardware limit:
|
|
|
178
178
|
> burst or a per-frame DMA), you overrun vblank, drop frames, and the
|
|
179
179
|
> scroll judders. **Paint the planes ONCE at setup; the loop only nudges
|
|
180
180
|
> scroll registers and re-stages sprites.** Use the
|
|
181
|
-
> `
|
|
181
|
+
> `two_plane_parallax` example as the known-good shape.
|
|
182
182
|
|
|
183
183
|
### Hardware scroll, the whole loop
|
|
184
184
|
|
|
@@ -241,7 +241,7 @@ if (newTileCol != lastTileCol) {
|
|
|
241
241
|
```
|
|
242
242
|
|
|
243
243
|
That's ~28 tile writes per 8 px of travel, not a 1792-cell plane redraw.
|
|
244
|
-
The `
|
|
244
|
+
The `platformer` example scrolls within one plane (no
|
|
245
245
|
streaming); add the column-stream above to go wider. (Real Sonic also
|
|
246
246
|
splits the screen with H-blank raster effects for independent strips —
|
|
247
247
|
that's an IRQ/raster topic, see the `asm` template.)
|
|
@@ -481,7 +481,7 @@ build pipeline computes the checksum on link.
|
|
|
481
481
|
|
|
482
482
|
## Where the SDK lives (and how to read it)
|
|
483
483
|
|
|
484
|
-
`
|
|
484
|
+
`examples({op:'fork'})` (any Genesis example) ships the full SGDK include
|
|
485
485
|
tree into the new project at `vendor/sgdk/`. So when your code does
|
|
486
486
|
`#include <genesis.h>`, those headers come from
|
|
487
487
|
`vendor/sgdk/include/`:
|
|
@@ -239,8 +239,8 @@ Genesis scrolls in HARDWARE — moving the world is two register writes
|
|
|
239
239
|
plane every frame (a `VDP_fillTileMapRect` / `VDP_loadTileMap` / big
|
|
240
240
|
`DMA_*` each frame), you overrun the vblank DMA budget and drop frames →
|
|
241
241
|
judder. Fix: paint the planes ONCE at setup; the loop only nudges scroll
|
|
242
|
-
registers + re-stages sprites. The `
|
|
243
|
-
|
|
242
|
+
registers + re-stages sprites. The `two_plane_parallax`
|
|
243
|
+
example is the known-good shape.
|
|
244
244
|
|
|
245
245
|
Diagnose it without guessing (no core rebuild):
|
|
246
246
|
|
|
@@ -14,7 +14,7 @@ $E000-$FFFB Work RAM mirror
|
|
|
14
14
|
$FFFC-$FFFF Mapper control registers
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
Most 32 KB
|
|
17
|
+
Most 32 KB example games fit in banks 0+1 and never touch the mapper.
|
|
18
18
|
|
|
19
19
|
## VDP (display)
|
|
20
20
|
|
|
@@ -36,7 +36,7 @@ GG VDP = SMS VDP in Mode 4, smaller visible viewport.
|
|
|
36
36
|
OR draw the text via the BG name table (no per-line limit).
|
|
37
37
|
|
|
38
38
|
**Always render gameplay content inside (48, 24)..(207, 167)** so it's
|
|
39
|
-
visible on real hardware. The bundled
|
|
39
|
+
visible on real hardware. The bundled example games work without this
|
|
40
40
|
because gpgx shows the full framebuffer.
|
|
41
41
|
|
|
42
42
|
### Sprite coords are hardware-space, NOT visible-space
|
|
@@ -78,9 +78,9 @@ on its own anymore.
|
|
|
78
78
|
`gg_vdp_init()` sets R6 = 0xFF. R6 bit 2 is the SA13 select for
|
|
79
79
|
sprite tile data — bit 2 is **SET** in 0xFF, so sprite tiles read
|
|
80
80
|
from `$2000-$3FFF`, in their **own bank** separate from BG tiles at
|
|
81
|
-
$0000. This is the baseline because every bundled
|
|
81
|
+
$0000. This is the baseline because every bundled example uploads
|
|
82
82
|
its sprite tiles to `$2000` (`gg_load_tiles(0x2000, …)`) — the
|
|
83
|
-
default and the
|
|
83
|
+
default and the examples match, so sprites Just Show Up.
|
|
84
84
|
|
|
85
85
|
Watch the bit: 0xFB has SA13 **CLEAR** = sprite tiles at $0000
|
|
86
86
|
(sharing the BG bank). If you ever set R6=0xFB you MUST also upload
|
|
@@ -16,7 +16,7 @@ Anything you draw outside `(48, 24)..(207, 167)` is in the border —
|
|
|
16
16
|
visible in headless emulator screenshots (gpgx shows the whole frame)
|
|
17
17
|
but invisible on real hardware.
|
|
18
18
|
|
|
19
|
-
The bundled
|
|
19
|
+
The bundled example games are direct ports of the SMS examples and target
|
|
20
20
|
the full 256×192 area. Works fine for development under gpgx; for
|
|
21
21
|
shipping to a real GG, reposition sprite + tilemap content to the
|
|
22
22
|
visible center.
|
|
@@ -28,7 +28,7 @@ active low), separate from the D-pad/A/B which are on `$DC` like
|
|
|
28
28
|
SMS. `gg_joypad_read()` already merges them — START shows up in bit 7
|
|
29
29
|
of the returned byte (`JOY_START` mask).
|
|
30
30
|
|
|
31
|
-
If you copied an SMS
|
|
31
|
+
If you copied an SMS example that uses PAUSE-as-START semantics
|
|
32
32
|
(SMS pause button is at port $DD bit 4 IIRC), it won't work on GG;
|
|
33
33
|
swap to JOY_START.
|
|
34
34
|
|
|
@@ -85,7 +85,7 @@ two-byte little-endian CRAM entry.
|
|
|
85
85
|
|
|
86
86
|
## "Linking error: undefined reference to sms_joypad_read_p2"
|
|
87
87
|
|
|
88
|
-
GG only has one controller. The SMS
|
|
88
|
+
GG only has one controller. The SMS examples use `sms_joypad_read_p2`
|
|
89
89
|
for the two-controller patterns (Pong, 2P shmup). When porting, drop
|
|
90
90
|
the P2 read + force `p2 = 0` so the AI fallback always engages.
|
|
91
91
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
GG shares its toolchain (SDCC z80) + emulator (genesis_plus_gx) +
|
|
4
4
|
most of the runtime with SMS (`sms_crt0.s` ≡ `gg_crt0.s` byte-for-
|
|
5
5
|
byte; PSG protocol identical). The GG tree at `src/platforms/gg/`
|
|
6
|
-
holds the GG-specific
|
|
6
|
+
holds the GG-specific example games and runtime helpers; SMS docs apply
|
|
7
7
|
for everything else.
|
|
8
8
|
|
|
9
9
|
GG-specific:
|
|
@@ -20,3 +20,32 @@ uint8_t gg_joypad_read(void) {
|
|
|
20
20
|
uint8_t start = ~PORT_GG_INPUT; /* GG-specific port bit 7 = START */
|
|
21
21
|
return (uint8_t)((a & 0x3F) | (start & 0x80));
|
|
22
22
|
}
|
|
23
|
+
|
|
24
|
+
/*
|
|
25
|
+
* Player 2 read — for ALTERNATING-TURNS or 2-controller play.
|
|
26
|
+
*
|
|
27
|
+
* HONEST NOTE: a real Game Gear has only ONE controller port on the unit; its
|
|
28
|
+
* 2P story is the Gear-to-Gear LINK CABLE (a second console). But the GG VDP
|
|
29
|
+
* and I/O chip are the SMS's, and gpgx wires the SMS's full split-across-
|
|
30
|
+
* $DC/$DD second-controller layout for GG too — so a SECOND PAD does drive
|
|
31
|
+
* port B in the emulator (and on an SMS-pad adapter), which is exactly what an
|
|
32
|
+
* alternating-turns 2P platformer needs (the two players never play at once).
|
|
33
|
+
*
|
|
34
|
+
* The hardware layout is the SMS's awkward split:
|
|
35
|
+
* PORT_JOY_A bits 6-7 = P2 UP, P2 DOWN
|
|
36
|
+
* PORT_JOY_B bits 0-3 = P2 LEFT, P2 RIGHT, P2 B1, P2 B2
|
|
37
|
+
* Reassembled into the same bit layout P1 uses:
|
|
38
|
+
* bit 0 = UP, 1 = DOWN, 2 = LEFT, 3 = RIGHT, 4 = B1, 5 = B2.
|
|
39
|
+
* Returns 0 when no P2 pad is present (all bits high = released after invert).
|
|
40
|
+
*/
|
|
41
|
+
uint8_t gg_joypad_read_p2(void) {
|
|
42
|
+
uint8_t a = ~PORT_JOY_A; /* P2 UP in bit 6, DOWN in bit 7 */
|
|
43
|
+
uint8_t b = ~PORT_JOY_B; /* P2 LEFT bit 0, RIGHT 1, B1 2, B2 3 */
|
|
44
|
+
uint8_t up = (a >> 6) & 0x01; /* bit 6 -> bit 0 */
|
|
45
|
+
uint8_t down = (a >> 6) & 0x02; /* bit 7 -> bit 1 */
|
|
46
|
+
uint8_t left = (b << 2) & 0x04; /* bit 0 -> bit 2 */
|
|
47
|
+
uint8_t right = (b << 2) & 0x08; /* bit 1 -> bit 3 */
|
|
48
|
+
uint8_t b1 = (b << 2) & 0x10; /* bit 2 -> bit 4 */
|
|
49
|
+
uint8_t b2 = (b << 2) & 0x20; /* bit 3 -> bit 5 */
|
|
50
|
+
return (uint8_t)(up | down | left | right | b1 | b2);
|
|
51
|
+
}
|
|
@@ -96,7 +96,7 @@ void main(void) {
|
|
|
96
96
|
`tgi_updatedisplay()` is the frame heartbeat — it ping-pongs the
|
|
97
97
|
double-buffered display and waits for vblank.
|
|
98
98
|
|
|
99
|
-
## Drawing many rectangles in one frame (game
|
|
99
|
+
## Drawing many rectangles in one frame (example-game pattern)
|
|
100
100
|
|
|
101
101
|
The minimal example above draws "one rect per frame." For an actual
|
|
102
102
|
game with HUD + background + sprites you'll do many tgi_bar / tgi_setcolor
|
|
@@ -44,7 +44,7 @@ Two things that trip agents up:
|
|
|
44
44
|
Skipping it is the #1 "Lynx is blank" trap.
|
|
45
45
|
- **Don't rely on `tgi_clear()`** to blank the screen in this
|
|
46
46
|
toolchain/emulator path — use a full-screen `tgi_bar(0,0,maxx,maxy)`
|
|
47
|
-
in the background colour instead. The bundled `shmup`
|
|
47
|
+
in the background colour instead. The bundled `shmup` example uses
|
|
48
48
|
this exact loop; copy it.
|
|
49
49
|
|
|
50
50
|
## "tgi_outtextxy renders nothing"
|
|
@@ -54,7 +54,7 @@ cc65's default TGI on Lynx ships without a font. Either:
|
|
|
54
54
|
live in `$cc65_share/target/lynx/fonts/`.
|
|
55
55
|
2. Draw your own glyphs with `tgi_bar`/`tgi_line`.
|
|
56
56
|
|
|
57
|
-
The bundled
|
|
57
|
+
The bundled example games work around this by using simple rectangles
|
|
58
58
|
for game content + only short text strings. For game UI text, embed
|
|
59
59
|
a bitmap font directly in your code.
|
|
60
60
|
|
|
@@ -93,7 +93,7 @@ cc65 is C89. No mixed declarations + code, no inline `for (uint8_t i
|
|
|
93
93
|
= 0; ...)`, no compound literals, no // comments in some configs.
|
|
94
94
|
Declare all variables at the top of each block.
|
|
95
95
|
|
|
96
|
-
The bundled Lynx
|
|
96
|
+
The bundled Lynx example games are C89-clean — copy that pattern.
|
|
97
97
|
|
|
98
98
|
## "Compile fails: no rule to make target lynx-bll.cfg"
|
|
99
99
|
|
|
@@ -12,13 +12,13 @@ romdev ships a **hardware helper library** (`src/platforms/msx/lib/c/`:
|
|
|
12
12
|
`msx_psg_tone()` in plain C. It uses DIRECT Z80 I/O ports (the reliable path —
|
|
13
13
|
NOT fragile inline-asm BIOS wrappers).
|
|
14
14
|
|
|
15
|
-
The fastest way to a working game:
|
|
16
|
-
"shmup"})`** — or any
|
|
17
|
-
genre set. For a smaller
|
|
18
|
-
|
|
15
|
+
The fastest way to a working game: **fork the example game whose core loop is
|
|
16
|
+
nearest yours — `examples({op:'fork', example:"msx/shmup", name, path})`** — or any
|
|
17
|
+
of `platformer` / `puzzle` / `sports` / `racing`, the full genre set. For a smaller
|
|
18
|
+
starting point fork `msx/sprite_move` (also `music_sfx`, `catch_game`). Either drops
|
|
19
19
|
a complete, *building* project — a verified playable example + the helper lib +
|
|
20
20
|
the cart crt0 + docs. Read the example's `main.c`, then change it. Examples live in
|
|
21
|
-
`examples/msx/`. The `platformer`
|
|
21
|
+
`examples/msx/`. The `platformer` example column-streams the SCREEN 2 name table
|
|
22
22
|
for a tile-by-tile side-scroll. **Gotcha:** read joystick **port 1**
|
|
23
23
|
(`msx_read_joystick(1)`) — port 0 is the keyboard, which an emulator's gamepad
|
|
24
24
|
doesn't drive.
|
|
@@ -85,5 +85,5 @@ ticker) already do this; copy the pattern for any direct PSG access you write.
|
|
|
85
85
|
The old `msx_crt0.s` placed the SDCC `_INITIALIZER` area in RAM, so the boot
|
|
86
86
|
copy duplicated uninitialised RAM onto itself: every value-initialised static
|
|
87
87
|
read 0 and BSS was never zeroed. The bundled crt0 has been fixed (ROM-placed
|
|
88
|
-
`_INITIALIZER` + a BSS-zero loop). If a project
|
|
89
|
-
shows ghost zeros, refresh its `msx_crt0.s` from a
|
|
88
|
+
`_INITIALIZER` + a BSS-zero loop). If a project forked before 2026-06-09
|
|
89
|
+
shows ghost zeros, refresh its `msx_crt0.s` from a freshly forked example.
|
|
@@ -87,6 +87,7 @@ void msx_clear_sprites(void);
|
|
|
87
87
|
void msx_vblank_wait(void);
|
|
88
88
|
uint8_t msx_read_joystick(uint8_t stick);
|
|
89
89
|
void msx_psg_tone(uint8_t chan, uint16_t period, uint8_t vol);
|
|
90
|
+
void msx_psg_noise(uint8_t chan, uint8_t rate, uint8_t vol); /* vol 0 = off */
|
|
90
91
|
void msx_music(uint8_t on); /* background melody on channel C — ON by default; 0 = off */
|
|
91
92
|
void msx_music_tick(void); /* call once per frame (scaffolds do) */
|
|
92
93
|
void msx_psg_off(uint8_t chan);
|
|
@@ -146,6 +146,31 @@ void msx_psg_tone(uint8_t chan, uint16_t period, uint8_t vol) {
|
|
|
146
146
|
__asm__("ei");
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
/* Play NOISE on PSG channel 0/1/2: the AY's one shared noise generator
|
|
150
|
+
* (reg 6, 5-bit period — bigger = lower rumble) routed into this channel by
|
|
151
|
+
* clearing its noise-disable mixer bit (reg 7 bits 3-5) while setting its
|
|
152
|
+
* tone-disable bit. The classic explosion/impact voice.
|
|
153
|
+
* msx_psg_noise(chan, rate, 0) silences the channel and re-masks its noise
|
|
154
|
+
* bit (msx_psg_off only re-masks TONE — it doesn't know about noise). */
|
|
155
|
+
void msx_psg_noise(uint8_t chan, uint8_t rate, uint8_t vol) {
|
|
156
|
+
uint8_t mixer;
|
|
157
|
+
__asm__("di"); /* same KEYINT race as above */
|
|
158
|
+
if (vol) {
|
|
159
|
+
PSGADDR = 6; /* noise period (shared) */
|
|
160
|
+
PSGWRITE = (uint8_t)(rate & 0x1F);
|
|
161
|
+
}
|
|
162
|
+
PSGADDR = (uint8_t)(8 + chan);
|
|
163
|
+
PSGWRITE = (uint8_t)(vol & 0x0F);
|
|
164
|
+
PSGADDR = 7;
|
|
165
|
+
mixer = PSGREAD;
|
|
166
|
+
mixer |= (uint8_t)(1 << chan); /* tone OFF for this channel */
|
|
167
|
+
if (vol) mixer &= (uint8_t)~(1 << (3 + chan)); /* noise ON */
|
|
168
|
+
else mixer |= (uint8_t)(1 << (3 + chan)); /* noise OFF */
|
|
169
|
+
PSGADDR = 7;
|
|
170
|
+
PSGWRITE = mixer;
|
|
171
|
+
__asm__("ei");
|
|
172
|
+
}
|
|
173
|
+
|
|
149
174
|
/* Silence a PSG channel: zero its volume and re-disable its tone bit. */
|
|
150
175
|
void msx_psg_off(uint8_t chan) {
|
|
151
176
|
uint8_t mixer;
|
|
@@ -207,9 +207,9 @@ names also resolve (east→A, west→B). So `input({op:'set', a: true})` presses
|
|
|
207
207
|
expected — unlike the genesis_plus_gx platforms (Genesis/SMS/GG), there's no
|
|
208
208
|
surprise here.
|
|
209
209
|
|
|
210
|
-
## What `
|
|
210
|
+
## What `examples({op:'fork'})` copies into your project
|
|
211
211
|
|
|
212
|
-
`
|
|
212
|
+
`examples({op:'fork', example:"nes/hello_sprite"|"nes/tile_engine"|"nes/default", name, path})`
|
|
213
213
|
writes these files into your project directory. **They're yours** — every
|
|
214
214
|
byte that compiles is in the repo. Edit, fork, replace; nothing is auto-injected
|
|
215
215
|
at build time.
|
|
@@ -59,44 +59,60 @@ static uint8_t oam_index = 0;
|
|
|
59
59
|
static void oam_hide_unused(void); /* fwd decl — used by ppu_wait_nmi (NES-1) */
|
|
60
60
|
|
|
61
61
|
/* ── VRAM write queue ─────────────────────────────────────────────
|
|
62
|
-
*
|
|
63
|
-
* PPUADDR(hi); PPUADDR(lo); PPUDATA(byte)
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
*
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
62
|
+
* A ring buffer of { hi, lo, byte } entries. The NMI drains it with
|
|
63
|
+
* PPUADDR(hi); PPUADDR(lo); PPUDATA(byte) per entry — but only up to
|
|
64
|
+
* FLUSH_BUDGET entries per vblank (see the idiom note below). Game code
|
|
65
|
+
* that outruns the drain just blocks in vram_queue_push until a slot
|
|
66
|
+
* frees up; a big batch appears over 2-3 frames, invisible to a human.
|
|
67
|
+
*
|
|
68
|
+
* ── HARDWARE IDIOM (load-bearing) — the VBLANK BUDGET ──
|
|
69
|
+
* Vblank is ~2273 CPU cycles and OAM DMA already spends 513 of them.
|
|
70
|
+
* A flush that keeps writing past the end of vblank writes PPUDATA
|
|
71
|
+
* while RENDERING IS ACTIVE — the PPU's internal address register is
|
|
72
|
+
* busy fetching tiles, so those writes land at corrupted addresses.
|
|
73
|
+
* Symptom: a long batch of queued tiles where MOST land correctly but
|
|
74
|
+
* the tail is shifted or missing, identically every run. The budget
|
|
75
|
+
* caps the per-vblank drain so the flush always finishes inside vblank.
|
|
76
|
+
* The drain itself lives in the crt0's NMI handler IN ASSEMBLY (~40
|
|
77
|
+
* cycles/entry); compiled C spends 200+ cycles per entry, which blows
|
|
78
|
+
* the budget even for small batches — measured, not theoretical.
|
|
79
|
+
*
|
|
80
|
+
* ── HARDWARE IDIOM (load-bearing) — the NMI/main-thread race ──
|
|
81
|
+
* The NMI fires asynchronously; if it drained the queue WHILE
|
|
82
|
+
* vram_queue_push was mid-update, the in-flight entry would be lost
|
|
83
|
+
* and a stale slot replayed. The lock byte makes the flush skip any
|
|
84
|
+
* vblank that catches a push in progress (the queue drains a frame
|
|
85
|
+
* later). Symptom without it: HUD text with characters missing or
|
|
86
|
+
* shifted, coming and going with timing. */
|
|
87
|
+
#define QUEUE_MAX 32 /* power of two — indices wrap via & */
|
|
88
|
+
#define QUEUE_MASK (QUEUE_MAX - 1)
|
|
89
|
+
#define FLUSH_BUDGET 16 /* keep in sync with the crt0 asm */
|
|
90
|
+
/* NOT static — the crt0's NMI drains the ring in assembly (see the
|
|
91
|
+
* vblank-budget idiom above; symbol names are part of the crt0 contract). */
|
|
92
|
+
uint8_t vram_q_hi[QUEUE_MAX];
|
|
93
|
+
uint8_t vram_q_lo[QUEUE_MAX];
|
|
94
|
+
uint8_t vram_q_val[QUEUE_MAX];
|
|
95
|
+
uint8_t vram_queue_head = 0;
|
|
96
|
+
volatile uint8_t vram_queue_len = 0;
|
|
97
|
+
volatile uint8_t vram_queue_lock = 0;
|
|
98
|
+
|
|
99
|
+
/* Queue one byte. If full, wait for the NMI to drain a slot (lock
|
|
100
|
+
* RELEASED while waiting — holding it would deadlock), then enqueue
|
|
101
|
+
* under the lock. */
|
|
92
102
|
static void vram_queue_push(uint16_t ppu_addr, uint8_t v) {
|
|
93
|
-
|
|
103
|
+
uint8_t slot;
|
|
104
|
+
for (;;) {
|
|
105
|
+
vram_queue_lock = 1;
|
|
106
|
+
if (vram_queue_len < QUEUE_MAX) break;
|
|
107
|
+
vram_queue_lock = 0;
|
|
94
108
|
ppu_wait_nmi();
|
|
95
109
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
110
|
+
slot = (uint8_t)((vram_queue_head + vram_queue_len) & QUEUE_MASK);
|
|
111
|
+
vram_q_hi[slot] = (uint8_t)(ppu_addr >> 8);
|
|
112
|
+
vram_q_lo[slot] = (uint8_t)(ppu_addr & 0xFF);
|
|
113
|
+
vram_q_val[slot] = v;
|
|
99
114
|
++vram_queue_len;
|
|
115
|
+
vram_queue_lock = 0;
|
|
100
116
|
}
|
|
101
117
|
|
|
102
118
|
/* ── PPU control ──────────────────────────────────────────────── */
|
|
@@ -408,3 +424,102 @@ void sound_play_noise(uint8_t period_4bit, uint8_t vol_4bit, uint8_t length_fram
|
|
|
408
424
|
void sound_off(void) {
|
|
409
425
|
APUSTATUS = 0x00;
|
|
410
426
|
}
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
/* ════════════════════════════════════════════════════════════════════
|
|
430
|
+
* Text + font (0.29.0 examples contract)
|
|
431
|
+
* ════════════════════════════════════════════════════════════════════ */
|
|
432
|
+
|
|
433
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
434
|
+
* Font glyphs are 1bpp (plane 0 only → colour index 1 of the BG palette).
|
|
435
|
+
* They upload into the BACKGROUND pattern table at $1400+ — tile ids $40+ —
|
|
436
|
+
* NOT the sprite table at $0000 (the runtime maps BG to $1000, sprites to
|
|
437
|
+
* $0000 via PPUCTRL). Requires: PPU rendering OFF during font_upload (raw
|
|
438
|
+
* $2007 writes), 37*16 = 592 bytes of CHR-RAM free at $1400-$164F. */
|
|
439
|
+
static const uint8_t font8[37][8] = {
|
|
440
|
+
/* 0-9 */
|
|
441
|
+
{0x3C,0x66,0x6E,0x76,0x66,0x66,0x3C,0x00}, {0x18,0x38,0x18,0x18,0x18,0x18,0x7E,0x00},
|
|
442
|
+
{0x3C,0x66,0x06,0x0C,0x18,0x30,0x7E,0x00}, {0x3C,0x66,0x06,0x1C,0x06,0x66,0x3C,0x00},
|
|
443
|
+
{0x0C,0x1C,0x3C,0x6C,0x7E,0x0C,0x0C,0x00}, {0x7E,0x60,0x7C,0x06,0x06,0x66,0x3C,0x00},
|
|
444
|
+
{0x1C,0x30,0x60,0x7C,0x66,0x66,0x3C,0x00}, {0x7E,0x06,0x0C,0x18,0x30,0x30,0x30,0x00},
|
|
445
|
+
{0x3C,0x66,0x66,0x3C,0x66,0x66,0x3C,0x00}, {0x3C,0x66,0x66,0x3E,0x06,0x0C,0x38,0x00},
|
|
446
|
+
/* A-Z */
|
|
447
|
+
{0x18,0x3C,0x66,0x66,0x7E,0x66,0x66,0x00}, {0x7C,0x66,0x66,0x7C,0x66,0x66,0x7C,0x00},
|
|
448
|
+
{0x3C,0x66,0x60,0x60,0x60,0x66,0x3C,0x00}, {0x78,0x6C,0x66,0x66,0x66,0x6C,0x78,0x00},
|
|
449
|
+
{0x7E,0x60,0x60,0x7C,0x60,0x60,0x7E,0x00}, {0x7E,0x60,0x60,0x7C,0x60,0x60,0x60,0x00},
|
|
450
|
+
{0x3C,0x66,0x60,0x6E,0x66,0x66,0x3E,0x00}, {0x66,0x66,0x66,0x7E,0x66,0x66,0x66,0x00},
|
|
451
|
+
{0x7E,0x18,0x18,0x18,0x18,0x18,0x7E,0x00}, {0x06,0x06,0x06,0x06,0x66,0x66,0x3C,0x00},
|
|
452
|
+
{0x66,0x6C,0x78,0x70,0x78,0x6C,0x66,0x00}, {0x60,0x60,0x60,0x60,0x60,0x60,0x7E,0x00},
|
|
453
|
+
{0x63,0x77,0x7F,0x6B,0x63,0x63,0x63,0x00}, {0x66,0x76,0x7E,0x7E,0x6E,0x66,0x66,0x00},
|
|
454
|
+
{0x3C,0x66,0x66,0x66,0x66,0x66,0x3C,0x00}, {0x7C,0x66,0x66,0x7C,0x60,0x60,0x60,0x00},
|
|
455
|
+
{0x3C,0x66,0x66,0x66,0x6A,0x6C,0x36,0x00}, {0x7C,0x66,0x66,0x7C,0x6C,0x66,0x66,0x00},
|
|
456
|
+
{0x3C,0x66,0x60,0x3C,0x06,0x66,0x3C,0x00}, {0x7E,0x18,0x18,0x18,0x18,0x18,0x18,0x00},
|
|
457
|
+
{0x66,0x66,0x66,0x66,0x66,0x66,0x3C,0x00}, {0x66,0x66,0x66,0x66,0x66,0x3C,0x18,0x00},
|
|
458
|
+
{0x63,0x63,0x63,0x6B,0x7F,0x77,0x63,0x00}, {0x66,0x66,0x3C,0x18,0x3C,0x66,0x66,0x00},
|
|
459
|
+
{0x66,0x66,0x66,0x3C,0x18,0x18,0x18,0x00}, {0x7E,0x06,0x0C,0x18,0x30,0x60,0x7E,0x00},
|
|
460
|
+
/* '-' */
|
|
461
|
+
{0x00,0x00,0x00,0x7E,0x00,0x00,0x00,0x00},
|
|
462
|
+
};
|
|
463
|
+
#define FONT_BASE_TILE 0x40
|
|
464
|
+
|
|
465
|
+
void font_upload(void) {
|
|
466
|
+
uint8_t g, r;
|
|
467
|
+
uint8_t tile[16];
|
|
468
|
+
for (r = 8; r < 16; r++) tile[r] = 0; /* plane 1 = 0 (colour 1) */
|
|
469
|
+
for (g = 0; g < 37; g++) {
|
|
470
|
+
for (r = 0; r < 8; r++) tile[r] = font8[g][r];
|
|
471
|
+
chr_ram_upload((uint16_t)(0x1000 + ((FONT_BASE_TILE + g) << 4)), tile, 16);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/* char → BG tile id (space → tile 0 = blank). */
|
|
476
|
+
static uint8_t font_tile(char ch) {
|
|
477
|
+
if (ch >= '0' && ch <= '9') return (uint8_t)(FONT_BASE_TILE + (ch - '0'));
|
|
478
|
+
if (ch >= 'A' && ch <= 'Z') return (uint8_t)(FONT_BASE_TILE + 10 + (ch - 'A'));
|
|
479
|
+
if (ch >= 'a' && ch <= 'z') return (uint8_t)(FONT_BASE_TILE + 10 + (ch - 'a'));
|
|
480
|
+
if (ch == '-') return (uint8_t)(FONT_BASE_TILE + 36);
|
|
481
|
+
return 0;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
void text_draw_unsafe(uint16_t ppu_addr, const char *s) {
|
|
485
|
+
while (*s) vram_unsafe_set(ppu_addr++, font_tile(*s++));
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
void text_draw(uint8_t nt, uint8_t x, uint8_t y, const char *s) {
|
|
489
|
+
while (*s) tile_set(nt, x++, y, font_tile(*s++));
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
void text_draw_u16(uint8_t nt, uint8_t x, uint8_t y, uint16_t v) {
|
|
493
|
+
uint8_t d[5];
|
|
494
|
+
uint8_t i;
|
|
495
|
+
for (i = 0; i < 5; i++) { d[i] = v % 10; v /= 10; }
|
|
496
|
+
for (i = 0; i < 5; i++) tile_set(nt, (uint8_t)(x + i), y, (uint8_t)(FONT_BASE_TILE + d[4 - i]));
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/* ════════════════════════════════════════════════════════════════════
|
|
500
|
+
* Hi-score persistence (battery PRG-RAM at $6000)
|
|
501
|
+
* ════════════════════════════════════════════════════════════════════ */
|
|
502
|
+
|
|
503
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
504
|
+
* Requires: the iNES BATTERY flag in the crt0 header (flags6 bit 1 — the
|
|
505
|
+
* bundled chr-ram-runtime crt0 sets it). Without it, NROM leaves
|
|
506
|
+
* $6000-$7FFF UNMAPPED: reads return open bus (looks like data, isn't),
|
|
507
|
+
* writes vanish, and nothing persists. With it the emulator maps 8KB
|
|
508
|
+
* persistent PRG-RAM there (the save_ram region) like a real battery cart.
|
|
509
|
+
* First boot is GARBAGE, not zeros — that's why the magic + checksum. */
|
|
510
|
+
#define SRAM ((volatile uint8_t *)0x6000)
|
|
511
|
+
|
|
512
|
+
uint16_t hiscore_load(void) {
|
|
513
|
+
uint16_t v;
|
|
514
|
+
if (SRAM[0] != 'H' || SRAM[1] != 'S') return 0;
|
|
515
|
+
v = (uint16_t)SRAM[2] | ((uint16_t)SRAM[3] << 8);
|
|
516
|
+
if (SRAM[4] != (uint8_t)(SRAM[2] ^ SRAM[3] ^ 0xA5)) return 0;
|
|
517
|
+
return v;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
void hiscore_save(uint16_t v) {
|
|
521
|
+
SRAM[0] = 'H'; SRAM[1] = 'S';
|
|
522
|
+
SRAM[2] = (uint8_t)(v & 0xFF);
|
|
523
|
+
SRAM[3] = (uint8_t)(v >> 8);
|
|
524
|
+
SRAM[4] = (uint8_t)(SRAM[2] ^ SRAM[3] ^ 0xA5);
|
|
525
|
+
}
|
|
@@ -146,10 +146,43 @@ void sound_play_tone(uint8_t channel, uint16_t period, uint8_t vol_4bit, uint8_t
|
|
|
146
146
|
void sound_play_noise(uint8_t period_4bit, uint8_t vol_4bit, uint8_t length_frames);
|
|
147
147
|
void sound_off(void);
|
|
148
148
|
void sound_music(uint8_t on); /* background triangle melody — ON by default; 0 = off */
|
|
149
|
-
void sound_music_tick(void); /* call once per frame (
|
|
149
|
+
void sound_music_tick(void); /* call once per frame (the example games do) */
|
|
150
150
|
|
|
151
151
|
/* ── Globals ──────────────────────────────────────────────────── */
|
|
152
152
|
extern uint8_t shadow_oam[256]; /* at $0200, DMA'd by NMI */
|
|
153
153
|
extern volatile uint8_t nmi_counter; /* increments each NMI */
|
|
154
154
|
|
|
155
|
+
/* ── Text + font (0.29.0 examples contract) ─────────────────────── */
|
|
156
|
+
/*
|
|
157
|
+
* font_upload()
|
|
158
|
+
* Upload the built-in 8x8 font (digits 0-9, A-Z, space, dash) into the
|
|
159
|
+
* BACKGROUND pattern table at tile $40+ ('0'-'9' = $40-$49, 'A'-'Z' =
|
|
160
|
+
* $4A-$63, '-' = $64; space maps to tile 0). Call once during init
|
|
161
|
+
* (PPU off), after your other CHR uploads.
|
|
162
|
+
*
|
|
163
|
+
* text_draw_unsafe(ppu_addr, s) — PPU OFF only (init/title paint).
|
|
164
|
+
* text_draw(nt, x, y, s) — queued, safe during rendering (NMI
|
|
165
|
+
* commits next vblank; 16-entry queue).
|
|
166
|
+
* text_draw_u16(nt, x, y, v) — 5 right-aligned decimal digits (queued).
|
|
167
|
+
*/
|
|
168
|
+
void font_upload(void);
|
|
169
|
+
void text_draw_unsafe(uint16_t ppu_addr, const char *s);
|
|
170
|
+
void text_draw(uint8_t nt, uint8_t x, uint8_t y, const char *s);
|
|
171
|
+
void text_draw_u16(uint8_t nt, uint8_t x, uint8_t y, uint16_t v);
|
|
172
|
+
|
|
173
|
+
/* ── Hi-score persistence (battery PRG-RAM at $6000) ────────────── */
|
|
174
|
+
/*
|
|
175
|
+
* The bundled chr-ram-runtime crt0 sets the iNES BATTERY flag, so the
|
|
176
|
+
* emulator maps 8KB persistent PRG-RAM at $6000-$7FFF (the save_ram
|
|
177
|
+
* region) and persists it like a real battery cart. Layout used here:
|
|
178
|
+
* $6000-$6001 magic "HS", $6002-$6003 score (LE), $6004 checksum
|
|
179
|
+
* (score lo ^ score hi ^ $A5).
|
|
180
|
+
*
|
|
181
|
+
* hiscore_load() → the saved score, or 0 when the SRAM is empty/corrupt
|
|
182
|
+
* (first boot reads open-bus-like garbage — the magic+checksum reject it).
|
|
183
|
+
* hiscore_save(v) → store v. Call when a run ends with a new record.
|
|
184
|
+
*/
|
|
185
|
+
uint16_t hiscore_load(void);
|
|
186
|
+
void hiscore_save(uint16_t v);
|
|
187
|
+
|
|
155
188
|
#endif /* NES_RUNTIME_H */
|