romdevtools 0.27.0 → 0.29.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 +56 -44
- package/CHANGELOG.md +355 -0
- package/README.md +4 -4
- package/examples/README.md +7 -7
- package/examples/atari2600/templates/platformer.asm +1227 -325
- package/examples/atari2600/templates/puzzle.asm +1056 -0
- package/examples/atari2600/templates/racing.asm +909 -257
- package/examples/atari2600/templates/shmup.asm +1035 -218
- package/examples/atari2600/templates/sports.asm +1143 -229
- package/examples/atari7800/templates/hello_sprite.c +8 -4
- package/examples/atari7800/templates/platformer.c +991 -152
- package/examples/atari7800/templates/puzzle.c +1091 -145
- package/examples/atari7800/templates/racing.c +949 -118
- package/examples/atari7800/templates/shmup.c +812 -130
- package/examples/atari7800/templates/sports.c +820 -181
- package/examples/c64/templates/platformer.c +876 -157
- package/examples/c64/templates/puzzle.c +881 -143
- package/examples/c64/templates/racing.c +873 -88
- package/examples/c64/templates/shmup.c +762 -154
- package/examples/c64/templates/sports.c +755 -95
- package/examples/gb/templates/platformer.c +841 -175
- package/examples/gb/templates/puzzle.c +1094 -176
- package/examples/gb/templates/racing.c +761 -169
- package/examples/gb/templates/shmup.c +679 -169
- package/examples/gb/templates/sports.c +790 -153
- package/examples/gba/templates/platformer.c +624 -169
- package/examples/gba/templates/puzzle.c +535 -207
- package/examples/gba/templates/racing.c +513 -196
- package/examples/gba/templates/shmup.c +565 -168
- package/examples/gba/templates/sports.c +454 -162
- package/examples/gbc/templates/platformer.c +944 -176
- package/examples/gbc/templates/puzzle.c +1131 -177
- package/examples/gbc/templates/racing.c +891 -175
- package/examples/gbc/templates/shmup.c +827 -179
- package/examples/gbc/templates/sports.c +870 -156
- package/examples/genesis/templates/platformer.c +747 -129
- package/examples/genesis/templates/puzzle.c +702 -208
- package/examples/genesis/templates/racing.c +728 -193
- package/examples/genesis/templates/shmup.c +535 -142
- package/examples/genesis/templates/shmup_2p.c +13 -1
- package/examples/genesis/templates/sports.c +495 -158
- package/examples/gg/templates/platformer.c +883 -214
- package/examples/gg/templates/puzzle.c +906 -181
- package/examples/gg/templates/racing.c +919 -160
- package/examples/gg/templates/shmup.c +716 -177
- package/examples/gg/templates/sports.c +735 -128
- package/examples/lynx/templates/platformer.c +604 -50
- package/examples/lynx/templates/puzzle.c +533 -130
- package/examples/lynx/templates/racing.c +538 -102
- package/examples/lynx/templates/shmup.c +461 -122
- package/examples/lynx/templates/sports.c +496 -69
- package/examples/msx/platformer/main.c +648 -159
- package/examples/msx/puzzle/main.c +750 -185
- package/examples/msx/racing/main.c +669 -177
- package/examples/msx/shmup/main.c +460 -177
- package/examples/msx/sports/main.c +591 -124
- package/examples/nes/templates/platformer.c +586 -160
- package/examples/nes/templates/puzzle.c +603 -222
- package/examples/nes/templates/racing.c +505 -197
- package/examples/nes/templates/shmup.c +339 -144
- package/examples/nes/templates/sports.c +341 -182
- package/examples/pce/platformer/main.c +875 -204
- package/examples/pce/puzzle/main.c +797 -216
- package/examples/pce/racing/main.c +782 -206
- package/examples/pce/shmup/main.c +638 -211
- package/examples/pce/sports/main.c +585 -167
- package/examples/porting-across-platforms/README.md +1 -1
- package/examples/sms/templates/platformer.c +765 -176
- package/examples/sms/templates/puzzle.c +783 -177
- package/examples/sms/templates/racing.c +812 -133
- package/examples/sms/templates/shmup.c +601 -148
- package/examples/sms/templates/shmup_2p.c +17 -1
- package/examples/sms/templates/sports.c +633 -121
- 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 +587 -149
- package/examples/snes/templates/puzzle-data.asm +116 -21
- package/examples/snes/templates/puzzle-hdr.asm +57 -0
- package/examples/snes/templates/puzzle.c +632 -185
- 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 -177
- 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 -180
- 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 -156
- 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 +304 -11
- package/src/http/tool-registry.js +11 -11
- package/src/mcp/server.js +6 -0
- package/src/mcp/tools/cheats.js +2 -1
- package/src/mcp/tools/disasm-rebuild.js +315 -65
- package/src/mcp/tools/disasm.js +149 -28
- package/src/mcp/tools/find-references.js +216 -51
- package/src/mcp/tools/frame.js +14 -6
- package/src/mcp/tools/index.js +18 -4
- package/src/mcp/tools/input.js +31 -7
- package/src/mcp/tools/lifecycle.js +6 -4
- package/src/mcp/tools/memory.js +208 -39
- package/src/mcp/tools/platform-docs.js +1 -1
- package/src/mcp/tools/playtest.js +56 -4
- package/src/mcp/tools/preview-tile.js +6 -2
- package/src/mcp/tools/project.js +1114 -120
- package/src/mcp/tools/rom-id.js +5 -1
- package/src/mcp/tools/run-until.js +4 -2
- 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 +55 -11
- package/src/mcp/tools/watch-memory.js +145 -27
- 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 +64 -17
- package/src/platforms/atari2600/MENTAL_MODEL.md +5 -1
- package/src/platforms/atari2600/TROUBLESHOOTING.md +40 -0
- package/src/platforms/atari7800/MENTAL_MODEL.md +32 -11
- 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 +19 -4
- package/src/platforms/gb/TROUBLESHOOTING.md +101 -6
- 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 +19 -6
- 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 +16 -4
- package/src/platforms/gbc/TROUBLESHOOTING.md +24 -3
- 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/font.h +43 -0
- package/src/platforms/gbc/lib/c/gb_crt0.s +26 -3
- package/src/platforms/gbc/lib/c/patch-header.js +19 -6
- package/src/platforms/genesis/MENTAL_MODEL.md +43 -9
- package/src/platforms/genesis/TROUBLESHOOTING.md +2 -2
- package/src/platforms/genesis/lib/c/genesis_sfx.c +37 -0
- package/src/platforms/genesis/lib/c/genesis_sfx.h +1 -0
- package/src/platforms/gg/MENTAL_MODEL.md +4 -4
- package/src/platforms/gg/TROUBLESHOOTING.md +14 -18
- package/src/platforms/gg/UPSTREAM_SOURCES.md +1 -1
- package/src/platforms/gg/lib/c/gg_crt0.s +14 -2
- 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/lynx/lib/c/lynx_sfx.c +38 -2
- package/src/platforms/lynx/lib/c/lynx_sfx.h +1 -0
- package/src/platforms/msx/MENTAL_MODEL.md +11 -5
- package/src/platforms/msx/TROUBLESHOOTING.md +21 -0
- package/src/platforms/msx/lib/c/msx_crt0.s +27 -0
- package/src/platforms/msx/lib/c/msx_hw.h +3 -0
- package/src/platforms/msx/lib/c/msx_vdp.c +70 -0
- package/src/platforms/nes/MENTAL_MODEL.md +12 -5
- package/src/platforms/nes/lib/c/nes_runtime.c +190 -34
- package/src/platforms/nes/lib/c/nes_runtime.h +35 -0
- package/src/platforms/pce/MENTAL_MODEL.md +14 -5
- package/src/platforms/pce/TROUBLESHOOTING.md +9 -0
- package/src/platforms/pce/lib/c/pce_hw.h +13 -1
- package/src/platforms/pce/lib/c/pce_sound.c +22 -0
- package/src/platforms/pce/lib/c/pce_video.c +32 -0
- package/src/platforms/sms/MENTAL_MODEL.md +11 -6
- package/src/platforms/sms/TROUBLESHOOTING.md +6 -0
- package/src/platforms/sms/lib/c/sms_crt0.s +14 -2
- package/src/platforms/snes/MENTAL_MODEL.md +7 -2
- package/src/platforms/snes/TROUBLESHOOTING.md +40 -1
- package/src/playtest/playtest.js +73 -3
- 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 +64 -19
|
@@ -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,27 +85,23 @@ 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
|
|
|
92
92
|
The bundled GG `sports.c` already does this — copy that pattern when
|
|
93
93
|
porting other SMS multiplayer code.
|
|
94
94
|
|
|
95
|
-
## "
|
|
95
|
+
## "TMR SEGA" header / ROM boots in the wrong video mode
|
|
96
96
|
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
The build pipeline now stamps the 16-byte header at `$7FF0` automatically
|
|
98
|
+
("TMR SEGA" + checksum + the region/size byte at `$7FFF`) and pads every
|
|
99
|
+
image to 32 KB — you never hand-write it for romdev builds.
|
|
99
100
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
db 0x4C ; ROM size (0x4C = 32 KB)
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
The bundled scaffolds build without a header — sufficient for the
|
|
111
|
-
emulator-driven workflow. Add one before shipping to a cartridge.
|
|
101
|
+
The byte that matters is `$7FFF`: **high nibble = region, low nibble = ROM
|
|
102
|
+
size**. romdev writes `$7C` (GG international, 32 KB) on `.gg` builds.
|
|
103
|
+
If you patch a ROM by hand and leave an SMS region nibble there (`$4C` =
|
|
104
|
+
SMS export), gpgx boots the `.gg` file in **SMS compatibility mode** —
|
|
105
|
+
256×192 timing, SMS palette depth — and everything renders dark and
|
|
106
|
+
mis-cropped even though your code is fine. Check `$7FFF` first when a GG
|
|
107
|
+
ROM suddenly looks like an SMS ROM.
|
|
@@ -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:
|
|
@@ -37,10 +37,13 @@
|
|
|
37
37
|
;; ─── Reset vector at $0000 ────────────────────────────────────────
|
|
38
38
|
.area _HEADER (ABS)
|
|
39
39
|
.org 0x0000
|
|
40
|
+
;; ONLY 8 BYTES fit before the RST $08 vector. The old block here
|
|
41
|
+
;; (di/im 1/ld sp/jp = 9 bytes) overflowed into .org 0x0008, whose
|
|
42
|
+
;; `ret` stomped the jp's high target byte -> boot jumped into
|
|
43
|
+
;; garbage. di+im 1+jp = 6 bytes; SP setup moved to _boot below.
|
|
40
44
|
di ; interrupts off until we're ready
|
|
41
45
|
im 1 ; mode 1 — IRQs jump to $0038
|
|
42
|
-
|
|
43
|
-
jp gsinit ; skip the interrupt vector table
|
|
46
|
+
jp _boot ; continue past the vector table
|
|
44
47
|
|
|
45
48
|
;; ─── RST handlers (default = return) ──────────────────────────────
|
|
46
49
|
.org 0x0008
|
|
@@ -80,6 +83,15 @@
|
|
|
80
83
|
.org 0x0066
|
|
81
84
|
retn
|
|
82
85
|
|
|
86
|
+
;; ─── Boot continuation (right after the NMI vector) ───────────────
|
|
87
|
+
;; SP first, then the C runtime init. Lives in the ABS header area so
|
|
88
|
+
;; it exists at a known address regardless of where _CODE is linked
|
|
89
|
+
;; (_CODE must start at >= $0100 so it can't overwrite this table).
|
|
90
|
+
.org 0x0068
|
|
91
|
+
_boot:
|
|
92
|
+
ld sp, #0xDFF0 ; stack at top of WRAM minus 16
|
|
93
|
+
jp gsinit
|
|
94
|
+
|
|
83
95
|
;; ─── crt0 body ────────────────────────────────────────────────────
|
|
84
96
|
;; Standard SDCC pattern: jump to a code area, run initializers, then
|
|
85
97
|
;; call main. The initializer area is filled by sdcc when it sees
|
|
@@ -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
|
|
|
@@ -103,21 +103,57 @@ static void sfx_flush_pending(void) {
|
|
|
103
103
|
POKE(VOICE_BASE(i) + 1, 0x80); /* feedback off */
|
|
104
104
|
POKE(VOICE_BASE(i) + 4, sfx_pending_period[i]);
|
|
105
105
|
POKE(VOICE_BASE(i) + 5, 0x18); /* RELOAD + COUNT + 16us clock */
|
|
106
|
-
POKE(VOICE_BASE(i) + 0,
|
|
106
|
+
POKE(VOICE_BASE(i) + 0, 100); /* volume (was 64 — read as near-silent on hardware) */
|
|
107
107
|
} else if (sfx_pending_kind[i] == 2) {
|
|
108
108
|
/* Noise on voice 3. */
|
|
109
109
|
POKE(VOICE_BASE(i) + 7, 0x01); /* 12-bit LFSR */
|
|
110
110
|
POKE(VOICE_BASE(i) + 1, 0x95); /* classic noise feedback */
|
|
111
111
|
POKE(VOICE_BASE(i) + 4, 40);
|
|
112
112
|
POKE(VOICE_BASE(i) + 5, 0x18);
|
|
113
|
-
POKE(VOICE_BASE(i) + 0,
|
|
113
|
+
POKE(VOICE_BASE(i) + 0, 100);
|
|
114
114
|
}
|
|
115
115
|
sfx_pending_kind[i] = 0;
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
/* ── background music: 16-step melody loop on voice 1 ───────────────
|
|
120
|
+
* Ticked from sfx_update() through the SAME staged-write path (R57),
|
|
121
|
+
* so every scaffold that already calls sfx_init() + sfx_update() gets
|
|
122
|
+
* continuous music for free — "no sound at all" was the Lynx playtest
|
|
123
|
+
* verdict. sfx_music(0) turns it off. SFX use voice 0 (+ noise on 3).
|
|
124
|
+
* MIKEY period at the 16us clock: freq ~= 31250 / period. */
|
|
125
|
+
static const uint8_t music_period[16] = {
|
|
126
|
+
119, 95, 80, 60, 80, 95, 119, 0, /* C4 E4 G4 C5 G4 E4 C4 - */
|
|
127
|
+
142, 119, 95, 71, 95, 119, 142, 0, /* A3 C4 E4 A4 E4 C4 A3 - */
|
|
128
|
+
};
|
|
129
|
+
static uint8_t music_enabled = 1;
|
|
130
|
+
static uint8_t music_step, music_timer;
|
|
131
|
+
|
|
132
|
+
void sfx_music(uint8_t on) {
|
|
133
|
+
music_enabled = on;
|
|
134
|
+
music_step = 0;
|
|
135
|
+
music_timer = 0;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
static void music_tick(void) {
|
|
139
|
+
uint8_t p;
|
|
140
|
+
if (!music_enabled) return;
|
|
141
|
+
if (music_timer == 0) {
|
|
142
|
+
p = music_period[music_step & 15];
|
|
143
|
+
if (p) {
|
|
144
|
+
sfx_pending_kind[1] = 1; /* staged like any tone (R57-safe) */
|
|
145
|
+
sfx_pending_period[1] = p;
|
|
146
|
+
sfx_remaining[1] = 8; /* hold 8 of 9 frames — articulated */
|
|
147
|
+
}
|
|
148
|
+
music_step++;
|
|
149
|
+
}
|
|
150
|
+
music_timer++;
|
|
151
|
+
if (music_timer >= 9) music_timer = 0;
|
|
152
|
+
}
|
|
153
|
+
|
|
119
154
|
void sfx_update(void) {
|
|
120
155
|
uint8_t i;
|
|
156
|
+
music_tick();
|
|
121
157
|
/* R57: flush any sfx_tone/sfx_noise requests from THIS frame. The
|
|
122
158
|
* caller is expected to have just returned from tgi_updatedisplay
|
|
123
159
|
* (or wait_vblank), so the synchronous timer-event sweep that handy
|
|
@@ -54,6 +54,7 @@ void sfx_init(void);
|
|
|
54
54
|
void sfx_tone(uint8_t channel, uint8_t period, uint8_t length_frames);
|
|
55
55
|
void sfx_noise(uint8_t length_frames);
|
|
56
56
|
void sfx_update(void);
|
|
57
|
+
void sfx_music(uint8_t on); /* background melody on voice 1 — ON by default; 0 = off */
|
|
57
58
|
void sfx_off(void);
|
|
58
59
|
|
|
59
60
|
#endif
|
|
@@ -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.
|
|
@@ -117,6 +117,12 @@ exactly this.
|
|
|
117
117
|
generator + the envelope (period + shape bits).
|
|
118
118
|
- `memory({op:'read'})` regions: `msx_vram`, `msx_vdp_regs`, `msx_vdp_status`,
|
|
119
119
|
`msx_palette`, `msx_cpu_regs`, `msx_psg_regs`, plus `system_ram` (work RAM).
|
|
120
|
+
- `disasm({target:'rom'|'references'|'project'})` — native binutils z80
|
|
121
|
+
`objdump`. MegaROMs (>32 KB) are handled per 16 KB bank: `references` scans
|
|
122
|
+
bank 0 at `$4000` (after the "AB" header) and banks 1+ at `$8000` (an
|
|
123
|
+
assumed ASCII16-style window), refs tagged `romBank`;
|
|
124
|
+
`disasm({target:'project'})` splits the header into its own data region and
|
|
125
|
+
emits a bank-by-bank native rebuild recipe in `BUILD.md`.
|
|
120
126
|
|
|
121
127
|
## MCP debug & inspection tooling
|
|
122
128
|
|
|
@@ -66,3 +66,24 @@ fixed hardware colors — you choose indices, not RGB.
|
|
|
66
66
|
The build worker pool can transiently fail. Re-run the build. If it fails
|
|
67
67
|
consistently, read the `log` — SDCC's C89 parser errors are terse; common causes
|
|
68
68
|
are `//` comments, mid-block declarations, or file-scope inline asm (see above).
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
## PSG writes get eaten — sound code "runs" but the chip stays silent
|
|
72
|
+
|
|
73
|
+
The BIOS KEYINT interrupt fires every frame and reads PSG register 14 (the
|
|
74
|
+
joystick row) — and it CLOBBERS the PSGADDR latch. If an interrupt lands
|
|
75
|
+
between your `PSGADDR = n` and the matching `PSGWRITE`, your byte goes into
|
|
76
|
+
R14 instead of the register you selected. Symptom: the mixer looks right but
|
|
77
|
+
periods/volumes stay 0 — total silence even though your code clearly ran.
|
|
78
|
+
|
|
79
|
+
**Rule: wrap every PSGADDR/PSGWRITE sequence in `__asm__("di")` /
|
|
80
|
+
`__asm__("ei")`.** The bundled `msx_psg_tone`/`msx_psg_off` (and the music
|
|
81
|
+
ticker) already do this; copy the pattern for any direct PSG access you write.
|
|
82
|
+
|
|
83
|
+
## A `static x = 5;` boots as 0 (historical — fixed in the bundled crt0)
|
|
84
|
+
|
|
85
|
+
The old `msx_crt0.s` placed the SDCC `_INITIALIZER` area in RAM, so the boot
|
|
86
|
+
copy duplicated uninitialised RAM onto itself: every value-initialised static
|
|
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 forked before 2026-06-09
|
|
89
|
+
shows ghost zeros, refresh its `msx_crt0.s` from a freshly forked example.
|
|
@@ -27,6 +27,8 @@
|
|
|
27
27
|
.globl _main
|
|
28
28
|
.globl l__INITIALIZER
|
|
29
29
|
.globl s__INITIALIZER
|
|
30
|
+
.globl s__DATA
|
|
31
|
+
.globl l__DATA
|
|
30
32
|
.globl s__INITIALIZED
|
|
31
33
|
|
|
32
34
|
;; ─── Cartridge ROM header at $4000 ────────────────────────────────
|
|
@@ -44,7 +46,15 @@
|
|
|
44
46
|
;; ─── crt0 body ────────────────────────────────────────────────────
|
|
45
47
|
;; Standard SDCC area order so the linker fills _GSINIT with the global
|
|
46
48
|
;; initializer fragments sdcc emits, then _GSFINAL.
|
|
49
|
+
;; AREA ORDERING IS LOAD-BEARING (same bug class fixed in the SMS/GG
|
|
50
|
+
;; crt0s 2026-06-08): `_INITIALIZER` (the ROM image of every value-
|
|
51
|
+
;; initialised static) MUST be declared in the ROM group — otherwise
|
|
52
|
+
;; sdld places it in RAM after `_INITIALIZED` and the init copy below
|
|
53
|
+
;; copies uninitialised RAM onto itself, so every `static x = N;`
|
|
54
|
+
;; boots as 0. On MSX that silenced ALL scaffold audio (the PSG
|
|
55
|
+
;; music/sfx state booted zeroed) among other ghosts.
|
|
47
56
|
.area _HOME
|
|
57
|
+
.area _INITIALIZER
|
|
48
58
|
.area _CODE
|
|
49
59
|
.area _GSINIT
|
|
50
60
|
.area _GSFINAL
|
|
@@ -59,6 +69,23 @@
|
|
|
59
69
|
|
|
60
70
|
;; INIT entry — the BIOS CALLs here with interrupts on and a valid stack.
|
|
61
71
|
init:
|
|
72
|
+
;; ── Zero the BSS segment (`_DATA`) ── every uninitialised static
|
|
73
|
+
;; must read back 0 at boot (power-on RAM is garbage).
|
|
74
|
+
ld bc, #l__DATA
|
|
75
|
+
ld a, b
|
|
76
|
+
or a, c
|
|
77
|
+
jr Z, bss_done
|
|
78
|
+
ld hl, #s__DATA
|
|
79
|
+
ld (hl), #0x00
|
|
80
|
+
ld d, h
|
|
81
|
+
ld e, l
|
|
82
|
+
inc de
|
|
83
|
+
dec bc
|
|
84
|
+
ld a, b
|
|
85
|
+
or a, c
|
|
86
|
+
jr Z, bss_done
|
|
87
|
+
ldir
|
|
88
|
+
bss_done:
|
|
62
89
|
;; Copy initialized-data image from ROM to RAM (SDCC global inits).
|
|
63
90
|
ld bc, #l__INITIALIZER
|
|
64
91
|
ld a, b
|
|
@@ -87,6 +87,9 @@ 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 */
|
|
91
|
+
void msx_music(uint8_t on); /* background melody on channel C — ON by default; 0 = off */
|
|
92
|
+
void msx_music_tick(void); /* call once per frame (scaffolds do) */
|
|
90
93
|
void msx_psg_off(uint8_t chan);
|
|
91
94
|
|
|
92
95
|
#endif /* MSX_HW_H */
|
|
@@ -121,6 +121,13 @@ void msx_psg_tone(uint8_t chan, uint16_t period, uint8_t vol) {
|
|
|
121
121
|
uint8_t fine = (uint8_t)(period & 0xFF);
|
|
122
122
|
uint8_t coarse = (uint8_t)((period >> 8) & 0x0F);
|
|
123
123
|
|
|
124
|
+
/* DI around the whole register sequence: the BIOS KEYINT ISR reads
|
|
125
|
+
* PSG R14 (joystick row) every frame, and it CLOBBERS the PSGADDR
|
|
126
|
+
* latch — an IRQ between our PSGADDR and PSGWRITE sent the period/
|
|
127
|
+
* volume bytes into R14 instead. Symptom: mixer set, period 0,
|
|
128
|
+
* amplitude 0 → every MSX scaffold was silent. */
|
|
129
|
+
__asm__("di");
|
|
130
|
+
|
|
124
131
|
/* tone period: regs 0/1 (A), 2/3 (B), 4/5 (C) */
|
|
125
132
|
PSGADDR = (uint8_t)(chan << 1); PSGWRITE = fine;
|
|
126
133
|
PSGADDR = (uint8_t)((chan << 1) + 1); PSGWRITE = coarse;
|
|
@@ -136,15 +143,78 @@ void msx_psg_tone(uint8_t chan, uint16_t period, uint8_t vol) {
|
|
|
136
143
|
mixer &= (uint8_t)~(1 << chan); /* tone ON for this channel */
|
|
137
144
|
PSGADDR = 7;
|
|
138
145
|
PSGWRITE = mixer;
|
|
146
|
+
__asm__("ei");
|
|
147
|
+
}
|
|
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");
|
|
139
172
|
}
|
|
140
173
|
|
|
141
174
|
/* Silence a PSG channel: zero its volume and re-disable its tone bit. */
|
|
142
175
|
void msx_psg_off(uint8_t chan) {
|
|
143
176
|
uint8_t mixer;
|
|
177
|
+
__asm__("di"); /* same KEYINT race as above */
|
|
144
178
|
PSGADDR = (uint8_t)(8 + chan); PSGWRITE = 0; /* volume 0 */
|
|
145
179
|
PSGADDR = 7;
|
|
146
180
|
mixer = PSGREAD;
|
|
147
181
|
mixer |= (uint8_t)(1 << chan); /* tone OFF for this channel */
|
|
148
182
|
PSGADDR = 7;
|
|
149
183
|
PSGWRITE = mixer;
|
|
184
|
+
__asm__("ei");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/* ── background music: 16-step melody loop on PSG channel C (2) ─────
|
|
188
|
+
* Call msx_music_tick() once per frame (the scaffolds wire it in after
|
|
189
|
+
* their vsync wait); msx_music(0) turns it off. SFX use channels A/B,
|
|
190
|
+
* so effects always cut through. AY period = 1789773 / (16 * freq). */
|
|
191
|
+
static const uint16_t _msx_music_per[16] = {
|
|
192
|
+
427, 339, 285, 214, 285, 339, 427, 0, /* C4 E4 G4 C5 G4 E4 C4 - */
|
|
193
|
+
508, 427, 339, 254, 339, 427, 508, 0, /* A3 C4 E4 A4 E4 C4 A3 - */
|
|
194
|
+
};
|
|
195
|
+
static uint8_t _msx_music_on = 1;
|
|
196
|
+
static uint8_t _msx_music_step;
|
|
197
|
+
static uint8_t _msx_music_timer;
|
|
198
|
+
|
|
199
|
+
void msx_music(uint8_t on) {
|
|
200
|
+
_msx_music_on = on;
|
|
201
|
+
_msx_music_step = 0;
|
|
202
|
+
_msx_music_timer = 0;
|
|
203
|
+
if (!on) msx_psg_off(2);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
void msx_music_tick(void) {
|
|
207
|
+
uint16_t p;
|
|
208
|
+
if (!_msx_music_on) return;
|
|
209
|
+
if (_msx_music_timer == 0) {
|
|
210
|
+
p = _msx_music_per[_msx_music_step & 15];
|
|
211
|
+
if (p) {
|
|
212
|
+
msx_psg_tone(2, p, 12); /* AY volume is ~logarithmic — 9 was a whisper */
|
|
213
|
+
} else {
|
|
214
|
+
msx_psg_off(2); /* rest */
|
|
215
|
+
}
|
|
216
|
+
++_msx_music_step;
|
|
217
|
+
}
|
|
218
|
+
++_msx_music_timer;
|
|
219
|
+
if (_msx_music_timer >= 9) _msx_music_timer = 0;
|
|
150
220
|
}
|
|
@@ -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.
|
|
@@ -393,7 +393,11 @@ build({ output:'rom', platform:'nes',
|
|
|
393
393
|
inesHeader:{ prgBanks:2, chrBanks:1, mapper:0, mirroring:"vertical" } })
|
|
394
394
|
```
|
|
395
395
|
Mutually exclusive with `linkerConfig`. Works for any NROM (mapper 0, ≤2 PRG
|
|
396
|
-
banks)
|
|
396
|
+
banks). For a BANKED mapper you don't hand-write the glue anymore:
|
|
397
|
+
`disasm({target:'project'})` emits a HEADER segment (the original 16 iNES
|
|
398
|
+
bytes), a `.segment "PRGn"` wrapper per bank, and a multi-bank `nes_rebuild.cfg`
|
|
399
|
+
(switchable banks at $8000, fixed top bank at $C000), all wired into
|
|
400
|
+
`rebuild.json` via `linkerConfigPath` — a one-call byte-exact rebuild.
|
|
397
401
|
|
|
398
402
|
**2. `linkerConfig:"chr-rom"` — for homebrew C that ships FIXED tile art.**
|
|
399
403
|
A cc65-C preset (segment split + a CHARS segment in an 8 KB ROM2 bank). Put your
|
|
@@ -402,8 +406,11 @@ tiles in `.segment "CHARS"` (`.incbin "tiles.chr"`) + pass the blob via
|
|
|
402
406
|
other bank configs, prefer `inesHeader`.
|
|
403
407
|
|
|
404
408
|
**3. `disasm({target:'project'})` — disassemble → rebuild, in two calls.**
|
|
405
|
-
For NES it
|
|
406
|
-
exact `build({
|
|
409
|
+
For NES it extracts the CHR-ROM to `chr.bin`, writes a `rebuild.json` (the
|
|
410
|
+
exact `build({...})` call, with absolute paths) and a `BUILD.md`. NROM gets the
|
|
411
|
+
`inesHeader` one-call form; BANKED mappers (UxROM/MMC1/MMC3…) get per-bank
|
|
412
|
+
`PRGn` segment wrappers + the original-bytes HEADER segment + a generated
|
|
413
|
+
multi-bank `.cfg` referenced via `linkerConfigPath`. Either way: feed
|
|
407
414
|
`rebuild.json` straight back to `build` and you get a byte-identical ROM. This
|
|
408
415
|
is the RE workhorse loop: `disasm({target:'project'})` → edit the `.asm` →
|
|
409
416
|
rebuild → `diffRoms` to confirm your patch landed.
|