romdevtools 0.56.1 → 0.71.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.
Files changed (49) hide show
  1. package/CHANGELOG.md +338 -0
  2. package/README.md +9 -7
  3. package/examples/dreamcast/hello/main.c +24 -0
  4. package/examples/dreamcast/platformer/main.c +31 -0
  5. package/examples/dreamcast/puzzle/main.c +44 -0
  6. package/examples/dreamcast/racing/main.c +39 -0
  7. package/examples/dreamcast/shmup/main.c +50 -0
  8. package/examples/dreamcast/sports/main.c +39 -0
  9. package/package.json +5 -3
  10. package/src/analysis/analyze.js +60 -5
  11. package/src/analysis/decompile.js +3 -0
  12. package/src/analysis/rizin.js +3 -1
  13. package/src/cores/capabilities.js +43 -7
  14. package/src/cores/registry.js +13 -8
  15. package/src/host/LibretroGL.js +26 -23
  16. package/src/host/LibretroHost.js +302 -24
  17. package/src/host/callbacks.js +72 -1
  18. package/src/host/coreLoader.js +17 -4
  19. package/src/host/cpu-state.js +32 -0
  20. package/src/host/dc-aica-state.js +67 -0
  21. package/src/mcp/tools/audio.js +1 -1
  22. package/src/mcp/tools/disasm.js +1 -1
  23. package/src/mcp/tools/index.js +1 -1
  24. package/src/mcp/tools/platform-docs.js +1 -1
  25. package/src/mcp/tools/platform-tools.js +9 -1
  26. package/src/mcp/tools/project.js +16 -0
  27. package/src/mcp/tools/toolchain.js +115 -10
  28. package/src/platforms/dreamcast/MENTAL_MODEL.md +87 -0
  29. package/src/platforms/dreamcast/TROUBLESHOOTING.md +55 -0
  30. package/src/platforms/dreamcast/UPSTREAM_SOURCES.md +57 -0
  31. package/src/platforms/n64/MENTAL_MODEL.md +84 -0
  32. package/src/platforms/n64/TROUBLESHOOTING.md +60 -0
  33. package/src/platforms/n64/UPSTREAM_SOURCES.md +52 -0
  34. package/src/platforms/n64/lib/c/n64.c +181 -80
  35. package/src/platforms/ps1/MENTAL_MODEL.md +85 -0
  36. package/src/platforms/ps1/TROUBLESHOOTING.md +55 -0
  37. package/src/platforms/ps1/UPSTREAM_SOURCES.md +54 -0
  38. package/src/platforms/snes/TROUBLESHOOTING.md +10 -0
  39. package/src/toolchains/asar/asar.js +84 -14
  40. package/src/toolchains/index.js +30 -0
  41. package/src/toolchains/mips-c/mips-c.js +35 -1
  42. package/src/toolchains/sh-c/lib/dc-crt0.s +23 -0
  43. package/src/toolchains/sh-c/lib/dc.h +152 -0
  44. package/src/toolchains/sh-c/lib/dc.ld +11 -0
  45. package/src/toolchains/sh-c/lib/libc.a +0 -0
  46. package/src/toolchains/sh-c/lib/libgcc.a +0 -0
  47. package/src/toolchains/sh-c/lib/libm.a +0 -0
  48. package/src/toolchains/sh-c/sh-c.js +104 -0
  49. package/src/toolchains/sh-elf-gcc/gcc.js +122 -0
