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
|
@@ -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 ──────────────────────────────────────────────── */
|
|
@@ -330,6 +346,47 @@ void sound_init(void) {
|
|
|
330
346
|
APUFRAMECTR = 0x40; /* 4-step frame counter, disable frame IRQ */
|
|
331
347
|
}
|
|
332
348
|
|
|
349
|
+
/* ── background music: 16-step melody on the TRIANGLE channel ───────
|
|
350
|
+
* The pulse channels + noise stay free for sound_play_tone/noise SFX.
|
|
351
|
+
* Call sound_music_tick() once per frame (after ppu_wait_nmi); the
|
|
352
|
+
* scaffolds wire it in. sound_music(0) silences it. ("No sound" was
|
|
353
|
+
* the dominant NES playtest complaint — a rare 6-frame blip isn't
|
|
354
|
+
* enough; continuous triangle gives every scaffold a musical floor.)
|
|
355
|
+
* NTSC triangle period for note f ≈ 1789773/(32*f) - 1. */
|
|
356
|
+
static const uint16_t music_period_tbl[16] = {
|
|
357
|
+
213, 168, 141, 106, 141, 168, 213, 0, /* C4 E4 G4 C5 G4 E4 C4 - */
|
|
358
|
+
253, 213, 168, 126, 168, 213, 253, 0, /* A3 C4 E4 A4 E4 C4 A3 - */
|
|
359
|
+
};
|
|
360
|
+
static uint8_t music_enabled = 1;
|
|
361
|
+
static uint8_t music_step;
|
|
362
|
+
static uint8_t music_timer;
|
|
363
|
+
|
|
364
|
+
void sound_music(uint8_t on) {
|
|
365
|
+
music_enabled = on;
|
|
366
|
+
music_step = 0;
|
|
367
|
+
music_timer = 0;
|
|
368
|
+
if (!on) { TRI_LINEAR = 0x00; TRI_HI = 0x08; } /* reload 0 linear → silent */
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
void sound_music_tick(void) {
|
|
372
|
+
uint16_t p;
|
|
373
|
+
if (!music_enabled) return;
|
|
374
|
+
if (music_timer == 0) {
|
|
375
|
+
p = music_period_tbl[music_step & 15];
|
|
376
|
+
if (p) {
|
|
377
|
+
TRI_LINEAR = 0xFF; /* halt flag + max linear: sustain */
|
|
378
|
+
TRI_LO = (uint8_t)(p & 0xFF);
|
|
379
|
+
TRI_HI = (uint8_t)((p >> 8) & 0x07); /* also reloads the linear counter */
|
|
380
|
+
} else {
|
|
381
|
+
TRI_LINEAR = 0x00; /* rest */
|
|
382
|
+
TRI_HI = 0x08;
|
|
383
|
+
}
|
|
384
|
+
++music_step;
|
|
385
|
+
}
|
|
386
|
+
++music_timer;
|
|
387
|
+
if (music_timer >= 9) music_timer = 0;
|
|
388
|
+
}
|
|
389
|
+
|
|
333
390
|
void sound_play_tone(uint8_t channel, uint16_t period, uint8_t vol_4bit, uint8_t length_frames) {
|
|
334
391
|
uint8_t len5 = length_frames & 0x1F;
|
|
335
392
|
uint8_t v = vol_4bit & 0x0F;
|
|
@@ -367,3 +424,102 @@ void sound_play_noise(uint8_t period_4bit, uint8_t vol_4bit, uint8_t length_fram
|
|
|
367
424
|
void sound_off(void) {
|
|
368
425
|
APUSTATUS = 0x00;
|
|
369
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
|
+
}
|
|
@@ -145,9 +145,44 @@ void sound_init(void);
|
|
|
145
145
|
void sound_play_tone(uint8_t channel, uint16_t period, uint8_t vol_4bit, uint8_t length_frames);
|
|
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
|
+
void sound_music(uint8_t on); /* background triangle melody — ON by default; 0 = off */
|
|
149
|
+
void sound_music_tick(void); /* call once per frame (the example games do) */
|
|
148
150
|
|
|
149
151
|
/* ── Globals ──────────────────────────────────────────────────── */
|
|
150
152
|
extern uint8_t shadow_oam[256]; /* at $0200, DMA'd by NMI */
|
|
151
153
|
extern volatile uint8_t nmi_counter; /* increments each NMI */
|
|
152
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
|
+
|
|
153
188
|
#endif /* NES_RUNTIME_H */
|
|
@@ -12,13 +12,13 @@ romdev ships a **hardware helper library** (`src/platforms/pce/lib/c/`:
|
|
|
12
12
|
`psg_tone()` instead of poking VDC/VCE registers by hand. cc65 has **no** sprite
|
|
13
13
|
library, so this lib is how you get pixels on screen.
|
|
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:"pce/shmup", name, path})`** — or any
|
|
17
|
+
of `platformer` / `puzzle` / `sports` / `racing`, the full genre set. For a smaller
|
|
18
|
+
starting point fork `pce/sprite_move` (also `music_sfx`, `catch_game`). Either drops
|
|
19
19
|
a complete, *building* project — a verified playable example + the helper lib +
|
|
20
20
|
docs. Read the example's `main.c`, then change it. The examples live in
|
|
21
|
-
`examples/pce/`. The genre
|
|
21
|
+
`examples/pce/`. The genre examples fill the BAT (32×32 virtual screen); the
|
|
22
22
|
`platformer` smooth-scrolls the background via the VDC BXR (R7) register.
|
|
23
23
|
**Gotcha:** `#include <stdint.h>` for int8/16/32_t — `pce.h` only typedefs u8/u16.
|
|
24
24
|
|
|
@@ -101,3 +101,12 @@ screen. Keep at least one (2+ byte) global. See TROUBLESHOOTING.md.
|
|
|
101
101
|
+ LFO.
|
|
102
102
|
- `memory({op:'read'})` regions: `pce_vdc_vram`, `pce_vdc_satb`, `pce_vdc_regs`,
|
|
103
103
|
`pce_vce_palette`, `pce_cpu_regs`, `pce_psg_regs`.
|
|
104
|
+
- `disasm({target:'rom'|'references'|'project'})` — da65's native `huc6280`
|
|
105
|
+
CPU mode. HuCards >32 KB are handled per 8 KB page (page 0 at `$E000`,
|
|
106
|
+
where MPR7 maps it at reset — the vectors live there; pages 1+ at `$8000`,
|
|
107
|
+
an assumed window since the game's MPR writes decide at runtime).
|
|
108
|
+
`references` tags refs with `romBank`; `disasm({target:'project'})` emits
|
|
109
|
+
per-page regions + segment wrappers + a generated `.cfg`, and — because the
|
|
110
|
+
PCE asm toolchain IS cc65/ca65 — a **one-call byte-identical `build()`
|
|
111
|
+
rebuild** via `rebuild.json` (flat and banked; a 512-byte copier header is
|
|
112
|
+
split out and re-emitted as a HEADER segment).
|
|
@@ -58,3 +58,12 @@ you likely hit a BRK or an unhandled IRQ — check that you didn't enable a VDC
|
|
|
58
58
|
`clrscr()` must run before `cputs()` (it inits the font + VDC). Also confirm
|
|
59
59
|
`.bss` is non-empty (see the first entry) — a broken crt0 means conio's font
|
|
60
60
|
upload never happened.
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
## PSG tone plays but is nearly inaudible
|
|
64
|
+
|
|
65
|
+
The 5-bit channel volume (`PSG_CHAN_CTRL` low bits, 0-31) is roughly an
|
|
66
|
+
ATTENUATOR: each step below 31 costs ~1.5 dB. A "middle" value like 13 is
|
|
67
|
+
about -27 dB — effectively silence on real hardware and most cores. Use
|
|
68
|
+
**29-31 for SFX/music** and treat anything under ~20 as a deliberate whisper.
|
|
69
|
+
(The bundled `psg_tone` example helper and the music ticker default loud.)
|
|
@@ -103,8 +103,19 @@ void disp_enable(void); /* VDC R5: BG + SPR + VBlank IRQ on at once
|
|
|
103
103
|
void vblank_irq_enable(void); /* just the VBlank IRQ bit (waitvsync needs it) */
|
|
104
104
|
void load_tiles(u16 vram, const u16 *src, u16 n); /* alias of vram_write (tiles) */
|
|
105
105
|
void set_sprite(u8 slot, u16 x, u16 y, u16 pattern, u8 palette); /* fill shadow SATB */
|
|
106
|
+
void set_sprite_ex(u8 slot, u16 x, u16 y, u16 pattern, u8 palette, u16 attr_ex);
|
|
106
107
|
void satb_dma(void); /* DMA shadow SATB -> VDC (R19) */
|
|
107
108
|
|
|
109
|
+
/* attr_ex bits for set_sprite_ex() — the HuC6270 large-sprite size + flip
|
|
110
|
+
* bits in SATB word3. A 32-wide sprite needs a 2-aligned pattern code, 32x32
|
|
111
|
+
* needs 4-aligned, 32x64 needs 8-aligned; the data is consecutive 16x16 cells
|
|
112
|
+
* (left-to-right, then down). See the set_sprite_ex() comment in pce_video.c. */
|
|
113
|
+
#define SPR_CGX_32 0x0100 /* width 32px (two cells side by side) */
|
|
114
|
+
#define SPR_CGY_32 0x1000 /* height 32px (two cell rows) */
|
|
115
|
+
#define SPR_CGY_64 0x3000 /* height 64px (four cell rows) */
|
|
116
|
+
#define SPR_XFLIP 0x0800 /* mirror horizontally */
|
|
117
|
+
#define SPR_YFLIP 0x8000 /* mirror vertically */
|
|
118
|
+
|
|
108
119
|
/* The shadow SATB lives in VRAM at this word address; satb_dma() points the VDC
|
|
109
120
|
* SATB-DMA source (R19) here. Pattern base for tiles is your choice; sprites
|
|
110
121
|
* default to using the same VRAM you load_tiles() into. */
|
|
@@ -126,6 +137,7 @@ u8 pce_joy_read(void); /* read pad 1 -> clean bitmask (PCE_JOY_* below) */
|
|
|
126
137
|
|
|
127
138
|
/* ---- sound helpers (pce_sound.c) ----------------------------------------- */
|
|
128
139
|
void psg_tone(u8 chan, u16 freq, u8 vol); /* play a wavetable tone (vol 0..31) */
|
|
129
|
-
void psg_off(u8 chan);
|
|
140
|
+
void psg_off(u8 chan);
|
|
141
|
+
void psg_music_tick(void); /* call once per frame (scaffolds do) */ /* silence + disable a channel */
|
|
130
142
|
|
|
131
143
|
#endif /* PCE_HW_H */
|
|
@@ -51,3 +51,25 @@ void psg_off(u8 chan) {
|
|
|
51
51
|
PSG_CHAN_SELECT = chan;
|
|
52
52
|
PSG_CHAN_CTRL = 0x00; /* bit7 clear = channel off, volume 0 */
|
|
53
53
|
}
|
|
54
|
+
|
|
55
|
+
/* ── background music: 8-step melody loop on channel 5 ─────────────
|
|
56
|
+
* Call psg_music_tick() once per frame (the scaffolds wire it in after
|
|
57
|
+
* their vsync wait). Deliberately MINIMAL — the PCE boot bank is 8KB
|
|
58
|
+
* and the puzzle scaffold sits within ~100 bytes of the ceiling, so
|
|
59
|
+
* there's no on/off toggle and no rests (re-trigger every note).
|
|
60
|
+
* SFX use channels 0-3; the melody never fights an effect.
|
|
61
|
+
* PCE freq is a DIVIDER: pitch ~= 3.58MHz / (32 * freq). */
|
|
62
|
+
static const u16 _music_div[8] = {
|
|
63
|
+
427, 339, 285, 214, 508, 427, 339, 254, /* C4 E4 G4 C5 A3 C4 E4 A4 */
|
|
64
|
+
};
|
|
65
|
+
static u8 _music_step;
|
|
66
|
+
static u8 _music_timer;
|
|
67
|
+
|
|
68
|
+
void psg_music_tick(void) {
|
|
69
|
+
if (_music_timer == 0) {
|
|
70
|
+
psg_tone(5, _music_div[_music_step & 7], 29); /* PCE vol is ~-1.5dB/step from 31 — 13 was -27dB, inaudible */
|
|
71
|
+
++_music_step;
|
|
72
|
+
}
|
|
73
|
+
++_music_timer;
|
|
74
|
+
if (_music_timer >= 9) _music_timer = 0;
|
|
75
|
+
}
|
|
@@ -131,6 +131,38 @@ void set_sprite(u8 slot, u16 x, u16 y, u16 pattern, u8 palette) {
|
|
|
131
131
|
e[3] = (u16)(0x0080 | (palette & 0x0F)); /* word3: SPBG-front + pal */
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
/* set_sprite() with the HuC6270's LARGE-SPRITE size bits — the PCE's signature
|
|
135
|
+
* trick (sprites up to 32x64 from ONE SATB entry, where the NES needs 8+).
|
|
136
|
+
*
|
|
137
|
+
* SATB word3 (the attribute word) layout:
|
|
138
|
+
* bit 15 Y-flip
|
|
139
|
+
* bits13:12 CGY — sprite HEIGHT: 00=16px, 01=32px, 11=64px (10 is invalid)
|
|
140
|
+
* bit 11 X-flip
|
|
141
|
+
* bit 8 CGX — sprite WIDTH: 0=16px, 1=32px
|
|
142
|
+
* bit 7 SPBG — 1 = sprite in front of background
|
|
143
|
+
* bits 3:0 sprite sub-palette (0-15)
|
|
144
|
+
*
|
|
145
|
+
* `attr_ex` is OR'd into word3 — pass the SPR_* constants from pce_hw.h
|
|
146
|
+
* (e.g. SPR_CGX_32 | SPR_CGY_32 for a 32x32 sprite). SPBG-front is still set
|
|
147
|
+
* for you, same as set_sprite().
|
|
148
|
+
*
|
|
149
|
+
* PATTERN LAYOUT for large sprites: the hardware ignores the low bit(s) of the
|
|
150
|
+
* pattern code and fetches consecutive 16x16 cells (64 words each) instead:
|
|
151
|
+
* 32 wide: cell N = left, N+1 = right (N multiple of 2)
|
|
152
|
+
* 32 tall: row r adds 2*r: N, N+1 / N+2, N+3 (N multiple of 4)
|
|
153
|
+
* 64 tall: rows 0-3 add 2*r: N .. N+7 (N multiple of 8)
|
|
154
|
+
* So a 32x32 sprite's VRAM data is FOUR cells in TL, TR, BL, BR order, and
|
|
155
|
+
* its pattern code must be 4-aligned (pattern = VRAM>>6 like set_sprite). */
|
|
156
|
+
void set_sprite_ex(u8 slot, u16 x, u16 y, u16 pattern, u8 palette, u16 attr_ex) {
|
|
157
|
+
u16 *e;
|
|
158
|
+
if (slot >= 64) return;
|
|
159
|
+
e = &_pce_satb[slot * 4];
|
|
160
|
+
e[0] = (u16)(y + 64); /* word0: Y (biased) */
|
|
161
|
+
e[1] = (u16)(x + 32); /* word1: X (biased) */
|
|
162
|
+
e[2] = (u16)((pattern & 0x3FF) << 1); /* word2: pattern (cell<<1) */
|
|
163
|
+
e[3] = (u16)(0x0080 | (palette & 0x0F) | attr_ex); /* word3: size/flip + SPBG + pal */
|
|
164
|
+
}
|
|
165
|
+
|
|
134
166
|
/* Copy the shadow SATB into VRAM at PCE_SATB_VRAM, then tell the VDC to DMA it
|
|
135
167
|
* into its internal sprite table (R19 = SATB source). */
|
|
136
168
|
void satb_dma(void) {
|
|
@@ -78,7 +78,7 @@ R1 = 0x80 display OFF, vblank IRQ off, 192-line
|
|
|
78
78
|
R2 = 0xFF name table at $3800
|
|
79
79
|
R4 = 0xFF BG tile data at $0000
|
|
80
80
|
R5 = 0xFF sprite attr table at $3F00
|
|
81
|
-
R6 = 0xFF sprite tile data at $2000 (own bank;
|
|
81
|
+
R6 = 0xFF sprite tile data at $2000 (own bank; the example games upload here)
|
|
82
82
|
R7 = 0x00 border colour
|
|
83
83
|
```
|
|
84
84
|
|
|
@@ -114,7 +114,7 @@ So Y bytes and X/tile pairs are split into TWO regions of the SAT.
|
|
|
114
114
|
`src/platforms/sms/lib/c/sprite_table.c` keeps a 256-byte shadow
|
|
115
115
|
buffer in WRAM and uploads it to the SAT each vblank.
|
|
116
116
|
|
|
117
|
-
### Two footguns the bundled
|
|
117
|
+
### Two footguns the bundled example games keep hitting
|
|
118
118
|
|
|
119
119
|
1. **8 sprites per scanline limit.** The VDP draws up to 8 sprites per
|
|
120
120
|
scanline; the 9th+ are silently dropped. If you draw a "CATCH THE
|
|
@@ -147,9 +147,9 @@ buffer in WRAM and uploads it to the SAT each vblank.
|
|
|
147
147
|
`sms_vdp_init()` sets R6 = 0xFF. R6 bit 2 is the SA13 select for
|
|
148
148
|
sprite tile data — bit 2 is **SET** in 0xFF, so sprite tiles read
|
|
149
149
|
from `$2000-$3FFF`, their **own bank** separate from BG tiles at
|
|
150
|
-
$0000. This matches every bundled
|
|
150
|
+
$0000. This matches every bundled example, which uploads sprite
|
|
151
151
|
tiles to `$2000` (`sms_load_tiles(0x2000, …)`) — default and
|
|
152
|
-
|
|
152
|
+
examples agree, so sprites render.
|
|
153
153
|
|
|
154
154
|
Watch the bit: 0xFB has SA13 **CLEAR** = sprite tiles at $0000
|
|
155
155
|
(shared with the BG bank). If you set R6=0xFB you MUST upload your
|
|
@@ -222,7 +222,7 @@ PSG (SN76489) on port $7F. 4 channels: 3 square waves + 1 noise.
|
|
|
222
222
|
Writes are byte-wise; the high bit selects "latch register" vs
|
|
223
223
|
"continue previous register".
|
|
224
224
|
|
|
225
|
-
A full driver is beyond the scope of these
|
|
225
|
+
A full driver is beyond the scope of these example games. For
|
|
226
226
|
playable SFX, manually pulse $7F with the latch-register byte
|
|
227
227
|
followed by data bytes. Real games ship a music driver in WRAM.
|
|
228
228
|
|
|
@@ -318,9 +318,14 @@ CB/ED/DD/FD/DDCB/FDCB — and feeds the same auto-label, register-annotation,
|
|
|
318
318
|
file-offset, and `untilReturn` pipeline used by the NES and SNES
|
|
319
319
|
disassemblers.
|
|
320
320
|
|
|
321
|
+
Sega-mapper banked carts (>48 KB) are handled per-bank: `references` scans
|
|
322
|
+
every 16 KB bank (bank 0 @ `$0000`, bank 1 @ `$4000`, banks 2+ @ their slot-2
|
|
323
|
+
window `$8000`), refs tagged `romBank`; `disasm({target:'project'})` emits one
|
|
324
|
+
region per bank with a bank-by-bank native rebuild recipe in `BUILD.md`.
|
|
325
|
+
|
|
321
326
|
## Horizontal scrolling (for side-scrollers)
|
|
322
327
|
|
|
323
|
-
The `platformer`
|
|
328
|
+
The `platformer` example is single-screen. To make it a side-scroller:
|
|
324
329
|
|
|
325
330
|
- **Hardware scroll:** write VDP register 8 (horizontal scroll) each frame =
|
|
326
331
|
`-camX & 0xFF` (the reg scrolls the screen; the name table is 32×28 and
|
|
@@ -118,6 +118,12 @@ If you ported an SMS ROM straight to `.gg` it'll boot and run, but
|
|
|
118
118
|
the colours will be very dark (2-bit values reinterpreted as 4-bit)
|
|
119
119
|
and the visible area is in the top-left corner.
|
|
120
120
|
|
|
121
|
+
Also check the header region byte at `$7FFF` (high nibble = region,
|
|
122
|
+
low nibble = size): romdev stamps `$7C` (GG international) on `.gg`
|
|
123
|
+
builds, but an SMS nibble there (`$4x`) makes gpgx boot the file in
|
|
124
|
+
SMS compatibility mode — wrong resolution and palette depth no matter
|
|
125
|
+
what your code does.
|
|
126
|
+
|
|
121
127
|
## "ROM > 32 KB doesn't run"
|
|
122
128
|
|
|
123
129
|
The default template is single-bank (32 KB). To use the Sega
|
|
@@ -28,10 +28,13 @@
|
|
|
28
28
|
;; ─── Reset vector at $0000 ────────────────────────────────────────
|
|
29
29
|
.area _HEADER (ABS)
|
|
30
30
|
.org 0x0000
|
|
31
|
+
;; ONLY 8 BYTES fit before the RST $08 vector. The old block here
|
|
32
|
+
;; (di/im 1/ld sp/jp = 9 bytes) overflowed into .org 0x0008, whose
|
|
33
|
+
;; `ret` stomped the jp's high target byte -> boot jumped into
|
|
34
|
+
;; garbage. di+im 1+jp = 6 bytes; SP setup moved to _boot below.
|
|
31
35
|
di ; interrupts off until we're ready
|
|
32
36
|
im 1 ; mode 1 — IRQs jump to $0038
|
|
33
|
-
|
|
34
|
-
jp gsinit ; skip the interrupt vector table
|
|
37
|
+
jp _boot ; continue past the vector table
|
|
35
38
|
|
|
36
39
|
;; ─── RST handlers (default = return) ──────────────────────────────
|
|
37
40
|
.org 0x0008
|
|
@@ -69,6 +72,15 @@
|
|
|
69
72
|
.org 0x0066
|
|
70
73
|
retn
|
|
71
74
|
|
|
75
|
+
;; ─── Boot continuation (right after the NMI vector) ───────────────
|
|
76
|
+
;; SP first, then the C runtime init. Lives in the ABS header area so
|
|
77
|
+
;; it exists at a known address regardless of where _CODE is linked
|
|
78
|
+
;; (_CODE must start at >= $0100 so it can't overwrite this table).
|
|
79
|
+
.org 0x0068
|
|
80
|
+
_boot:
|
|
81
|
+
ld sp, #0xDFF0 ; stack at top of WRAM minus 16
|
|
82
|
+
jp gsinit
|
|
83
|
+
|
|
72
84
|
;; ─── crt0 body ────────────────────────────────────────────────────
|
|
73
85
|
;; Standard SDCC pattern: jump to a code area, run initializers, then
|
|
74
86
|
;; call main. The initializer area is filled by sdcc when it sees
|
|
@@ -222,6 +222,11 @@ video are fully readable, so you assert live state instead of guessing:
|
|
|
222
222
|
cpu:'spc700'})` for the sound CPU.
|
|
223
223
|
- **Audio:** the S-DSP is fully decodable — full per-voice state plus the
|
|
224
224
|
master mixer (see "Debugging sound" above for `audioDebug`).
|
|
225
|
+
- **`disasm({target:'references'})`** scans EVERY 32 KB LoROM bank (refs
|
|
226
|
+
tagged `romBank`) — a hit in bank 12 of a 1 MB cart shows up, not just
|
|
227
|
+
bank 0. `disasm({target:'project'})` likewise splits per-bank with a
|
|
228
|
+
native ca65/ld65 rebuild recipe (build() is asar, which can't consume
|
|
229
|
+
the disasm's ca65 output).
|
|
225
230
|
- **Memory regions:** `memory({op:'read'})` exposes OAM, CGRAM, ARAM (SPC700
|
|
226
231
|
audio RAM), and **FillRAM**. Note the FillRAM quirk: snes9x mirrors the
|
|
227
232
|
PPU registers $2100-$213F (OBSEL/BGMODE/TM/TS/color-math, etc.) into
|
|
@@ -248,7 +253,7 @@ PVSnesLib's `hdr.asm` fills these in.
|
|
|
248
253
|
|
|
249
254
|
## Where the SDK lives (and how to read it)
|
|
250
255
|
|
|
251
|
-
`
|
|
256
|
+
`examples({op:'fork'})` (any SNES example) ships the FULL PVSnesLib source +
|
|
252
257
|
header tree into the new project at `vendor/pvsneslib/`. So when
|
|
253
258
|
your code does `#include <snes.h>`, those headers come from
|
|
254
259
|
`vendor/pvsneslib/include/`:
|
|
@@ -291,7 +296,7 @@ Loadable via snes9x (`loadMedia`).
|
|
|
291
296
|
|
|
292
297
|
## Horizontal scrolling (for side-scrollers)
|
|
293
298
|
|
|
294
|
-
The `platformer`
|
|
299
|
+
The `platformer` example is single-screen. SNES scrolling is the easiest of
|
|
295
300
|
the tile platforms because each BG layer has its own hardware scroll register
|
|
296
301
|
and parallax is nearly free.
|
|
297
302
|
|
|
@@ -160,6 +160,45 @@ Three layers:
|
|
|
160
160
|
PVSnesLib's API is the path of least resistance. Roll your own SPC
|
|
161
161
|
driver only when you really need the control.
|
|
162
162
|
|
|
163
|
+
## "Music never starts (sfx works, sfx_init returned 0)"
|
|
164
|
+
|
|
165
|
+
A command sent to the bundled snes_sfx driver IMMEDIATELY after
|
|
166
|
+
`sfx_init()` returns can be silently swallowed. `sfx_init` returns the
|
|
167
|
+
instant the SPC echoes the jump command, but the driver then spends
|
|
168
|
+
~50 DSP port writes initialising before it seeds its command
|
|
169
|
+
edge-detector from $2140. A `sfx_music_play()` issued inside that
|
|
170
|
+
window becomes the SEED — no edge, no dispatch, music never starts.
|
|
171
|
+
|
|
172
|
+
Symptoms via the debug tools: `getAudioState({chip:'dsp'})` shows
|
|
173
|
+
voice 1 with pitch 0 / env 0; ARAM $00 (prev_cmd) already equals your
|
|
174
|
+
command byte while ARAM $01 (music_on) is 0.
|
|
175
|
+
|
|
176
|
+
Fix: put one `WaitForVBlank()` between `sfx_init()` and the first
|
|
177
|
+
`sfx_play`/`sfx_music_play` — a frame is thousands of SPC cycles, the
|
|
178
|
+
driver is guaranteed to be in its command loop. The racing example does
|
|
179
|
+
exactly this (see its `sfx_init` call site).
|
|
180
|
+
|
|
181
|
+
## "My HDMA table stops landing / OAM gets corrupted" (HDMA channel fights the OAM DMA)
|
|
182
|
+
|
|
183
|
+
A DMA channel cannot serve general-purpose DMA and HDMA in the same
|
|
184
|
+
frame — and PVSnesLib's runtime OWNS two channels for GP-DMA:
|
|
185
|
+
|
|
186
|
+
- **channel 0** — `dmaCopyVram` and friends (console text upload,
|
|
187
|
+
`oamInitGfxSet`, `consoleVblank`)
|
|
188
|
+
- **channel 7** — the VBlank ISR's OAM upload (vblank.asm rewrites
|
|
189
|
+
$4370-$4375 EVERY NMI)
|
|
190
|
+
|
|
191
|
+
Park an HDMA effect on channel 7 and it works for exactly zero frames:
|
|
192
|
+
each NMI silently rewrites the channel's DMAP/BBAD/A1T with OAM-DMA
|
|
193
|
+
parameters, so your per-scanline writes stop landing — and worse, the
|
|
194
|
+
HDMA unit then feeds your table bytes into $2104 (OAM data). The
|
|
195
|
+
failure is maddeningly partial: channels 1-6 keep working, so a
|
|
196
|
+
multi-channel effect (e.g. a Mode 7 split) comes up ALMOST right with
|
|
197
|
+
one register mysteriously stuck at a stale value.
|
|
198
|
+
|
|
199
|
+
Fix: keep HDMA on channels 1-6. The Mode 7 racing example uses 2-6 and
|
|
200
|
+
documents the assignment at its `road_hdma_on()`.
|
|
201
|
+
|
|
163
202
|
## "consoleDrawText output is corrupt / shifted"
|
|
164
203
|
|
|
165
204
|
`consoleInitText(palnum, palsize, tilfont, palfont)` configures the
|
|
@@ -220,7 +259,7 @@ synthesizes a fallback `issues[]` entry with a hint. The idioms to avoid:
|
|
|
220
259
|
crossed a bank boundary and the layout is wrong. Native interrupt vectors live
|
|
221
260
|
at `$FFE4-$FFEE`, emulation vectors at `$FFF4-$FFFF` — keep your header/vector
|
|
222
261
|
block where the layout expects it. Use
|
|
223
|
-
`
|
|
262
|
+
`examples({op:'snippets', platform:"snes", mode:"get", snippetName:"lorom_header.asm"})`
|
|
224
263
|
for the canonical layout (and `lorom_multibank.asm` for multi-bank).
|
|
225
264
|
|
|
226
265
|
(This is the asar/asm path. The default PVSnesLib **C** path goes through
|