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,55 @@
1
+ # PlayStation (PS1) — troubleshooting
2
+
3
+ Read `platform({op:'doc', platform:'ps1', name:'mental_model'})` first — PS1 renders
4
+ by issuing **GPU primitives (GP0)** on the hardware renderer, not by writing a CPU
5
+ framebuffer.
6
+
7
+ ## "ROM builds + boots but the screen is BLACK"
8
+
9
+ 1. **You're trying to write a raw framebuffer.** PS1's renderer is the hardware GPU
10
+ (`beetle_psx_hw`, `hwRender:true`) — it rasterizes the **GP0 command stream**, not
11
+ a CPU-written framebuffer. Draw with GPU primitives (the bundled `psx.c` emits
12
+ them); a CPU memset of "VRAM" won't show.
13
+ 2. **You never set the display area / drawing environment.** GP1 sets the display
14
+ mode + display start; GP0 `0xE1..0xE6` set the drawing area/offset. Without a valid
15
+ draw env + display env the GPU has nowhere to put pixels. The helper lib's
16
+ `gpu_init` does this — call it before drawing.
17
+ 3. **Nothing was actually submitted to GP0.** Confirm with `frame({op:'verify'})`
18
+ (nearlyBlank) + check you're writing the GP0 port (`0x1F80_1810`) / running a DMA
19
+ to it, not just touching VRAM.
20
+
21
+ ## "Rectangles stretch to the edge of the screen / primitives are garbage"
22
+
23
+ `GP0 0x60` (and the rect family) is a **variable-size rectangle**: 3 words = color,
24
+ top-left corner, size. If you feed it 4 polygon vertices it reads corner+size+garbage
25
+ and stretches. Use the **triangle** family (`0x20`/`0x24`/`0x28`/`0x2C`) for polys and
26
+ the **rect** family (`0x60`/`0x64`/`0x68`/`0x6C`) for rects. Match the command to the
27
+ primitive and word count.
28
+
29
+ ## "Geometry is wrong / inside-out / clipped"
30
+
31
+ Software-3D front-end math (the helper lib transforms before emitting GP0): check
32
+ 16.16 fixed-point throughout, back-face winding matches your vertex order, perspective
33
+ divide before the GP0 vertex coords, and coords fit the 11-bit signed GPU range.
34
+
35
+ ## "disasm/decompile of a multi-function program returns junk (fcn.00000000)"
36
+
37
+ Known rizin trap: for **absolute-addressed `jal`** (PS1 `jal 0x80010518`), rizin
38
+ ignores `-B` on a raw `malloc://` buffer and addresses flat from 0, so cross-function
39
+ discovery dangles outside the image. romdev fixes this by left-padding `.text` so the
40
+ flat offset == the VA's low 20 bits, seeding analysis there, and rebasing the high bits
41
+ back — so reported addresses are real VAs. If you see a lone `fcn.00000000`, you're on
42
+ a path that bypassed the rebase; build a **multi-function** program to exercise it (a
43
+ single-instruction smoke test hides this).
44
+
45
+ ## "breakpoint / watch return N/A"
46
+
47
+ `cpu({op:'read'})` and `audioDebug({op:'inspect', chip:'spu'})` ARE wired on PS1 (the
48
+ core exports `romdev_mips_regs_get` + `romdev_spu_get`). `breakpoint`/`watch` are not
49
+ yet — those need interpreter-step + memory-path hooks patched into beetle (cpuState/
50
+ audioDebug are plain reads). Use `cpu`/`audioDebug` + `memory` + `disasm`/`decompile`
51
+ meanwhile.
52
+
53
+ ## "renderingContext returns N/A"
54
+
55
+ Correct — PS1 is a 3D GPU machine with no 2D tile/sprite VDP for that op. Not a bug.
@@ -0,0 +1,54 @@
1
+ # PlayStation (PS1) — source you can read
2
+
3
+ Trust hierarchy (try in order before filing a feedback round):
4
+
5
+ 1. **Bundled examples** (`examples/ps1/{shmup,platformer,puzzle,racing,sports}/main.c` +
6
+ `sprite_move/`) — verified building + rendering on the GPU. Start here.
7
+ 2. **Bundled helper lib source** (`src/platforms/ps1/lib/c/psx.c` + `psx.h`) — the GPU
8
+ front end. Read this when a primitive renders wrong: it shows how the software-3D
9
+ transform feeds **GP0 GPU primitives** (the real console path — the GPU rasterizes the
10
+ command stream; this is NOT a software framebuffer). The GP0 `0x60` rect-vs-poly gotcha
11
+ is in here + TROUBLESHOOTING.
12
+ 3. **The core source** (NOT bundled — fetch on demand):
13
+
14
+ | What | Upstream |
15
+ |---|---|
16
+ | beetle-psx (Mednafen PSX, libretro, HW renderer) | https://github.com/libretro/beetle-psx-libretro |
17
+ | R3000A CPU / SPU / GPU internals | (in the beetle tree: `mednafen/psx/cpu.c`, `spu.c`, `gpu.cpp`) |
18
+
19
+ The cpuState/audioDebug exports we patch in (`romdev_mips_regs_get`,
20
+ `romdev_spu_get`) live in `scripts/patches/romdev-snippets/beetle-psx-regsnap.c` +
21
+ `beetle-psx-spu.c`.
22
+
23
+ 4. **The toolchain** — a from-scratch `mips-elf-gcc` cross-compiler (little-endian) built
24
+ to WASM (`scripts/build-mips-toolchain.sh` — one toolchain emits both N64 BE + PS1 LE):
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 MIPS (R3000A):
33
+
34
+ | What | Upstream |
35
+ |---|---|
36
+ | Rizin | https://github.com/rizinorg/rizin |
37
+ | rz-ghidra | https://github.com/rizinorg/rz-ghidra |
38
+
39
+ ## PS1 hardware docs
40
+
41
+ - **psx-spx** (Nocash, the canonical reference): https://psx-spx.consoledev.net/
42
+ - **PSn00bSDK** (a real open PS1 SDK — read its GPU/GTE/SPU code for the standard GP0/GP1
43
+ encodings + SPU register layout): https://github.com/Lameguy64/PSn00bSDK
44
+
45
+ ## When to use what
46
+
47
+ - "Why is my screen BLACK?" → MENTAL_MODEL + TROUBLESHOOTING (you must issue GP0
48
+ primitives + set the draw/display environment — a CPU memset won't show).
49
+ - "My rectangles stretch to the edge" → the GP0 `0x60` variable-size-rect gotcha
50
+ (TROUBLESHOOTING).
51
+ - "How does the helper emit a triangle?" → `psx.c` + psx-spx GPU section.
52
+ - "audioDebug SPU volumes look off" → the SPU volume/sweep registers read processed
53
+ values; the decode reads the raw mirror (see `psx.c` / beetle `spu.c`).
54
+ - "disasm returns a single `fcn.00000000`" → TROUBLESHOOTING (the absolute-`jal` rebase).
@@ -261,6 +261,16 @@ synthesizes a fallback `issues[]` entry with a hint. The idioms to avoid:
261
261
  block where the layout expects it. Use
262
262
  `examples({op:'snippets', platform:"snes", mode:"get", snippetName:"lorom_header.asm"})`
263
263
  for the canonical layout (and `lorom_multibank.asm` for multi-bank).
264
+ - **`readfile1`/`filesize`/`canreadfile` across MANY distinct files.** Converting
265
+ assets at assemble time (the commercial-disassembly idiom — e.g. an `incpal`
266
+ macro reading every `.pal` palette to emit 15-bit color) reads many *distinct*
267
+ files via the readfile API. asar-WASM can **abort with no diagnostic** above a
268
+ threshold of distinct files read that way (one file read 200× is fine — it's
269
+ the *count of distinct files*). `incbin` does NOT have this problem. The build
270
+ log carries an `[asar advisory]`/abort-hint when it sees a source over the
271
+ threshold. **Fix: pre-convert the assets to `.bin` blobs offline and `incbin`
272
+ them** — the output is byte-identical. (Put the converted blobs in a subdir;
273
+ `output:'project'` stages subdirectory assets recursively.)
264
274
 
265
275
  (This is the asar/asm path. The default PVSnesLib **C** path goes through
266
276
  tcc-65816 + wla-65816 and has its own C89 trap, above.)
@@ -74,6 +74,17 @@ export async function runAsar(args) {
74
74
  });
