romdevtools 0.71.1 → 0.84.1
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 +19 -16
- package/CHANGELOG.md +184 -0
- package/README.md +5 -5
- package/examples/README.md +1 -0
- package/examples/gametank/templates/gt_draw.h +91 -0
- package/examples/gametank/templates/gt_hud.h +89 -0
- package/examples/gametank/templates/gt_palette.h +59 -0
- package/examples/gametank/templates/gt_sound.h +90 -0
- package/examples/gametank/templates/gt_sprites.h +53 -0
- package/examples/gametank/templates/platformer.c +318 -0
- package/examples/gametank/templates/puzzle.c +295 -0
- package/examples/gametank/templates/racing.c +139 -0
- package/examples/gametank/templates/shmup.c +277 -0
- package/examples/gametank/templates/sports.c +119 -0
- package/package.json +24 -22
- package/src/cores/capabilities.js +24 -0
- package/src/cores/registry.js +7 -1
- package/src/host/LibretroHost.js +90 -17
- package/src/host/callbacks.js +20 -2
- package/src/host/cpu-state.js +17 -0
- package/src/host/gametank-acp-state.js +53 -0
- package/src/http/skill-doc.js +10 -6
- package/src/mcp/tools/audio.js +1 -1
- package/src/mcp/tools/cart-parts.js +76 -0
- package/src/mcp/tools/lifecycle.js +1 -1
- package/src/mcp/tools/platform-tools.js +9 -1
- package/src/platforms/gametank/lib/gt/audio/acp_image.bin +0 -0
- package/src/platforms/gametank/lib/gt/audio/audio_coprocessor.c +55 -0
- package/src/platforms/gametank/lib/gt/audio/audio_coprocessor.h +34 -0
- package/src/platforms/gametank/lib/gt/audio/audio_fw.asm +223 -0
- package/src/platforms/gametank/lib/gt/audio/audio_fw_blob.s +8 -0
- package/src/platforms/gametank/lib/gt/audio/instruments.c +79 -0
- package/src/platforms/gametank/lib/gt/audio/instruments.h +25 -0
- package/src/platforms/gametank/lib/gt/audio/music.c +370 -0
- package/src/platforms/gametank/lib/gt/audio/music.h +35 -0
- package/src/platforms/gametank/lib/gt/audio/note_numbers.h +132 -0
- package/src/platforms/gametank/lib/gt/audio/scaled_sines.raw +0 -0
- package/src/platforms/gametank/lib/gt/audio/sine_256_-63_63.bin +0 -0
- package/src/platforms/gametank/lib/gt/banking.c +29 -0
- package/src/platforms/gametank/lib/gt/banking.h +8 -0
- package/src/platforms/gametank/lib/gt/banking2.s +41 -0
- package/src/platforms/gametank/lib/gt/crt0.s +102 -0
- package/src/platforms/gametank/lib/gt/draw_logo.s +55 -0
- package/src/platforms/gametank/lib/gt/feature/persist/persist.c +103 -0
- package/src/platforms/gametank/lib/gt/feature/persist/persist.h +13 -0
- package/src/platforms/gametank/lib/gt/feature/random/random.c +40 -0
- package/src/platforms/gametank/lib/gt/feature/random/random.h +18 -0
- package/src/platforms/gametank/lib/gt/feature/text/text.c +111 -0
- package/src/platforms/gametank/lib/gt/feature/text/text.h +25 -0
- package/src/platforms/gametank/lib/gt/gametank.c +12 -0
- package/src/platforms/gametank/lib/gt/gametank.h +80 -0
- package/src/platforms/gametank/lib/gt/gametank_logo.inc +393 -0
- package/src/platforms/gametank/lib/gt/gen/assets/assets_index.h +9 -0
- package/src/platforms/gametank/lib/gt/gen/assets/sdk_default.h +5 -0
- package/src/platforms/gametank/lib/gt/gen/bank_nums.h +11 -0
- package/src/platforms/gametank/lib/gt/gen/modules_enabled.h +4 -0
- package/src/platforms/gametank/lib/gt/gen/modules_enabled.inc +4 -0
- package/src/platforms/gametank/lib/gt/gfx/draw_direct.c +132 -0
- package/src/platforms/gametank/lib/gt/gfx/draw_direct.h +75 -0
- package/src/platforms/gametank/lib/gt/gfx/draw_queue.c +177 -0
- package/src/platforms/gametank/lib/gt/gfx/draw_queue.h +35 -0
- package/src/platforms/gametank/lib/gt/gfx/draw_util.s +157 -0
- package/src/platforms/gametank/lib/gt/gfx/gfx_sys.c +43 -0
- package/src/platforms/gametank/lib/gt/gfx/gfx_sys.h +46 -0
- package/src/platforms/gametank/lib/gt/gfx/sprites.c +216 -0
- package/src/platforms/gametank/lib/gt/gfx/sprites.h +41 -0
- package/src/platforms/gametank/lib/gt/init.c +9 -0
- package/src/platforms/gametank/lib/gt/input.c +26 -0
- package/src/platforms/gametank/lib/gt/input.h +20 -0
- package/src/platforms/gametank/lib/gt/interrupt.s +67 -0
- package/src/platforms/gametank/lib/gt/vectors.s +14 -0
- package/src/platforms/gametank/lib/gt/wait.s +53 -0
- package/src/playtest/playtest.js +174 -26
- package/src/toolchains/arm-none-eabi-gcc/gcc.js +39 -188
- package/src/toolchains/asar/asar.js +10 -15
- package/src/toolchains/cc65/cc65.js +82 -92
- package/src/toolchains/cc65/da65.js +12 -17
- package/src/toolchains/cc65/preset-resolver.js +13 -2
- package/src/toolchains/cc65/presets/gametank/gametank.h +80 -0
- package/src/toolchains/cc65/presets/gametank/sdk.cfg +32 -0
- package/src/toolchains/cc65/presets/gametank/sdk.crt0.s +61 -0
- package/src/toolchains/cc65/presets/gametank/sdk.vectors.s +11 -0
- package/src/toolchains/cc65/presets/gametank/single-bank.cfg +28 -0
- package/src/toolchains/cc65/presets/gametank/single-bank.crt0.s +71 -0
- package/src/toolchains/cc65/presets/gametank/single-bank.vectors.s +12 -0
- package/src/toolchains/common/c-build.js +109 -0
- package/src/toolchains/common/gcc-toolchain.js +164 -0
- package/src/toolchains/common/wasm-tool.js +101 -0
- package/src/toolchains/dasm/dasm.js +12 -18
- package/src/toolchains/gba-c/gba-c.js +253 -305
- package/src/toolchains/genesis-c/genesis-c.js +196 -225
- package/src/toolchains/index.js +58 -4
- package/src/toolchains/m68k-elf-gcc/gcc.js +39 -202
- package/src/toolchains/mips-c/mips-c.js +68 -78
- package/src/toolchains/mips-elf-gcc/gcc.js +55 -118
- package/src/toolchains/rgbds/rgbds.js +7 -19
- package/src/toolchains/sdcc/sdcc.js +35 -26
- package/src/toolchains/sh-c/sh-c.js +44 -52
- package/src/toolchains/sh-elf-gcc/gcc.js +55 -110
- package/src/toolchains/sjasm/sjasm.js +10 -14
- package/src/toolchains/snes-c/snes-c.js +125 -150
- package/src/toolchains/tcc816/tcc816.js +10 -15
- package/src/toolchains/vasm68k/vasm68k.js +11 -16
- package/src/toolchains/wladx/wladx.js +5 -17
- package/src/toolchains/z80/binutils.js +5 -11
package/AGENTS.md
CHANGED
|
@@ -4,7 +4,7 @@ This is romdev's GENERIC orientation — read it once. The platform-specific det
|
|
|
4
4
|
|
|
5
5
|
## What this server does
|
|
6
6
|
|
|
7
|
-
Drives the full homebrew ROM dev loop for
|
|
7
|
+
Drives the full homebrew ROM dev loop for 18 retro game platforms (NES, SNES, Game Boy, Game Boy Color, Game Boy Advance, Genesis, Sega Master System, Game Gear, Atari 2600/7800, Atari Lynx, Commodore 64, PC Engine / TurboGrafx-16, and MSX / MSX2, plus the open-hardware GameTank — and the 3D consoles Nintendo 64, Sony PlayStation, and Sega Dreamcast). Build → run → screenshot → inspect → patch → iterate. Also a strong reverse-engineering kit: disassemble existing ROMs into byte-exact rebuildable projects (`disasm({target:'project'})`/`disasm({target:'references'})` — the workhorse for any structural hack), find a value's address with the Cheat-Engine search loop (`memory({op:'search'})`/`memory({op:'searchNext'})`), find the EXACT instruction that wrote a RAM byte (`breakpoint({on:'write'})`, a core-level write watchpoint), confirm a patch is live in the running image (`memory({op:'readCart'})`), tell whether a "found table" is really ASCII (`memory({op:'classify'})`), trace where an on-screen graphic comes from (`watch({on:'copy'})` on the 15 classic platforms — writer PC per VRAM write; `watch({on:'dma'})` for Genesis DMA sources), drive menus by screen-change (`navigate`), and look up cheats (`cheats({op:'lookup'})`/`cheats({op:'search'})`: a free, crowd-sourced labeled RAM/code map for known ROMs), apply + create cheats, convert assets, study patterns from real games. **Doing a romhack? Start with `platform({op:'doc', platform:'romhacking', name:'playbook'})`** — the decision tree that wires all of the above together. Bundled WASM toolchains and emulator cores — no system dependencies, no installs.
|
|
8
8
|
|
|
9
9
|
You drive the work. The human is a director — they may want a game, a ROM disassembly, a tool-assisted reverse-engineering session, or anything else this server can do.
|
|
10
10
|
|
|
@@ -20,7 +20,7 @@ This rule is about **compilers and emulators only** — NOT about content tools.
|
|
|
20
20
|
|
|
21
21
|
## The second rule: READ YOUR TARGET PLATFORM'S DOCS BEFORE YOU WRITE CODE FOR IT
|
|
22
22
|
|
|
23
|
-
This doc is deliberately GENERIC — it can't hold
|
|
23
|
+
This doc is deliberately GENERIC — it can't hold 18 platforms' worth of detail without bloating every session. The knowledge that actually saves you — the memory map, the input/control quirks, the render-enable order, the codegen traps, the SDK's footguns — lives in each platform's docs, read on demand:
|
|
24
24
|
|
|
25
25
|
- **`platform({op:'doc', platform, name:'mental_model'})`** — read this for EVERY system you're about to build or RE on, BEFORE you write code. It's a couple hundred tokens and most "why won't this work" dead-ends are a documented footgun you'd have seen there (a C64 game that needs a keyboard key to start; an SDCC WRAM-layout trap; a platform's render-enable order; gambatte exposing `gb_vram` not `video_ram`).
|
|
26
26
|
- **`platform({op:'doc', platform, name:'troubleshooting'})`** — the symptom→fix list; read it the moment something's broken.
|
|
@@ -65,12 +65,12 @@ Skip playtest only when there's clearly no human in the loop: CI runs, automated
|
|
|
65
65
|
- `run` — load ROMs, step frames, screenshot (works for existing ROMs you didn't compile)
|
|
66
66
|
- `input` — drive controllers, look up hardware bit layouts. `navigate` walks menus by advancing on SCREEN CHANGE (not fixed frames) and reports whether each press was consumed — the fast, reliable way to script a UI.
|
|
67
67
|
- `state` — savestates and forensic state inspection (`state({op:'save'})`, `state({op:'load'})`, `state({op:'export'})` a slot to disk without touching the live host, `state({op:'list'})`, `state({op:'dump'})`)
|
|
68
|
-
- `memory` — read/write VRAM/OAM/CGRAM/ARAM and other regions (all
|
|
69
|
-
- `debug` — **`frame({op:'verify'})`** (NO-VISION render-health: one call answers "is the game actually rendering / alive?" on all
|
|
68
|
+
- `memory` — read/write VRAM/OAM/CGRAM/ARAM and other regions (all 18 platforms). `memory({op:'read'})` takes `offsets:[…]` to batch scattered reads in one call. **`memory({op:'search'})`/`memory({op:'searchNext'})`** = the Cheat-Engine value-search loop ("find the address of X, narrow as X changes") — relative compares (`inc`/`dec`/`changed`) work as the FIRST narrow (baselines recorded at seed), and `as:'bcd'`/`as:'digits'` search packed-BCD scores and digit-per-byte HUD buffers (any constant tile base) when stored ≠ displayed. **`memory({op:'searchUnknown'})`** is the unknown-initial-value hunt — seed the whole region with no value, then narrow by `dec`/`inc`/`changed` across events (the value you can't read off the HUD). **`memory({op:'readCart'})`** reads the loaded cart image to confirm a patch is live (pass `{cpuAddress, bank}` to read a banked CPU address on NES/SNES). **`memory({op:'classify'})`** says whether bytes look like ASCII/code/tile-data (kills the "found table that's really a string" trap). `memory({op:'snapshot'})` + `memory({op:'diff'})` answer "which bytes changed across this event?" (diff defaults to a clustered summary with stride detection; small clusters carry before/after hex, `minDelta` filters churn, and predicate filters — `changeDir:'inc'|'dec'`, `deltaEq`, `beforeMin/Max`, `afterMin/Max` — keep only the bytes that moved the way you expect; `outputPath`+`echo:false` route the full list to disk); **`memory({op:'diffRuns', portsA, portsB?})`** answers "which byte does this INPUT drive?" in one call (same start state run twice under two inputs, only the divergent bytes return); `state({op:'diff'})` is the coarse whole-machine version. Reads routed to disk take `echo:false` to skip the inline hex.
|
|
69
|
+
- `debug` — **`frame({op:'verify'})`** (NO-VISION render-health: one call answers "is the game actually rendering / alive?" on all 15 classic platforms — fuses a framebuffer pixel scan with the per-platform render-enable/NMI decode; `{verified:true|false|null, issues[], pixels, render}`, frame-0-guarded so it never cries wolf on boot), `sprites({op:'inspect'})`, `palette({source:'live'})`, `cpu({op:'read'})` (all 18), `audioDebug({op:'inspect'})` (the 13 systems with a sound chip — all but Atari 2600/7800; incl. GameTank's ACP coprocessor via `chip:'acp'`; pass `frames:N` to TRACE a per-channel note-timeline for headless melody asserts), `background({view:'renderState'})`, `breakpoint({on:'write'})` (write watchpoint, all 18; EVERY hit on EVERY platform carries `registersAtHit` — the register file frozen at the hit instant, the only honest read since live regs drift after a hit — and the CPU stays frozen until the hit is cleared), **`watch({on:'dma', precision:'sampled'})`** (Genesis: which ROM offset a VRAM graphic was DMA'd from), **`watch({on:'copy'})`** (the 15 classic platforms: every write landing in a VRAM window logged with the EXECUTING instruction's PC — the generic 'which routine uploads this graphic?'; port-based video memory hooked in-core incl. the SNES DMA path, CPU-mapped VRAM via the range log), **`disasm({target:'bytes'|'rom'|'references'|'project'})`** (`bytes`/`rom`/`references` on all 18, `project` byte-exact reassembly on the 15 classic platforms (incl. GameTank, cc65/6502) — native binutils objdump per CPU, incl. GBA ARM7/Thumb; the byte-exact `disasm({target:'project'})` reassembles through native as/ld/objcopy; banked carts — NES mappers, SNES LoROM, GB MBC, Sega mapper, MSX megaROM, 2600 F8/F6/F4, 7800 SuperGame, >32KB HuCards — are split and reference-scanned PER BANK, refs tagged `prgBank`/`romBank`), plus the **Rizin/Ghidra RE engine** `disasm({target:'cfg'|'xrefs'|'functions'|'decompile'|'resolveJumptable'})` (ALL 18, incl. the 3D consoles' MIPS R3000/R4300 + SH-4 — control-flow graphs, deep xrefs, auto-detected functions [sorted real-code-first, `looksLikeData` flagged], and Ghidra C pseudocode; quality excellent on GBA/Genesis, rough on 6502; decompile output NAMES hardware registers [`PPUMASK` not `*0x2001`] and on the 6502 family folds SLEIGH clutter to readable C99 [`uint8_t`, `zp_FD`]; `resolveJumptable` resolves computed dispatchers live via `breakpoint({on:'jumptable'})`) + `symbols({op:'analyze'})` (one-shot structural map), `symbols({op})` lookup, `background({view:'rendered'})`, plus **`cheats({op})`** (`cheats({op:'lookup'})` = a free labeled RAM/code map for known ROMs, `cheats({op:'search'})` to fuzzy-find a game by name, `cheats({op:'apply'})`/`cheats({op:'clear'})` non-destructively, `cheats({op:'make'})` to create codes)
|
|
70
70
|
- `assets` — convert PNGs to tiles (`encodeArt`/`importArt`), WAVs to BRR, identify ROMs (`cart({op:'identify'})`), plus the hacking toolkit (`romPatch({op})` — write/writeMany/spliceCHR/relocate/makeStored/findFree/findPointer/diff, `assembleSnippet`, `cart({op:'extract'})`, `cart({op:'wrap'})`)
|
|
71
71
|
- `project` — the example-game library (`examples`: list / fork / show, plus the legacy snippet ops)
|
|
72
72
|
- `show` — `playtest({op})`: `op:'open'` opens the live SDL window for a human, `op:'stop'` closes it, `op:'status'` reports liveness, `op:'framebuffer'` captures exactly what the human's window shows
|
|
73
|
-
- `advanced` — `runUntil`, **`watch({on:'mem'|'range'|'pc'})`** (LOG-ALL tracing; `range`/`pc` take **`fromState`**/`fromStatePath` to trace from a restored savestate moment), **`breakpoint({on:'write'})`** (the EXACT instruction that wrote a byte, via a core watchpoint — fixes the frame-sampled-PC problem; runs to END OF FRAME and reports the LAST matching write with `hits`=count; `condition:'increase'|'decrease'|'equals'`+`conditionValue` filters to the MEANINGFUL write — the score going UP, not the per-frame restore churn (core-level on all
|
|
73
|
+
- `advanced` — `runUntil`, **`watch({on:'mem'|'range'|'pc'})`** (LOG-ALL tracing; `range`/`pc` take **`fromState`**/`fromStatePath` to trace from a restored savestate moment), **`breakpoint({on:'write'})`** (the EXACT instruction that wrote a byte, via a core watchpoint — fixes the frame-sampled-PC problem; runs to END OF FRAME and reports the LAST matching write with `hits`=count; `condition:'increase'|'decrease'|'equals'`+`conditionValue` filters to the MEANINGFUL write — the score going UP, not the per-frame restore churn (core-level on all 18, `oldValueByte` reported); `precision:'sampled'` is the cheap frame-PC version; on a `pressDuring` run pass **`abortIf:[{region,offset,label}]`** to stop early if the driven scenario derails — a guard byte changing returns `{aborted, abortedBy, before, after}` instead of burning all `maxFrames` on a meaningless `found:false`), **`breakpoint({on:'pc'})`** (execution breakpoint — freeze the CPU AT an instruction and read its registers), **`breakpoint({on:'read'})`** (the EXACT instruction that read a byte), **`breakpoint({on:'jumptable'})`** (RESOLVE a computed-jump dispatcher static analysis can't follow — `JMP (table,X)` / RTS-trick state machines, script/battle VMs: break at the dispatcher, single-step through the indirect transfer, record the COMPUTED targets live across frames/inputs; the varying arms are isolated from fixed trampolines; `disasm({target:'resolveJumptable'})` points here. No static-only tool can do this — it needs the live emulator), **`frame({op:'stepInstruction'})`** (CPU single-step) — all 18 platforms; input recording
|
|
74
74
|
|
|
75
75
|
**"Disassemble this NES ROM"** is now just: `disasm({target:'rom', path, startAddress, length})`. No discovery step.
|
|
76
76
|
|
|
@@ -116,7 +116,7 @@ questions, and running both early is normal.** A good default:
|
|
|
116
116
|
than the da65 `references` scan — it follows the analysis graph). For C-like PSEUDOCODE,
|
|
117
117
|
`disasm({target:'decompile', address})` runs Ghidra's decompiler (carries the decompiler's
|
|
118
118
|
own WARNINGs; quality excellent on GBA/Genesis, good on GB/Z80, rough on 6502). All
|
|
119
|
-
Rizin/Ghidra analysis ops cover
|
|
119
|
+
Rizin/Ghidra analysis ops cover 18/18 platforms (incl. the 3D consoles' MIPS/SH-4). For a no-cheats game or a
|
|
120
120
|
logic/text/graphics change, this is where the real work is — start here, don't wait on a
|
|
121
121
|
cheat lookup.
|
|
122
122
|
4. **VERIFY before patching**: `memory({op:'write'})` the address live and watch the effect
|
|
@@ -311,10 +311,10 @@ For maintainers: the platform / core / patch / region-ID matrix and the recipe f
|
|
|
311
311
|
|
|
312
312
|
## Deep debug tooling status per platform
|
|
313
313
|
|
|
314
|
-
Different platforms have different levels of MCP-exposed debugging — different hardware needs different tools, and we've patched the cores where it's been worth it. The generic shapes — `cpu({op:'read'})`, `breakpoint({on:'write'})`, `disasm({target:'rom'})`/`disasm({target:'references'})
|
|
314
|
+
Different platforms have different levels of MCP-exposed debugging — different hardware needs different tools, and we've patched the cores where it's been worth it. The generic shapes — `cpu({op:'read'})`, `breakpoint({on:'write'})`, `disasm({target:'rom'})`/`disasm({target:'references'})`, `memory({op:'search'})`/`memory({op:'readCart'})`/`memory({op:'classify'})`, cheats — work on **all 18 platforms** (disassembly via native binutils `objdump` compiled to WASM, one per CPU family — incl. GBA ARM7/Thumb and the 3D consoles' MIPS/SH-4); byte-exact `disasm({target:'project'})` reassembly is the **15 classic platforms** (incl. GameTank, cc65/6502). The deep per-platform inspectors (`sprites({op:'inspect'})`, `palette({source:'live'})`, `background({view:'renderState'})`, `audioDebug({op:'inspect'})`) are detailed for **12 systems** below; **PC Engine and MSX** currently have the generic shapes + their core's native regions but not yet the full custom-inspector treatment (extend by patching their cores per the snes9x/gpgx pattern). `audioDebug({op:'inspect'})` covers the **13 with a sound chip** (all but Atari 2600/7800; incl. GameTank's ACP coprocessor). A few are honest hardware-shaped exceptions, noted inline below (the Lynx has no fixed OAM so `sprites({op:'inspect'})` returns the SCB list head; GameTank is a blitter framebuffer with no sprite OAM / no tilemap, so `sprites({op:'inspect'})`/`background` are N/A). Coverage detail per platform:
|
|
315
315
|
|
|
316
|
-
> **Universal across
|
|
317
|
-
> instruction write watchpoint — the exact PC that wrote a RAM byte, all
|
|
316
|
+
> **Universal across the 15 classic platforms:** `breakpoint({on:'write'})` (the core-level
|
|
317
|
+
> instruction write watchpoint — the exact PC that wrote a RAM byte, all 15 classic CPU
|
|
318
318
|
> families), **`breakpoint({on:'pc'})`** (execution breakpoint — freeze the CPU AT an
|
|
319
319
|
> instruction and read its registers), **`breakpoint({on:'read'})`** (the exact PC that read
|
|
320
320
|
> a byte — read-side mirror of `breakpoint({on:'write'})`), **`frame({op:'stepInstruction'})`** (CPU single-step),
|
|
@@ -332,7 +332,7 @@ Different platforms have different levels of MCP-exposed debugging — different
|
|
|
332
332
|
> pass **`fromState`**/`fromStatePath` to restore a savestate FIRST so the trace runs from a
|
|
333
333
|
> known, repeatable moment — jump to the boss, then see what writes HP),
|
|
334
334
|
> **`watch({on:'pc'})`** (coverage trace — distinct PCs executed in a window; also takes `fromState`),
|
|
335
|
-
> **the RE-INJECT trio** (put an edited asset BACK, all
|
|
335
|
+
> **the RE-INJECT trio** (put an edited asset BACK, all 15 classic platforms): **`romPatch({op:'findPointer'})`**
|
|
336
336
|
> (find every pointer to a ROM offset — Genesis 32-bit BE, SNES LoROM/HiROM, GBA
|
|
337
337
|
> 0x08000000+offset incl. literal pools, banked 8-bit 16-bit-LE aliases),
|
|
338
338
|
> **`romPatch({op:'makeStored'})`** (wrap raw bytes so the game's OWN decompressor expands them
|
|
@@ -342,11 +342,11 @@ Different platforms have different levels of MCP-exposed debugging — different
|
|
|
342
342
|
> `cheats({op:'lookup'})`/`cheats({op:'search'})`/`cheats({op:'apply'})`/`cheats({op:'make'})` (cheat
|
|
343
343
|
> lookup/apply/create), `cpu({op:'read'})`, `memory({op:'search'})`/`memory({op:'searchNext'})`/`memory({op:'readCart'})`/`memory({op:'classify'})`,
|
|
344
344
|
> `memory({op:'snapshot'})`/`memory({op:'diff'})`/`state({op:'diff'})`, `watch({on:'mem'})`/`breakpoint({on:'write',precision:'sampled'})`.
|
|
345
|
-
> `audioDebug({op:'inspect'})` covers the
|
|
345
|
+
> `audioDebug({op:'inspect'})` covers the 13 systems with a sound chip (all but Atari 2600/7800; incl. GameTank's ACP coprocessor).
|
|
346
346
|
> **`watch({on:'dma', precision:'exact'})`** (which DMA wrote a VRAM tile, and from where) is **Genesis-only**
|
|
347
347
|
> (VDP DMA) — elsewhere use `breakpoint({on:'write'})`/`watch({on:'range'})`. All other RE tools above
|
|
348
|
-
> work on every platform that has the register-write/watch core hooks (
|
|
349
|
-
> `disasm({target:'rom'})` + `disasm({target:'references'})`
|
|
348
|
+
> work on every platform that has the register-write/watch core hooks (the 15 classic platforms).
|
|
349
|
+
> `disasm({target:'rom'})` + `disasm({target:'references'})` cover **all 18** (incl. the 3D consoles' MIPS/SH-4), and `disasm({target:'project'})` byte-exact reassembly the **15 classic platforms** (incl. GameTank) — every
|
|
350
350
|
> CPU family disassembles through a native binutils `objdump` (WASM), and
|
|
351
351
|
> `disasm({target:'project'})` reassembles byte-exact through the matching native
|
|
352
352
|
> `as`/`ld`/`objcopy`. The per-platform notes below cover the platform-SPECIFIC
|
|
@@ -365,6 +365,7 @@ The deep per-platform inspectors + the exact memory-region names, core quirks, a
|
|
|
365
365
|
- **Atari Lynx** — `sprites({op:'inspect'})` returns an SCB list head (no fixed OAM), or you need the Mikey palette/audio, 65C02 regs, or the `lynx_hw_regs` $FC00-$FDFF window.
|
|
366
366
|
- **MSX** — VDP/PSG inspection or AY8910 `audioDebug`. (ColecoVision is bring-up-only: standard `system_ram`/`save_ram`/`video_ram`, no custom inspectors — extend by patching its core per the snes9x/gpgx pattern.)
|
|
367
367
|
- **PC Engine** — generic shapes + the core's native regions only so far (no custom-inspector treatment yet).
|
|
368
|
+
- **GameTank** — Clyde Shaffer's open-hardware W65C02S console (cc65 toolchain, ext `.gtr`). Full classic-style Tier-1: `cpu({op:'read'})` (65c02 regsnap), `audioDebug({op:'inspect', chip:'acp'})` (the ACP audio coprocessor), `breakpoint({on:'write'|'read'|'pc'})` + watchdog + coverage, `disasm` (`bytes`/`rom`/`references` + the 6502 Rizin/Ghidra cfg/xrefs/functions/decompile path), `cart({op:'extract'|'wrap'})`, and the 6502 re-inject path (`romPatch`) same as NES/C64/Lynx. **No `sprites({op:'inspect'})`/`background`** — it's a blitter framebuffer with no sprite OAM / no tilemap (like the Dreamcast). No genre example games yet (brand-new platform).
|
|
368
369
|
|
|
369
370
|
Starter snippets per platform live under `src/platforms/<platform>/lib/`. Discover via `examples({op:'snippets', platform})` (default `mode:'list'`), fetch one via `examples({op:'snippets', platform, mode:'get', snippetName})`. SNES + NES + Genesis + SMS + Game Boy + Atari 2600 + Atari 7800 have substantial snippet libraries; others are minimal.
|
|
370
371
|
|
|
@@ -483,7 +484,7 @@ to avoid), plus the per-asset round-trips (text, compressed assets, graphics) an
|
|
|
483
484
|
Quick-reference table. Don't reconstruct the flow from this summary — it's only here so
|
|
484
485
|
you know the capability exists and where the detail lives.
|
|
485
486
|
|
|
486
|
-
Key primitives (all bundled, all
|
|
487
|
+
Key primitives (all bundled, all 15 classic tier-1 systems unless noted; full detail in the
|
|
487
488
|
playbook):
|
|
488
489
|
|
|
489
490
|
- **Find a value's RAM address** — the Cheat-Engine loop `memory({op:'search'})` →
|
|
@@ -523,7 +524,9 @@ breakpoint trio (`pc`/`read`/`stepInstruction`) is in `advanced`.
|
|
|
523
524
|
|
|
524
525
|
## Disassembler
|
|
525
526
|
|
|
526
|
-
`disasm({target:'rom'|'references'
|
|
527
|
+
`disasm({target:'rom'|'references'})` covers **all 18 platforms** (incl. the 3D consoles'
|
|
528
|
+
MIPS R3000/R4300 + SH-4); byte-exact `disasm({target:'project'})` reassembly is the **15
|
|
529
|
+
classic platforms** (incl. GameTank, cc65/6502). Every CPU
|
|
527
530
|
family disassembles through a native binutils disassembler compiled to WASM (no
|
|
528
531
|
hand-rolled JS decoders): 6502/65816 via cc65's `da65`, Z80 (SMS/GG/MSX) + SM83
|
|
529
532
|
(GB/GBC) via one z80-elf `objdump` (`-m z80` / `-m gbz80`), m68k (Genesis) via
|
|
@@ -706,7 +709,7 @@ Two tools that save real time and frustration:
|
|
|
706
709
|
|
|
707
710
|
`input({op:'set'})` accepts an Xbox-shaped controller: D-pad, 4 face buttons (use `north/east/south/west` for portable code — they translate per platform), shoulders (`l/r`), triggers (`l2/r2`), sticks (`l3/r3`), plus `start`/`select`. Older platforms are subsets — `input({op:'layout'})` tells you which buttons are real. Pressing a non-existent button is a silent no-op.
|
|
708
711
|
|
|
709
|
-
⚠ **The raw libretro names `a`/`b`/`x`/`y` are NOT the platform's printed button labels — and on the three genesis_plus_gx platforms (Genesis, SMS, Game Gear) they're INVERTED.** Verified live across all 14 platforms:
|
|
712
|
+
⚠ **The raw libretro names `a`/`b`/`x`/`y` are NOT the platform's printed button labels — and on the three genesis_plus_gx platforms (Genesis, SMS, Game Gear) they're INVERTED.** Verified live across all 14 classic platforms:
|
|
710
713
|
- **Genesis**: gpgx maps Genesis A/B/C onto libretro **y/b/a** — so `input({op:'set', a:true})` presses Genesis **C**, and Genesis A (SGDK `BUTTON_A`) is `{y:true}` / `{west:true}`.
|
|
711
714
|
- **SMS / Game Gear**: button 1 (TL, main fire) is libretro **b**, button 2 (TR) is libretro **a** — so `{a:true}` presses button 2, not 1.
|
|
712
715
|
- **Every other core maps straight through** (`{a}`→A, `{b}`→B): NES, GB/GBC, SNES (incl. x/y/l/r), GBA, PC Engine (a=I, b=II), MSX (a=trig 1), Lynx (a=A). C64 + Atari 2600 are single-fire — fire is `{b}`/`{south}`, `{a}` is a no-op. Atari 7800 boots in 1-button mode (both fires read INPT4) until you enable 2-button mode.
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,190 @@ All notable changes to `romdevtools`. Dates are release dates.
|
|
|
4
4
|
(Published as `romdev-mcp` through 0.11.0; renamed to `romdevtools` in 0.13.0 —
|
|
5
5
|
the `romdev-mcp` bin is kept as an alias.)
|
|
6
6
|
|
|
7
|
+
## 0.84.1 — 2026-06-30
|
|
8
|
+
|
|
9
|
+
- **New package `romdev-audio-resampler`** — the WASM+SIMD linear audio resampler (interleaved
|
|
10
|
+
S16LE stereo, any src→dst rate) is carved out of `src/playtest/resampler/` into its own
|
|
11
|
+
standalone, zero-dep npm package so other projects can consume it (retroemu's terminal audio
|
|
12
|
+
path needs it to resample fractional-rate cores like the GameTank ACP's ~13983 Hz up to a
|
|
13
|
+
fixed device rate — the way a libretro frontend does). romdevtools now depends on it; the
|
|
14
|
+
playtest audio sink imports it instead of the in-tree copy. No behavior change here.
|
|
15
|
+
|
|
16
|
+
## 0.84.0 — 2026-06-30
|
|
17
|
+
|
|
18
|
+
**The 3D GL cores are now PLAYABLE in the SDL playtest window — N64 + PS1 at full speed.**
|
|
19
|
+
Until now the hardware-rendered cores (n64/ps1/dreamcast, all `hwRender:true`) rendered
|
|
20
|
+
headlessly but couldn't be driven interactively. Verified end-to-end: **Mario Kart 64
|
|
21
|
+
(N64)** and **Crash Bandicoot (PS1)** play in the window at full speed with sound and a
|
|
22
|
+
standard controller. Five fixes, all in `src/playtest/playtest.js` + `src/host/`:
|
|
23
|
+
- **Software-blit window for hwRender cores.** An `accelerated:true` SDL window makes a GLX
|
|
24
|
+
context that collides with native-gles's EGL context → `X_GLXMakeCurrent BadAccess` server
|
|
25
|
+
crash. hwRender cores now open `accelerated:false` (they only present CPU readback pixels).
|
|
26
|
+
- **RGBA8888 in the window's pixel converter** — the hwRender readback format was unhandled →
|
|
27
|
+
black window. Added (with the alpha=255 force the GL FBO needs).
|
|
28
|
+
- **Audio-paced stepping, wall-clock budgeted.** Step extra frames per tick to keep SDL's
|
|
29
|
+
buffer full, but cap the burst by WALL-CLOCK (≤1.5 ticks), not a frame count. A frame-count
|
|
30
|
+
cap froze the event loop on slow cores (8 frames × 60ms = 480ms) → stutter death-spiral;
|
|
31
|
+
the time budget keeps the window responsive and the audio steady. (Fixes the N64 choppy
|
|
32
|
+
audio too — parallel-n64 emits ~91 frames of audio per real second, not 60.)
|
|
33
|
+
- **N64 analog stick + full pad map.** `inputStateCb` handles `RETRO_DEVICE_ANALOG` (synthesize
|
|
34
|
+
±32767 from the d-pad); N64-specific pad map (Z = a free face button, C-buttons = right
|
|
35
|
+
stick — the analog triggers idle half-pressed and aren't used by N64).
|
|
36
|
+
- **No per-frame serialize on hwRender.** The rewind buffer AND the auto-checkpoint both call
|
|
37
|
+
`serializeState()` — fine for 8/16-bit (KB) but brutal on a 16MB N64/DC state (~18ms/call).
|
|
38
|
+
Skipped entirely when `host.hwRender`.
|
|
39
|
+
|
|
40
|
+
**Dreamcast: boots commercial GD-ROM games, but EXPERIMENTAL / not yet playable.** flycast now
|
|
41
|
+
loads + renders real discs (Sonic Adventure, Crazy Taxi) — a first — via two fixes: the
|
|
42
|
+
**GLES (WebGL2) renderer** (`-DUSE_GLES=ON`; the desktop-GL path called `glClearDepth` which
|
|
43
|
+
WebGL lacks → a frame-58 crash), and **NODERAWFS disc streaming** (`-s NODERAWFS=1` + a host
|
|
44
|
+
`noderawfs` path) so libchdr seeks the disc off Node's real fs instead of loading the whole
|
|
45
|
+
CHD into the WASM heap (an 872MB Sonic CHD OOM'd a 1GB heap). BUT on the interpreter core DC
|
|
46
|
+
runs at ~5 fps — **unplayable.** It ships on the correct interpreter; the WASM SH-4 JIT (78fps,
|
|
47
|
+
integrated behind `ROMDEV_FLYCAST_JIT=1`) has native-emit bugs that hang Sonic's boot and is a
|
|
48
|
+
documented next step (internal). DC is labeled experimental, not a playable platform.
|
|
49
|
+
|
|
50
|
+
## 0.83.0 — 2026-06-30
|
|
51
|
+
|
|
52
|
+
**GameTank examples: 5 genre games reworked into shippable, hardware-tested shape.**
|
|
53
|
+
The GameTank example set (`examples/gametank/templates/`) went from thin scaffolds
|
|
54
|
+
to five complete, play-tested games (verified on real RetroDeck hardware): `shmup`,
|
|
55
|
+
`platformer`, `puzzle`, `sports`, `racing`. Along the way several real GameTank
|
|
56
|
+
blitter/SDK footguns were isolated and are now documented in the shared headers so
|
|
57
|
+
future games dodge them:
|
|
58
|
+
- **A box of width OR height exactly 128 is silently DROPPED** by the blitter (a
|
|
59
|
+
full-screen-dimension fill never draws). Clamp to 127. (Hit as a "vanishing
|
|
60
|
+
ground" / "no road" bug.) Documented in `gt_draw.h`.
|
|
61
|
+
- **Box top-edge scanlines flicker between the two double-buffer pages** — only the
|
|
62
|
+
border-clear path paints the screen's edge rows cleanly. Frames that draw near the
|
|
63
|
+
top finish with `queue_clear_border(topColor)` (drawn LAST; HUD text that must sit
|
|
64
|
+
in rows 0-6 goes AFTER it). Documented in `gt_draw.h`.
|
|
65
|
+
- **The SDK `rnd()` corrupts game state on the single-bank build** — use the inline
|
|
66
|
+
`rnd8()` xorshift in `gt_draw.h` instead. (Broke enemy spawning until found.)
|
|
67
|
+
- **GRAM sprite blits are the per-frame cost** — too many overrun the vblank window
|
|
68
|
+
and the draw queue drops rects (platforms flicker/vanish). Keep sprites few (the
|
|
69
|
+
platformer blits only the hero; coins are cheap rects).
|
|
70
|
+
- New shared helpers: `gt_hud.h` gained a 3×5 A–Z text font (`hud_text`); `gt_draw.h`
|
|
71
|
+
exposes `rnd8()`. `gt_sound.h` SFX wired into sports (wall/paddle/score) and racing
|
|
72
|
+
(crash). `puzzle` is now a faithful Columns-style 3-jewel matcher (CHROMA WELL):
|
|
73
|
+
3-tile falling column, A/B color-cycle, horizontal/vertical/diagonal matching with
|
|
74
|
+
gravity chains. Platformer gained a 3-frame walk cycle, signed/clamped jump physics
|
|
75
|
+
(no more unsigned-wrap fall-death), swept platform-landing (no tunneling), and a
|
|
76
|
+
coin pickup chime. `examples/README.md` gained the GameTank row.
|
|
77
|
+
|
|
78
|
+
## 0.82.0 — 2026-06-29
|
|
79
|
+
|
|
80
|
+
**New platform: GameTank — the 18th system, a full Tier-1.** Clyde Shaffer's
|
|
81
|
+
open-hardware [GameTank](https://gametank.zone/) (a W65C02S console: 128×128
|
|
82
|
+
hardware-blitter framebuffer + a second 65C02 audio coprocessor) is now a
|
|
83
|
+
first-class romdev platform, held to the SAME Tier-1 contract as the 14 classic
|
|
84
|
+
2D systems — not the partial 3D-console tier.
|
|
85
|
+
- **Build** via cc65 (`--cpu 65c02`, `-t none`) + a bundled single-bank 32 KB
|
|
86
|
+
preset (linker cfg + crt0 + vectors + `gametank.h`). Output is a flat `.gtr`
|
|
87
|
+
(EEPROM32K, size-keyed mapper). The cc65 dispatch gained a per-platform CPU
|
|
88
|
+
override (the `-t none` path needs an explicit `--cpu`) + preset-bundled
|
|
89
|
+
headers; both reusable by future cc65 platforms.
|
|
90
|
+
- **Run + render** through `monteslu/gametank-libretro` (built to WASM).
|
|
91
|
+
- **Full debug surface** — the core is patched with the shared romdev_debug
|
|
92
|
+
hooks: `cpuState` (live 65C02 regs), write/read **watchpoints**, **pc-break**,
|
|
93
|
+
**watchdog**, **coverage**, `setReg`/`getReg`. Same machinery every other
|
|
94
|
+
instrumented core links.
|
|
95
|
+
- **audioDebug** (`chip:'acp'`) — the ACP audio coprocessor's state (DAC output,
|
|
96
|
+
IRQ/sample rate, run/mute, the audio-CPU PC).
|
|
97
|
+
- **cart** extract/wrap — split a `.gtr` into body + the `$FFFA` vector table
|
|
98
|
+
(size-keyed mapper detect), and re-wrap for the extract→patch→rebuild cycle.
|
|
99
|
+
- **disasm/decompile** — the existing 6502 Rizin/Ghidra path covers it.
|
|
100
|
+
- N/A by hardware (like Dreamcast): `inspectSprites` (blitter, no OAM),
|
|
101
|
+
`inspectBackground` (framebuffer, no tilemap). No genre scaffolds yet.
|
|
102
|
+
|
|
103
|
+
New packages: `romdev-core-gametank`. The capability conformance test holds
|
|
104
|
+
GameTank to the full Tier-1 contract (13/13).
|
|
105
|
+
|
|
106
|
+
## 0.81.0 — 2026-06-29
|
|
107
|
+
|
|
108
|
+
**Shared toolchain layers.** The same de-duplication pass the debug ABI got, applied
|
|
109
|
+
to `src/toolchains/` — three small shared modules under `common/` collapse what every
|
|
110
|
+
wrapper and C-SDK builder re-implemented. Pure de-duplication: every argv, `/work`
|
|
111
|
+
path, output encoding, log header, stage name, and return shape is byte-for-byte
|
|
112
|
+
unchanged — no behavior change.
|
|
113
|
+
- **`common/wasm-tool.js`** — the glue-resolution dance (`import.meta.resolve(pkg)` →
|
|
114
|
+
local `src/` fallback → throw install hint) that ~15 wrappers each duplicated, plus
|
|
115
|
+
the `runIsolated` input-file marshalling. `makeGlueResolver` / `resolveGlueFile`
|
|
116
|
+
(file-glue: asar, wladx, rgbds, dasm, vasm68k, sjasm, tcc816, z80/sdas, da65) +
|
|
117
|
+
`resolveToolBaseDir` (base-dir: cc65, sdcc).
|
|
118
|
+
- **`common/gcc-toolchain.js`** — `makeGccToolchain(config)`. The arm/m68k/mips/sh GCC
|
|
119
|
+
wrappers were ~73% identical (cc1→as→ld→objcopy); each is now a ~67-line config object
|
|
120
|
+
+ thin named re-exports (callers keep `runM68kAs` etc). MIPS endian (N64 big / PS1
|
|
121
|
+
little) handled via per-stage flag functions. ~210 → 67 lines each.
|
|
122
|
+
- **`common/c-build.js`** — `CBuild` stage-runner + `BuildError`. The 5 GCC/tcc C-SDK
|
|
123
|
+
builders (gba/genesis/mips/sh/snes-c) plus cc65 repeated the
|
|
124
|
+
`run → log += → if (fail) return {ok:false,…,stage,…crash}` dance ~73×; it's now
|
|
125
|
+
`cb.stage(name, run, pick)` (throws `BuildError`) + a single try/catch that maps back
|
|
126
|
+
to the exact result via `e.toResult(extra)`. `BuildError.fields()` serves the cc65
|
|
127
|
+
shape (no `ok` field — index.js adds it). All 17 systems' C compilers now run through
|
|
128
|
+
the shared layers; sdcc is a documented partial fit (shares the log accumulator, keeps
|
|
129
|
+
its bespoke per-TU failure context). Full suite 1084/1084.
|
|
130
|
+
|
|
131
|
+
## 0.80.0 — 2026-06-29
|
|
132
|
+
|
|
133
|
+
**Shared debug ABI library.** The per-core debug instrumentation (watchpoints,
|
|
134
|
+
read-watch, range-watch, PC coverage, PC breakpoints, the instruction watchdog,
|
|
135
|
+
and the at-hit register snapshot) was ~700 lines of nearly-identical C inlined
|
|
136
|
+
into every core's patch. It's now ONE shared library — `scripts/romdev-debug/
|
|
137
|
+
romdev_debug.{h,c}` — that every migrated core links against. Each core keeps only
|
|
138
|
+
a small hook shim (bus taps + dispatch hook + its CPU-specific register snapshot);
|
|
139
|
+
the machinery and every host-probed `romdev_*` export live in exactly one place.
|
|
140
|
+
|
|
141
|
+
This is purely internal — **no tool or ABI behavior changes** for callers. The
|
|
142
|
+
payoff: the host ↔ core debug contract can no longer drift per core, adding the
|
|
143
|
+
debug surface to a new core drops from ~700 lines to ~80, and a new conformance
|
|
144
|
+
test makes any drift a hard failure.
|
|
145
|
+
|
|
146
|
+
- **Shared lib + ABI contract** (`romdev_debug.h`): the ~16 host-probed exports +
|
|
147
|
+
the core-hook interface (`romdev_on_write/on_read/on_dispatch` returning hit/freeze
|
|
148
|
+
signals; `romdev_is_frozen`/`romdev_pc_hit_kind`/`romdev_wp_wants_old`/`romdev_any_armed`
|
|
149
|
+
accessors).
|
|
150
|
+
- **10 cores migrated**, spanning every CPU family in the project: gambatte (sm83),
|
|
151
|
+
fceumm (6502), genesis-plus-gx (m68k **and** z80 — both CPUs now feed the SAME shared
|
|
152
|
+
debug state, replacing the old "m68kcpu.c defines, z80.c externs" split), snes9x
|
|
153
|
+
(65816), vice (6510/C64), prosystem (6502/7800), stella2014 (6507/2600), geargrafx
|
|
154
|
+
(HuC6280/PCE), bluemsx (z80/MSX), and mgba (ARM7TDMI/GBA — the lone 32-bit RISC core).
|
|
155
|
+
Each per-core patch shrank substantially (e.g. fceumm 869→421, snes9x 814→449,
|
|
156
|
+
genesis 1249→738, mgba 934→437 lines).
|
|
157
|
+
- **handy (Lynx 65C02) migrated too — the last inline holdout.** A first attempt regressed
|
|
158
|
+
CPU execution; the root cause turned out to be the read hook. The Lynx's `CPU_PEEK`
|
|
159
|
+
reads memory OR a hardware I/O register (`mSystem.Peek_CPU` at $FC00+), and reading an
|
|
160
|
+
I/O register has SIDE EFFECTS (clears a status bit / advances a FIFO). The first migrated
|
|
161
|
+
read macro was an expression (comma-operator) form that re-evaluated the underlying read
|
|
162
|
+
2-3× → the I/O register got read multiple times per access → corrupted hardware state →
|
|
163
|
+
the CPU never reached its main loop (looked like a "freeze"). Fixed by reading the source
|
|
164
|
+
EXACTLY ONCE into a temp via a GCC statement-expression before calling `romdev_on_read`.
|
|
165
|
+
Patch shrank 640→251 lines. With this, **all 13 instrumented cores share the lib** — no
|
|
166
|
+
inline holdouts remain. (Also fixed the libretro.cpp frame-loop guard, which referenced
|
|
167
|
+
the now-private `romdev_pc_hit` symbol directly → switched to the `romdev_is_frozen()`
|
|
168
|
+
accessor like every other migrated core.)
|
|
169
|
+
- **The newer cores too — and ONE PS1 core now.** N64 (parallel-n64, MIPS R4300) had its
|
|
170
|
+
own divergent debug snippet (a second copy of the machinery with a 2-arg pcbreak_set that
|
|
171
|
+
silently dropped the host's step arg → single-step was BROKEN); it now uses the shared lib
|
|
172
|
+
via a thin shim (R4300 snapshot + RDRAM canon + call-site adapters), single-step fixed.
|
|
173
|
+
**PS1 was split across TWO cores** — beetle-psx-hw (the GPU/GL renderer, but read-only
|
|
174
|
+
state) and pcsx-rearmed (full debug, but software-render + not even wired into the
|
|
175
|
+
registry). That split was useless — you couldn't get GPU *and* debugging in one core. So
|
|
176
|
+
the full debug surface (watchpoints, pc-break, single-step, range, coverage) was added
|
|
177
|
+
DIRECTLY to **beetle-psx-hw** (shared lib + a shim hooking mednafen's WriteMemory/
|
|
178
|
+
ReadMemory + the CPU_RunReal interpreter loop), and **pcsx-rearmed was deleted entirely**
|
|
179
|
+
(package, build script, snippets, version pin). PS1 is now ONE core that GPU-renders AND
|
|
180
|
+
fully debugs. Dreamcast (flycast, SH-4) has no debug machinery — only read-only SH-4/AICA
|
|
181
|
+
state accessors (irreducibly per-core), nothing to migrate. **all 13 instrumented cores share the lib** (Lynx included — see below). Also fixed a missing package-stage step in the
|
|
182
|
+
beetle build (rebuilds appeared to "do nothing" since the registry loads the package copy).
|
|
183
|
+
- **`test/romdev-debug-abi.test.js`**: loads each migrated core's wasm and asserts the
|
|
184
|
+
full shared ABI is exported — drift fails the suite (now guards all 13, incl N64 + beetle PS1 + Lynx).
|
|
185
|
+
**`test/romdev-debug-lib.test.js`** compiles the lib natively and checks its logic.
|
|
186
|
+
- Build wiring: each migrated `build-<core>.sh` stages + includes (`INCFLAGS_PLATFORM`)
|
|
187
|
+
+ compiles/archives `romdev_debug.c`. Author guide for the shim-only flow is in
|
|
188
|
+
`BUILDING.md`. (CRLF cores — gpgx z80, vice, prosystem — generate the patch against an
|
|
189
|
+
LF-normalized baseline so `git diff` doesn't explode.)
|
|
190
|
+
|
|
7
191
|
## 0.71.1 — 2026-06-29
|
|
8
192
|
|
|
9
193
|
Feedback from the first run against the `pret/pokeruby` GBA decompilation.
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# romdev
|
|
2
2
|
|
|
3
|
-
The entry point for **romdev** — vibe-code real retro games. Build, run, inspect, and reverse-engineer actual homebrew ROMs (NES, SNES, Game Boy, Genesis, Atari, C64, GBA, PC Engine, MSX — and the 3D consoles N64, PlayStation, and Dreamcast) with one command — drive it yourself or let a coding assistant do it.
|
|
3
|
+
The entry point for **romdev** — vibe-code real retro games. Build, run, inspect, and reverse-engineer actual homebrew ROMs (NES, SNES, Game Boy, Genesis, Atari, C64, GBA, PC Engine, MSX, GameTank — and the 3D consoles N64, PlayStation, and Dreamcast) with one command — drive it yourself or let a coding assistant do it.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
npx romdevtools
|
|
@@ -9,9 +9,9 @@ npx romdevtools
|
|
|
9
9
|
**What you get:**
|
|
10
10
|
|
|
11
11
|
- **Build** — bundled per-platform toolchains (cc65, SDCC, RGBDS, asar, vasm, SGDK, PVSnesLib, libtonc, …) as WASM. Write source, compile, get a real ROM.
|
|
12
|
-
- **Run + see + drive** — load the ROM into an emulated console (libretro cores as WASM), step frames, screenshot, script controller input. The 2D consoles render in software; the 3D consoles (N64 via glide64, PlayStation via Beetle PSX HW, Dreamcast via Flycast) render on the **real GPU** through [`native-gles`](https://github.com/monteslu/native-gles) — headless OpenGL/EGL, no browser. PlayStation ships [OpenBIOS](https://github.com/grumpycoders/pcsx-redux) (MIT) embedded, so there's no proprietary firmware to supply.
|
|
12
|
+
- **Run + see + drive** — load the ROM into an emulated console (libretro cores as WASM), step frames, screenshot, script controller input. The 2D consoles render in software; the 3D consoles (N64 via glide64, PlayStation via Beetle PSX HW, Dreamcast via Flycast) render on the **real GPU** through [`native-gles`](https://github.com/monteslu/native-gles) — headless OpenGL/EGL, no browser. PlayStation ships [OpenBIOS](https://github.com/grumpycoders/pcsx-redux) (MIT) embedded, so there's no proprietary firmware to supply. **Dreamcast is EXPERIMENTAL** — it boots and renders, but the WASM build runs the SH-4 (+ AICA) as interpreters, so it's currently too slow to be playable; treat it as analysis/RE-only for now.
|
|
13
13
|
- **Inspect + romhack** — read CPU/video/save RAM, watch memory, write-breakpoints, the Cheat-Engine value-search loop, a bundled cheat database, mapper-aware disassembly, and a byte-exact rebuildable-project disassembler.
|
|
14
|
-
- **Reverse-engineering analysis engine (all
|
|
14
|
+
- **Reverse-engineering analysis engine (all 18 platforms — incl. the 3D consoles' MIPS R3000/R4300 + SH-4)** — control-flow graphs, deep cross-references, auto-detected functions (ranked real-code-first), a one-shot structural map, and a Ghidra **decompiler** (C-like pseudocode, with hardware registers named and 6502 SLEIGH clutter folded to readable C): `disasm({target:'cfg'|'xrefs'|'functions'|'decompile'})` and `symbols({op:'analyze'})`. And the piece no static tool has: **live computed-jumptable recovery** — `breakpoint({on:'jumptable'})` runs the emulator to resolve the `JMP (table,X)` / RTS-trick dispatchers (state machines, script/battle VMs) that static analysis collapses to "could not recover." Understand *how* a routine works before you touch it — no $3,000 IDA license, no install.
|
|
15
15
|
- **Convert assets** — PNG → platform tiles/tilemaps, quantize-to-palette, audio importers (BRR for SNES, XGM2 PCM for Genesis).
|
|
16
16
|
|
|
17
17
|
Point any coding agent at it three ways:
|
|
@@ -36,12 +36,12 @@ This package contains all the JavaScript — the tool surface, the WASM emulator
|
|
|
36
36
|
|
|
37
37
|
`romdevtools` depends on the binary/data packages it needs (exact-pinned), so a single install gets a matched, tested set:
|
|
38
38
|
|
|
39
|
-
- 2D cores: `romdev-core-{fceumm,gambatte,gpgx,vice,handy,prosystem,geargrafx,bluemsx}`
|
|
39
|
+
- 2D cores: `romdev-core-{fceumm,gambatte,gpgx,vice,handy,prosystem,geargrafx,bluemsx,gametank}`
|
|
40
40
|
- 3D / GPU cores (rendered through `native-gles`): `romdev-core-{parallel-n64,beetle-psx-hw,flycast}`
|
|
41
41
|
- Platforms (core + dedicated toolchain bundled): `romdev-platform-{snes,gba,atari2600}`
|
|
42
42
|
- Toolchains: `romdev-toolchain-{cc65,sdcc,m68k-gcc,vasm,rgbds,mips-gcc,sh-gcc}` (mips-gcc = N64/PS1 C; sh-gcc = Dreamcast C)
|
|
43
43
|
- GPU deps (OPTIONAL — only the 3D cores need them; the 2D cores never touch GL): `native-gles` + `webgl-node`. A headless user without a GPU stack can still run all the 2D platforms.
|
|
44
|
-
- Analysis: `romdev-analysis` (Rizin → WASM: control-flow graphs, cross-references, function detection) and `romdev-analysis-decompiler` (Ghidra's C++ decompiler → WASM + SLEIGH processor specs for all
|
|
44
|
+
- Analysis: `romdev-analysis` (Rizin → WASM: control-flow graphs, cross-references, function detection) and `romdev-analysis-decompiler` (Ghidra's C++ decompiler → WASM + SLEIGH processor specs for all 18 CPUs, incl. MIPS R3000/R4300 + SH-4). Power `disasm({target:'cfg'|'xrefs'|'functions'|'decompile'})` and `symbols({op:'analyze'})`. Lazy-loaded on first use.
|
|
45
45
|
- Data: `romdev_game_codes` — the bundled game-code / cheat database (a free labeled RAM/code map for thousands of known ROMs), split out so it can grow independently. Lazy-loaded one platform at a time.
|
|
46
46
|
|
|
47
47
|
`@kmamal/sdl` is used only by `playtest()` / `romdevtools-cli play` (the live window). It ships its native binary via its own install script, which npm skips when romdev is a transitive dep (e.g. under `npx`) — so romdev's `postinstall` fetches it, and `playtest()` also self-heals at runtime if the binary is still missing (downloading the prebuilt before the first window open). Either way, if the binary can't be fetched (offline/locked-down network), the headless server is unaffected — only the live window degrades, and the error tells you the one command to fix it.
|
package/examples/README.md
CHANGED
|
@@ -26,6 +26,7 @@ Each example fits the convention:
|
|
|
26
26
|
| gba | `gba/templates/*.c` | arm-none-eabi-gcc | Default runtime = **libtonc** (`#include <tonc.h>`). 9 examples incl. `tonc_hello`, `tonc_hello_sprite`, the 5 genre games, and `maxmod_demo` (music). Pass `runtime:"libgba"` for the devkitPro API, `runtime:"none"` for bare newlib. **Always call `irq_init(NULL); irq_add(II_VBLANK, NULL);` before `VBlankIntrWait()`** — otherwise the BIOS halts forever. |
|
|
27
27
|
| pce | `pce/<template>/main.c` | cc65 (HuC6280) | HuCard homebrew, no BIOS. Ships a direct-register VDC/PSG helper lib (`pce.h` + `pce.lib`) — cc65 has no PCE sprite/sound library. Examples: `sprite_move`, `catch_game`, `music_sfx`, plus the 5 genre games (shmup/platformer/puzzle/sports/racing). **`#include <stdint.h>`** for int8/16/32_t — `pce.h` only typedefs u8/u16. The genre games fill the BAT (32×32 virtual screen); the platformer smooth-scrolls via the VDC BXR register. |
|
|
28
28
|
| msx | `msx/<template>/main.c` | sdcc (z80) | Boots cartridge homebrew on the open C-BIOS (no proprietary ROM). Ships an AY-3-8910 + TMS9918/V9938 VDP helper lib (`msx_hw.h` + `msx_vdp.c`). Examples: `sprite_move`, `catch_game`, `music_sfx`, plus the 5 genre games. The bundled `msx_crt0.s` (applied by the dir-build recipe automatically) emits the `"AB"` cartridge header at $4000 + INIT pointer — **C-BIOS shows its logo for ~2-3 s, then CALLs INIT**, so run ≥240 frames before screenshotting. The platformer column-streams the SCREEN 2 name table for a tile-by-tile scroll. |
|
|
29
|
+
| gametank | `gametank/templates/*.c` | cc65 (W65C02S) | Build with `linkerConfig:"sdk"` + the bundled SDK runtime (`src/platforms/gametank/lib/gt/*`). FIVE complete genre games, fully reworked and play-tested on real hardware (RetroDeck): `shmup`, `platformer`, `puzzle` (CHROMA WELL — a Columns-style 3-jewel matcher), `sports` (2-player paddle), `racing`. The GameTank is a **framebuffer + blitter** machine — NO tilemap/nametable: every frame is redrawn from scratch via the SDK **draw QUEUE** (`gt_draw.h`), the only correct way to drive the blitter (synchronous blits corrupt the FB + tank the framerate). Shared helper headers the games fork: `gt_draw.h` (queue wrappers + `rnd8`), `gt_palette.h` (VERIFIED color indices), `gt_sprites.h` (GRAM sprite load/blit), `gt_hud.h` (box-font digits + 3×5 A–Z text), `gt_sound.h` (one-shot ACP SFX). **HARD-WON GOTCHAS (all documented in `gt_draw.h`): (1) a box of width OR height 128 is silently DROPPED — clamp to 127; (2) box top-edge scanlines flicker between the double-buffer pages — finish a frame that draws near the top with `queue_clear_border(topColor)`; (3) use `rnd8()`, NOT the SDK `rnd()`, which corrupts state on this single-bank build.** Per-frame GRAM **sprite** blits are the expensive part — too many overrun vblank and the queue drops draws (flicker), so keep sprites few (e.g. the platformer blits only the hero; coins are rects). |
|
|
29
30
|
|
|
30
31
|
## Guides
|
|
31
32
|
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/* ── gt_draw.h — GameTank drawing for the romdev examples (SDK draw-queue) ─────
|
|
2
|
+
*
|
|
3
|
+
* Thin wrappers over the GameTank SDK's draw QUEUE — the ONLY correct way to drive
|
|
4
|
+
* the blitter. You enqueue rects (queue_draw_box); the blit-done IRQ pulls them off
|
|
5
|
+
* the queue one at a time and triggers each blit when the previous one finishes
|
|
6
|
+
* (interrupt.s _irq_int → next_draw_queue). await_draw_queue() blocks until the
|
|
7
|
+
* queue has fully drained, then you wait one VBLANK and flip the double buffer.
|
|
8
|
+
*
|
|
9
|
+
* WHY NOT fire blits synchronously: a blit takes WIDTH*HEIGHT CPU cycles to run
|
|
10
|
+
* (the core schedules its done-IRQ that many cycles ahead; instant_mode is off).
|
|
11
|
+
* Triggering the next blit before the current one finishes calls ClearIRQ and
|
|
12
|
+
* reschedules — stomping the in-flight blit. Firing ~30 rects back-to-back that
|
|
13
|
+
* way both corrupts the framebuffer (static) and piles scheduled cycles against
|
|
14
|
+
* await_vsync (≈2 fps). The queue exists precisely to serialize blits correctly;
|
|
15
|
+
* use it.
|
|
16
|
+
*
|
|
17
|
+
* Frame pattern:
|
|
18
|
+
* gt_clear(bg); ...gt_rect(...) for everything... ; gt_present();
|
|
19
|
+
* where gt_present() = await_draw_queue() + await_vsync(1) + flip_pages().
|
|
20
|
+
*
|
|
21
|
+
* ── TWO BLITTER FOOTGUNS (cost many hours each — heed them) ──
|
|
22
|
+
* 1. A box whose WIDTH or HEIGHT is exactly 128 (the full screen dimension) is
|
|
23
|
+
* SILENTLY DROPPED — it never draws. A full-width ground/background/HUD slab at
|
|
24
|
+
* width 128 just vanishes, leaving whatever was under it. ALWAYS clamp to 127.
|
|
25
|
+
* (The bottom row / right column is TV overscan anyway, so 127 loses nothing.)
|
|
26
|
+
* 2. A box's TOP scanline flickers between the two double-buffer pages — only
|
|
27
|
+
* gt_clear's border-clear path can paint the screen's edge rows (0-6 top, the
|
|
28
|
+
* bottom overscan, the side columns) cleanly. If your frame draws sprites or
|
|
29
|
+
* full-height rects near the top, finish it with `queue_clear_border(topColor)`
|
|
30
|
+
* as the LAST draw so the border scanlines stay solid (no stray flickering line).
|
|
31
|
+
* If you also want HUD text up at the very top (rows 0-6), draw it AFTER the
|
|
32
|
+
* border-clear (the clear would otherwise erase it).
|
|
33
|
+
*
|
|
34
|
+
* Colors are palette indices — see gt_palette.h for VERIFIED vibrant ones.
|
|
35
|
+
* Audio (gt_sound.h) runs on the ACP independently. Random: use rnd8() (below), NOT
|
|
36
|
+
* the SDK rnd() — rnd() corrupts game state on this single-bank build.
|
|
37
|
+
*/
|
|
38
|
+
#ifndef GT_DRAW_H
|
|
39
|
+
#define GT_DRAW_H
|
|
40
|
+
#include "gametank.h"
|
|
41
|
+
#include "draw_queue.h" /* queue_draw_box, queue_clear_screen, queue_clear_border, await_draw_queue */
|
|
42
|
+
#include "gfx_sys.h" /* await_vsync, flip_pages, init_graphics */
|
|
43
|
+
|
|
44
|
+
#define SCREEN_W 128
|
|
45
|
+
#define SCREEN_H 128
|
|
46
|
+
|
|
47
|
+
/* Tiny xorshift byte PRNG. USE THIS, NOT the SDK's rnd() — rnd() (feature/random/
|
|
48
|
+
* random.c) corrupts game state on the EEPROM32K single-bank build (it broke enemy
|
|
49
|
+
* spawning in the shooter for a long time). rnd8() needs no init and never zero-locks. */
|
|
50
|
+
static unsigned char gt_rng_state = 0x5A;
|
|
51
|
+
static unsigned char rnd8(void) {
|
|
52
|
+
gt_rng_state ^= (unsigned char)(gt_rng_state << 1);
|
|
53
|
+
gt_rng_state ^= (unsigned char)(gt_rng_state >> 1);
|
|
54
|
+
gt_rng_state ^= (unsigned char)(gt_rng_state << 2);
|
|
55
|
+
return gt_rng_state;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* enqueue one filled rectangle (drawn by the blit-done IRQ, in order). */
|
|
59
|
+
#define gt_rect(x, y, w, h, color) queue_draw_box((x), (y), (w), (h), (color))
|
|
60
|
+
|
|
61
|
+
/* clear the whole frame: fill the play area + the border (queue_clear_screen
|
|
62
|
+
* leaves a border uncleared — without queue_clear_border it shows VRAM static). */
|
|
63
|
+
static void gt_clear(unsigned char color) {
|
|
64
|
+
queue_clear_screen(color);
|
|
65
|
+
queue_clear_border(color);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/* present the frame: drain the draw queue (wait for every enqueued blit to
|
|
69
|
+
* finish), wait one VBLANK, then flip the double buffer. Once per frame, after
|
|
70
|
+
* all your gt_rect()s. This is the SDK's canonical end-of-frame sequence. */
|
|
71
|
+
static void gt_present(void) {
|
|
72
|
+
await_draw_queue();
|
|
73
|
+
await_vsync(1);
|
|
74
|
+
flip_pages();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* "press to start" gate. Returns 1 once the START or A button has been HELD for a
|
|
78
|
+
* couple of frames. A short settle counter at the top of each title screen ignores
|
|
79
|
+
* the first few frames (so a button still held from the previous screen / boot can't
|
|
80
|
+
* auto-skip), then any held press starts. Level-triggered → can't miss a tap, and
|
|
81
|
+
* doesn't depend on ever reading a clean all-zero input frame. Call once per
|
|
82
|
+
* title-loop iteration AFTER update_inputs(). (Needs <input.h>.) */
|
|
83
|
+
static unsigned char gt_start_settle;
|
|
84
|
+
#define GT_START_MASK (INPUT_MASK_START | INPUT_MASK_A)
|
|
85
|
+
static void gt_start_reset(void) { gt_start_settle = 0; } /* call before each title loop */
|
|
86
|
+
static unsigned char gt_start_pressed(void) {
|
|
87
|
+
if (gt_start_settle < 12) { gt_start_settle++; return 0; } /* ignore first ~12 frames */
|
|
88
|
+
return (player1_buttons & GT_START_MASK) ? 1 : 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
#endif /* GT_DRAW_H */
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/* ── gt_hud.h — box-drawn HUD digits for the romdev GameTank examples ─────────
|
|
2
|
+
*
|
|
3
|
+
* The SDK's text_print_string() draws a FONT that's baked by the Node asset
|
|
4
|
+
* pipeline (a .bmp loaded into GRAM) — not available on romdev's bare single-bank
|
|
5
|
+
* path. So these examples render score/lives digits as small `gt_rect`
|
|
6
|
+
* segments (a 7-seg-style 3x5 cell). Pure SDK draw-queue API, no assets.
|
|
7
|
+
*
|
|
8
|
+
* Each digit is a 3-wide x 5-tall grid of `px`-sized boxes; we draw one box per
|
|
9
|
+
* lit cell. Cheap for short HUD numbers (score, lives, level).
|
|
10
|
+
*/
|
|
11
|
+
#ifndef GT_HUD_H
|
|
12
|
+
#define GT_HUD_H
|
|
13
|
+
#include "gametank.h"
|
|
14
|
+
#include "gt_draw.h"
|
|
15
|
+
|
|
16
|
+
/* 3x5 bitmaps for 0-9, row-major, bit 2 = leftmost of the 3 columns. */
|
|
17
|
+
static const unsigned char GT_DIG[10][5] = {
|
|
18
|
+
{0x7,0x5,0x5,0x5,0x7}, {0x2,0x6,0x2,0x2,0x7}, {0x7,0x1,0x7,0x4,0x7},
|
|
19
|
+
{0x7,0x1,0x3,0x1,0x7}, {0x5,0x5,0x7,0x1,0x1}, {0x7,0x4,0x7,0x1,0x7},
|
|
20
|
+
{0x7,0x4,0x7,0x5,0x7}, {0x7,0x1,0x2,0x2,0x2}, {0x7,0x5,0x7,0x5,0x7},
|
|
21
|
+
{0x7,0x5,0x7,0x1,0x7},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/* draw one digit (0-9) at (x,y), box size px, in color c. */
|
|
25
|
+
static void hud_digit(unsigned char d, unsigned char x, unsigned char y,
|
|
26
|
+
unsigned char px, unsigned char c) {
|
|
27
|
+
unsigned char r, col;
|
|
28
|
+
for (r = 0; r < 5; r++) {
|
|
29
|
+
unsigned char bits = GT_DIG[d % 10][r];
|
|
30
|
+
for (col = 0; col < 3; col++)
|
|
31
|
+
if (bits & (0x4 >> col))
|
|
32
|
+
gt_rect(x + col * px, y + r * px, px, px, c);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* draw a number right-aligned with its UNITS digit's left edge at x. */
|
|
37
|
+
static void hud_number(unsigned int n, unsigned char x, unsigned char y,
|
|
38
|
+
unsigned char px, unsigned char c) {
|
|
39
|
+
unsigned char cx = x;
|
|
40
|
+
do {
|
|
41
|
+
hud_digit((unsigned char)(n % 10), cx, y, px, c);
|
|
42
|
+
n /= 10;
|
|
43
|
+
cx -= 4 * px;
|
|
44
|
+
} while (n);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/* a row of `n` small pips (lives/ammo) starting at x, gap-spaced. */
|
|
48
|
+
static void hud_pips(unsigned char n, unsigned char x, unsigned char y,
|
|
49
|
+
unsigned char size, unsigned char c) {
|
|
50
|
+
unsigned char i;
|
|
51
|
+
for (i = 0; i < n; i++)
|
|
52
|
+
gt_rect(x + i * (size + 2), y, size, size, c);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* ── 3x5 box-letter font (A-Z + space) so titles can show real WORDS without the
|
|
56
|
+
* asset-pipeline font. Same row-major bit format as GT_DIG (bit2 = leftmost). */
|
|
57
|
+
static const unsigned char GT_LET[27][5] = {
|
|
58
|
+
{0x2,0x5,0x7,0x5,0x5}, /*A*/ {0x6,0x5,0x6,0x5,0x6}, /*B*/ {0x3,0x4,0x4,0x4,0x3}, /*C*/
|
|
59
|
+
{0x6,0x5,0x5,0x5,0x6}, /*D*/ {0x7,0x4,0x6,0x4,0x7}, /*E*/ {0x7,0x4,0x6,0x4,0x4}, /*F*/
|
|
60
|
+
{0x3,0x4,0x5,0x5,0x3}, /*G*/ {0x5,0x5,0x7,0x5,0x5}, /*H*/ {0x7,0x2,0x2,0x2,0x7}, /*I*/
|
|
61
|
+
{0x1,0x1,0x1,0x5,0x2}, /*J*/ {0x5,0x6,0x4,0x6,0x5}, /*K*/ {0x4,0x4,0x4,0x4,0x7}, /*L*/
|
|
62
|
+
{0x5,0x7,0x7,0x5,0x5}, /*M*/ {0x5,0x7,0x7,0x7,0x5}, /*N*/ {0x2,0x5,0x5,0x5,0x2}, /*O*/
|
|
63
|
+
{0x6,0x5,0x6,0x4,0x4}, /*P*/ {0x2,0x5,0x5,0x6,0x3}, /*Q*/ {0x6,0x5,0x6,0x5,0x5}, /*R*/
|
|
64
|
+
{0x3,0x4,0x2,0x1,0x6}, /*S*/ {0x7,0x2,0x2,0x2,0x2}, /*T*/ {0x5,0x5,0x5,0x5,0x7}, /*U*/
|
|
65
|
+
{0x5,0x5,0x5,0x5,0x2}, /*V*/ {0x5,0x5,0x7,0x7,0x5}, /*W*/ {0x5,0x5,0x2,0x5,0x5}, /*X*/
|
|
66
|
+
{0x5,0x5,0x2,0x2,0x2}, /*Y*/ {0x7,0x1,0x2,0x4,0x7}, /*Z*/ {0x0,0x0,0x0,0x0,0x0}, /*space*/
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/* draw one 3x5 char (A-Z, space) at (x,y), box size px, color c. */
|
|
70
|
+
static void hud_char(char ch, unsigned char x, unsigned char y,
|
|
71
|
+
unsigned char px, unsigned char c) {
|
|
72
|
+
unsigned char r, col, idx;
|
|
73
|
+
if (ch >= 'A' && ch <= 'Z') idx = ch - 'A';
|
|
74
|
+
else if (ch >= 'a' && ch <= 'z') idx = ch - 'a';
|
|
75
|
+
else idx = 26; /* space / unknown */
|
|
76
|
+
for (r = 0; r < 5; r++) {
|
|
77
|
+
unsigned char bits = GT_LET[idx][r];
|
|
78
|
+
for (col = 0; col < 3; col++)
|
|
79
|
+
if (bits & (0x4 >> col)) gt_rect(x + col * px, y + r * px, px, px, c);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/* draw a string (A-Z + spaces) starting at (x,y); 4*px per char advance. */
|
|
84
|
+
static void hud_text(const char *s, unsigned char x, unsigned char y,
|
|
85
|
+
unsigned char px, unsigned char c) {
|
|
86
|
+
while (*s) { hud_char(*s, x, y, px, c); x += 4 * px; s++; }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
#endif /* GT_HUD_H */
|