@@ -0,0 +1,67 @@
1
+ // Dreamcast AICA sound-chip decode — the "what is each of the 64 PCM channels
2
+ // doing this frame?" view that getAudioState gives the other platforms' chips.
3
+ // Input is the raw AICA register window (Uint8Array) from host.getAicaRegs():
4
+ // 64 channels × 0x80 bytes starting at 0x000, plus CommonData at 0x2800.
5
+ //
6
+ // The AICA is a 64-channel PCM/ADPCM sampler. Canonical per-channel register map
7
+ // (offset within the channel's 0x80 block), little-endian 16-bit registers:
8
+ // 0x00 : KYONEX(15) KYONB(14) ... LPCTL(9) PCMS(8..7) SA-high(6..0)
9
+ // 0x04 : SA-low (sample start address, low 16)
10
+ // 0x08 : LSA (loop start) 0x0C : LEA (loop end)
11
+ // 0x10 : D2R/D1R/AR (attack/decay) 0x14 : RR/DL/KRS/LS (release/sustain)
12
+ // 0x18 : LPSLNK / OCT / FNS (pitch: OCT bits 14..11 signed, FNS bits 9..0)
13
+ // 0x24 : DISDL (direct send level, bits 11..8) / DIPAN (pan, bits 4..0)
14
+ // CommonData (0x2800): 0x00 MVOL (master volume, bits 3..0) + MONO/MEM8MB/etc.
15
+
16
+ const CH_STRIDE = 0x80;
17
+ const NUM_CH = 64;
18
+ const COMMON = 0x2800;
19
+
20
+ /**
21
+ * @param {Uint8Array|null} reg the AICA register window from getAicaRegs()
22
+ * @returns {{ chip:"aica", masterVolume:number, voices:Array<object>,
23
+ * activeVoices:number } | null}
24
+ */
25
+ export function decodeAica(reg) {
26
+ if (!reg || reg.length < COMMON + 2) return null;
27
+ const u16 = (o) => (reg[o] | (reg[o + 1] << 8)) & 0xffff;
28
+
29
+ const voices = [];
30
+ let active = 0;
31
+ for (let c = 0; c < NUM_CH; c++) {
32
+ const base = c * CH_STRIDE;
33
+ if (base + 0x28 > reg.length) break;
34
+ const play = u16(base + 0x00);
35
+ const keyOn = !!(play & 0x4000); // KYONB
36
+ const loop = !!(play & 0x0200); // LPCTL
37
+ const pcms = (play >> 7) & 0x3; // 0/1 = 16/8-bit PCM, 2 = ADPCM
38
+ const saHi = play & 0x7f;
39
+ const saLo = u16(base + 0x04);
40
+ const sampleAddr = ((saHi << 16) | saLo) >>> 0;
41
+ const lsa = u16(base + 0x08);
42
+ const lea = u16(base + 0x0c);
43
+ const pitch = u16(base + 0x18);
44
+ const oct = (pitch >> 11) & 0xf; // signed 4-bit
45
+ const fns = pitch & 0x3ff;
46
+ const env = u16(base + 0x24);
47
+ const disdl = (env >> 8) & 0xf; // direct send level (volume)
48
+ const dipan = env & 0x1f; // pan
49
+ if (keyOn && disdl > 0) active++;
50
+ voices.push({
51
+ ch: c,
52
+ keyOn,
53
+ loop,
54
+ format: pcms === 2 ? "ADPCM" : (pcms === 1 ? "PCM8" : "PCM16"),
55
+ sampleAddr: "$" + sampleAddr.toString(16).toUpperCase(),
56
+ loopStart: lsa,
57
+ loopEnd: lea,
58
+ octave: oct >= 8 ? oct - 16 : oct, // 4-bit signed
59
+ fns,
60
+ volume: disdl, // 0..15 direct send level
61
+ pan: dipan,
62
+ });
63
+ }
64
+
65
+ const mvol = u16(COMMON + 0x00) & 0xf;
66
+ return { chip: "aica", masterVolume: mvol, activeVoices: active, voices };
67
+ }
@@ -367,7 +367,7 @@ export function registerAudioTools(server, z, sessionKey) {
367
367
  "sampled SFX fire' on Genesis is still record-and-FFT.",
368
368
  {
369
369
  op: z.enum(["inspect", "record"]).describe("inspect a sound chip's live state (single-frame, or a frames trace); or record audio to a WAV."),
370
- chip: z.enum(["nes", "gb", "gba", "dsp", "psg", "ym2612", "sid", "mikey", "pce", "ay8910"]).optional().describe("op=inspect: which sound chip to decode (all 14 systems mapped)."),
370
+ chip: z.enum(["nes", "gb", "gba", "dsp", "psg", "ym2612", "sid", "mikey", "pce", "ay8910", "spu", "ai", "aica"]).optional().describe("op=inspect: which sound chip to decode. Tile systems: nes/gb/gba/dsp(SNES)/psg(SN76489)/ym2612(Genesis FM)/sid(C64)/mikey(Lynx)/pce/ay8910(MSX). 3D systems: spu (PS1, 24 ADPCM voices), ai (N64 AI output), aica (Dreamcast, 64 PCM/ADPCM channels)."),
371
371
  frames: z.number().int().min(1).max(60000).optional().describe("op=record: emulator frames to capture (default 180 = 3s NTSC). op=inspect: if set, TRACE the chip over N frames into a per-channel timeline; OMIT for a single-frame snapshot."),
372
372
  sampleEvery: z.number().int().min(1).default(1).describe("op=inspect trace: sample the chip only every Nth frame (thins a long trace; the last frame is always sampled). Changes on skipped frames surface at the next sampled frame, so >1 loses fine timing — keep at 1 to catch every transition."),
373
373
  path: z.string().optional().describe("op=record: absolute path to write the WAV (required for record)."),
@@ -1461,7 +1461,7 @@ export function registerDisasmTools(server, z) {
1461
1461
  "read/write); also walks the vector table. Banked carts are scanned PER BANK (all of the formats above) — " +
1462
1462
  "refs carry `prgBank` (NES) / `romBank` (everything else). LIMITATION: direct addressing only " +
1463
1463
  "(indirect/computed jumps are missed).\n" +
1464
- "── RE ENGINE (Rizin + Ghidra, all 14 platforms) ──\n" +
1464
+ "── RE ENGINE (Rizin + Ghidra, all platforms incl. the 3D CPUs: MIPS R3000/R4300 + SH-4) ──\n" +
1465
1465
  "'functions' = Rizin auto-detected function list {address,size,nbbs,cc,callers,callees,looksLikeData}; the " +
1466
1466
  "structural map of an unknown ROM. Sorted REAL CODE FIRST (by nbbs/cc) — don't rank by `size`, which is a lie " +
1467
1467
  "(rizin folds data tables into giant pseudo-functions). `looksLikeData:true` (and the top-level `dataCount`) flags " +
@@ -155,7 +155,7 @@ const CATEGORIES = [
155
155
  },
156
156
  {
157
157
  name: "debug",
158
- description: "Cross-platform debugging + reverse-engineering: inspectSprites, inspectPalette, getCPUState (main/spc700/z80), getAudioState (dsp/psg/ym2612), and the disasm/RE engine — disassemble (raw/ROM/rebuildable-project), plus the Rizin/Ghidra ops disasm({target:'cfg'|'xrefs'|'functions'|'decompile'}) (control-flow graphs, deep xrefs, auto-detected functions, and C pseudocode — all 14 platforms) and symbols({op:'analyze'}) (one-shot structural map).",
158
+ description: "Cross-platform debugging + reverse-engineering: inspectSprites, inspectPalette, getCPUState (main/spc700/z80, plus the 3D CPUs: MIPS R3000/R4300, SH-4), getAudioState (dsp/psg/ym2612/… + spu/ai/aica for PS1/N64/Dreamcast), and the disasm/RE engine — disassemble (raw/ROM/rebuildable-project), plus the Rizin/Ghidra ops disasm({target:'cfg'|'xrefs'|'functions'|'decompile'}) (control-flow graphs, deep xrefs, auto-detected functions, and C pseudocode) and symbols({op:'analyze'}) (one-shot structural map).",
159
159
  useWhen: ["sprites rendering wrong", "audio silent or distorted", "CPU stuck in unknown state", "need to read what existing ROM bytes do", "reverse-engineering an unknown ROM — carve its functions/structure before labeling them live", "want C-like pseudocode to understand a routine"],
160
160
  register: (s, z, k) => {
161
161
  registerPlatformSpecificTools(s, z, k); // inspectSprites/Palette, getCPUState, getDspState, ...
@@ -70,7 +70,7 @@ export async function listPlatformDocsCore({ platform }) {
70
70
  docs,
71
71
  note: docs.length === 0
72
72
  ? `No docs shipped for '${platform}' yet. Try a different platform, or fork an example game (examples({op:'fork'})) for boilerplate. (For RE/patching workflow, see platform({op:'doc', platform:'romhacking', name:'playbook'}).)`
73
- : `Call platform({op:'doc', platform, name}) to read one. 'name' is 'mental_model' or 'troubleshooting'. For RE/patching workflow across platforms, see platform({op:'doc', platform:'romhacking', name:'playbook'}).`,
73
+ : `Call platform({op:'doc', platform, name}) to read one. 'name' is 'mental_model', 'troubleshooting', or 'upstream_sources' (whichever this platform ships — see the docs[] list above). For RE/patching workflow across platforms, see platform({op:'doc', platform:'romhacking', name:'playbook'}).`,
74
74
  };
75
75
  }
76
76
 
@@ -42,6 +42,7 @@ import { decodeLynxMikey, decodeLynxPalette } from "../../host/lynx-mikey-state.
42
42
  import { getPcePsgState } from "../../host/pce-psg-state.js";
43
43
  import { decodePs1Spu } from "../../host/ps1-spu-state.js";
44
44
  import { decodeN64Ai } from "../../host/n64-ai-state.js";
45
+ import { decodeAica } from "../../host/dc-aica-state.js";
45
46
  import { getMsxAyState } from "../../host/msx-ay-state.js";
46
47
  import { decodeGbaSprites, decodeGbaPalette } from "../../host/gba-video-state.js";
47
48
 
@@ -440,7 +441,14 @@ export function registerPlatformTools(server, z, sessionKey) {
440
441
  if (!regs) throw new Error("getAudioState chip:'ai' — no AI region (load an N64 ROM into the rebuilt parallel_n64 core).");
441
442
  return { platform: "n64", ...decodeN64Ai(regs) };
442
443
  }
443
- throw new Error(`getAudioState: unknown chip '${chip}'. Use 'nes' (NES 2A03), 'gb' (Game Boy/GBC), 'gba' (GBA), 'dsp' (SNES), 'psg' (Genesis/SMS/GG SN76489), 'ym2612' (Genesis FM), 'sid' (C64), 'mikey' (Lynx), 'pce', 'ay8910' (MSX), or 'spu' (PS1).`);
444
+ if (chip === "aica") {
445
+ // Dreamcast AICA — 64 PCM/ADPCM channels (key-on/volume/pitch/loop) + master
446
+ // volume, from the rebuilt flycast core's romdev_aica_get register window.
447
+ const regs = host.getAicaRegs?.();
448
+ if (!regs) throw new Error("getAudioState chip:'aica' — no AICA region (load a Dreamcast program into the rebuilt flycast core).");
449
+ return { platform: "dreamcast", ...decodeAica(regs) };
450
+ }
451
+ throw new Error(`getAudioState: unknown chip '${chip}'. Use 'nes' (NES 2A03), 'gb' (Game Boy/GBC), 'gba' (GBA), 'dsp' (SNES), 'psg' (Genesis/SMS/GG SN76489), 'ym2612' (Genesis FM), 'sid' (C64), 'mikey' (Lynx), 'pce', 'ay8910' (MSX), 'spu' (PS1), or 'aica' (Dreamcast).`);
444
452
  }
445
453
 
446
454
  getAudioStateCore = async ({ chip }, callerSessionKey) => jsonContent(readAudioChip(chip, callerSessionKey));
@@ -646,6 +646,22 @@ const TEMPLATES = {
646
646
  },
647
647
  };
648
648
  })(),