75
75
  let { binary, log, exitCode } = first;
76
76
 
77
+ // asar exits a NORMAL error build via its C++ exception path (quit throws a
78
+ // numeric heap pointer, not a clean status), so the worker appends a generic
79
+ // `[worker] Abort in WASM: <ptr>` line even though asar DID write proper
80
+ // diagnostics (e.g. `…: error: (Elabel_not_found): …`). That line reads as a
81
+ // silent crash + the heap-pointer "<number>" looks like an OOM/trap — it cost
82
+ // real diagnosis time (v0.70.0 feedback #5). When the log already has a real
83
+ // asar diagnostic, this was an ordinary error exit: drop the misleading line.
84
+ if (exitCode !== 0 && /:\s*(error|warning)\s*:/i.test(log)) {
85
+ log = log.replace(/\n?\s*\[worker\] Abort in WASM:[^\n]*\n?/g, "\n").replace(/\n{3,}/g, "\n\n").trimEnd() + "\n";
86
+ }
87
+
77
88
  // Silent-fail recovery. When asar throws a numeric value through Emscripten's
78
89
  // C++-exception path, that "number" is a heap pointer to the thrown object,
79
90
  // not an exit code — and we've lost the actual error message. Under
@@ -101,9 +112,19 @@ export async function runAsar(args) {
101
112
 
102
113
  const hex = "0x" + (exitCode >>> 0).toString(16).toUpperCase();
103
114
  const looksLikeHeapPointer = exitCode > 1024;
104
- const hint = looksLikeHeapPointer
115
+ // Count DISTINCT files touched via the readfile/filesize/canreadfile API.
116
+ // asar-WASM has a known resource issue where reading enough distinct files
117
+ // that way (the canonical commercial-disassembly asset-conversion idiom, e.g.
118
+ // an incpal macro converting many .pal palettes) can abort with no diagnostic
119
+ // (v0.70.0 feedback #1). Surfacing the count turns a silent dead-end into a
120
+ // lead. (incbin of the same files does NOT trip it — only readfile1/filesize.)
121
+ const distinctReadfileFiles = countReadfileFiles(source);
122
+ const readfileHint = distinctReadfileFiles >= READFILE_DISTINCT_WARN
123
+ ? ` This source reads ${distinctReadfileFiles} DISTINCT files via readfile1/filesize/canreadfile — asar-WASM can abort with no diagnostic above a threshold of distinct files read that way (a known limitation). If you're converting assets at assemble time (e.g. .pal -> 15-bit), pre-convert them to .bin blobs offline and incbin those instead (incbin does NOT leak) — the output is byte-identical.`
124
+ : "";
125
+ const hint = (looksLikeHeapPointer
105
126
  ? "Exit code looks like a heap pointer, not a real exit code — likely a C++ exception that escaped without writing to stderr. Common causes: bank-border-crossed, section overlap, ROM size exceeded, or out-of-memory while assembling."
106
- : "asar exited non-zero with no output.";
127
+ : "asar exited non-zero with no output.") + readfileHint;
107
128
 
108
129
  const layoutText = formatLayout(summarizeWrittenRegions(verboseBinary, baseRom, { flatBinary }));
109
130
 
@@ -155,6 +176,20 @@ export async function runAsar(args) {
155
176
  includesInfo[name] = { size };
156
177
  }
157
178
 
179
+ // Proactive advisory: even when THIS build squeaked by, a source reading many
180
+ // distinct files via readfile1/filesize is at risk of the asar-WASM abort —
181
+ // surface it (non-fatal) so a growing disassembly doesn't dead-end later.
182
+ {
183
+ const distinct = countReadfileFiles(source);
184
+ if (distinct >= READFILE_DISTINCT_WARN && !/readfile.*distinct files/i.test(log)) {
185
+ log =
186
+ `[asar advisory] this source reads ${distinct} distinct files via readfile1/filesize — ` +
187
+ `asar-WASM can abort (no diagnostic) above a threshold of distinct files read that way. ` +
188
+ `If you're converting assets at assemble time, prefer pre-converting them to .bin and ` +
189
+ `'incbin' (byte-identical, no leak).\n` + log;
190
+ }
191
+ }
192
+
158
193
  // Try to read the symbol file that asar emitted, if any.
159
194
  let symbolsText = null;
160
195
  if (wantSymbols) {
@@ -346,20 +381,30 @@ function asarPreflight(source, { binaryIncludes = {}, includes = {} } = {}) {
346
381
  }
347
382
 
348
383
  // -----------------------------------------------------------------------
349
- // Header-overlap detection: if an `incbin "foo.bin"` lands in a 32 KB
350
- // bank window starting at $XX8000 and the file size would extend past
351
- // $XXFFC0 (16 KB of usable area before the LoROM header), warn the user
352
- // BEFORE asar happily clobbers the header during pass 2.
384
+ // Header-overlap detection: an `incbin "foo.bin"` whose span would reach
385
+ // $00FFC0 clobbers the LoROM header during pass 2. **The LoROM header lives
386
+ // ONLY in bank $00** ($00FFC0..$00FFFF) for banks $01+, $XXFFC0 is ordinary
387
+ // ROM, and games (e.g. SMW's GFX banks 08-0B) legitimately fill across those
388
+ // boundaries under `check bankcross off`. So only flag BANK $00 (v0.70.0 #6:
389
+ // the old check false-positived on every bank as "$08FFC0 header"). Also honor
390
+ // `check bankcross off` (the user is explicitly opting into bank-crossing fills).
353
391
  //
354
- // We only check incbin's pointing at files in binaryIncludes (where we
355
- // know the size). We assume each incbin is preceded by an `org $XX8000`
356
- // and that the file fits or doesn't from that base.
392
+ // We only check incbin's pointing at files in binaryIncludes (size known) and
393
+ // assume each is preceded by an `org $008000`. Note: this scans only the entry
394
+ // source `incbin`s inside `incsrc`'d files aren't seen (so the check is a
395
+ // best-effort early warning, not full coverage).
357
396
  // -----------------------------------------------------------------------
358
397
  const incbinDirective = /^\s*incbin\s+"([^"]+)"/;
