romdevtools 0.26.0 → 0.28.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 +5 -3
- package/CHANGELOG.md +322 -3
- package/README.md +1 -1
- package/examples/README.md +1 -1
- package/examples/atari2600/templates/platformer.asm +18 -9
- package/examples/atari2600/templates/racing.asm +25 -4
- package/examples/atari2600/templates/shmup.asm +30 -5
- package/examples/atari2600/templates/sports.asm +41 -9
- package/examples/atari7800/templates/hello_sprite.c +8 -4
- package/examples/atari7800/templates/platformer.c +12 -8
- package/examples/atari7800/templates/puzzle.c +7 -4
- package/examples/atari7800/templates/racing.c +5 -2
- package/examples/atari7800/templates/shmup.c +8 -4
- package/examples/atari7800/templates/sports.c +6 -3
- package/examples/c64/templates/platformer.c +28 -24
- package/examples/c64/templates/puzzle.c +77 -16
- package/examples/c64/templates/racing.c +9 -0
- package/examples/c64/templates/shmup.c +13 -1
- package/examples/c64/templates/sports.c +9 -4
- package/examples/gb/templates/platformer.c +6 -2
- package/examples/gb/templates/puzzle.c +279 -101
- package/examples/gb/templates/racing.c +13 -1
- package/examples/gb/templates/shmup.c +13 -1
- package/examples/gb/templates/sports.c +9 -3
- package/examples/gba/templates/platformer.c +7 -13
- package/examples/gba/templates/puzzle.c +93 -15
- package/examples/gba/templates/racing.c +13 -1
- package/examples/gba/templates/shmup.c +13 -1
- package/examples/gba/templates/sports.c +17 -5
- package/examples/gbc/templates/platformer.c +6 -2
- package/examples/gbc/templates/puzzle.c +878 -178
- package/examples/gbc/templates/racing.c +13 -1
- package/examples/gbc/templates/shmup.c +13 -1
- package/examples/gbc/templates/sports.c +9 -3
- package/examples/genesis/templates/puzzle.c +76 -15
- package/examples/genesis/templates/racing.c +13 -1
- package/examples/genesis/templates/shmup_2p.c +13 -1
- package/examples/gg/templates/platformer.c +4 -0
- package/examples/gg/templates/puzzle.c +80 -14
- package/examples/gg/templates/racing.c +17 -1
- package/examples/gg/templates/shmup.c +17 -1
- package/examples/gg/templates/sports.c +4 -0
- package/examples/lynx/templates/platformer.c +25 -6
- package/examples/lynx/templates/puzzle.c +77 -14
- package/examples/lynx/templates/shmup.c +13 -1
- package/examples/lynx/templates/sports.c +5 -2
- package/examples/msx/platformer/main.c +2 -0
- package/examples/msx/puzzle/main.c +78 -15
- package/examples/msx/racing/main.c +1 -0
- package/examples/msx/shmup/main.c +1 -0
- package/examples/msx/sports/main.c +3 -2
- package/examples/nes/templates/platformer.c +11 -3
- package/examples/nes/templates/puzzle.c +81 -21
- package/examples/nes/templates/racing.c +15 -1
- package/examples/nes/templates/shmup.c +1 -0
- package/examples/nes/templates/sports.c +1 -0
- package/examples/pce/platformer/main.c +3 -1
- package/examples/pce/puzzle/main.c +78 -12
- package/examples/pce/racing/main.c +1 -0
- package/examples/pce/shmup/main.c +5 -4
- package/examples/pce/sports/main.c +4 -3
- package/examples/sms/templates/platformer.c +4 -0
- package/examples/sms/templates/puzzle.c +80 -14
- package/examples/sms/templates/racing.c +17 -1
- package/examples/sms/templates/shmup.c +17 -1
- package/examples/sms/templates/shmup_2p.c +17 -1
- package/examples/sms/templates/sports.c +4 -0
- package/examples/snes/templates/platformer.c +32 -15
- package/examples/snes/templates/puzzle.c +84 -16
- package/examples/snes/templates/racing.c +20 -1
- package/examples/snes/templates/shmup.c +20 -2
- package/examples/snes/templates/sports.c +7 -0
- 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 +245 -10
- package/src/mcp/server.js +6 -0
- 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 +11 -4
- package/src/mcp/tools/index.js +15 -1
- package/src/mcp/tools/input.js +26 -3
- package/src/mcp/tools/memory.js +208 -39
- package/src/mcp/tools/playtest.js +56 -4
- package/src/mcp/tools/project.js +35 -9
- package/src/mcp/tools/toolchain.js +43 -10
- package/src/mcp/tools/watch-memory.js +172 -25
- 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 +27 -6
- package/src/platforms/gb/MENTAL_MODEL.md +16 -1
- package/src/platforms/gb/TROUBLESHOOTING.md +42 -0
- package/src/platforms/gb/lib/c/patch-header.js +7 -4
- package/src/platforms/gbc/MENTAL_MODEL.md +12 -0
- package/src/platforms/gbc/TROUBLESHOOTING.md +21 -0
- package/src/platforms/gbc/lib/c/font.h +43 -0
- package/src/platforms/gbc/lib/c/patch-header.js +7 -4
- package/src/platforms/genesis/MENTAL_MODEL.md +40 -6
- 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/TROUBLESHOOTING.md +13 -17
- package/src/platforms/gg/lib/c/gg_crt0.s +14 -2
- 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 +6 -0
- 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 +2 -0
- package/src/platforms/msx/lib/c/msx_vdp.c +45 -0
- package/src/platforms/nes/MENTAL_MODEL.md +10 -3
- package/src/platforms/nes/lib/c/nes_runtime.c +41 -0
- package/src/platforms/nes/lib/c/nes_runtime.h +2 -0
- package/src/platforms/pce/MENTAL_MODEL.md +9 -0
- package/src/platforms/pce/TROUBLESHOOTING.md +9 -0
- package/src/platforms/pce/lib/c/pce_hw.h +2 -1
- package/src/platforms/pce/lib/c/pce_sound.c +22 -0
- package/src/platforms/sms/MENTAL_MODEL.md +5 -0
- 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 +5 -0
- package/src/playtest/playtest.js +73 -3
- package/src/toolchains/index.js +37 -8
|
@@ -237,10 +237,23 @@ read:
|
|
|
237
237
|
|
|
238
238
|
```c
|
|
239
239
|
uint8_t pad = ~SWCHA;
|
|
240
|
-
if (pad & JOY_UP) /* P1 up */
|
|
241
|
-
if (pad & JOY_DOWN) /* P1 down */
|
|
242
|
-
if (pad & JOY_LEFT) /* P1 left */
|
|
243
240
|
if (pad & JOY_RIGHT) /* P1 right */
|
|
241
|
+
if (pad & JOY_LEFT) /* P1 left */
|
|
242
|
+
if (pad & JOY_DOWN) /* P1 down */
|
|
243
|
+
if (pad & JOY_UP) /* P1 up */
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
**The bit order is the #1 7800 input footgun.** From bit 7 down the P1 nibble
|
|
247
|
+
is **Right ($80), Left ($40), Down ($20), Up ($10)** — same as the 2600. Defining
|
|
248
|
+
`JOY_UP 0x80 … JOY_RIGHT 0x10` (the "reads naturally" order) is exactly
|
|
249
|
+
REVERSED, and the symptom is bizarre enough to misdiagnose: up/down steer
|
|
250
|
+
left/right and vice versa. Always:
|
|
251
|
+
|
|
252
|
+
```c
|
|
253
|
+
#define JOY_RIGHT 0x80
|
|
254
|
+
#define JOY_LEFT 0x40
|
|
255
|
+
#define JOY_DOWN 0x20
|
|
256
|
+
#define JOY_UP 0x10
|
|
244
257
|
```
|
|
245
258
|
|
|
246
259
|
Fire button on `INPT4` at `$0C`, also active low.
|
|
@@ -343,9 +356,17 @@ What you can read:
|
|
|
343
356
|
P / SP / PC) read from prosystem's `sally` globals.
|
|
344
357
|
- **`background({view:'renderState'})`** — the MARIA CTRL bits, DPP,
|
|
345
358
|
CHARBASE, and the current `dlistPtr`.
|
|
346
|
-
- **`disasm({target:'rom'})`**
|
|
347
|
-
|
|
348
|
-
|
|
359
|
+
- **`disasm({target:'rom'})`** — defaults to the top 16 KB
|
|
360
|
+
(`$C000-$FFFF`), where the reset vector lands.
|
|
361
|
+
- **`disasm({target:'references'})`** — scans the WHOLE cart: flat carts
|
|
362
|
+
(≤48 KB) in one pass at their top-of-space org, SuperGame banked carts
|
|
363
|
+
(>48 KB) per 16 KB bank (last bank fixed at `$C000`, others at `$8000`),
|
|
364
|
+
refs tagged `romBank`. A 128-byte `.a78` header is stripped automatically.
|
|
365
|
+
- **`disasm({target:'project'})`** — flat carts rebuild with one flat cc65
|
|
366
|
+
build; SuperGame carts get per-bank regions + NES-style glue (HEADER
|
|
367
|
+
segment with the original 128 header bytes, `BANKn` wrappers, multi-bank
|
|
368
|
+
`.cfg` via `linkerConfigPath`) — a one-call byte-identical
|
|
369
|
+
`build()` rebuild either way.
|
|
349
370
|
|
|
350
371
|
Memory regions for **`memory({op:'read'})`**:
|
|
351
372
|
|
|
@@ -68,7 +68,10 @@ returns nothing.
|
|
|
68
68
|
`disasm({target:'project'})` route through the native binutils z80 `objdump` in
|
|
69
69
|
its `gbz80` machine (WASM, `-m gbz80`) — full CB-prefix coverage plus the
|
|
70
70
|
SM83-specific opcodes (`ld (hl+),a`, `ldh`, `reti`, `ld hl,sp+e8`). One z80-elf
|
|
71
|
-
binutils serves both plain Z80 (SMS/GG/MSX) and the GB CPU.
|
|
71
|
+
binutils serves both plain Z80 (SMS/GG/MSX) and the GB CPU. MBC-banked carts
|
|
72
|
+
(>32 KB) are scanned per 16 KB bank by `references` (bank 0 @ `$0000`, banks
|
|
73
|
+
1+ @ their `$4000` window; refs tagged `romBank`) and split per-bank by
|
|
74
|
+
`disasm({target:'project'})`.
|
|
72
75
|
|
|
73
76
|
## Five silent-failure footguns to know before you start (R26 + R27)
|
|
74
77
|
|
|
@@ -103,6 +106,18 @@ check these first. All five have shipped fixes in the bundled runtime
|
|
|
103
106
|
(volatile-safe by construction) or cast through `volatile uint8_t *`.
|
|
104
107
|
See `gb_runtime/lib/c/SDCC_GOTCHAS.md` § "Writes to VRAM" for detail.
|
|
105
108
|
|
|
109
|
+
3b. **OAM DMA goes FIRST after `wait_vblank()` — before any staging work.**
|
|
110
|
+
The vblank window is ~10 scanlines (~1140 cycles) and SDCC call overhead
|
|
111
|
+
is brutal: even a few dozen `oam_set()` CALLS before the flush push the
|
|
112
|
+
DMA out of vblank into active display, where it tears the sprites on one
|
|
113
|
+
FIXED scanline every frame (the "horizontal line a third of the way down"
|
|
114
|
+
glitch). The robust frame shape is: stage OAM/BG writes into RAM any time
|
|
115
|
+
during the frame, then `wait_vblank(); oam_dma_flush();` as the very first
|
|
116
|
+
thing, then a small bounded batch of BG map writes. One frame of sprite
|
|
117
|
+
latency is imperceptible. Also: statics belong at `dataLoc 0xC200` or
|
|
118
|
+
above (the project recipe sets this) so they can't collide with
|
|
119
|
+
`shadow_oam` at $C100.
|
|
120
|
+
|
|
106
121
|
4. **OAM DMA must run from HRAM.** During the ~160 µs OAM DMA window
|
|
107
122
|
the CPU can ONLY fetch from HRAM ($FF80-$FFFE). The bundled
|
|
108
123
|
`oam_dma_copy()` now installs a 9-byte stub at $FF80 and CALLs it;
|
|
@@ -166,6 +166,48 @@ at $0150. If you see code in that window, either:
|
|
|
166
166
|
works; don't override it for GB/GBC unless you know what you're
|
|
167
167
|
doing.
|
|
168
168
|
|
|
169
|
+
## "BG map updates randomly don't stick" / a tile updates one frame late forever
|
|
170
|
+
|
|
171
|
+
The core (like real hardware mid-frame) DROPS writes to VRAM ($8000-$9FFF)
|
|
172
|
+
that land outside vblank while the LCD is on — silently. A game loop that
|
|
173
|
+
pokes the BG map "whenever the state changes" will have SOME of those pokes
|
|
174
|
+
land mid-frame and vanish: stale cells, a piece that visually lags the
|
|
175
|
+
logical grid, glitches that move around as code timing shifts.
|
|
176
|
+
|
|
177
|
+
The robust pattern (used by the bundled puzzle scaffolds):
|
|
178
|
+
|
|
179
|
+
1. **COLLECT** — during the frame, don't touch VRAM. Append (addr, tile)
|
|
180
|
+
pairs to a small RAM queue whenever game state changes a cell.
|
|
181
|
+
2. **FLUSH** — immediately after `wait_vblank()` (right after the OAM DMA),
|
|
182
|
+
drain the queue with pure writes. No scanning, no logic — vblank is only
|
|
183
|
+
~1140 cycles, so the flush must be writes only and bounded.
|
|
184
|
+
3. **Scrub** — repaint one or two rows per frame round-robin as insurance,
|
|
185
|
+
so any cell that ever got dropped self-heals within a second.
|
|
186
|
+
|
|
187
|
+
If you must write outside that structure, turn the LCD off first (only
|
|
188
|
+
acceptable during init/load screens — mid-game it flashes white).
|
|
189
|
+
|
|
190
|
+
## "BG map updates randomly don't stick" / a tile updates one frame late forever
|
|
191
|
+
|
|
192
|
+
The core (like real hardware mid-frame) DROPS writes to VRAM ($8000-$9FFF)
|
|
193
|
+
that land outside vblank while the LCD is on — silently. A game loop that
|
|
194
|
+
pokes the BG map "whenever the state changes" will have SOME of those pokes
|
|
195
|
+
land mid-frame and vanish: stale cells, a piece that visually lags the
|
|
196
|
+
logical grid, glitches that move around as code timing shifts.
|
|
197
|
+
|
|
198
|
+
The robust pattern (used by the bundled puzzle scaffolds):
|
|
199
|
+
|
|
200
|
+
1. **COLLECT** — during the frame, don't touch VRAM. Append (addr, tile)
|
|
201
|
+
pairs to a small RAM queue whenever game state changes a cell.
|
|
202
|
+
2. **FLUSH** — immediately after `wait_vblank()` (right after the OAM DMA),
|
|
203
|
+
drain the queue with pure writes. No scanning, no logic — vblank is only
|
|
204
|
+
~1140 cycles, so the flush must be writes only and bounded.
|
|
205
|
+
3. **Scrub** — repaint one or two rows per frame round-robin as insurance,
|
|
206
|
+
so any cell that ever got dropped self-heals within a second.
|
|
207
|
+
|
|
208
|
+
If you must write outside that structure, turn the LCD off first (only
|
|
209
|
+
acceptable during init/load screens — mid-game it flashes white).
|
|
210
|
+
|
|
169
211
|
## Debug recipes
|
|
170
212
|
|
|
171
213
|
A few high-leverage tools you might not know exist:
|
|
@@ -7,10 +7,13 @@
|
|
|
7
7
|
//
|
|
8
8
|
// WHY: most libretro GB cores (gambatte) refuse to load a ROM whose
|
|
9
9
|
// Nintendo logo at $0104-$0133 doesn't match the canonical bytes or
|
|
10
|
-
// whose header checksum at $014D doesn't validate.
|
|
11
|
-
//
|
|
12
|
-
// pipeline
|
|
13
|
-
//
|
|
10
|
+
// whose header checksum at $014D doesn't validate.
|
|
11
|
+
//
|
|
12
|
+
// NOTE: romdev's own build pipeline DOES auto-patch the header now (it
|
|
13
|
+
// runs a bundled rgbfix after every gb/gbc link — see the
|
|
14
|
+
// "rgbfix (auto header fix)" line in build logs), so you only need this
|
|
15
|
+
// script when rebuilding the project OUTSIDE romdev with stock SDCC and
|
|
16
|
+
// no RGBDS installed. It's what keeps the scaffold self-contained.
|
|
14
17
|
//
|
|
15
18
|
// The bundled gb_crt0.s reserves $0100-$014F for the header window,
|
|
16
19
|
// so the bytes patched in here land on actual cartridge-header
|
|
@@ -31,6 +31,18 @@ the same wall.
|
|
|
31
31
|
cast through `volatile uint8_t *`. See `lib/c/SDCC_GOTCHAS.md`
|
|
32
32
|
§ "Writes to VRAM".
|
|
33
33
|
|
|
34
|
+
3b. **OAM DMA goes FIRST after `wait_vblank()` — before any staging work.**
|
|
35
|
+
The vblank window is ~10 scanlines (~1140 cycles) and SDCC call overhead
|
|
36
|
+
is brutal: even a few dozen `oam_set()` CALLS before the flush push the
|
|
37
|
+
DMA out of vblank into active display, where it tears the sprites on one
|
|
38
|
+
FIXED scanline every frame (the "horizontal line a third of the way down"
|
|
39
|
+
glitch). The robust frame shape is: stage OAM/BG writes into RAM any time
|
|
40
|
+
during the frame, then `wait_vblank(); oam_dma_flush();` as the very first
|
|
41
|
+
thing, then a small bounded batch of BG map writes. One frame of sprite
|
|
42
|
+
latency is imperceptible. Also: statics belong at `dataLoc 0xC200` or
|
|
43
|
+
above (the project recipe sets this) so they can't collide with
|
|
44
|
+
`shadow_oam` at $C100.
|
|
45
|
+
|
|
34
46
|
4. **OAM DMA must run from HRAM.** During the ~160 µs OAM DMA window
|
|
35
47
|
the CPU can ONLY fetch from HRAM ($FF80-$FFFE). The bundled
|
|
36
48
|
`oam_dma_copy()` installs a 9-byte stub at $FF80 and CALLs it; the
|
|
@@ -120,6 +120,27 @@ in DMG mode. To switch a DMG ROM to CGB:
|
|
|
120
120
|
2. Run `romPatch({op:'gbHeader', path:"out.gbc"})` — also fixes the global
|
|
121
121
|
checksum that the boot ROM checks
|
|
122
122
|
|
|
123
|
+
## "BG map updates randomly don't stick" / a tile updates one frame late forever
|
|
124
|
+
|
|
125
|
+
The core (like real hardware mid-frame) DROPS writes to VRAM ($8000-$9FFF)
|
|
126
|
+
that land outside vblank while the LCD is on — silently. A game loop that
|
|
127
|
+
pokes the BG map "whenever the state changes" will have SOME of those pokes
|
|
128
|
+
land mid-frame and vanish: stale cells, a piece that visually lags the
|
|
129
|
+
logical grid, glitches that move around as code timing shifts.
|
|
130
|
+
|
|
131
|
+
The robust pattern (used by the bundled puzzle scaffolds):
|
|
132
|
+
|
|
133
|
+
1. **COLLECT** — during the frame, don't touch VRAM. Append (addr, tile)
|
|
134
|
+
pairs to a small RAM queue whenever game state changes a cell.
|
|
135
|
+
2. **FLUSH** — immediately after `wait_vblank()` (right after the OAM DMA),
|
|
136
|
+
drain the queue with pure writes. No scanning, no logic — vblank is only
|
|
137
|
+
~1140 cycles, so the flush must be writes only and bounded.
|
|
138
|
+
3. **Scrub** — repaint one or two rows per frame round-robin as insurance,
|
|
139
|
+
so any cell that ever got dropped self-heals within a second.
|
|
140
|
+
|
|
141
|
+
If you must write outside that structure, turn the LCD off first (only
|
|
142
|
+
acceptable during init/load screens — mid-game it flashes white).
|
|
143
|
+
|
|
123
144
|
## "Sound is the same as DMG"
|
|
124
145
|
|
|
125
146
|
That's correct — CGB has the **identical** 4-channel APU as DMG. The
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/* AUTO-GENERATED by gen_font.py — 5x7 font, GB 2bpp, ink=value 3. */
|
|
2
|
+
#ifndef FONT_H
|
|
3
|
+
#define FONT_H
|
|
4
|
+
#define FONT_GLYPHS 36
|
|
5
|
+
static const uint8_t font_data[576] = {
|
|
6
|
+
0x38, 0x38, 0x44, 0x44, 0x4C, 0x4C, 0x54, 0x54, 0x64, 0x64, 0x44, 0x44, 0x38, 0x38, 0x00, 0x00, /* 0 */
|
|
7
|
+
0x10, 0x10, 0x30, 0x30, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x38, 0x38, 0x00, 0x00, /* 1 */
|
|
8
|
+
0x38, 0x38, 0x44, 0x44, 0x04, 0x04, 0x08, 0x08, 0x10, 0x10, 0x20, 0x20, 0x7C, 0x7C, 0x00, 0x00, /* 2 */
|
|
9
|
+
0x7C, 0x7C, 0x08, 0x08, 0x10, 0x10, 0x08, 0x08, 0x04, 0x04, 0x44, 0x44, 0x38, 0x38, 0x00, 0x00, /* 3 */
|
|
10
|
+
0x08, 0x08, 0x18, 0x18, 0x28, 0x28, 0x48, 0x48, 0x7C, 0x7C, 0x08, 0x08, 0x08, 0x08, 0x00, 0x00, /* 4 */
|
|
11
|
+
0x7C, 0x7C, 0x40, 0x40, 0x78, 0x78, 0x04, 0x04, 0x04, 0x04, 0x44, 0x44, 0x38, 0x38, 0x00, 0x00, /* 5 */
|
|
12
|
+
0x18, 0x18, 0x20, 0x20, 0x40, 0x40, 0x78, 0x78, 0x44, 0x44, 0x44, 0x44, 0x38, 0x38, 0x00, 0x00, /* 6 */
|
|
13
|
+
0x7C, 0x7C, 0x04, 0x04, 0x08, 0x08, 0x10, 0x10, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x00, 0x00, /* 7 */
|
|
14
|
+
0x38, 0x38, 0x44, 0x44, 0x44, 0x44, 0x38, 0x38, 0x44, 0x44, 0x44, 0x44, 0x38, 0x38, 0x00, 0x00, /* 8 */
|
|
15
|
+
0x38, 0x38, 0x44, 0x44, 0x44, 0x44, 0x3C, 0x3C, 0x04, 0x04, 0x08, 0x08, 0x30, 0x30, 0x00, 0x00, /* 9 */
|
|
16
|
+
0x38, 0x38, 0x44, 0x44, 0x44, 0x44, 0x7C, 0x7C, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x00, 0x00, /* A */
|
|
17
|
+
0x78, 0x78, 0x44, 0x44, 0x44, 0x44, 0x78, 0x78, 0x44, 0x44, 0x44, 0x44, 0x78, 0x78, 0x00, 0x00, /* B */
|
|
18
|
+
0x38, 0x38, 0x44, 0x44, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x44, 0x44, 0x38, 0x38, 0x00, 0x00, /* C */
|
|
19
|
+
0x70, 0x70, 0x48, 0x48, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x48, 0x48, 0x70, 0x70, 0x00, 0x00, /* D */
|
|
20
|
+
0x7C, 0x7C, 0x40, 0x40, 0x40, 0x40, 0x78, 0x78, 0x40, 0x40, 0x40, 0x40, 0x7C, 0x7C, 0x00, 0x00, /* E */
|
|
21
|
+
0x7C, 0x7C, 0x40, 0x40, 0x40, 0x40, 0x78, 0x78, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x00, 0x00, /* F */
|
|
22
|
+
0x38, 0x38, 0x44, 0x44, 0x40, 0x40, 0x5C, 0x5C, 0x44, 0x44, 0x44, 0x44, 0x3C, 0x3C, 0x00, 0x00, /* G */
|
|
23
|
+
0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x7C, 0x7C, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x00, 0x00, /* H */
|
|
24
|
+
0x38, 0x38, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x38, 0x38, 0x00, 0x00, /* I */
|
|
25
|
+
0x1C, 0x1C, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x48, 0x48, 0x30, 0x30, 0x00, 0x00, /* J */
|
|
26
|
+
0x44, 0x44, 0x48, 0x48, 0x50, 0x50, 0x60, 0x60, 0x50, 0x50, 0x48, 0x48, 0x44, 0x44, 0x00, 0x00, /* K */
|
|
27
|
+
0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x7C, 0x7C, 0x00, 0x00, /* L */
|
|
28
|
+
0x44, 0x44, 0x6C, 0x6C, 0x54, 0x54, 0x54, 0x54, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x00, 0x00, /* M */
|
|
29
|
+
0x44, 0x44, 0x44, 0x44, 0x64, 0x64, 0x54, 0x54, 0x4C, 0x4C, 0x44, 0x44, 0x44, 0x44, 0x00, 0x00, /* N */
|
|
30
|
+
0x38, 0x38, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x38, 0x38, 0x00, 0x00, /* O */
|
|
31
|
+
0x78, 0x78, 0x44, 0x44, 0x44, 0x44, 0x78, 0x78, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x00, 0x00, /* P */
|
|
32
|
+
0x38, 0x38, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x54, 0x54, 0x48, 0x48, 0x34, 0x34, 0x00, 0x00, /* Q */
|
|
33
|
+
0x78, 0x78, 0x44, 0x44, 0x44, 0x44, 0x78, 0x78, 0x50, 0x50, 0x48, 0x48, 0x44, 0x44, 0x00, 0x00, /* R */
|
|
34
|
+
0x3C, 0x3C, 0x40, 0x40, 0x40, 0x40, 0x38, 0x38, 0x04, 0x04, 0x04, 0x04, 0x78, 0x78, 0x00, 0x00, /* S */
|
|
35
|
+
0x7C, 0x7C, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x00, 0x00, /* T */
|
|
36
|
+
0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x38, 0x38, 0x00, 0x00, /* U */
|
|
37
|
+
0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x28, 0x28, 0x10, 0x10, 0x00, 0x00, /* V */
|
|
38
|
+
0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x54, 0x54, 0x54, 0x54, 0x6C, 0x6C, 0x44, 0x44, 0x00, 0x00, /* W */
|
|
39
|
+
0x44, 0x44, 0x44, 0x44, 0x28, 0x28, 0x10, 0x10, 0x28, 0x28, 0x44, 0x44, 0x44, 0x44, 0x00, 0x00, /* X */
|
|
40
|
+
0x44, 0x44, 0x44, 0x44, 0x28, 0x28, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x00, 0x00, /* Y */
|
|
41
|
+
0x7C, 0x7C, 0x04, 0x04, 0x08, 0x08, 0x10, 0x10, 0x20, 0x20, 0x40, 0x40, 0x7C, 0x7C, 0x00, 0x00, /* Z */
|
|
42
|
+
};
|
|
43
|
+
#endif
|
|
@@ -7,10 +7,13 @@
|
|
|
7
7
|
//
|
|
8
8
|
// WHY: most libretro GB cores (gambatte) refuse to load a ROM whose
|
|
9
9
|
// Nintendo logo at $0104-$0133 doesn't match the canonical bytes or
|
|
10
|
-
// whose header checksum at $014D doesn't validate.
|
|
11
|
-
//
|
|
12
|
-
// pipeline
|
|
13
|
-
//
|
|
10
|
+
// whose header checksum at $014D doesn't validate.
|
|
11
|
+
//
|
|
12
|
+
// NOTE: romdev's own build pipeline DOES auto-patch the header now (it
|
|
13
|
+
// runs a bundled rgbfix after every gb/gbc link — see the
|
|
14
|
+
// "rgbfix (auto header fix)" line in build logs), so you only need this
|
|
15
|
+
// script when rebuilding the project OUTSIDE romdev with stock SDCC and
|
|
16
|
+
// no RGBDS installed. It's what keeps the scaffold self-contained.
|
|
14
17
|
//
|
|
15
18
|
// The bundled gb_crt0.s reserves $0100-$014F for the header window,
|
|
16
19
|
// so the bytes patched in here land on actual cartridge-header
|
|
@@ -50,9 +50,13 @@ memory({op:'read', region:'system_ram', offset: sym.ramOffset, length:2})
|
|
|
50
50
|
- **`static` file-local globals resolve too** (SGDK emits per-symbol sections).
|
|
51
51
|
A non-`static` global that's never *read* can be optimised away at -O2 — mark
|
|
52
52
|
game-state vars you inspect `volatile` (you want that anyway).
|
|
53
|
-
- **
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
- **WRAM (`system_ram`) is normalized to CPU byte order** — offset X is the
|
|
54
|
+
byte the 68k sees at $FF0000+X, words read big-endian as expected, and
|
|
55
|
+
offsets line up with disassembly addresses and cheat-DB maps. (gpgx stores
|
|
56
|
+
work RAM host-LE word-swapped internally; the host un-swaps it. Before
|
|
57
|
+
0.28.0 the raw swapped layout leaked through — value-search/diff loops were
|
|
58
|
+
self-consistent, but any offset cross-referenced against a `move.b $FFxxxx`
|
|
59
|
+
in a disassembly was off-by-XOR-1.)
|
|
56
60
|
- **PC → which function?** `symbols({op:'addr', pc, symbolsText: b.mapText})` maps
|
|
57
61
|
a live `cpu({op:'read'}).pc` to the enclosing C function.
|
|
58
62
|
|
|
@@ -427,10 +431,40 @@ Video is deeply readable; the FM audio chip is only partially exposed:
|
|
|
427
431
|
`getPsgState` decodes the SN76489 (3 tone + 1 noise channels).
|
|
428
432
|
- **Memory regions:** `memory({op:'read'})` exposes CRAM, VSRAM, VDP_REGS,
|
|
429
433
|
Z80_RAM (the sound CPU's RAM), M68K work RAM, YM2612, PSG, and VRAM.
|
|
430
|
-
Remember the gpgx byte-swap quirk
|
|
434
|
+
Remember the gpgx byte-swap quirk for VRAM: it reads host-LE
|
|
431
435
|
word-byte-swapped (a 16-bit value's two bytes are swapped at the offset)
|
|
432
|
-
—
|
|
433
|
-
|
|
436
|
+
— use tiles({op:'pixels'}) to decode in render order. M68K work RAM
|
|
437
|
+
(`system_ram`) is NOT affected: it's normalized to CPU byte order (see
|
|
438
|
+
"Reading your C globals headlessly").
|
|
439
|
+
|
|
440
|
+
## Break-instant truth: registersAtHit + pure calls (0.28.0)
|
|
441
|
+
|
|
442
|
+
gpgx schedules its CPUs per scanline, so a `breakpoint` hit mid-frame used to
|
|
443
|
+
leave the LIVE register file hundreds of instructions past the hit by the time
|
|
444
|
+
you could read it — chasing pointer registers read that way burned a real
|
|
445
|
+
session for ~2h. Fixed two ways:
|
|
446
|
+
|
|
447
|
+
- **`registersAtHit`** — `breakpoint({on:'pc'|'write'|'read'})` hits now carry
|
|
448
|
+
the FULL register file (d0-d7/a0-a7/pc/sr/sp) frozen by the core at the hit
|
|
449
|
+
instant. Use it, never a follow-up `cpu({op:'read'})`. The reported `pc` for
|
|
450
|
+
write/read hits is the EXECUTING instruction's first byte (pre-0.28.0 it was
|
|
451
|
+
the post-prefetch PC — one instruction late). On a pc-break the 68k also
|
|
452
|
+
stays FROZEN for the rest of the frame, so even live reads agree.
|
|
453
|
+
- **`cpu({op:'call', pure:true})`** — steps ONLY the 68k: no VDP lines, no
|
|
454
|
+
Z80, no interrupts raised. Without it, a driven routine that spans frames
|
|
455
|
+
runs the game's own VBlank logic concurrently — which can stomp the output
|
|
456
|
+
buffer you're capturing (a real session diffed a CORRECT codec
|
|
457
|
+
reimplementation against that poisoned "ground truth" for hours). Prefer
|
|
458
|
+
`pure:true` for every decompressor/codec call; non-pure results carry a ⚠
|
|
459
|
+
caveat when frame logic ran. (SMS/GG get the same via the shared core; the
|
|
460
|
+
OTHER platforms get the same guarantee via interrupt blocking —
|
|
461
|
+
`pureMode:'irq-blocked'` — so the technique transfers everywhere.)
|
|
462
|
+
- **`watch({on:'copy'})`** — the CPU-port complement of `watch({on:'dma'})`:
|
|
463
|
+
logs every data-port write landing in a VRAM window with the executing
|
|
464
|
+
instruction's PC. Use `dma` when the upload is DMA'd (most Genesis
|
|
465
|
+
graphics), `copy` when the game pokes the data port directly (the
|
|
466
|
+
"video_ram writes don't reach the renderer" class of confusion — `copy`
|
|
467
|
+
shows you who's writing and where).
|
|
434
468
|
|
|
435
469
|
## ROM layout
|
|
436
470
|
|
|
@@ -32,6 +32,42 @@ void sfx_noise(u8 length_frames) {
|
|
|
32
32
|
sfx_remaining[3] = length_frames;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/* ── background music: a 16-step melody loop on PSG channel 2 ───────
|
|
36
|
+
* Ticked from sfx_update(), so every scaffold that already calls
|
|
37
|
+
* sfx_init() + sfx_update() gets continuous music for free ("no sound"
|
|
38
|
+
* was the #1 playtest complaint — a lone 6-frame blip on a rare event
|
|
39
|
+
* reads as silence). sfx_music(0) turns it off. SFX own channels 0-1 +
|
|
40
|
+
* noise, so effects always cut through. */
|
|
41
|
+
static const u16 music_hz[16] = {
|
|
42
|
+
262, 330, 392, 523, 392, 330, 262, 0, /* C4 E4 G4 C5 G4 E4 C4 - */
|
|
43
|
+
220, 262, 330, 440, 330, 262, 220, 0, /* A3 C4 E4 A4 E4 C4 A3 - */
|
|
44
|
+
};
|
|
45
|
+
static u8 music_enabled = 1;
|
|
46
|
+
static u8 music_step, music_timer;
|
|
47
|
+
|
|
48
|
+
void sfx_music(u8 on) {
|
|
49
|
+
music_enabled = on;
|
|
50
|
+
music_step = 0;
|
|
51
|
+
music_timer = 0;
|
|
52
|
+
if (!on) PSG_setEnvelope(2, PSG_ENVELOPE_MIN);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
static void music_tick(void) {
|
|
56
|
+
if (!music_enabled) return;
|
|
57
|
+
if (music_timer == 0) {
|
|
58
|
+
u16 hz = music_hz[music_step & 15];
|
|
59
|
+
if (hz) {
|
|
60
|
+
PSG_setFrequency(2, hz);
|
|
61
|
+
PSG_setEnvelope(2, 5); /* moderate, under the SFX */
|
|
62
|
+
} else {
|
|
63
|
+
PSG_setEnvelope(2, PSG_ENVELOPE_MIN);
|
|
64
|
+
}
|
|
65
|
+
music_step++;
|
|
66
|
+
}
|
|
67
|
+
music_timer++;
|
|
68
|
+
if (music_timer >= 9) music_timer = 0; /* ~6.6 notes/sec */
|
|
69
|
+
}
|
|
70
|
+
|
|
35
71
|
void sfx_update(void) {
|
|
36
72
|
for (u8 i = 0; i < 4; i++) {
|
|
37
73
|
if (sfx_remaining[i] > 0) {
|
|
@@ -41,6 +77,7 @@ void sfx_update(void) {
|
|
|
41
77
|
}
|
|
42
78
|
}
|
|
43
79
|
}
|
|
80
|
+
music_tick();
|
|
44
81
|
}
|
|
45
82
|
|
|
46
83
|
void sfx_off(void) {
|
|
@@ -42,6 +42,7 @@ void sfx_noise(u8 length_frames);
|
|
|
42
42
|
* decrement the auto-silence countdown. Without this, notes never
|
|
43
43
|
* stop ringing. */
|
|
44
44
|
void sfx_update(void);
|
|
45
|
+
void sfx_music(u8 on); /* background melody loop on PSG ch2 — ON by default; 0 = off */
|
|
45
46
|
|
|
46
47
|
/* Power down all PSG channels immediately. */
|
|
47
48
|
void sfx_off(void);
|
|
@@ -92,20 +92,16 @@ the P2 read + force `p2 = 0` so the AI fallback always engages.
|
|
|
92
92
|
The bundled GG `sports.c` already does this — copy that pattern when
|
|
93
93
|
porting other SMS multiplayer code.
|
|
94
94
|
|
|
95
|
-
## "
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
The bundled scaffolds build without a header — sufficient for the
|
|
111
|
-
emulator-driven workflow. Add one before shipping to a cartridge.
|
|
95
|
+
## "TMR SEGA" header / ROM boots in the wrong video mode
|
|
96
|
+
|
|
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.
|
|
100
|
+
|
|
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.
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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 scaffolded before 2026-06-09
|
|
89
|
+
shows ghost zeros, refresh its `msx_crt0.s` from a new scaffold.
|
|
@@ -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,8 @@ 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_music(uint8_t on); /* background melody on channel C — ON by default; 0 = off */
|
|
91
|
+
void msx_music_tick(void); /* call once per frame (scaffolds do) */
|
|
90
92
|
void msx_psg_off(uint8_t chan);
|
|
91
93
|
|
|
92
94
|
#endif /* MSX_HW_H */
|