649
+
650
+ // ── Sega Dreamcast (sh-elf-gcc SH-4) — bare PowerVR2 framebuffer via the bundled
651
+ // dc.h helper; the output ELF boots DIRECTLY on Flycast's reios HLE BIOS (no GD-ROM
652
+ // image, no firmware) and renders on the real GPU through native-gles. dc.h is
653
+ // auto-bundled by the toolchain, so the example is a single main.c. No KallistiOS
654
+ // and no genre scaffolds yet — `hello` is the verified renderable starting point. ──
655
+ dreamcast: {
656
+ default: { main: "hello/main.c", runtime: [], lang: "C (sh-elf-gcc)", ext: ".elf",
657
+ describe: "DCHELLO — the canonical Dreamcast starter: bring up the PowerVR2 640x480 RGB565 framebuffer (via the auto-bundled dc.h: FB_R_CTRL/SIZE/SOF1 + SPG) and paint a test pattern (dark-blue field, red/green/blue bars, white frame). Proves the SH-4 build → Flycast reios HLE boot → native-gles GPU render pipeline end-to-end. The base to grow your own DC graphics from. Same as 'hello'." },
658
+ hello: { main: "hello/main.c", runtime: [], lang: "C (sh-elf-gcc)", ext: ".elf",
659
+ describe: "DCHELLO — a minimal Dreamcast homebrew. dc_video_init() programs the PowerVR2 framebuffer registers for 640x480 RGB565 at VRAM 0xA5000000; then dc_clear/dc_rect paint a recognizable pattern. Boots directly on Flycast's reios HLE BIOS (no firmware) and presents through native-gles with flycast_emulate_framebuffer — no TA display list needed.",
660
+ players: "1 (Dreamcast has 4 controller ports; the bare path doesn't wire the Maple bus yet)",
661
+ sram: "none in this starter — DC saves go to the VMU via the Maple bus; not wired in the bare path.",
662
+ mechanics: ["direct framebuffer paint (clear + solid rects)", "recognizable test pattern"],
663
+ techniques: ["PowerVR2 framebuffer bring-up (FB_R_CTRL/FB_R_SIZE/FB_R_SOF1 + SPG)", "640x480 RGB565 at VRAM 0xA5000000", "SH-4 bare crt0 (stack + .bss + main)", "boots on Flycast reios HLE — no firmware"] },
664
+ },
649
665
  };
650
666
  // R37: GBC has its own scaffold tree at examples/gbc/templates/ +
651
667
  // src/platforms/gbc/lib/c/. Same runtime files as GB (the APU + Z80 +
@@ -470,8 +470,16 @@ export function registerToolchainTools(server, z, sessionKey) {
470
470
  return jsonContent(payload);
471
471
  }
472
472
 