359
- // Walk top-to-bottom, tracking the most recent org as the "cursor".
398
+ const bankcrossOffRe = /^\s*check\s+bankcross\s+off\b/i;
399
+ const bankcrossOnRe = /^\s*check\s+bankcross\s+(on|half)\b/i;
400
+ // Walk top-to-bottom, tracking the most recent org as the "cursor" and the
401
+ // current `check bankcross` state (off = the user opted into crossing fills).
360
402
  let cursor = null;
403
+ let bankcrossOff = false;
361
404
  for (let i = 0; i < lines.length; i++) {
362
405
  const line = stripComment(lines[i]);
406
+ if (bankcrossOffRe.test(line)) { bankcrossOff = true; continue; }
407
+ if (bankcrossOnRe.test(line)) { bankcrossOff = false; continue; }
363
408
  const orgMatch = line.match(orgDirective);
364
409
  if (orgMatch) {
365
410
  cursor = parseInt(orgMatch[1], 16);
@@ -373,15 +418,16 @@ function asarPreflight(source, { binaryIncludes = {}, includes = {} } = {}) {
373
418
  const size = blob instanceof Uint8Array ? blob.length : Buffer.from(blob, "base64").length;
374
419
  const bank = (cursor >>> 16) & 0xFF;
375
420
  const off = cursor & 0xFFFF;
376
- const headerStart = 0xFFC0; // LoROM header lives at $XXFFC0..$XXFFFF
377
- if (off >= 0x8000 && off + size > headerStart) {
421
+ const headerStart = 0xFFC0; // LoROM header lives at $00FFC0..$00FFFF (bank $00 ONLY)
422
+ if (!bankcrossOff && bank === 0x00 && off >= 0x8000 && off + size > headerStart) {
378
423
  const fmtAddr = (a) => "$" + a.toString(16).padStart(6, "0").toUpperCase();
379
424
  const endAddr = (bank << 16) | (off + size - 1);
380
425
  issues.push(
381
426
  `[asar preflight] line ${i + 1}: 'incbin "${name}"' (${size} bytes) starting at ` +
382
427
  `${fmtAddr(cursor)} would extend to ${fmtAddr(endAddr)}, overlapping the LoROM ` +
383
- `header at $${bank.toString(16).padStart(2, "0").toUpperCase()}FFC0. Move this data ` +
384
- `to a higher bank (e.g. 'org $018000') or shrink it below ${headerStart - off} bytes.`,
428
+ `header at $00FFC0. Move this data to a higher bank (e.g. 'org $018000') or ` +
429
+ `shrink it below ${headerStart - off} bytes. (Add 'check bankcross off' if this ` +
430
+ `crossing is intentional.)`,
385
431
  );
386
432
  }
387
433
  // Advance cursor so consecutive incbins in the same bank stack.
@@ -531,3 +577,27 @@ function asarPreflight(source, { binaryIncludes = {}, includes = {} } = {}) {
531
577
  }
532
578
  return null;
533
579
  }
580
+
581
+ // asar-WASM aborts (no diagnostic) once readfile1/filesize/canreadfile touch
582
+ // enough DISTINCT files — the canonical commercial-disassembly asset-conversion
583
+ // idiom (an incpal-style macro converting many .pal palettes at assemble time).
584
+ // We can't always predict the exact ceiling, so warn above this many distinct
585
+ // files so the build doesn't silently dead-end (v0.70.0 feedback #1).
586
+ const READFILE_DISTINCT_WARN = 40;
587
+
588
+ /**
589
+ * Count DISTINCT file names passed to the readfile / filesize / canreadfile
590
+ * functions. Only literal string filenames are counted (computed names can't be
591
+ * resolved statically). Used for the readfile-leak advisory.
592
+ * @param {string} source
593
+ * @returns {number}
594
+ */
595
+ function countReadfileFiles(source) {
596
+ const names = new Set();
597
+ // readfile1("x",..) / readfile2(...) / readfile3 / readfile4 / filesize("x")
598
+ // / canreadfile("x",..) / canreadfile1..4 — first string arg is the filename.
599
+ const re = /\b(?:readfile[1-4]|canreadfile[1-4]?|filesize)\s*\(\s*"([^"]+)"/gi;
600
+ let m;
601
+ while ((m = re.exec(source)) !== null) names.add(m[1]);
602
+ return names.size;
603
+ }
@@ -91,6 +91,9 @@ const LANGUAGE_TOOLCHAIN = {
91
91
  n64: {
92
92
  c: { toolchain: "mips-elf-gcc", available: true, note: "C for N64 via gcc 14.2.0 + binutils + newlib (mips-elf, big-endian R4300), compiled to WASM. Bare path: minimal crt0 (stack + .bss + main()) → flat code image at 0x80000400. NOTE: a fully bootable N64 ROM needs the IPL3 bootcode + a libdragon-style header (libdragon SDK forthcoming) — this path exercises the toolchain + is the basis for the SDK." },
93
93
  },
94
+ dreamcast: {
95
+ c: { toolchain: "sh-elf-gcc", available: true, note: "C for Dreamcast via gcc 14.2.0 + binutils + newlib (sh-elf, little-endian SH-4, m4-single-only FP), compiled to WASM. Bare path: a minimal crt0 sets the stack + clears .bss + calls main(); the output is an ELF that Flycast's reios HLE BIOS boots DIRECTLY (no GD-ROM/CDI image, no firmware). No KallistiOS yet — bring up the PowerVR2 framebuffer yourself: program FB_R_CTRL/FB_R_SIZE/FB_R_SOF1 + SPG for 640x480 RGB565 at VRAM 0xA5000000, then write pixels (see the dc.h helper). With flycast_emulate_framebuffer on, a plain pixel-writing program presents — no TA list needed." },
96
+ },
94
97
  };
95
98
 
96
99
  /**
@@ -123,6 +126,7 @@ const PLATFORM_DEFAULT_LANGUAGE = {
123
126
  msx: "c",
124
127
  ps1: "c",
125
128
  n64: "c",
129
+ dreamcast: "c",
126
130
  };
127
131
 
128
132
  /**
@@ -502,6 +506,32 @@ export async function buildForPlatform(args) {
502
506
  };
503
507
  }
504
508
 
509
+ if (args.platform === "dreamcast") {
510
+ // Dreamcast SH-4 C: the bare gcc+newlib+libgcc path through the sh-elf-gcc WASM
511
+ // toolchain (cc1→as→ld). The output is an ELF that Flycast's reios HLE BIOS boots
512
+ // directly (no GD-ROM/CDI image). Bring up the PowerVR2 framebuffer yourself (see
513
+ // the dc.h helper); language defaults to "c".
514
+ const { buildShC } = await import("./sh-c/sh-c.js");
515
+ const r = await buildShC({
516
+ source: args.source,
517
+ sources: args.sources,
518
+ headers: args.includes,
519
+ cc1Options: args.options,
520
+ });
521
+ return {
522
+ ok: r.ok,
523
+ binary: r.binary,
524
+ listing: "",
525
+ symbols: r.symbols ?? "",
526
+ log: r.log,
527
+ issues: parseBuildLog(r.log),
528
+ exitCode: r.exitCode,
529
+ toolchain: "sh-elf-gcc",
530
+ stage: r.stage,
531
+ ...(r.crash ? { crash: r.crash } : {}),
532
+ };
533
+ }
534
+
505
535
  if (args.platform === "gba") {
506
536
  // R24 + R28: language:"c" routes through the arm-none-eabi gcc +
507
537
  // binutils WASM toolchain (cc1-arm → as → ld → objcopy). Three
@@ -77,9 +77,30 @@ function wrapN64Rom(text, entry = 0x80000400) {
77
77
  export async function buildMipsC(args) {
78
78
  const platform = args.platform;
79
79
  const endian = platform === "ps1" ? "little" : "big";
80
- const headers = args.headers ?? {};
81
80
  const cc1Options = [...(args.cc1Options ?? []), "-O2", "-G0", "-ffreestanding", "-fno-builtin", "-Wall"];
82
81
  const sources = args.sources ?? (args.source != null ? { "main.c": args.source } : {});
82
+
83
+ // Auto-bundle the platform helper lib so `#include "n64.h"` / `#include "psx.h"`
84
+ // just works (parity with the Dreamcast sh-c path that auto-bundles dc.h). The
85
+ // header is added to the virtual headers; the matching .c is compiled + linked as
86
+ // an extra source — UNLESS the caller already provides their own (caller wins, and
87
+ // we skip auto-linking the .c if a same-named source is already present so there's
88
+ // no duplicate-symbol clash). The helper lives in platforms/<platform>/lib/c/.
89
+ const helperName = platform === "ps1" ? "psx" : platform === "n64" ? "n64" : null;
90
+ const headers = { ...(args.headers ?? {}) };
91
+ /** @type {Record<string,string>} */
92
+ let autoHelperSrc = null;
93
+ if (helperName) {
94
+ const helperDir = path.join(__dirname, "..", "..", "platforms", platform, "lib", "c");
95
+ const hPath = path.join(helperDir, `${helperName}.h`);
96
+ const cPath = path.join(helperDir, `${helperName}.c`);
97
+ if (headers[`${helperName}.h`] == null) {
98
+ const hSrc = await readFile(hPath, "utf-8").catch(() => null);
99
+ if (hSrc != null) headers[`${helperName}.h`] = hSrc;
100
+ }
101
+ const callerHasC = (args.sources && (args.sources[`${helperName}.c`] != null));
102
+ if (!callerHasC) autoHelperSrc = await readFile(cPath, "utf-8").catch(() => null);
103
+ }
83
104
  let log = "";
84
105
 
85
106
  /** @type {Record<string, Uint8Array>} */
@@ -93,6 +114,19 @@ export async function buildMipsC(args) {
93
114
  if (as.exitCode !== 0 || !as.object) return { ok: false, binary: null, log, exitCode: as.exitCode || 1, stage: `as (${cName})`, ...(as.crash ? { crash: as.crash } : {}) };
94
115
  userObjs[cName.replace(/\.c$/i, ".o")] = as.object;
95
116
  }
117
+ // Auto-bundled helper .c (n64.c / psx.c) — compiled with the SAME headers so it can
118
+ // see its own header, linked alongside the user objects. Skipped when the caller
119
+ // supplied their own helper .c (callerHasC) above.
120
+ if (autoHelperSrc != null) {
121
+ const cc = await runCc1mips({ source: autoHelperSrc, headers, options: cc1Options, endian });
122
+ log += `--- cc1 (${helperName}.c, bundled) ---\n${cc.log || "(ok)"}\n`;
123
+ if (cc.exitCode !== 0 || !cc.asmSource) return { ok: false, binary: null, log, exitCode: cc.exitCode || 1, stage: `cc1 (${helperName}.c bundled)`, ...(cc.crash ? { crash: cc.crash } : {}) };
124
+ const as = await runMipsAs({ source: cc.asmSource, endian });
125
+ log += `--- as (${helperName}.c, bundled) ---\n${as.log || "(ok)"}\n`;
126
+ if (as.exitCode !== 0 || !as.object) return { ok: false, binary: null, log, exitCode: as.exitCode || 1, stage: `as (${helperName}.c bundled)` };
127
+ userObjs[`${helperName}.o`] = as.object;
128
+ }
129
+
96
130
  // raw .s sources too
97
131
  for (const sName of Object.keys(sources).filter((n) => /\.(s|asm)$/i.test(n))) {
98
132
  const as = await runMipsAs({ source: sources[sName], endian });
@@ -0,0 +1,23 @@
1
+ .section .text.start, "ax"
2
+ .global _start
3
+ _start:
4
+ mov.l stack_top, r15 ! stack = top of 16MB DC RAM
5
+ mov.l bss_start, r0 ! clear .bss
6
+ mov.l bss_end, r1
7
+ mov #0, r2
8
+ 1: cmp/hs r1, r0
9
+ bt 2f
10
+ mov.l r2, @r0
11
+ add #4, r0
12
+ bra 1b
13
+ nop
14
+ 2: mov.l main_addr, r0 ! call main()
15
+ jsr @r0
16
+ nop
17
+ hang: bra hang
18
+ nop
19
+ .align 4
20
+ stack_top: .long 0x8d000000
21
+ bss_start: .long __bss_start
22
+ bss_end: .long _end
23
+ main_addr: .long _main
@@ -0,0 +1,152 @@
1
+ /* dc.h — minimal Dreamcast helper for romdev homebrew.
2
+ *
3
+ * Brings up the PowerVR2 video output for a plain 640x480 RGB565 framebuffer and
4
+ * exposes pixel/fill/rect primitives. No KallistiOS dependency — just the registers
5
+ * the Flycast (reios HLE) core needs to scan out a direct framebuffer.
6
+ *
7
+ * Memory map:
8
+ * PVR/HOLLY registers : 0xA05F8000 (uncached)
9
+ * VRAM (64-bit area) : 0xA5000000 (16 MB) — the framebuffer lives here
10
+ *
11
+ * The DC has two VRAM views; the 64-bit area (0xA5000000) is the linear one we draw to.
12
+ */
13
+ #ifndef ROMDEV_DC_H
14
+ #define ROMDEV_DC_H
15
+
16
+ typedef unsigned char u8;
17
+ typedef unsigned short u16;
18
+ typedef unsigned int u32;
19
+
20
+ #define DC_W 640
21
+ #define DC_H 480
22
+
23
+ /* PVR register access */
24
+ #define PVR_BASE 0xA05F8000u
25
+ #define PVR_REG(off) (*(volatile u32 *)(PVR_BASE + (off)))
26
+
27
+ /* The framebuffer we draw to (offset 0 in the 64-bit VRAM area). */
28
+ #define DC_FB_OFFSET 0x000000u
29
+ #define DC_VRAM ((volatile u16 *)(0xA5000000u + DC_FB_OFFSET))
30
+
31
+ /* RGB565 helper */
32
+ static inline u16 dc_rgb(u8 r, u8 g, u8 b) {
33
+ return (u16)(((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3));
34
+ }
35
+
36
+ /* Bring up 640x480 RGB565 video. Call once at start. The register values are the
37
+ * documented PowerVR2 settings for a progressive NTSC 640x480 RGB565 framebuffer;
38
+ * Flycast's reios + EmulateFramebuffer scanout reads FB_R_CTRL/FB_R_SIZE/FB_R_SOF1. */
39
+ static inline void dc_video_init(void) {
40
+ /* FB_R_CTRL: fb_enable=1, fb_depth=1 (RGB565), fb_concat=4 (fill low bits). */
41
+ PVR_REG(0x044) = 0x00000001u | (1u << 2) | (4u << 4); /* = 0x45 */
42
+ /* FB_W_CTRL: fb_packmode=1 (RGB565). */
43
+ PVR_REG(0x048) = 0x00000001u;
44
+ /* FB_R_SOF1 / FB_W_SOF1: framebuffer start = DC_FB_OFFSET. */
45
+ PVR_REG(0x050) = DC_FB_OFFSET; /* FB_R_SOF1 */
46
+ PVR_REG(0x060) = DC_FB_OFFSET; /* FB_W_SOF1 */
47
+ /* FB_R_SIZE: fb_x_size = (line bytes / 4) - 1, fb_y_size = lines - 1, modulus = 1.
48
+ * 640px * 2 bytes = 1280 bytes/line -> 1280/4 - 1 = 319. */
49
+ PVR_REG(0x05C) = (u32)(DC_W * 2 / 4 - 1) /* fb_x_size = 319 */
50
+ | ((u32)(DC_H - 1) << 10) /* fb_y_size = 479 */
51
+ | ((u32)1u << 20); /* fb_modulus = 1 */
52
+ /* FB_W_LINESTRIDE: line stride in 64-bit (8-byte) units = 1280/8 = 160. */
53
+ PVR_REG(0x11C) = (u32)(DC_W * 2 / 8);
54
+ /* VO_CONTROL: pixel double off, normal output. */
55
+ PVR_REG(0x0E8) = 0x00000000u;
56
+ /* VO_BORDER_COL: black border. */
57
+ PVR_REG(0x040) = 0x00000000u;
58
+ /* SPG_LOAD / SPG_CONTROL: NTSC 640x480 INTERLACED (480i). Non-interlace (240p)
59
+ * only displays 240 lines, so the top half of a 480-line framebuffer never shows.
60
+ * interlace=1 outputs the full 480-line image. */
61
+ PVR_REG(0x0D8) = (524u << 16) | 857u; /* SPG_LOAD: vcount, hcount */
62
+ PVR_REG(0x0D0) = 0x00000050u; /* SPG_CONTROL: NTSC, interlace=1 (bit4), PAL=0 */
63
+ }
64
+
65
+ /* Plot a pixel (no bounds clamp on the hot path; caller keeps in range). */
66
+ static inline void dc_plot(int x, int y, u16 c) {
67
+ DC_VRAM[y * DC_W + x] = c;
68
+ }
69
+
70
+ /* Fill the whole framebuffer with one color. */
71
+ static inline void dc_clear(u16 c) {
72
+ int i;
73
+ for (i = 0; i < DC_W * DC_H; i++)
74
+ DC_VRAM[i] = c;
75
+ }
76
+
77
+ /* Fill an axis-aligned rectangle (clipped to the screen). */
78
+ static inline void dc_rect(int x0, int y0, int w, int h, u16 c) {
79
+ int x, y;
80
+ if (x0 < 0) { w += x0; x0 = 0; }
81
+ if (y0 < 0) { h += y0; y0 = 0; }
82
+ if (x0 + w > DC_W) w = DC_W - x0;
83
+ if (y0 + h > DC_H) h = DC_H - y0;
84
+ for (y = 0; y < h; y++)
85
+ for (x = 0; x < w; x++)
86
+ DC_VRAM[(y0 + y) * DC_W + (x0 + x)] = c;
87
+ }
88
+
89
+ /* ── Controller input (Maple bus, port 0) ─────────────────────────────────────
90
+ * Digital-button bits, matching Flycast's DC kcode (active-LOW on the wire — a bit
91
+ * is 0 when pressed). dc_pad() does a real Maple "Get Condition" DMA, inverts the
92
+ * kcode to active-HIGH, and masks to the real button bits. The romdev host's
93
+ * setInput drives the emulated pad. */
94
+ #define DC_BTN_C (1u << 0)
95
+ #define DC_BTN_B (1u << 1)
96
+ #define DC_BTN_A (1u << 2)
97
+ #define DC_BTN_START (1u << 3)
98
+ #define DC_BTN_UP (1u << 4)
99
+ #define DC_BTN_DOWN (1u << 5)
100
+ #define DC_BTN_LEFT (1u << 6)
101
+ #define DC_BTN_RIGHT (1u << 7)
102
+ #define DC_BTN_Y (1u << 9)
103
+ #define DC_BTN_X (1u << 10)
104
+ /* only the bits above are real digital buttons; mask out reserved bits on read. */
105
+ #define DC_BTN_MASK 0x06FFu
106
+
107
+ /* Maple DMA registers (Holly system bus). */
108
+ #define DC_SB(o) (*(volatile u32 *)(0xA05F6C00u + (o)))
109
+ #define DC_MDSTAR 0x04 /* command-table address */
110
+ #define DC_MDEN 0x14 /* DMA enable */
111
+ #define DC_MDST 0x18 /* DMA start */
112
+
113
+ /* Read controller port 0 via a Maple "Get Condition" DMA. Builds a one-shot command
114
+ * frame (recipient = port-0 controller AP 0x20, cmd 0x09 GetCondition, function =
115
+ * controller 0x01000000), points the recv buffer at a scratch, kicks the DMA, then
116
+ * scans the response for the controller's function-id marker (0x01000000) and reads
117
+ * the 16-bit kcode that immediately follows. kcode is active-LOW on the wire, so we
118
+ * invert + mask to the real button bits → DC_BTN_* set == pressed.
119
+ *
120
+ * NOTE: the exact word offset of the kcode in the recv frame can vary; we locate it
121
+ * by the MFID marker rather than a fixed index, which is robust across the response
122
+ * framing. Returns 0 if no controller response is found. */
123
+ static inline u32 dc_pad(void) {
124
+ static volatile u32 cmd[8] __attribute__((aligned(32)));
125
+ static volatile u32 rsp[16] __attribute__((aligned(32)));
126
+ u32 phys_cmd = ((u32)(unsigned long)cmd) & 0x1FFFFFFF;
127
+ u32 phys_rsp = ((u32)(unsigned long)rsp) & 0x1FFFFFFF;
128
+ int i;
129
+ for (i = 0; i < 16; i++) rsp[i] = 0;
130
+ cmd[0] = 0x80000000u; /* last xfer (bit31), op=START(0), frame len-1=0 */
131
+ cmd[1] = phys_rsp; /* receive address */
132
+ cmd[2] = (1u << 24) /* data length in words (1) */
133
+ | (0x20u << 16) /* sender address */
134
+ | (0x20u << 8) /* recipient: port 0 controller (AP 0x20) */
135
+ | 0x09u; /* command: Get Condition */
136
+ cmd[3] = 0x01000000u; /* function: controller */
137
+ DC_SB(DC_MDEN) = 1;
138
+ DC_SB(DC_MDSTAR) = phys_cmd;
139
+ DC_SB(DC_MDST) = 1; /* synchronous under the romdev/HLE core */
140
+ /* find the controller function-id (0x01000000) in the response; the kcode is the
141
+ * next word's low half. The response data starts after the frame header word. */
142
+ for (i = 0; i < 15; i++) {
143
+ if (rsp[i] == 0x01000000u) {
144
+ u32 kcode = rsp[i + 1] & 0xFFFF;
145
+ return ((~kcode) & DC_BTN_MASK);
146
+ }
147
+ }
148
+ return 0;
149
+ }
150
+ static inline int dc_pressed(u32 mask) { return (dc_pad() & mask) ? 1 : 0; }
151
+
152
+ #endif /* ROMDEV_DC_H */
@@ -0,0 +1,11 @@
1
+ ENTRY(_start)
2
+ MEMORY { ram (rwx) : ORIGIN = 0x8c010000, LENGTH = 0x800000 }
3
+ SECTIONS {
4
+ .text : { *(.text.start) *(.text*) *(.rodata*) } > ram
5
+ .data : { *(.data*) } > ram
6
+ . = ALIGN(4);
7
+ __bss_start = .;
8
+ .bss : { *(.bss*) *(COMMON) } > ram
9
+ . = ALIGN(4);
10
+ _end = .;
11
+ }
Binary file
Binary file
Binary file