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
package/src/playtest/playtest.js
CHANGED
|
@@ -323,6 +323,43 @@ export function letterbox(winW, winH, targetAspect) {
|
|
|
323
323
|
};
|
|
324
324
|
}
|
|
325
325
|
|
|
326
|
+
// How recently (in window ticks ≈ frames at 60fps real time) the human must
|
|
327
|
+
// have pressed something for the session to count as "human input active".
|
|
328
|
+
// 120 ticks ≈ 2 s — long enough to span the natural gaps WITHIN active play
|
|
329
|
+
// (between taps), short enough that an agent isn't warned off long after the
|
|
330
|
+
// human set the pad down.
|
|
331
|
+
export const HUMAN_INPUT_ACTIVE_FRAMES = 120;
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Any button held in a built input-port object? The C64 virtual keys
|
|
335
|
+
* (c64_f1 …) count too — any truthy value is a press.
|
|
336
|
+
* @param {Record<string, boolean>} port
|
|
337
|
+
*/
|
|
338
|
+
export function anyButtonHeld(port) {
|
|
339
|
+
for (const k in port) if (port[k]) return true;
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Pure "when did the human last actually press something" tracker behind the
|
|
345
|
+
* co-drive detection. The tick loop calls note() every unpaused frame; the
|
|
346
|
+
* session handle (and through it catalog/frame/input warnings) asks active()/
|
|
347
|
+
* framesSince(). Pure + exported so the activity contract is unit-testable
|
|
348
|
+
* without an SDL window.
|
|
349
|
+
* @param {number} [activeWindow] ticks within which a press counts as active
|
|
350
|
+
*/
|
|
351
|
+
export function createHumanInputTracker(activeWindow = HUMAN_INPUT_ACTIVE_FRAMES) {
|
|
352
|
+
let lastTick = null;
|
|
353
|
+
return {
|
|
354
|
+
/** @param {boolean} pressing @param {number} tick */
|
|
355
|
+
note(pressing, tick) { if (pressing) lastTick = tick; },
|
|
356
|
+
/** @param {number} tick @returns {number | null} null = never pressed */
|
|
357
|
+
framesSince(tick) { return lastTick == null ? null : Math.max(0, tick - lastTick); },
|
|
358
|
+
/** @param {number} tick */
|
|
359
|
+
active(tick) { return lastTick != null && tick - lastTick <= activeWindow; },
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
326
363
|
function tvAspectFor(platform, displayAspect) {
|
|
327
364
|
switch (platform) {
|
|
328
365
|
case "nes":
|
|
@@ -505,6 +542,15 @@ export async function playtest(args) {
|
|
|
505
542
|
let closeResolver = null;
|
|
506
543
|
const closedPromise = new Promise((r) => { closeResolver = r; });
|
|
507
544
|
|
|
545
|
+
// Human co-drive detection. tickCount advances every tick (even paused /
|
|
546
|
+
// mid-rebuild) so "frames since the human pressed" tracks wall time at
|
|
547
|
+
// ~60fps. humanInputDirty = the host's input state currently holds buttons
|
|
548
|
+
// WE wrote for the human — it buys exactly one release write after they let
|
|
549
|
+
// go, after which an idle window leaves the agent's setInput alone.
|
|
550
|
+
let tickCount = 0;
|
|
551
|
+
const humanInput = createHumanInputTracker();
|
|
552
|
+
let humanInputDirty = false;
|
|
553
|
+
|
|
508
554
|
// Track pixel-size from resize events instead of polling window.width every
|
|
509
555
|
// tick — that's the retroemu pattern. window.pixelWidth/height is the real
|
|
510
556
|
// backing-store size (which is what dstRect cares about); on HiDPI it
|
|
@@ -581,6 +627,7 @@ export async function playtest(args) {
|
|
|
581
627
|
|
|
582
628
|
function tick() {
|
|
583
629
|
if (!running || window.destroyed) { stop(); return; }
|
|
630
|
+
tickCount++;
|
|
584
631
|
// Resolve the session's CURRENT host this frame. A `runSource`/`loadMedia`
|
|
585
632
|
// rebuild swapped it; we follow it so the window shows the latest build.
|
|
586
633
|
// If there's transiently no host or no media loaded (mid-swap), skip this
|
|
@@ -601,8 +648,9 @@ export async function playtest(args) {
|
|
|
601
648
|
const paused = !!h.status.paused || !!h._renderTickSuspended;
|
|
602
649
|
// Read controller state for each slot independently. Slot 0 = port 0
|
|
603
650
|
// (player 1), slot 1 = port 1 (player 2). Each slot's input is built
|
|
604
|
-
// into its own port object
|
|
605
|
-
//
|
|
651
|
+
// into its own port object. The agent's setInput is only overwritten
|
|
652
|
+
// while the human is ACTUALLY pressing (see the write below) — an idle
|
|
653
|
+
// window leaves it alone. Select+Start on any controller quits.
|
|
606
654
|
let quit = false;
|
|
607
655
|
const isC64 = h.status?.platform === "c64";
|
|
608
656
|
function readControllerInto(port, inst) {
|
|
@@ -667,7 +715,12 @@ export async function playtest(args) {
|
|
|
667
715
|
if (heldKeys.has(keyName)) port0[vbtn] = true;
|
|
668
716
|
}
|
|
669
717
|
}
|
|
718
|
+
// Did the human actually press anything this tick (pad or keyboard,
|
|
719
|
+
// either port)? Rewind-scrubbing counts as activity too — the human is
|
|
720
|
+
// actively manipulating emulator state even though R maps to no button.
|
|
721
|
+
const humanPressing = anyButtonHeld(port0) || anyButtonHeld(port1);
|
|
670
722
|
const isRewinding = heldKeys.has("r") && rewindBuffer.length > 0;
|
|
723
|
+
humanInput.note(humanPressing || isRewinding, tickCount);
|
|
671
724
|
if (isRewinding) {
|
|
672
725
|
// Restore the previous snapshot and run one frame to produce its visual.
|
|
673
726
|
const snap = rewindBuffer.pop();
|
|
@@ -687,7 +740,16 @@ export async function playtest(args) {
|
|
|
687
740
|
if (rewindBuffer.length > MAX_REWIND_FRAMES) rewindBuffer.shift();
|
|
688
741
|
} catch {}
|
|
689
742
|
}
|
|
690
|
-
|
|
743
|
+
// Write input ONLY while the human is actually pressing, plus ONE
|
|
744
|
+
// release write after they let go (humanInputDirty). The old behavior
|
|
745
|
+
// wrote all-zeros EVERY tick, which silently clobbered the agent's
|
|
746
|
+
// input({op:'set'}) even when nobody was touching the pad. An idle
|
|
747
|
+
// window now leaves the host's input state alone; the human still
|
|
748
|
+
// wins the instant they press.
|
|
749
|
+
if (humanPressing || humanInputDirty) {
|
|
750
|
+
h.setInput({ ports: [port0, port1] });
|
|
751
|
+
humanInputDirty = humanPressing;
|
|
752
|
+
}
|
|
691
753
|
let stepped = 0;
|
|
692
754
|
try {
|
|
693
755
|
stepped = h.stepFrames(1);
|
|
@@ -817,6 +879,14 @@ export async function playtest(args) {
|
|
|
817
879
|
// hot-plug), so a caller can decide whether to surface the keyboard help.
|
|
818
880
|
// 0 → the user has no pad and is on the keyboard fallback.
|
|
819
881
|
get controllerCount() { return controllers.filter(Boolean).length; },
|
|
882
|
+
// Human co-drive detection: has the human pressed anything (pad, keyboard,
|
|
883
|
+
// or rewind-scrub) within the last ~2 s of window ticks? Drives the
|
|
884
|
+
// catalog({op:'status'}) flags and the frame/input co-drive warnings so an
|
|
885
|
+
// agent KNOWS when a human is driving the same emulator.
|
|
886
|
+
humanInputActive() { return humanInput.active(tickCount); },
|
|
887
|
+
// Ticks (≈ frames at 60fps real time) since the last human press; null if
|
|
888
|
+
// the human hasn't touched anything since the window opened.
|
|
889
|
+
framesSinceHumanInput() { return humanInput.framesSince(tickCount); },
|
|
820
890
|
// The emulator host the window is CURRENTLY rendering. The window follows
|
|
821
891
|
// the session's live host (a `runSource`/`loadMedia` rebuild updates it in
|
|
822
892
|
// place), so this is whatever the human is looking at right now. Exposed so
|
|
@@ -19,13 +19,15 @@
|
|
|
19
19
|
# 3. Write CHR data from C at runtime: PPUADDR = 0x00; PPUDATA = byte; etc.
|
|
20
20
|
|
|
21
21
|
SYMBOLS {
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
|
|
22
|
+
# C parameter stack is ONE page ($0600-$06FF, grows down from $0700).
|
|
23
|
+
# NROM-sized C games use far less than 256 B of it (shallow call
|
|
24
|
+
# depth, mostly static data), and shrinking it frees $0500-$05FF as
|
|
25
|
+
# the USER SCRATCH PAGE: game code may place absolute-addressed
|
|
26
|
+
# arrays there (e.g. `#define BOARD ((unsigned char*)0x0500)`) when
|
|
27
|
+
# BSS ($0300-$04FF) is full — the puzzle example game does exactly
|
|
28
|
+
# this. The top page ($0700-$07FF) stays reserved for a music
|
|
29
|
+
# driver's scratch RAM (FamiTone2 et al.).
|
|
30
|
+
__STACKSIZE__: type = weak, value = $0100;
|
|
29
31
|
}
|
|
30
32
|
MEMORY {
|
|
31
33
|
ZP: file = "", start = $0002, size = $001A, type = rw, define = yes;
|
|
@@ -41,7 +43,10 @@ MEMORY {
|
|
|
41
43
|
|
|
42
44
|
# NO ROM2 / CHARS — this is the whole point of the CHR-RAM preset.
|
|
43
45
|
|
|
44
|
-
|
|
46
|
+
# $0500-$05FF: user scratch page (see SYMBOLS note) — NOT a segment;
|
|
47
|
+
# game code addresses it absolutely so the linker never places
|
|
48
|
+
# anything here.
|
|
49
|
+
SRAM: file = "", start = $0600, size = __STACKSIZE__, define = yes;
|
|
45
50
|
|
|
46
51
|
# Reserved page for a sound-driver's RAM scratch ($0700-$07FF). The
|
|
47
52
|
# bundled FamiTone2 engine (music_demo scaffold) pins FT_BASE_ADR here
|
|
@@ -28,7 +28,12 @@
|
|
|
28
28
|
.import _main, zerobss, copydata
|
|
29
29
|
.import __RAM_START__, __RAM_SIZE__
|
|
30
30
|
.import __SRAM_START__, __SRAM_SIZE__
|
|
31
|
-
.import
|
|
31
|
+
.import _vram_q_hi, _vram_q_lo, _vram_q_val
|
|
32
|
+
.import _vram_queue_head, _vram_queue_len, _vram_queue_lock
|
|
33
|
+
|
|
34
|
+
; Must match nes_runtime.c (QUEUE_MAX 32 ring buffer).
|
|
35
|
+
QUEUE_MASK = 31
|
|
36
|
+
FLUSH_BUDGET = 16
|
|
32
37
|
.import _scroll_x, _scroll_y, _ppuctrl_value, _nmi_counter
|
|
33
38
|
.importzp c_sp
|
|
34
39
|
|
|
@@ -39,7 +44,12 @@
|
|
|
39
44
|
.byte $4e, $45, $53, $1a ; "NES" + EOF
|
|
40
45
|
.byte 2 ; PRG-ROM banks (16K each) → 32K
|
|
41
46
|
.byte 0 ; CHR-ROM banks (8K each) → 0 = CHR-RAM
|
|
42
|
-
.byte %
|
|
47
|
+
.byte %00000011 ; flags6 — vertical mirroring + BATTERY.
|
|
48
|
+
; The battery bit maps persistent 8KB
|
|
49
|
+
; PRG-RAM at $6000 (the save_ram region)
|
|
50
|
+
; — hiscore_load/save in nes_runtime use
|
|
51
|
+
; it. Benign when unused; without it,
|
|
52
|
+
; $6000-$7FFF is OPEN BUS on NROM.
|
|
43
53
|
.byte %00000000 ; flags7 — mapper hi nybble
|
|
44
54
|
.byte 0, 0, 0, 0, 0, 0, 0, 0
|
|
45
55
|
|
|
@@ -129,9 +139,46 @@ nmi:
|
|
|
129
139
|
lda #$02 ; high byte of $0200
|
|
130
140
|
sta $4014 ; PPU OAMDMA — kicks off the copy
|
|
131
141
|
|
|
132
|
-
;
|
|
133
|
-
;
|
|
134
|
-
|
|
142
|
+
; ── Drain the VRAM queue — IN ASSEMBLY, on purpose ──────────────
|
|
143
|
+
; Vblank is ~2273 CPU cycles and the OAM DMA above just spent 513.
|
|
144
|
+
; Compiled C costs 200+ cycles per queue entry, so a C flush blows
|
|
145
|
+
; past the end of vblank — and PPUDATA writes during ACTIVE
|
|
146
|
+
; RENDERING land at corrupted addresses (the PPU's internal v
|
|
147
|
+
; register is busy fetching tiles; its coarse-X/fine-Y counters
|
|
148
|
+
; shear every late write). This loop costs ~40 cycles per entry,
|
|
149
|
+
; so FLUSH_BUDGET entries always finish safely inside vblank.
|
|
150
|
+
; QUEUE_MASK/FLUSH_BUDGET must match nes_runtime.c's ring buffer.
|
|
151
|
+
lda _vram_queue_lock
|
|
152
|
+
bne @flush_done ; a push is mid-flight — skip this vblank
|
|
153
|
+
lda _vram_queue_len
|
|
154
|
+
beq @flush_done
|
|
155
|
+
cmp #FLUSH_BUDGET
|
|
156
|
+
bcc @flush_n_ok
|
|
157
|
+
lda #FLUSH_BUDGET
|
|
158
|
+
@flush_n_ok:
|
|
159
|
+
sta nmi_drain ; loop counter
|
|
160
|
+
sta nmi_drained ; remembered for the length update
|
|
161
|
+
bit $2002 ; reset the PPUADDR write latch
|
|
162
|
+
ldx _vram_queue_head
|
|
163
|
+
@flush_loop:
|
|
164
|
+
lda _vram_q_hi,x
|
|
165
|
+
sta $2006
|
|
166
|
+
lda _vram_q_lo,x
|
|
167
|
+
sta $2006
|
|
168
|
+
lda _vram_q_val,x
|
|
169
|
+
sta $2007
|
|
170
|
+
inx
|
|
171
|
+
txa
|
|
172
|
+
and #QUEUE_MASK ; ring wrap
|
|
173
|
+
tax
|
|
174
|
+
dec nmi_drain
|
|
175
|
+
bne @flush_loop
|
|
176
|
+
stx _vram_queue_head
|
|
177
|
+
lda _vram_queue_len
|
|
178
|
+
sec
|
|
179
|
+
sbc nmi_drained
|
|
180
|
+
sta _vram_queue_len
|
|
181
|
+
@flush_done:
|
|
135
182
|
|
|
136
183
|
; Reset PPUADDR to $2000 (otherwise the queue's last $2006 write
|
|
137
184
|
; leaves it dangling and the PPU samples random VRAM as the BG).
|
|
@@ -172,6 +219,12 @@ irq: rti
|
|
|
172
219
|
_shadow_oam: .res 256
|
|
173
220
|
|
|
174
221
|
; ------------------------------------------------------------------------
|
|
222
|
+
; NMI-private temporaries — deliberately NOT cc65's zp tmp1-4 (the NMI
|
|
223
|
+
; would corrupt them under interrupted C code).
|
|
224
|
+
.segment "BSS"
|
|
225
|
+
nmi_drain: .res 1
|
|
226
|
+
nmi_drained: .res 1
|
|
227
|
+
|
|
175
228
|
.segment "VECTORS"
|
|
176
229
|
.word nmi ; $FFFA
|
|
177
230
|
.word start ; $FFFC
|
|
@@ -22,7 +22,12 @@
|
|
|
22
22
|
.import _main, zerobss, copydata
|
|
23
23
|
.import __RAM_START__, __RAM_SIZE__
|
|
24
24
|
.import __SRAM_START__, __SRAM_SIZE__
|
|
25
|
-
.import
|
|
25
|
+
.import _vram_q_hi, _vram_q_lo, _vram_q_val
|
|
26
|
+
.import _vram_queue_head, _vram_queue_len, _vram_queue_lock
|
|
27
|
+
|
|
28
|
+
; Must match nes_runtime.c (QUEUE_MAX 32 ring buffer).
|
|
29
|
+
QUEUE_MASK = 31
|
|
30
|
+
FLUSH_BUDGET = 16
|
|
26
31
|
.import _scroll_x, _scroll_y, _ppuctrl_value, _nmi_counter
|
|
27
32
|
.importzp c_sp
|
|
28
33
|
|
|
@@ -109,8 +114,46 @@ nmi:
|
|
|
109
114
|
lda #$02 ; high byte of $0200
|
|
110
115
|
sta $4014 ; PPU OAMDMA — kicks off the copy
|
|
111
116
|
|
|
112
|
-
;
|
|
113
|
-
|
|
117
|
+
; ── Drain the VRAM queue — IN ASSEMBLY, on purpose ──────────────
|
|
118
|
+
; Vblank is ~2273 CPU cycles and the OAM DMA above just spent 513.
|
|
119
|
+
; Compiled C costs 200+ cycles per queue entry, so a C flush blows
|
|
120
|
+
; past the end of vblank — and PPUDATA writes during ACTIVE
|
|
121
|
+
; RENDERING land at corrupted addresses (the PPU's internal v
|
|
122
|
+
; register is busy fetching tiles; its coarse-X/fine-Y counters
|
|
123
|
+
; shear every late write). This loop costs ~40 cycles per entry,
|
|
124
|
+
; so FLUSH_BUDGET entries always finish safely inside vblank.
|
|
125
|
+
; QUEUE_MASK/FLUSH_BUDGET must match nes_runtime.c's ring buffer.
|
|
126
|
+
lda _vram_queue_lock
|
|
127
|
+
bne @flush_done ; a push is mid-flight — skip this vblank
|
|
128
|
+
lda _vram_queue_len
|
|
129
|
+
beq @flush_done
|
|
130
|
+
cmp #FLUSH_BUDGET
|
|
131
|
+
bcc @flush_n_ok
|
|
132
|
+
lda #FLUSH_BUDGET
|
|
133
|
+
@flush_n_ok:
|
|
134
|
+
sta nmi_drain ; loop counter
|
|
135
|
+
sta nmi_drained ; remembered for the length update
|
|
136
|
+
bit $2002 ; reset the PPUADDR write latch
|
|
137
|
+
ldx _vram_queue_head
|
|
138
|
+
@flush_loop:
|
|
139
|
+
lda _vram_q_hi,x
|
|
140
|
+
sta $2006
|
|
141
|
+
lda _vram_q_lo,x
|
|
142
|
+
sta $2006
|
|
143
|
+
lda _vram_q_val,x
|
|
144
|
+
sta $2007
|
|
145
|
+
inx
|
|
146
|
+
txa
|
|
147
|
+
and #QUEUE_MASK ; ring wrap
|
|
148
|
+
tax
|
|
149
|
+
dec nmi_drain
|
|
150
|
+
bne @flush_loop
|
|
151
|
+
stx _vram_queue_head
|
|
152
|
+
lda _vram_queue_len
|
|
153
|
+
sec
|
|
154
|
+
sbc nmi_drained
|
|
155
|
+
sta _vram_queue_len
|
|
156
|
+
@flush_done:
|
|
114
157
|
|
|
115
158
|
; Reset PPUADDR to $2000 so the PPU doesn't sample random VRAM as BG.
|
|
116
159
|
bit $2002
|
|
@@ -147,6 +190,12 @@ irq: rti
|
|
|
147
190
|
_shadow_oam: .res 256
|
|
148
191
|
|
|
149
192
|
; ------------------------------------------------------------------------
|
|
193
|
+
; NMI-private temporaries — deliberately NOT cc65's zp tmp1-4 (the NMI
|
|
194
|
+
; would corrupt them under interrupted C code).
|
|
195
|
+
.segment "BSS"
|
|
196
|
+
nmi_drain: .res 1
|
|
197
|
+
nmi_drained: .res 1
|
|
198
|
+
|
|
150
199
|
.segment "VECTORS"
|
|
151
200
|
.word nmi ; $FFFA
|
|
152
201
|
.word start ; $FFFC
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# PC Engine 32KB HuCard ld65 config (romdev 'rom32k' preset).
|
|
2
|
+
#
|
|
3
|
+
# WHY THIS EXISTS: cc65's stock pce.cfg defaults to an 8KB image, and its
|
|
4
|
+
# documented 16K/32K option ($CARTSIZE) places STARTUP/VECTORS at the END of
|
|
5
|
+
# the file — but a HuCard maps file offset 0 as bank 0, and the HuC6280 reset
|
|
6
|
+
# maps MPR7 (=$E000-$FFFF, where the vectors live) to BANK 0. So a stock-cfg
|
|
7
|
+
# 32K image boots to a black screen (verified on geargrafx). This config puts
|
|
8
|
+
# bank 0 (STARTUP/VECTORS + hot code) FIRST in the file, at $E000, and the
|
|
9
|
+
# remaining 24KB of banks 1-3 at $8000-$DFFF — exactly where cc65's pce crt0
|
|
10
|
+
# TAMs them (MPR4=bank1, MPR5=bank2, MPR6=bank3) before calling main().
|
|
11
|
+
SYMBOLS {
|
|
12
|
+
__CARTSIZE__: type = weak, value = $8000; # crt0 compares >$8000 vs this
|
|
13
|
+
__STACKSIZE__: type = weak, value = $0300; # 3 pages stack
|
|
14
|
+
}
|
|
15
|
+
MEMORY {
|
|
16
|
+
ZP: file = "", start = $0000, define = yes, size = $0100;
|
|
17
|
+
# RAM bank ($F8 at MPR1)
|
|
18
|
+
MAIN: file = "", start = $2200, define = yes, size = $1E00 - __STACKSIZE__;
|
|
19
|
+
# HuCard bank 0 — hardware maps it at $E000 (MPR7) at reset. File offset 0.
|
|
20
|
+
ROM0: file = %O, start = $E000, size = $2000, fill = yes, fillval = $FF;
|
|
21
|
+
# HuCard banks 1-3 — crt0 maps them at $8000/$A000/$C000 (MPR4/5/6).
|
|
22
|
+
ROM: file = %O, start = $8000, size = $6000, fill = yes, fillval = $FF;
|
|
23
|
+
}
|
|
24
|
+
SEGMENTS {
|
|
25
|
+
ZEROPAGE: load = ZP, type = zp;
|
|
26
|
+
EXTZP: load = ZP, type = zp, optional = yes;
|
|
27
|
+
APPZP: load = ZP, type = zp, optional = yes;
|
|
28
|
+
DATA: load = ROM0, run = MAIN, type = rw, define = yes;
|
|
29
|
+
INIT: load = MAIN, type = bss, optional = yes;
|
|
30
|
+
BSS: load = MAIN, type = bss, define = yes;
|
|
31
|
+
LOWCODE: load = ROM0, type = ro, optional = yes;
|
|
32
|
+
ONCE: load = ROM0, type = ro, optional = yes;
|
|
33
|
+
CODE: load = ROM, type = ro;
|
|
34
|
+
RODATA: load = ROM, type = ro;
|
|
35
|
+
STARTUP: load = ROM0, type = ro, start = $FFF6 - $0066;
|
|
36
|
+
VECTORS: load = ROM0, type = ro, start = $FFF6;
|
|
37
|
+
}
|
|
38
|
+
FEATURES {
|
|
39
|
+
CONDES: type = constructor,
|
|
40
|
+
label = __CONSTRUCTOR_TABLE__,
|
|
41
|
+
count = __CONSTRUCTOR_COUNT__,
|
|
42
|
+
segment = ONCE;
|
|
43
|
+
CONDES: type = destructor,
|
|
44
|
+
label = __DESTRUCTOR_TABLE__,
|
|
45
|
+
count = __DESTRUCTOR_COUNT__,
|
|
46
|
+
segment = RODATA;
|
|
47
|
+
CONDES: type = interruptor,
|
|
48
|
+
label = __INTERRUPTOR_TABLE__,
|
|
49
|
+
count = __INTERRUPTOR_COUNT__,
|
|
50
|
+
segment = RODATA,
|
|
51
|
+
import = __CALLIRQ__;
|
|
52
|
+
}
|
package/src/toolchains/index.js
CHANGED
|
@@ -37,7 +37,7 @@ const CC65_TARGET = {
|
|
|
37
37
|
const LANGUAGE_TOOLCHAIN = {
|
|
38
38
|
atari2600: {
|
|
39
39
|
asm: { toolchain: "dasm", available: true },
|
|
40
|
-
basic: { toolchain: "batariBasic", available: false, note: "BASIC for 2600 via batariBasic — not bundled. bB's transpiler is written in Perl, which we don't ship as WASM. A port to C or JS would be a multi-day project. For now, write 2600 games in 6507 asm via dasm — the bundled
|
|
40
|
+
basic: { toolchain: "batariBasic", available: false, note: "BASIC for 2600 via batariBasic — not bundled. bB's transpiler is written in Perl, which we don't ship as WASM. A port to C or JS would be a multi-day project. For now, write 2600 games in 6507 asm via dasm — the bundled example games (default, paddle, single_screen) show the canonical race-the-beam pattern, and an LLM agent writes 2600 asm fluently." },
|
|
41
41
|
},
|
|
42
42
|
nes: {
|
|
43
43
|
asm: { toolchain: "cc65", available: true },
|
|
@@ -65,11 +65,11 @@ const LANGUAGE_TOOLCHAIN = {
|
|
|
65
65
|
},
|
|
66
66
|
snes: {
|
|
67
67
|
asm: { toolchain: "asar", available: true },
|
|
68
|
-
c: { toolchain: "tcc816+wladx", available: true, note: "C for SNES via tcc-65816 + wla-65816 + wlalink. The PVSnesLib runtime IS bundled (built from source) and auto-linked — #include <snes.h> gives you consoleDrawText, setMode, oamSet, WaitForVBlank, etc. out of the box. `
|
|
68
|
+
c: { toolchain: "tcc816+wladx", available: true, note: "C for SNES via tcc-65816 + wla-65816 + wlalink. The PVSnesLib runtime IS bundled (built from source) and auto-linked — #include <snes.h> gives you consoleDrawText, setMode, oamSet, WaitForVBlank, etc. out of the box. `examples({op:'fork'})` gives you a complete working PVSnesLib C project. Pass options.pvsneslib:false for the bare-main minimum-viable path." },
|
|
69
69
|
},
|
|
70
70
|
genesis: {
|
|
71
71
|
asm: { toolchain: "vasm68k", available: true },
|
|
72
|
-
c: { toolchain: "m68k-elf-gcc", available: true, note: "C for Genesis via gcc 14.2.0 + binutils + newlib, all compiled to WASM. The SGDK runtime IS bundled (built from source) and auto-linked — sprite engine, VDP, controller, PSG/Z80 sound, resource helpers all work; #include <genesis.h>. `
|
|
72
|
+
c: { toolchain: "m68k-elf-gcc", available: true, note: "C for Genesis via gcc 14.2.0 + binutils + newlib, all compiled to WASM. The SGDK runtime IS bundled (built from source) and auto-linked — sprite engine, VDP, controller, PSG/Z80 sound, resource helpers all work; #include <genesis.h>. `examples({op:'fork'})` gives you a complete working SGDK C project (the recommended path). Pass options.sgdk:false for the bare-gcc minimum-viable path." },
|
|
73
73
|
},
|
|
74
74
|
gba: {
|
|
75
75
|
c: { toolchain: "arm-none-eabi-gcc", available: true, note: "C for GBA via gcc 14.2.0 + binutils + newlib + libtonc 1.4.5 (default) OR libgba 0.5.4 (opt-in via runtime:\"libgba\"), all compiled to WASM (R24 + R28). #include <tonc.h> + tte_write/tte_printf works out of the box — that's the canonical Tonc-tutorial API every published GBA C resource uses. Caveat: tte_iohook (libtonc) and console.c (libgba) — the libsysbase-backed iprintf bridges — are NOT bundled. Use tte_printf directly, which is what the Tonc tutorial actually does." },
|
|
@@ -91,7 +91,7 @@ const LANGUAGE_TOOLCHAIN = {
|
|
|
91
91
|
* Default language per platform. The choice reflects what's fastest /
|
|
92
92
|
* smallest / best-matched to LLM fluency. Every platform that has a bundled
|
|
93
93
|
* C compiler + runtime defaults to C — that's the canonical, productive path
|
|
94
|
-
* and what `
|
|
94
|
+
* and what `examples({op:'fork'})` projects use (cc65 for NES/C64/Atari7800/
|
|
95
95
|
* Lynx, SDCC for GB/GBC/SMS/GG, gcc+SGDK for Genesis, tcc+PVSnesLib for SNES,
|
|
96
96
|
* gcc+libtonc for GBA). Platforms whose only bundled toolchain is an assembler
|
|
97
97
|
* default to asm (Atari 2600 → dasm; SNES/Genesis keep an asm option too, but
|
|
@@ -727,16 +727,25 @@ export async function buildForPlatform(args) {
|
|
|
727
727
|
// the cartridge header + reset vectors which the custom crt0 provides.
|
|
728
728
|
// MSX: _CODE goes at $4010 — a cartridge maps at $4000-$BFFF and the first
|
|
729
729
|
// 16 bytes are the ROM header ("AB" + INIT vector) the crt0 emits.
|
|
730
|
-
|
|
730
|
+
// SMS/GG: _CODE goes at $0100 — $0000-$00FF belongs to the crt0's ABS
|
|
731
|
+
// _HEADER area (reset + RST/IRQ/NMI vectors + _boot). The old default of
|
|
732
|
+
// $0000 linked _CODE ON TOP of the vector table: makebin emitted gsinit
|
|
733
|
+
// at $0000 and the di/im 1/SP-init/ISR vectors were GONE — it booted in a
|
|
734
|
+
// BIOS-less emulator by accident (gsinit happened to sit at the reset
|
|
735
|
+
// vector) but had no working IRQ/NMI/pause handling and was one EI away
|
|
736
|
+
// from jumping into garbage on real hardware.
|
|
737
|
+
const codeLoc = args.codeLoc ?? (
|
|
738
|
+
args.platform === "msx" ? MSX_CODE_LOC
|
|
739
|
+
: (args.platform === "sms" || args.platform === "gg") ? 0x0100
|
|
740
|
+
: 0x0000);
|
|
731
741
|
const romSize = SDCC_ROM_SIZE[args.platform] ?? 32 * 1024;
|
|
732
742
|
|
|
733
743
|
// crt0 + headers + sources come straight from the caller. The build
|
|
734
744
|
// pipeline does NOT auto-inject platform runtimes, custom crt0s,
|
|
735
745
|
// or post-link header patches. Every byte that compiles is visible
|
|
736
|
-
// to the caller's repo. Use `
|
|
737
|
-
//
|
|
738
|
-
//
|
|
739
|
-
// to fetch individual pieces.
|
|
746
|
+
// to the caller's repo. Use `examples({op:'fork'})` to get a
|
|
747
|
+
// self-contained project with the runtime files copied in, or
|
|
748
|
+
// `examples({op:'snippets'/'copySnippets'})` to fetch individual pieces.
|
|
740
749
|
const crt0 = args.crt0;
|
|
741
750
|
|
|
742
751
|
// Pre-flight lint: scan the C sources for known SDCC C89 violations
|
|
@@ -777,10 +786,27 @@ export async function buildForPlatform(args) {
|
|
|
777
786
|
// and RAM-size ($0149) bytes — without -m/-r, -v leaves them at the
|
|
778
787
|
// linker's garbage pad (e.g. type $3C), and emulators/hardware reject
|
|
779
788
|
// an unknown MBC type with "retro_load_game failed". -m 0x00 = ROM ONLY
|
|
780
|
-
// (no mapper), -r 0x00 = no cart RAM — correct for
|
|
789
|
+
// (no mapper), -r 0x00 = no cart RAM — correct for plain 32KB builds.
|
|
790
|
+
//
|
|
791
|
+
// Battery-cart passthrough (0.29.0 examples): a crt0 may DECLARE the
|
|
792
|
+
// cart in the header window (the GB equivalent of the NES crt0's iNES
|
|
793
|
+
// BATTERY bit — see the gbc lib gb_crt0.s, which emits $0147=$03 /
|
|
794
|
+
// $0149=$02 for MBC1+RAM+BATTERY so hi-scores persist in SAVE_RAM).
|
|
795
|
+
// If the linked image carries a KNOWN battery-MBC type byte with a
|
|
796
|
+
// sane RAM size, pass those through to rgbfix instead of stomping
|
|
797
|
+
// them to ROM-only; anything unrecognized (linker pad garbage) still
|
|
798
|
+
// falls back to the safe ROM-only default, so crt0s that don't
|
|
799
|
+
// declare a cart behave exactly as before.
|
|
800
|
+
const BATTERY_CART_TYPES = new Set([0x03, 0x06, 0x0F, 0x10, 0x13, 0x1B, 0x1E]); // MBC1/2/3/5 +BATTERY variants
|
|
801
|
+
const declType = binary.length > 0x149 ? binary[0x147] : 0x00;
|
|
802
|
+
const declRam = binary.length > 0x149 ? binary[0x149] : 0x00;
|
|
803
|
+
const cartByte = BATTERY_CART_TYPES.has(declType) ? declType : 0x00;
|
|
804
|
+
const ramByte = cartByte !== 0x00 && declRam >= 0x01 && declRam <= 0x05 ? declRam : 0x00;
|
|
805
|
+
const mArg = "0x" + cartByte.toString(16).padStart(2, "0").toUpperCase();
|
|
806
|
+
const rArg = "0x" + ramByte.toString(16).padStart(2, "0").toUpperCase();
|
|
781
807
|
const fixOpts = args.platform === "gbc"
|
|
782
|
-
? ["-v", "-p", "0xFF", "-C", "-m",
|
|
783
|
-
: ["-v", "-p", "0xFF", "-m",
|
|
808
|
+
? ["-v", "-p", "0xFF", "-C", "-m", mArg, "-r", rArg]
|
|
809
|
+
: ["-v", "-p", "0xFF", "-m", mArg, "-r", rArg];
|
|
784
810
|
const fix = await runRgbfix({ rom: binary, options: fixOpts });
|
|
785
811
|
if (fix.exitCode === 0 && fix.binary) {
|
|
786
812
|
binary = fix.binary;
|
|
@@ -809,32 +835,51 @@ export async function buildForPlatform(args) {
|
|
|
809
835
|
// rejected it. Checksum = sum of bytes $0000..$7FEF (everything before
|
|
810
836
|
// the header), stored little-endian. GG BIOS doesn't check, but writing
|
|
811
837
|
// it is harmless. Only touches ROMs that actually have the header.
|
|
812
|
-
if (binary && r.exitCode === 0 && (args.platform === "sms" || args.platform === "gg")
|
|
838
|
+
if (binary && r.exitCode === 0 && (args.platform === "sms" || args.platform === "gg")) {
|
|
839
|
+
// Pad to a full 32KB bank FIRST. sdld emits up to the highest used
|
|
840
|
+
// address, so a small program can come out under $8000 — which (a)
|
|
841
|
+
// skipped this whole header block before (the header guard required
|
|
842
|
+
// 32KB) and (b) odd-size ROMs misbehave on real mappers/flashcarts.
|
|
843
|
+
if (binary.length < 0x8000) {
|
|
844
|
+
const padded = new Uint8Array(0x8000);
|
|
845
|
+
padded.set(binary);
|
|
846
|
+
binary = padded;
|
|
847
|
+
}
|
|
813
848
|
const hdr = 0x7FF0;
|
|
814
849
|
const hasHeader = String.fromCharCode(...binary.slice(hdr, hdr + 8)) === "TMR SEGA";
|
|
850
|
+
// Region nibble is PLATFORM-SPECIFIC and load-bearing: 4 = SMS export,
|
|
851
|
+
// 7 = GG international. A .gg ROM stamped with an SMS region (3/4) makes
|
|
852
|
+
// Genesis Plus GX (RetroArch/RetroDECK's SMS+GG core) boot it in "GG
|
|
853
|
+
// running SMS software" COMPATIBILITY mode — wrong video mode + wrong
|
|
854
|
+
// CRAM format for a native-GG program → black/garbled screen on the
|
|
855
|
+
// user's device while our BIOS-less host looked fine. Size nibble $C =
|
|
856
|
+
// 32KB checksum range ($0000-$7FEF).
|
|
857
|
+
const regionSize = args.platform === "gg" ? 0x7C : 0x4C;
|
|
815
858
|
if (!hasHeader) {
|
|
816
859
|
// No header emitted by the crt0 → write a complete TMR SEGA header
|
|
817
860
|
// into the last 16 bytes of bank 0 ($7FF0-$7FFF). Without this the
|
|
818
861
|
// export (US/EU) SMS BIOS shows "SOFTWARE ERROR" and refuses to run.
|
|
819
862
|
// $7FF0-$7FF7 "TMR SEGA"; $7FF8-$7FF9 reserved ($00); $7FFA-$7FFB
|
|
820
863
|
// checksum (filled below); $7FFC-$7FFE product code/version (zeros
|
|
821
|
-
// ok for homebrew); $7FFF region+size
|
|
822
|
-
// size $C = 32KB, the checksum range that covers $0000-$7FEF).
|
|
864
|
+
// ok for homebrew); $7FFF region+size (see regionSize above).
|
|
823
865
|
const TMR = [0x54,0x4D,0x52,0x20,0x53,0x45,0x47,0x41]; // "TMR SEGA"
|
|
824
866
|
for (let i = 0; i < 8; i++) binary[hdr + i] = TMR[i];
|
|
825
867
|
binary[hdr + 8] = 0x00; binary[hdr + 9] = 0x00; // reserved
|
|
826
868
|
binary[hdr + 12] = 0x00; binary[hdr + 13] = 0x00; // product code lo
|
|
827
869
|
binary[hdr + 14] = 0x00; // product/version
|
|
828
|
-
binary[hdr + 15] = 0x4C; // region 4 (export) + size $C (32KB)
|
|
829
870
|
}
|
|
871
|
+
// Always stamp the platform-correct region/size — a crt0-provided header
|
|
872
|
+
// with an SMS region on a .gg build has the same compat-mode problem.
|
|
873
|
+
binary[hdr + 15] = regionSize;
|
|
830
874
|
// Checksum = sum of bytes $0000..$7FEF (everything before the header),
|
|
831
|
-
// stored little-endian at $7FFA.
|
|
832
|
-
// range, so the BIOS checksums $0000-$7FEF.
|
|
875
|
+
// stored little-endian at $7FFA. Size nibble $C declares the 32KB
|
|
876
|
+
// range, so the BIOS checksums $0000-$7FEF. (The GG BIOS doesn't
|
|
877
|
+
// checksum, but writing it is harmless and correct.)
|
|
833
878
|
let sum = 0;
|
|
834
879
|
for (let i = 0; i < 0x7FF0; i++) sum = (sum + binary[i]) & 0xFFFF;
|
|
835
880
|
binary[0x7FFA] = sum & 0xFF;
|
|
836
881
|
binary[0x7FFB] = (sum >> 8) & 0xFF;
|
|
837
|
-
r.log += `\n---
|
|
882
|
+
r.log += `\n--- ${args.platform.toUpperCase()} header ${hasHeader ? "checksum fixed" : "written + checksummed"} ($7FFA=${sum.toString(16).toUpperCase().padStart(4,"0")}, region/size=$${regionSize.toString(16).toUpperCase()}) ---`;
|
|
838
883
|
}
|
|
839
884
|
// MSX: the binary built with codeLoc=$4010 is a $4000-based page image.
|
|
840
885
|
// SDCC/sdldz80 emit an ihx that, converted to bin, starts at the lowest
|