473
- async function runSourceImpl({ platform, language, source, sourcePath, sources, sourcesPaths, includes, binaryIncludes, binaryIncludePaths, includePaths, runtime, maxmod, rebuildSdk, crt0, crt0Path, codeLoc, dataLoc, linkerConfig, linkerConfigPath, inesHeader, path: projPath, frames = 60, holdInputs, screenshotPath, projectName }) {
473
+ async function runSourceImpl({ platform, language, source, sourcePath, sources, sourcesPaths, includes, binaryIncludes, binaryIncludePaths, includePaths, runtime, maxmod, rebuildSdk, crt0, crt0Path, codeLoc, dataLoc, linkerConfig, linkerConfigPath, inesHeader, options, defines, entry, path: projPath, frames = 60, holdInputs, screenshotPath, projectName }) {
474
474
  const { buildForPlatform } = await import("../../toolchains/index.js");
475
+ // Merge `defines` ({_VER:1}) into `options` (--define) just like the project/rom paths,
476
+ // so a project-dir RUN honors them too.
477
+ const mergedOptions = [
478
+ ...(Array.isArray(options) ? options : []),
479
+ ...(defines && typeof defines === "object"
480
+ ? Object.entries(defines).flatMap(([k, v]) => ["--define", `${k}=${v}`])
481
+ : []),
482
+ ];
475
483
  const resolved = resolveCore(platform);
476
484
  if (!resolved) throw new Error(`no core available for platform '${platform}'`);
477
485
 
@@ -481,7 +489,7 @@ export function registerToolchainTools(server, z, sessionKey) {
481
489
  // happy path; without it, output:'run' + path errored ("requires source").
482
490
  const noExplicitSources = source == null && sourcePath == null && sources == null && sourcesPaths == null;
483
491
  if (projPath && noExplicitSources) {
484
- const r = await readProjectDir(projPath, platform);
492
+ const r = await readProjectDir(projPath, platform, { entry });
485
493
  includes = { ...(includes ?? {}), ...r.includes };
486
494
  binaryIncludes = { ...(binaryIncludes ?? {}), ...r.binaryIncludes };
487
495
  if (r.crt0 != null) crt0 = r.crt0;
@@ -586,6 +594,7 @@ export function registerToolchainTools(server, z, sessionKey) {
586
594
  crt0: crt0Rel2,
587
595
  codeLoc,
588
596
  dataLoc,
597
+ options: mergedOptions.length ? mergedOptions : undefined,
589
598
  });
590
599
  logBuildResult("build:run", platform, build);
591
600
  if (!build.ok || !build.binary) {
@@ -703,7 +712,7 @@ export function registerToolchainTools(server, z, sessionKey) {
703
712
  "• output:'rom' (default) — assemble or compile `source` (single) / `sources` ({name:contents}) / `sourcePath` / `sourcesPaths`. Returns the ROM (path by default; `inline:true` for binaryBase64) + build log. **`binaryIncludes`/`binaryIncludePaths` (base64/path CHR-ROM, music blobs for `.incbin`) — WITHOUT them no game with external assets builds.** `includes`/`includePaths` for `.include`d text. `linkerConfig` (cc65; NES preset 'chr-ram-runtime' RECOMMENDED). `crt0`/`crt0Path`/`codeLoc`/`dataLoc` (SDCC). `runtime`/`maxmod`/`rebuildSdk` (GBA/Genesis SDK). **`lint:'strict'` fails the build (stage:'lint', no binary) if the pre-flight SDCC crash-pattern scan flags anything (e.g. the uint8 loop-bound trap); 'advisory' (default) just lists hits in issues[].** **`includeSymbols:true` returns the .map text inline on a PLAIN rom build — distinct from output:'romWithDebug' which writes .dbg/.map FILES.** Language is inferred from extension/content — usually OMIT `language`.\n" +
704
713
  "• output:'romWithDebug' — like 'rom' but also emits linker debug info for the `symbols` tool: cc65 → `.dbg`, SDCC → sdld `.map`, Genesis m68k → GNU ld map (find where a RAM var landed). DEFAULT writes ROM + debug file + log to disk (`outputPath` required unless `inline:true`). **`resolveSymbols:['grid','score']` folds those names' addresses ({resolvedSymbols:{grid:{address,hex,region?,ramOffset?}}}) straight into the result — the cheap way to a WRAM variable's address without loading the whole map (or round-tripping it through `symbols`).**\n" +
705
714
  "• output:'run' — BUILD + LOAD + RUN + SCREENSHOT in one round trip — the fastest iteration loop. Same build args; runs `frames` frames and returns the screenshot INLINE. `holdInputs` holds controller state; `screenshotPath` writes the PNG to disk instead; `projectName` titles the playtest window.\n" +
706
- "• output:'project' — build a project DIRECTORY (`path`) without re-passing the file manifest each call. Entry point is `main.c` (C/SGDK Genesis, GBA, cc65/SDCC C) OR `main.s`/`main.asm` (asm). Every `.c`/`.s`/`.asm` in the dir is a translation unit (linked together), every `.h`/`.inc` an include, and `.bin/.chr/.pcm/.brr/.vgm/...` become binaryIncludes (for `.incbin`). Iterate an on-disk project by re-calling with just `{path, platform}`. **This is the no-boilerplate path for an examples({op:'fork'}) dir: the per-platform recipe auto-supplies the crt0 + load address — GB/GBC default `gb_crt0.s` + `codeLoc:0x150` (don't hand-pass them!), MSX routes `msx_crt0.s` + `codeLoc:0x4010`, SMS/GG auto-inject their bundled crt0, NES applies the chr-ram-runtime preset. PREFER this over re-passing `crt0Path`/`codeLoc` to output:'rom' for a forked project.**",
715
+ "• output:'project' — build a project DIRECTORY (`path`) without re-passing the file manifest each call. Entry point auto-detects `main.c` (C/SGDK Genesis, GBA, cc65/SDCC C) OR `main.s`/`main.asm` (asm); pass `entry:'smw.asm'` to point at a differently-named top-level file (e.g. an existing disassembly). Every `.c`/`.s`/`.asm` in the dir is a translation unit (linked together), every `.h`/`.inc` an include, and `.bin/.chr/.pcm/.brr/.vgm/...` (recursively, including subdirectories) become binaryIncludes (for `.incbin`). `options` (e.g. asar `--define _VER=1`) and `defines` are honored here too. Iterate an on-disk project by re-calling with just `{path, platform}`. **This is the no-boilerplate path for an examples({op:'fork'}) dir: the per-platform recipe auto-supplies the crt0 + load address — GB/GBC default `gb_crt0.s` + `codeLoc:0x150` (don't hand-pass them!), MSX routes `msx_crt0.s` + `codeLoc:0x4010`, SMS/GG auto-inject their bundled crt0, NES applies the chr-ram-runtime preset. PREFER this over re-passing `crt0Path`/`codeLoc` to output:'rom' for a forked project.**",
707
716
  {
708
717
  output: z.enum(["rom", "romWithDebug", "run", "project"])
709
718
  .describe("rom=produce a ROM (default); romWithDebug=ROM + .dbg/.map debug files; run=build+load+run+screenshot; project=build a project directory."),
@@ -722,7 +731,8 @@ export function registerToolchainTools(server, z, sessionKey) {
722
731
  crt0Path: z.string().optional().describe("Path-based `crt0`. NOT read by output:'romWithDebug' — pass `crt0` there."),
723
732
  codeLoc: z.coerce.number().int().optional().describe("SDCC — _CODE load address (default $0000; GB/GBC bundled crt0 wants 0x150)."),
724
733
  dataLoc: z.coerce.number().int().optional().describe("SDCC — _DATA (WRAM) load address (default $C000 on Z80). NOT read by output:'romWithDebug'."),
725
- options: z.array(z.string()).optional().describe("output:'rom' extra toolchain CLI options."),
734
+ options: z.array(z.string()).optional().describe("Extra toolchain CLI options. Honored by output:'rom' AND output:'project' (e.g. asar `--define _VER=1`, `--fix-checksum=off`, `-wno…`)."),
735
+ defines: z.record(z.string(), z.union([z.string(), z.number()])).optional().describe("Assembler defines as a map ({_VER:1}) — convenience for asar's `--define NAME=VALUE`. Merged into `options` for output:'rom'/'project'. (Equivalent to passing `--define _VER=1` yourself.)"),
726
736
  linkerConfig: z.string().optional().describe("ld65 linker config (cc65). NES presets: 'chr-ram-runtime' (RECOMMENDED for homebrew C — full crt0 + iNES header + NMI w/ OAM DMA + `_shadow_oam` at $0200), 'chr-ram' (bare nmi:rti stub), 'chr-rom' (cc65-C with FIXED CHR-ROM art — segment split + CHARS segment; supply CHR via binaryIncludePaths into a CHARS source + the header via `inesHeader`). Or full .cfg contents. Preset NAMES only resolve on output:'rom'/'run'; output:'romWithDebug' takes raw .cfg contents only. **For rebuilding a commercial NROM game from its disassembly, prefer `inesHeader` over a raw .cfg.**"),
727
737
  linkerConfigPath: z.string().optional().describe("Path-based `linkerConfig`: absolute path to a .cfg file on disk (the server reads it — the cfg never enters your context; e.g. the multi-bank cfg a banked-NES disasm project ships). Ignored when `linkerConfig` is passed inline."),
728
738
  inesHeader: z.object({
@@ -746,6 +756,7 @@ export function registerToolchainTools(server, z, sessionKey) {
746
756
  projectName: z.string().optional().describe("output:'run' — playtest window title (no effect on the ROM)."),
747
757
  // project-only
748
758
  path: z.string().optional().describe("output:'project' — absolute path to the project directory."),
759
+ entry: z.string().optional().describe("output:'project' — name of the top-level source file when it isn't main.c/main.s/main.asm (e.g. 'smw.asm' for an existing disassembly). Project-relative or a bare filename. Default: auto-detect main.c / main.s / main.asm."),
749
760
  // shared output
750
761
  outputPath: z.string().optional().describe("output:'rom'/'romWithDebug'/'project' — absolute path to write the ROM (romWithDebug writes .dbg/.map/.log alongside; REQUIRED for romWithDebug unless inline:true). output:'rom' omitted → temp-file path returned (or inline:true for base64)."),
751
762
  inline: z.boolean().default(false).describe("output:'rom'/'romWithDebug' — return binaryBase64 (+ debug text for romWithDebug) in the response instead of writing to disk."),
@@ -905,6 +916,45 @@ export function projectBuildRecipe(platform, names) {
905
916
  return r;
906
917
  }
907
918
 
919
+ /**
920
+ * Recursively collect files in SUBDIRECTORIES of a project (top-level files are
921
+ * handled by the recipe). Returns {rel, abs} with `rel` POSIX-relative to root
922
+ * (e.g. "col/misc/x.pal"), so the asar/incbin mount resolves the same path the
923
+ * source references. Skips dot-dirs and common build/VCS dirs to avoid hauling
924
+ * in junk. Caps total files to keep a pathological tree from blowing up.
925
+ * @param {string} root
926
+ * @returns {Promise<Array<{rel:string, abs:string}>>}
927
+ */
928
+ async function walkSubdirAssets(root) {
929
+ /** @type {Array<{rel:string, abs:string}>} */
930
+ const out = [];
931
+ const SKIP_DIR = new Set([".git", ".svn", "node_modules", "out", "build", "obj", ".vscode"]);
932
+ const MAX = 5000;
933
+ async function walk(dir, rel) {
934
+ let ents;
935
+ try { ents = await readdir(dir, { withFileTypes: true }); } catch { return; }
936
+ for (const e of ents) {
937
+ if (out.length >= MAX) return;
938
+ const childRel = rel ? `${rel}/${e.name}` : e.name;
939
+ if (e.isDirectory()) {
940
+ if (e.name.startsWith(".") || SKIP_DIR.has(e.name)) continue;
941
+ await walk(path.join(dir, e.name), childRel);
942
+ } else if (e.isFile()) {
943
+ out.push({ rel: childRel, abs: path.join(dir, e.name) });
944
+ }
945
+ }
946
+ }
947
+ // Only descend into subdirectories (top-level handled by the flat loop/recipe).
948
+ let top;
949
+ try { top = await readdir(root, { withFileTypes: true }); } catch { return out; }
950
+ for (const e of top) {
951
+ if (e.isDirectory() && !e.name.startsWith(".") && !SKIP_DIR.has(e.name)) {
952
+ await walk(path.join(root, e.name), e.name);
953
+ }
954
+ }
955
+ return out;
956
+ }
957
+
908
958
  /**
909
959
  * Read a scaffolded project DIRECTORY into the build inputs, applying the
910
960
  * per-platform recipe (crt0 routing, linker preset, runtime, skip-list) and
@@ -913,17 +963,40 @@ export function projectBuildRecipe(platform, names) {
913
963
  * can never drift. Returns crt0 as RAW source text (callers assemble it).
914
964
  * @param {string} projPath
915
965
  * @param {string} platform
966
+ * @param {{entry?: string}} [opts] entry: name of the top-level source when it isn't main.*
916
967
  */
917
- export async function readProjectDir(projPath, platform) {
968
+ export async function readProjectDir(projPath, platform, opts = {}) {
918
969
  const entries = await readdir(projPath, { withFileTypes: true });
919
970
  const files = entries.filter((e) => e.isFile());
920
971
 
972
+ // Subdirectory assets — a real disassembly keeps palettes/gfx in nested dirs
973
+ // (e.g. col/misc/back_area.pal) and `.incbin`s them by relative path. The old
974
+ // flat readdir never saw them → "file not found". Walk recursively and add any
975
+ // binary asset (regardless of extension/double-extension) keyed by its path
976
+ // RELATIVE to the project root, so the asar/incbin mount resolves it.
977
+ const subAssets = await walkSubdirAssets(projPath);
978
+
979
+ // Entry override: an existing project whose top file isn't main.* (e.g.
980
+ // smw.asm). Accept a project-relative or bare name; it becomes the single
981
+ // entry source. Without it, auto-detect main.c / main.s / main.asm.
982
+ const entryName = opts.entry ? path.normalize(opts.entry).replace(/^[./]+/, "") : null;
983
+ if (entryName) {
984
+ const exists = files.some((f) => f.name === entryName) ||
985
+ subAssets.some((a) => a.rel === entryName);
986
+ if (!exists) {
987
+ throw new Error(
988
+ `entry '${entryName}' not found in ${projPath}. Found top-level: ` +
989
+ `${files.map((f) => f.name).join(", ") || "(none)"}.`
990
+ );
991
+ }
992
+ }
993
+
921
994
  const hasC = files.some((f) => f.name === "main.c");
922
995
  const hasAsm = files.some((f) => f.name === "main.s" || f.name === "main.asm");
923
- if (!hasC && !hasAsm) {
996
+ if (!entryName && !hasC && !hasAsm) {
924
997
  throw new Error(
925
- `no entry point in ${projPath}: expected main.c (C/SGDK/GBA/cc65 project) or main.s / main.asm (asm project). ` +
926
- `Found: ${files.map((f) => f.name).join(", ") || "(empty)"}.`
998
+ `no entry point in ${projPath}: expected main.c (C/SGDK/GBA/cc65 project) or main.s / main.asm (asm project), ` +
999
+ `or pass entry:'<top-file>'. Found: ${files.map((f) => f.name).join(", ") || "(empty)"}.`
927
1000
  );
928
1001
  }
929
1002
 
@@ -933,6 +1006,15 @@ export async function readProjectDir(projPath, platform) {
933
1006
  // the dir build matches the hand-written build({output:'run'}) call.
934
1007
  const recipe = projectBuildRecipe(platform, files.map((f) => f.name));
935
1008
 
1009
+ // With a custom entry, the entry is the ONE source; every OTHER top-level
1010
+ // .asm/.s/.c routes as an include (asar/wla resolve `.include`/`#include`
1011
+ // from the includes mount), matching the single-source asm model.
1012
+ if (entryName && /\.(asm|s)$/i.test(entryName)) {
1013
+ for (const f of files) {
1014
+ if (/\.(c|s|asm)$/i.test(f.name) && f.name !== entryName) recipe.includeAsC.add(f.name);
1015
+ }
1016
+ }
1017
+
936
1018
  /** @type {Record<string,string>} */ const sources = {};
937
1019
  /** @type {Record<string,string>} */ const includes = {};
938
1020
  /** @type {Record<string,string>} */ const binaryIncludes = {};
@@ -953,6 +1035,19 @@ export async function readProjectDir(projPath, platform) {
953
1035
  }
954
1036
  }
955
1037
 
1038
+ // Stage subdirectory assets (any extension) keyed by their path relative to the
1039
+ // project root, so `incbin "col/misc/X.pal"` / `.incbin "gfx/a.bin"` resolve in
1040
+ // the mount. Text-ish includes (.asm/.s/.h/.inc) found in subdirs go to includes
1041
+ // (so `incsrc "lib/foo.asm"` works); everything else is a binary asset.
1042
+ for (const a of subAssets) {
1043
+ if (recipe.skip.has(a.rel) || a.rel === entryName) continue;
1044
+ if (/\.(h|inc|asm|s)$/i.test(a.rel)) {
1045
+ includes[a.rel] = (await readFile(a.abs)).toString("utf-8");
1046
+ } else {
1047
+ binaryIncludes[a.rel] = (await readFile(a.abs)).toString("base64");
1048
+ }
1049
+ }
1050
+
956
1051
  // SMS/GG with no crt0 file in the dir → fall back to the bundled crt0,
957
1052
  // exactly like the output:'rom'/'run' handlers do. Without this the link
958
1053
  // silently uses SDCC's stock z80 crt0, which never calls main() (black
@@ -972,8 +1067,17 @@ export async function readProjectDir(projPath, platform) {
972
1067
  return { sources, includes, binaryIncludes, crt0, codeLoc: recipe.codeLoc, dataLoc: recipe.dataLoc, linkerConfig: recipe.linkerConfig, runtime, maxmod: recipe.maxmod };
973
1068
  }
974
1069
 
975
- export async function buildProjectCore({ path: projPath, platform, outputPath }) {
976
- const { sources, includes, binaryIncludes, crt0, codeLoc, dataLoc, linkerConfig, runtime, maxmod } = await readProjectDir(projPath, platform);
1070
+ export async function buildProjectCore({ path: projPath, platform, outputPath, options, defines, entry }) {
1071
+ const { sources, includes, binaryIncludes, crt0, codeLoc, dataLoc, linkerConfig, runtime, maxmod } = await readProjectDir(projPath, platform, { entry });
1072
+
1073
+ // Thread `options` (e.g. asar `--define _VER=1`, `--fix-checksum=off`) + the `defines`
1074
+ // convenience map through to the toolchain — project mode used to silently drop them.
1075
+ const mergedOptions = [
1076
+ ...(Array.isArray(options) ? options : []),
1077
+ ...(defines && typeof defines === "object"
1078
+ ? Object.entries(defines).flatMap(([k, v]) => ["--define", `${k}=${v}`])
1079
+ : []),
1080
+ ];
977
1081
 
978
1082
  // Linker preset: the recipe names it (e.g. NES 'chr-ram-runtime', which ships
979
1083
  // the OAM/CHARS segments + its own crt0). resolveLinkerConfig also returns any
@@ -1012,6 +1116,7 @@ export async function buildProjectCore({ path: projPath, platform, outputPath })
1012
1116
  crt0: crt0Rel,
1013
1117
  codeLoc,
1014
1118
  dataLoc,
1119
+ options: mergedOptions.length ? mergedOptions : undefined,
1015
1120
  });
1016
1121
  if (outputPath && result.binary) {
1017
1122
  await mkdir(path.dirname(outputPath), { recursive: true });
@@ -0,0 +1,87 @@
1
+ # Sega Dreamcast — mental model
2
+
3
+ The Dreamcast is a **3D machine**: a 200 MHz Hitachi SH-4 CPU (with an on-die FPU/
4
+ vector unit) + the PowerVR2 (CLX2) **tile-based deferred renderer** drawing into
5
+ 8 MB VRAM, plus the AICA sound system (an ARM7 CPU + DSP). romdev runs it on
6
+ **Flycast** built to WASM, presenting through the **native-gles / WebGL2 hardware
7
+ GPU**, with the **reios HLE BIOS** so you don't ship a real `dc_boot.bin`.
8
+
9
+ ## The one thing to know about rendering
10
+
11
+ Flycast is **GPU-first**: the PowerVR2 is a Tile Accelerator (TA) — homebrew normally
12
+ submits TA display lists (polygon/vertex lists) that the GPU rasterizes. romdev runs
13
+ Flycast `hwRender: true` (the real GPU via WebGL2). **BUT** the core is configured
14
+ with `flycast_emulate_framebuffer: enabled`, which gives a working **direct-framebuffer
15
+ scanout** path — a program that writes the DC framebuffer displays (verified: a
16
+ framebuffer-writing program captured ~727k pixels). So both routes work:
17
+
18
+ - **Direct framebuffer** (simplest for 2D / first pixels): write VRAM and let the
19
+ emulated-framebuffer path scan it out. The starting point for "just show something."
20
+ - **TA / GPU** (the real path for 3D): submit PowerVR2 TA lists. The native SDK,
21
+ **KallistiOS (KOS)**, wraps the TA and ships an OpenGL port — lean on KOS rather than
22
+ hand-rolling the TA command encoding.
23
+
24
+ Decide which your homebrew uses; the bundled helper lib targets the framebuffer path
25
+ first (like a software front end) and grows toward TA.
26
+
27
+ ## CPU / memory map (SH-4, 32-bit, little-endian)
28
+
29
+ | Region | Address | Notes |
30
+ |--------|---------|-------|
31
+ | System RAM | `0x8C00_0000`+ | 16 MB main RAM (KOS links code here) |
32
+ | VRAM | `0xA500_0000`+ | 8 MB texture/framebuffer RAM |
33
+ | Hardware regs | `0xA05F_xxxx` | PowerVR2 (TA/CORE), Holly, GD-ROM, AICA, Maple |
34
+ | AICA RAM | `0x00800000` (AICA bus) | ARM7 sound RAM |
35
+
36
+ Addresses are **little-endian**. SH-4 uses **PC-relative loads** for constants/
37
+ addresses (`mov.l @(disp,PC)`) heavily — relevant for RE base-address alignment.
38
+ `memory({op:'read', region:'system_ram'})` reads main RAM.
39
+
40
+ ## Booting — KOS ELF + reios HLE (no disc needed for the dev loop)
41
+
42
+ A real Dreamcast boots from a GD-ROM with an `IP.BIN` bootstrap (a CDI/CHD disc image).
43
+ For homebrew, that's heavy. romdev uses Flycast's **reios HLE BIOS** (`flycast_hle_bios:
44
+ enabled`), and KallistiOS produces a ready-to-boot **ELF** — Flycast loads the raw KOS
45
+ ELF directly, so you skip the disc-image dance. (Build a disc image only if a specific
46
+ title demands it.) `build({platform:'dreamcast'})` produces the loadable image.
47
+
48
+ ## Input
49
+
50
+ Homebrew reads controllers over the **Maple bus** (`0xA05F_6C00`+) — the DC's
51
+ peripheral protocol. KOS abstracts this (`maple_enum_*`, controller state). The host's
52
+ `setInput`/`run` holdInputs drive the emulated controller; the game polls Maple.
53
+
54
+ ## Sound
55
+
56
+ The **AICA** = an ARM7 CPU + DSP with its own RAM. The SH-4 uploads a sound program +
57
+ samples to AICA RAM and triggers channels. KOS ships an AICA sound driver
58
+ (`snd_stream`). `audioChips` lists `aica`.
59
+
60
+ ## MCP debug & inspection tooling — current state
61
+
62
+ - **`run` / `screenshot` / `frame({op:'verify'})`** — boots + presents + render-health.
63
+ ✅ (reios HLE; the framebuffer path captures pixels.)
64
+ - **`memory({op:'read', region:'system_ram'})`** — SH-4 main RAM. ✅
65
+ - **`disasm` / `decompile`** — SH-4 via rizin (`sh`) + Ghidra (SuperH4 SLEIGH). ✅
66
+ (Good decompile quality; SH-4 is a clean 32-bit RISC.)
67
+ - **`build`** — sh-elf-gcc → WASM toolchain. ✅
68
+ - **`cpu({op:'read'})`** — live SH-4 register file (`romdev_sh4_regs_get`: r0..r15, pc,
69
+ pr, gbr, vbr, sr, mac, fpul + decoded SR flags). ✅
70
+ - **`audioDebug({op:'inspect', chip:'aica'})`** — the AICA's 64 PCM/ADPCM channels
71
+ (key-on / volume / pitch / loop / format) + master volume (`romdev_aica_get`). ✅
72
+ - **`breakpoint` / `watch`** — not yet (need interpreter-step + memory-path hooks in
73
+ flycast; cpuState/audioDebug are plain reads and ARE wired).
74
+ - **`renderingContext`** is **N/A** — 3D TA machine, no 2D tile VDP to decode.
75
+
76
+ ## Build pipeline
77
+
78
+ `build({platform:'dreamcast'})` cross-compiles with an **sh-elf-gcc → WASM** toolchain
79
+ (adapted from KOS's `dc-chain`; SH-4 is single-endian little, so no be/el split). Link
80
+ against KallistiOS for libc + the TA/AICA/Maple drivers. `#include` the bundled
81
+ `dc.h` helper for the framebuffer front end + input/audio helpers.
82
+
83
+ ## What's NOT bundled / hardware limits
84
+
85
+ - No GD-ROM/CDDA streaming or VMU save emulation in the dev loop.
86
+ - `breakpoint`/`watch` not yet (need interpreter-step hooks; cpuState/audioDebug ARE wired).
87
+ - `renderingContext` is N/A (3D TA, no tile VDP).
@@ -0,0 +1,55 @@
1
+ # Sega Dreamcast — troubleshooting
2
+
3
+ Read `platform({op:'doc', platform:'dreamcast', name:'mental_model'})` first — DC runs
4
+ on Flycast (hardware GPU + reios HLE BIOS), with a working emulated-framebuffer path.
5
+
6
+ ## "ROM/ELF builds but won't boot"
7
+
8
+ 1. **reios HLE must be on.** Flycast only boots a raw homebrew ELF via the **reios HLE
9
+ BIOS** (`flycast_hle_bios: enabled`) — it parses the ELF + jumps. With it off,
10
+ Flycast wants a real `dc_boot.bin` you don't ship. romdev's DC core config already
11
+ enables reios; if you swapped the core/config, re-enable it.
12
+ 2. **You built a flat binary, not a KOS ELF.** Flycast loads the KallistiOS ELF
13
+ (linked at `0x8C00_0000`). Use `build({platform:'dreamcast'})` — it links against
14
+ KOS and emits the loadable image. A bare `.bin` with no ELF/entry won't boot.
15
+
16
+ ## "Boots but the screen is BLACK"
17
+
18
+ 1. **Nothing was drawn to the framebuffer OR submitted to the TA.** DC has two render
19
+ routes (see mental model). For the simple path, write the DC framebuffer (the
20
+ `flycast_emulate_framebuffer: enabled` path scans it out — a framebuffer-writing
21
+ program shows pixels). For 3D, submit PowerVR2 **TA** lists via KOS. A blank screen
22
+ = neither happened. Confirm with `frame({op:'verify'})` (nearlyBlank).
23
+ 2. **No display/render init.** The PowerVR2 needs its video mode + render target set
24
+ up (KOS `vid_set_mode` / `pvr_init`). Without it there's no scanout. Call the KOS
25
+ init (or the helper lib's init) before drawing.
26
+ 3. **You expected raw VRAM writes to a TA-only setup to show.** With the framebuffer-
27
+ emulation path enabled they do; if you disabled it and use pure TA, only submitted
28
+ TA geometry renders.
29
+
30
+ ## "Geometry/3D is wrong or missing (TA path)"
31
+
32
+ PowerVR2 is a **tile-based DEFERRED** renderer — opaque, punch-through, and translucent
33
+ lists are submitted + sorted per tile. Submitting polys to the wrong list type, or not
34
+ finalizing the list, drops them. Use KOS's `pvr_*` list API (it orders this correctly)
35
+ rather than hand-encoding TA commands.
36
+
37
+ ## "disasm/decompile returns junk addresses on a multi-function program"
38
+
39
+ SH-4 decompiles well, but it uses **PC-relative loads** (`mov.l @(disp,PC)`) heavily,
40
+ so the analysis buffer's flat offsets must line up with the binary's load VA or
41
+ constant/address resolution breaks. Build a **multi-function** test program to verify
42
+ `functions` returns real VAs that round-trip — a single-instruction smoke test hides
43
+ base-address misalignment. (Same class of trap as the PS1 rebase issue.)
44
+
45
+ ## "breakpoint / watch return N/A"
46
+
47
+ `cpu({op:'read'})` and `audioDebug({op:'inspect', chip:'aica'})` ARE wired on DC (the
48
+ flycast core exports `romdev_sh4_regs_get` + `romdev_aica_get`). `breakpoint`/`watch`
49
+ are not yet — those need interpreter-step + memory-path hooks in flycast (cpuState/
50
+ audioDebug are plain register reads). Use `cpu`/`audioDebug` + `memory` +
51
+ `disasm`/`decompile` meanwhile.
52
+
53
+ ## "renderingContext returns N/A"
54
+
55
+ Correct — DC is a 3D TA machine with no 2D tile/sprite VDP for that op. Not a bug.
@@ -0,0 +1,57 @@
1
+ # Sega Dreamcast — source you can read
2
+
3
+ Trust hierarchy (try in order before filing a feedback round):
4
+
5
+ 1. **Bundled examples** (`examples/dreamcast/{hello,shmup,platformer,puzzle,racing,sports}/main.c`)
6
+ — verified building + rendering on the GPU (full 480-line frame). Start here.
7
+ 2. **Bundled helper lib source** (`src/toolchains/sh-c/lib/dc.h`) — the PowerVR2
8
+ framebuffer bring-up + drawing + input. Read this when something doesn't render or input
9
+ doesn't read: it shows `dc_video_init` (FB_R_CTRL/SIZE/SOF1 + SPG, **480i interlace** —
10
+ 240p only shows the top 240 lines), `dc_clear`/`dc_rect`, and the Maple-DMA `dc_pad()`.
11
+ 3. **The core source** (NOT bundled — fetch on demand):
12
+
13
+ | What | Upstream |
14
+ |---|---|
15
+ | Flycast (libretro core, PowerVR2 + reios HLE BIOS) | https://github.com/flyinghead/flycast |
16
+ | SH-4 / PowerVR2 / AICA / Maple internals | (in the flycast tree: `core/hw/sh4/`, `core/hw/pvr/`, `core/hw/aica/`, `core/hw/maple/`) |
17
+
18
+ The cpuState/audioDebug exports we patch in (`romdev_sh4_regs_get`, `romdev_aica_get`)
19
+ live in `scripts/patches/romdev-snippets/flycast-debug.c`.
20
+
21
+ 4. **The toolchain** — a from-scratch `sh-elf-gcc` cross-compiler (little-endian SH-4,
22
+ m4-single-only FP) built to WASM (`scripts/build-sh-toolchain.sh` +
23
+ `build-sh-wasm-tools.sh`). NOTE: cc1 defaults to **-O1** (the sh-elf cc1.wasm has an
24
+ -O2-only crash on common control flow):
25
+
26
+ | Component | Upstream |
27
+ |---|---|
28
+ | binutils | https://ftp.gnu.org/gnu/binutils/ |
29
+ | gcc | https://ftp.gnu.org/gnu/gcc/ |
30
+ | newlib | https://sourceware.org/pub/newlib/ |
31
+
32
+ 5. **Reverse engineering** — Rizin + Ghidra SH-4 (SuperH4 SLEIGH):
33
+
34
+ | What | Upstream |
35
+ |---|---|
36
+ | Rizin | https://github.com/rizinorg/rizin |
37
+ | rz-ghidra | https://github.com/rizinorg/rz-ghidra |
38
+
39
+ ## Dreamcast hardware docs
40
+
41
+ - **KallistiOS (KOS)** — the canonical open DC SDK; read its PowerVR2 TA, AICA, and Maple
42
+ drivers for the standard register sequences: https://github.com/KallistiOS/KallistiOS
43
+ - **Mc Spankled / DCEmulation docs + the Sega Dreamcast Hardware Specification** (PVR2/Holly
44
+ register maps): cross-check against flycast's `core/hw/pvr/pvr_regs.h` enum.
45
+ - **SH-4 ISA / SH7750 manual** (Renesas) for the CPU.
46
+
47
+ ## When to use what
48
+
49
+ - "Boots but the screen is BLACK / only the top half draws" → MENTAL_MODEL +
50
+ TROUBLESHOOTING (it's the framebuffer path + the 480i-interlace requirement).
51
+ - "How does the helper set up the PowerVR2 framebuffer?" → `dc.h` `dc_video_init` +
52
+ flycast `core/hw/pvr/pvr_regs.h` (SPG_CONTROL interlace bit).
53
+ - "dc_pad doesn't reflect button presses" → `dc.h` `dc_pad()` (Maple Get-Condition DMA) +
54
+ flycast `core/hw/maple/maple_devs.cpp` (the controller response framing). Reads the
55
+ resting state; press mapping is a known follow-up.
56
+ - "Which AICA register holds channel key-on/volume?" → `dc-aica-state.js` decode +
57
+ flycast `core/hw/aica/`.