romdevtools 0.44.0 → 0.70.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 (71) hide show
  1. package/CHANGELOG.md +572 -0
  2. package/README.md +9 -7
  3. package/examples/dreamcast/hello/main.c +24 -0
  4. package/examples/n64/platformer/main.c +158 -0
  5. package/examples/n64/puzzle/main.c +117 -0
  6. package/examples/n64/racing/main.c +147 -0
  7. package/examples/n64/shmup/main.c +122 -0
  8. package/examples/n64/sports/main.c +127 -0
  9. package/examples/ps1/platformer/main.c +158 -0
  10. package/examples/ps1/puzzle/main.c +125 -0
  11. package/examples/ps1/racing/main.c +147 -0
  12. package/examples/ps1/shmup/main.c +192 -0
  13. package/examples/ps1/sports/main.c +127 -0
  14. package/examples/ps1/sprite_move/main.c +38 -0
  15. package/package.json +11 -2
  16. package/src/analysis/analyze.js +224 -29
  17. package/src/analysis/decompile.js +7 -0
  18. package/src/analysis/decompiler/sleigh/mips.ldefs +25 -0
  19. package/src/analysis/decompiler/sleigh/mips32.pspec +78 -0
  20. package/src/analysis/decompiler/sleigh/mips32be.cspec +107 -0
  21. package/src/analysis/decompiler/sleigh/mips32be.sla +211162 -0
  22. package/src/analysis/decompiler/sleigh/mips32le.cspec +106 -0
  23. package/src/analysis/decompiler/sleigh/mips32le.sla +210624 -0
  24. package/src/analysis/rizin.js +24 -1
  25. package/src/cores/capabilities.js +122 -2
  26. package/src/cores/registry.js +17 -0
  27. package/src/host/LibretroGL.js +273 -0
  28. package/src/host/LibretroGLBridge.js +836 -0
  29. package/src/host/LibretroHost.js +203 -3
  30. package/src/host/callbacks.js +22 -2
  31. package/src/host/coreLoader.js +65 -5
  32. package/src/host/cpu-state.js +27 -0
  33. package/src/host/framebuffer.js +14 -1
  34. package/src/host/glOptionalDep.js +60 -0
  35. package/src/host/n64-ai-state.js +43 -0
  36. package/src/host/ps1-spu-state.js +65 -0
  37. package/src/host/retroConstants.js +14 -0
  38. package/src/mcp/tools/disasm.js +2 -0
  39. package/src/mcp/tools/frame.js +18 -9
  40. package/src/mcp/tools/lifecycle.js +1 -1
  41. package/src/mcp/tools/platform-tools.js +20 -4
  42. package/src/mcp/tools/platforms.js +38 -4
  43. package/src/mcp/tools/project.js +116 -0
  44. package/src/mcp/tools/rendering-context.js +2 -1
  45. package/src/mcp/tools/toolchain.js +1 -1
  46. package/src/platforms/n64/lib/c/n64.c +196 -0
  47. package/src/platforms/n64/lib/c/n64.h +68 -0
  48. package/src/platforms/ps1/lib/c/psx.c +200 -0
  49. package/src/platforms/ps1/lib/c/psx.h +83 -0
  50. package/src/toolchains/index.js +65 -0
  51. package/src/toolchains/mips-c/lib/be/libc.a +0 -0
  52. package/src/toolchains/mips-c/lib/be/libgcc.a +0 -0
  53. package/src/toolchains/mips-c/lib/be/libm.a +0 -0
  54. package/src/toolchains/mips-c/lib/el/libc.a +0 -0
  55. package/src/toolchains/mips-c/lib/el/libm.a +0 -0
  56. package/src/toolchains/mips-c/lib/n64-crt0.s +21 -0
  57. package/src/toolchains/mips-c/lib/n64-ipl3.s +30 -0
  58. package/src/toolchains/mips-c/lib/n64.ld +15 -0
  59. package/src/toolchains/mips-c/lib/ps1-crt0.s +20 -0
  60. package/src/toolchains/mips-c/lib/ps1.ld +15 -0
  61. package/src/toolchains/mips-c/lib/softint.c +37 -0
  62. package/src/toolchains/mips-c/mips-c.js +155 -0
  63. package/src/toolchains/mips-elf-gcc/gcc.js +130 -0
  64. package/src/toolchains/sh-c/lib/dc-crt0.s +23 -0
  65. package/src/toolchains/sh-c/lib/dc.h +102 -0
  66. package/src/toolchains/sh-c/lib/dc.ld +11 -0
  67. package/src/toolchains/sh-c/lib/libc.a +0 -0
  68. package/src/toolchains/sh-c/lib/libgcc.a +0 -0
  69. package/src/toolchains/sh-c/lib/libm.a +0 -0
  70. package/src/toolchains/sh-c/sh-c.js +101 -0
  71. package/src/toolchains/sh-elf-gcc/gcc.js +122 -0
@@ -0,0 +1,155 @@
1
+ // mips-c — minimal C → MIPS binary builder for PS1 (and N64), the bare
2
+ // gcc+newlib+libgcc path (no SDK yet). Mirrors genesis-c.js's buildMinimal.
3
+ //
4
+ // buildMipsC({ source, sources, headers, platform }) →
5
+ // PS1: a runnable PS-EXE ('PS-X EXE' header + .text at 0x80010000)
6
+ // N64: a flat .bin (boot glue is libdragon's job — minimal path for now)
7
+ //
8
+ // Pipeline per source: cc1 → as → (link all with crt0 + libc/libm/libgcc) →
9
+ // objcopy → wrap. Endianness from the platform (PS1 little / N64 big).
10
+
11
+ import { fileURLToPath } from "node:url";
12
+ import { readFile } from "node:fs/promises";
13
+ import path from "node:path";
14
+
15
+ import { runCc1mips, runMipsAs, runMipsLd, runMipsObjcopy } from "../mips-elf-gcc/gcc.js";
16
+
17
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
18
+ const LIB = path.join(__dirname, "lib");
19
+
20
+ /** Wrap a raw .text image in a PS-EXE header (2048 bytes). The PS1 BIOS loads
21
+ * `t_size` bytes from the file (after the header) to `t_addr` and jumps to pc0. */
22
+ function wrapPsExe(text, loadAddr = 0x80010000, spBase = 0x801ffff0) {
23
+ const hdr = new Uint8Array(2048);
24
+ hdr.set(new TextEncoder().encode("PS-X EXE"), 0);
25
+ const put = (o, v) => { hdr[o] = v & 0xff; hdr[o + 1] = (v >> 8) & 0xff; hdr[o + 2] = (v >> 16) & 0xff; hdr[o + 3] = (v >>> 24) & 0xff; };
26
+ put(0x10, loadAddr); // pc0 (entry)
27
+ put(0x18, loadAddr); // t_addr (load address)
28
+ put(0x1c, (text.length + 3) & ~3); // t_size
29
+ put(0x30, spBase); // s_addr (stack base)
30
+ const out = new Uint8Array(2048 + text.length);
31
+ out.set(hdr, 0); out.set(text, 2048);
32
+ return out;
33
+ }
34
+
35
+ // Minimal clean-room N64 IPL3 (assembled from lib/n64-ipl3.s): runs in SP DMEM at
36
+ // 0xA4000040 after the (HLE or real) PIF boot copies ROM[0x40..0xFFF] there. It
37
+ // PI-DMAs 1 MB from cart 0xB0001000 → RDRAM 0x80000400 and jumps to the game entry.
38
+ const N64_IPL3 = [
39
+ 0x3c08a460, 0x24090400, 0xad090000, 0x3c091000, 0x35291000, 0xad090004,
40
+ 0x3c09000f, 0x3529ffff, 0xad09000c, 0x8d0a0010, 0x314a0003, 0x1540fffd,
41
+ 0x00000000, 0x3c0b8000, 0x356b0400, 0x01600008, 0x00000000,
42
+ ];
43
+
44
+ /** Wrap a raw .text image (linked at 0x80000400) in a bootable big-endian .z64:
45
+ * 64-byte header (0x80371240 magic + entry) + IPL3 at 0x40 + game at 0x1000. The
46
+ * parallel_n64 HLE boot runs the IPL3, which copies the game to RDRAM and jumps. */
47
+ function wrapN64Rom(text, entry = 0x80000400) {
48
+ const GAME_OFF = 0x1000;
49
+ const size = GAME_OFF + ((text.length + 3) & ~3);
50
+ const rom = new Uint8Array(size < 0x101000 ? 0x101000 : size); // ≥1MB (IPL3 copies 1MB)
51
+ const be32 = (o, v) => { rom[o] = (v >>> 24) & 0xff; rom[o + 1] = (v >> 16) & 0xff; rom[o + 2] = (v >> 8) & 0xff; rom[o + 3] = v & 0xff; };
52
+ // header: PI BSD config / magic, clock, entry, release
53
+ be32(0x00, 0x80371240); // magic (Z64, native big-endian)
54
+ be32(0x04, 0x0000000f); // clock rate
55
+ be32(0x08, entry); // boot entry (informational; IPL3 jumps explicitly)
56
+ be32(0x0c, 0x00001444); // release
57
+ // name (0x20..0x33)
58
+ const name = "ROMDEV HOMEBREW ";
59
+ for (let i = 0; i < 20; i++) rom[0x20 + i] = name.charCodeAt(i);
60
+ be32(0x3c, 0x0000004e); // cartridge id "NE" placeholder + region
61
+ // IPL3 at 0x40
62
+ for (let i = 0; i < N64_IPL3.length; i++) be32(0x40 + i * 4, N64_IPL3[i]);
63
+ // game at 0x1000
64
+ rom.set(text, GAME_OFF);
65
+ return rom;
66
+ }
67
+
68
+ /**
69
+ * Build a minimal C program to a runnable PS1/N64 image.
70
+ * @param {Object} args
71
+ * @param {string} [args.source] single C source
72
+ * @param {Record<string,string>} [args.sources] multi-file (name → text)
73
+ * @param {Record<string,string>} [args.headers] virtual headers
74
+ * @param {string} args.platform "ps1" | "n64"
75
+ * @param {string[]} [args.cc1Options]
76
+ */
77
+ export async function buildMipsC(args) {
78
+ const platform = args.platform;
79
+ const endian = platform === "ps1" ? "little" : "big";
80
+ const headers = args.headers ?? {};
81
+ const cc1Options = [...(args.cc1Options ?? []), "-O2", "-G0", "-ffreestanding", "-fno-builtin", "-Wall"];
82
+ const sources = args.sources ?? (args.source != null ? { "main.c": args.source } : {});
83
+ let log = "";
84
+
85
+ /** @type {Record<string, Uint8Array>} */
86
+ const userObjs = {};
87
+ for (const cName of Object.keys(sources).filter((n) => /\.c$/i.test(n))) {
88
+ const cc = await runCc1mips({ source: sources[cName], headers, options: cc1Options, endian });
89
+ log += `--- cc1 (${cName}) ---\n${cc.log || "(ok)"}\n`;
90
+ if (cc.exitCode !== 0 || !cc.asmSource) return { ok: false, binary: null, log, exitCode: cc.exitCode || 1, stage: `cc1 (${cName})`, ...(cc.crash ? { crash: cc.crash } : {}) };
91
+ const as = await runMipsAs({ source: cc.asmSource, endian });
92
+ log += `--- as (${cName}) ---\n${as.log || "(ok)"}\n`;
93
+ 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
+ userObjs[cName.replace(/\.c$/i, ".o")] = as.object;
95
+ }
96
+ // raw .s sources too
97
+ for (const sName of Object.keys(sources).filter((n) => /\.(s|asm)$/i.test(n))) {
98
+ const as = await runMipsAs({ source: sources[sName], endian });
99
+ log += `--- as (${sName}) ---\n${as.log || "(ok)"}\n`;
100
+ if (as.exitCode !== 0 || !as.object) return { ok: false, binary: null, log, exitCode: as.exitCode || 1, stage: `as (${sName})`, ...(as.crash ? { crash: as.crash } : {}) };
101
+ userObjs[sName.replace(/\.(s|asm)$/i, ".o")] = as.object;
102
+ }
103
+
104
+ // crt0 (per platform)
105
+ const crt0Name = platform === "ps1" ? "ps1-crt0.s" : "n64-crt0.s";
106
+ const crt0Src = await readFile(path.join(LIB, crt0Name), "utf-8");
107
+ const crt0As = await runMipsAs({ source: crt0Src, endian });
108
+ log += `--- as (${crt0Name}) ---\n${crt0As.log || "(ok)"}\n`;
109
+ if (crt0As.exitCode !== 0 || !crt0As.object) return { ok: false, binary: null, log, exitCode: crt0As.exitCode || 1, stage: "as (crt0)", ...(crt0As.crash ? { crash: crt0As.crash } : {}) };
110
+
111
+ // softint.c — the few libgcc helpers (64-bit divide/mod) in plain C, so the
112
+ // link doesn't need an endian-specific libgcc.a (the EL libgcc isn't bundled).
113
+ const softSrc = await readFile(path.join(LIB, "softint.c"), "utf-8");
114
+ const softCc = await runCc1mips({ source: softSrc, options: cc1Options, endian });
115
+ const softAs = softCc.asmSource ? await runMipsAs({ source: softCc.asmSource, endian }) : { exitCode: 1 };
116
+ if (softCc.exitCode !== 0 || softAs.exitCode !== 0 || !softAs.object) {
117
+ return { ok: false, binary: null, log: log + (softCc.log || "") + (softAs.log || ""), exitCode: 1, stage: "softint" };
118
+ }
119
+
120
+ // link
121
+ const ldName = platform === "ps1" ? "ps1.ld" : "n64.ld";
122
+ const linkScript = await readFile(path.join(LIB, ldName), "utf-8");
123
+ // newlib + libgcc are endian-specific: el/ (PS1 little) vs be/ (N64 big). libgcc
124
+ // is only bundled for be/ — softint.c covers the EL case, so libgcc is optional.
125
+ const libDir = path.join(LIB, endian === "little" ? "el" : "be");
126
+ const [libc, libm] = await Promise.all([
127
+ readFile(path.join(libDir, "libc.a")), readFile(path.join(libDir, "libm.a")),
128
+ ]);
129
+ const archives = { "libc.a": new Uint8Array(libc), "libm.a": new Uint8Array(libm) };
130
+ const libraries = ["c", "m"];
131
+ try {
132
+ const libgcc = await readFile(path.join(libDir, "libgcc.a"));
133
+ archives["libgcc.a"] = new Uint8Array(libgcc);
134
+ libraries.unshift("gcc");
135
+ } catch { /* no endian libgcc — softint.c provides the needed helpers */ }
136
+ const ld = await runMipsLd({
137
+ objects: { "crt0.o": crt0As.object, "softint.o": softAs.object, ...userObjs },
138
+ linkScript, endian,
139
+ archives,
140
+ libraries,
141
+ libraryPaths: ["/work"],
142
+ options: ["--no-warn-rwx-segments"],
143
+ });
144
+ log += `--- ld ---\n${ld.log || "(ok)"}\n`;
145
+ if (ld.exitCode !== 0 || !ld.elf) return { ok: false, binary: null, log, exitCode: ld.exitCode || 1, stage: "ld", ...(ld.crash ? { crash: ld.crash } : {}) };
146
+
147
+ const oc = await runMipsObjcopy({ elf: ld.elf });
148
+ log += `--- objcopy ---\n${oc.log || "(ok)"}\n`;
149
+ if (oc.exitCode !== 0 || !oc.binary) return { ok: false, binary: null, log, exitCode: oc.exitCode || 1, stage: "objcopy", ...(oc.crash ? { crash: oc.crash } : {}) };
150
+
151
+ const binary = platform === "ps1" ? wrapPsExe(oc.binary)
152
+ : platform === "n64" ? wrapN64Rom(oc.binary)
153
+ : oc.binary;
154
+ return { ok: true, binary, log, exitCode: 0, stage: "done", ...(ld.map ? { symbols: ld.map } : {}) };
155
+ }
@@ -0,0 +1,130 @@
1
+ // mips-elf-gcc — WASM toolchain wrappers for N64 / PS1 C builds.
2
+ //
3
+ // Pipeline (mirrors m68k-elf-gcc/gcc.js — gcc-the-driver can't fork/exec under
4
+ // emscripten, so we orchestrate cc1 → as → ld → objcopy through callMain):
5
+ // runCc1mips({source, headers, options, endian}) → MIPS assembly (.s)
6
+ // runMipsAs({source, includes, endian}) → .o ELF object
7
+ // runMipsLd({objects, linkScript, ...}) → linked .elf (+ map)
8
+ // runMipsObjcopy({elf}) → raw .bin
9
+ //
10
+ // Endianness: N64 (R4300) is big-endian (default), PS1 (R3000) is little-endian
11
+ // (-EL). The same WASM toolchain emits both — pass endian:'little' for PS1.
12
+
13
+ import { fileURLToPath } from "node:url";
14
+ import { existsSync } from "node:fs";
15
+ import path from "node:path";
16
+
17
+ import { runIsolated, textFile, binaryFile, getOutputBytes, getOutputText } from "../_worker/run.js";
18
+
19
+ const __filename = fileURLToPath(import.meta.url);
20
+ const __dirname = path.dirname(__filename);
21
+
22
+ // The WASM ships in romdev-toolchain-mips-gcc; fall back to the in-tree dev copy.
23
+ function resolveMipsGlue(file) {
24
+ try {
25
+ const u = import.meta.resolve("romdev-toolchain-mips-gcc");
26
+ const p = path.join(path.dirname(fileURLToPath(u)), "wasm", file);
27
+ if (existsSync(p)) return p;
28
+ } catch { /* not resolvable — fall through */ }
29
+ const local = path.join(__dirname, "wasm", file);
30
+ if (existsSync(local)) return local;
31
+ throw new Error(`mips-elf-gcc WASM (${file}) not found — build it with scripts/build-mips-wasm-tools.sh`);
32
+ }
33
+ const _glue = {};
34
+ const mipsGlue = (file) => (_glue[file] ??= resolveMipsGlue(file));
35
+
36
+ /** Endian flags differ by TOOL: cc1 (the C frontend) wants `-mel`/`-meb`; the
37
+ * assembler + linker want `-EL`/`-EB`. `-mabi=32` (cc1) covers both R3000 (PS1)
38
+ * and R4300 (N64) 32-bit code. */
39
+ function cc1ArchFlags(endian) {
40
+ return [endian === "little" ? "-mel" : "-meb", "-mabi=32"];
41
+ }
42
+ function asArchFlags(endian) {
43
+ // -G0: never use GP-relative (small-data) addressing. Without it, statics land in
44
+ // .sdata/.sbss and the 16-bit GPREL offsets overflow ("relocation truncated") on
45
+ // anything but a tiny program. -G0 forces normal .data/.bss addressing.
46
+ return [endian === "little" ? "-EL" : "-EB", "-mabi=32", "-G0"];
47
+ }
48
+
49
+ // ── cc1 — MIPS gcc C frontend, source → assembly ─────────────────────
50
+ export async function runCc1mips(args) {
51
+ const { source, options = [], endian = "big" } = args;
52
+ const headers = args.headers ?? {};
53
+ const inputFiles = [textFile("/work/main.c", source)];
54
+ for (const [name, content] of Object.entries(headers)) inputFiles.push(textFile("/work/" + name, content));
55
+ const argv = [
56
+ ...cc1ArchFlags(endian),
57
+ "-iquote", "/work", "-I", "/work",
58
+ ...options,
59
+ "/work/main.c", "-o", "/work/main.s",
60
+ ];
61
+ const r = await runIsolated({
62
+ gluePath: mipsGlue("cc1.mjs"),
63
+ argv, inputFiles,
64
+ outputFiles: [{ vfsPath: "/work/main.s", encoding: "utf8" }],
65
+ });
66
+ return { log: r.log, exitCode: r.exitCode, asmSource: getOutputText(r, "/work/main.s") || null,
67
+ ...(r.crash ? { crash: r.crash, stage: "crash" } : {}) };
68
+ }
69
+
70
+ // ── mips-elf-as — GNU assembler, .s → .o ─────────────────────────────
71
+ export async function runMipsAs(args) {
72
+ const { source, options = [], endian = "big" } = args;
73
+ const includes = args.includes ?? {};
74
+ const binaryIncludes = args.binaryIncludes ?? {};
75
+ const inputFiles = [textFile("/work/main.s", source)];
76
+ for (const [name, content] of Object.entries(includes)) inputFiles.push(textFile("/work/" + name, content));
77
+ for (const [name, bytes] of Object.entries(binaryIncludes)) inputFiles.push(binaryFile("/work/" + name, bytes));
78
+ const argv = [...asArchFlags(endian), "-I", "/work", ...options, "/work/main.s", "-o", "/work/main.o"];
79
+ const r = await runIsolated({
80
+ gluePath: mipsGlue("mips-elf-as.mjs"),
81
+ argv, inputFiles,
82
+ outputFiles: [{ vfsPath: "/work/main.o", encoding: "base64" }],
83
+ });
84
+ return { log: r.log, exitCode: r.exitCode, object: getOutputBytes(r, "/work/main.o"),
85
+ ...(r.crash ? { crash: r.crash, stage: "crash" } : {}) };
86
+ }
87
+
88
+ // ── mips-elf-ld — GNU linker, .o + linker script → .elf ──────────────
89
+ export async function runMipsLd(args) {
90
+ const { objects, linkScript, libraries = [], libraryPaths = [], options = [], endian = "big" } = args;
91
+ const archives = args.archives ?? {};
92
+ const inputFiles = [textFile("/work/link.ld", linkScript)];
93
+ for (const [name, bytes] of Object.entries(objects)) inputFiles.push(binaryFile("/work/" + name, bytes));
94
+ for (const [name, bytes] of Object.entries(archives)) inputFiles.push(binaryFile("/work/" + name, bytes));
95
+ const argv = [
96
+ endian === "little" ? "-EL" : "-EB",
97
+ "-T", "/work/link.ld",
98
+ "-o", "/work/main.elf",
99
+ "-Map=/work/main.map",
100
+ ...libraryPaths.flatMap((p) => ["-L", p]),
101
+ ...Object.keys(objects).map((n) => "/work/" + n),
102
+ ...libraries.map((l) => `-l${l}`),
103
+ ...options,
104
+ ];
105
+ const r = await runIsolated({
106
+ gluePath: mipsGlue("mips-elf-ld.mjs"),
107
+ argv, inputFiles,
108
+ outputFiles: [
109
+ { vfsPath: "/work/main.elf", encoding: "base64" },
110
+ { vfsPath: "/work/main.map", encoding: "utf8" },
111
+ ],
112
+ });
113
+ return { log: r.log, exitCode: r.exitCode, elf: getOutputBytes(r, "/work/main.elf"),
114
+ map: getOutputText(r, "/work/main.map") || null,
115
+ ...(r.crash ? { crash: r.crash, stage: "crash" } : {}) };
116
+ }
117
+
118
+ // ── mips-elf-objcopy — ELF → raw .bin ────────────────────────────────
119
+ export async function runMipsObjcopy(args) {
120
+ const { elf, options = [] } = args;
121
+ const inputFiles = [binaryFile("/work/main.elf", elf)];
122
+ const argv = ["-O", "binary", ...options, "/work/main.elf", "/work/main.bin"];
123
+ const r = await runIsolated({
124
+ gluePath: mipsGlue("mips-elf-objcopy.mjs"),
125
+ argv, inputFiles,
126
+ outputFiles: [{ vfsPath: "/work/main.bin", encoding: "base64" }],
127
+ });
128
+ return { log: r.log, exitCode: r.exitCode, binary: getOutputBytes(r, "/work/main.bin"),
129
+ ...(r.crash ? { crash: r.crash, stage: "crash" } : {}) };
130
+ }
@@ -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,102 @@
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 progressive timing. */
59
+ PVR_REG(0x0D8) = (524u << 16) | 857u; /* SPG_LOAD: vcount, hcount */
60
+ PVR_REG(0x0D0) = 0x00000000u; /* SPG_CONTROL: NTSC, non-interlace */
61
+ }
62
+
63
+ /* Plot a pixel (no bounds clamp on the hot path; caller keeps in range). */
64
+ static inline void dc_plot(int x, int y, u16 c) {
65
+ DC_VRAM[y * DC_W + x] = c;
66
+ }
67
+
68
+ /* Fill the whole framebuffer with one color. */
69
+ static inline void dc_clear(u16 c) {
70
+ int i;
71
+ for (i = 0; i < DC_W * DC_H; i++)
72
+ DC_VRAM[i] = c;
73
+ }
74
+
75
+ /* Fill an axis-aligned rectangle (clipped to the screen). */
76
+ static inline void dc_rect(int x0, int y0, int w, int h, u16 c) {
77
+ int x, y;
78
+ if (x0 < 0) { w += x0; x0 = 0; }
79
+ if (y0 < 0) { h += y0; y0 = 0; }
80
+ if (x0 + w > DC_W) w = DC_W - x0;
81
+ if (y0 + h > DC_H) h = DC_H - y0;
82
+ for (y = 0; y < h; y++)
83
+ for (x = 0; x < w; x++)
84
+ DC_VRAM[(y0 + y) * DC_W + (x0 + x)] = c;
85
+ }
86
+
87
+ /* ── Controller input (Maple bus, port 0) ─────────────────────────────────────
88
+ * The DC Maple controller digital-button bit layout. Full Maple DMA setup is
89
+ * non-trivial; the romdev host injects input (setInput) that the core maps onto
90
+ * Maple, so these constants document the bit layout for game logic. */
91
+ #define DC_BTN_C 0x0001
92
+ #define DC_BTN_B 0x0002
93
+ #define DC_BTN_A 0x0004
94
+ #define DC_BTN_START 0x0008
95
+ #define DC_BTN_UP 0x0010
96
+ #define DC_BTN_DOWN 0x0020
97
+ #define DC_BTN_LEFT 0x0040
98
+ #define DC_BTN_RIGHT 0x0080
99
+ #define DC_BTN_Y 0x0200
100
+ #define DC_BTN_X 0x0400
101
+
102
+ #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
@@ -0,0 +1,101 @@
1
+ // sh-c — Dreamcast (SH-4) C build driver.
2
+ //
3
+ // buildShC({ source | sources, headers }) → { ok, binary (ELF), log, symbols }
4
+ //
5
+ // gcc-the-driver can't fork/exec under emscripten, so we orchestrate the stages
6
+ // directly (cc1 → as → ld → objcopy-not-needed). The output is an ELF that Flycast's
7
+ // reios HLE BIOS boots directly (no GD-ROM/CDI image, no scrambling): reios_loadElf
8
+ // copies the PT_LOAD segments to their vaddr (0x8c010000) and jumps to _start.
9
+ //
10
+ // Bare path: a small crt0 sets the stack (top of 16 MB DC RAM), zeroes .bss, and
11
+ // calls main(). newlib libc/libm + libgcc are linked (SH-4, little-endian). The
12
+ // romdev DC helper (rom-games/dreamcast/.../dc.h) brings up the PowerVR2 framebuffer.
13
+
14
+ import { readFile } from "node:fs/promises";
15
+ import { fileURLToPath } from "node:url";
16
+ import path from "node:path";
17
+
18
+ import { runCc1sh, runShAs, runShLd } from "../sh-elf-gcc/gcc.js";
19
+
20
+ const __filename = fileURLToPath(import.meta.url);
21
+ const __dirname = path.dirname(__filename);
22
+ const LIB = path.join(__dirname, "lib");
23
+
24
+ /**
25
+ * Compile + link Dreamcast SH-4 C to a bootable ELF.
26
+ * @param {object} args
27
+ * @param {string} [args.source] single C source (shorthand for sources["main.c"])
28
+ * @param {Record<string,string>} [args.sources] name → source (.c and .s)
29
+ * @param {Record<string,string>} [args.headers] name → header text
30
+ * @param {string[]} [args.cc1Options] extra cc1 flags
31
+ */
32
+ export async function buildShC(args) {
33
+ // The bundled DC helper (dc.h) is auto-available so `#include "dc.h"` just works —
34
+ // it brings up the PowerVR2 framebuffer (FB_R_CTRL/SIZE/SOF1 + SPG for 640x480 RGB565)
35
+ // that Flycast presents. A caller-supplied "dc.h" wins (override the bundled one).
36
+ const bundledDcH = await readFile(path.join(LIB, "dc.h"), "utf-8").catch(() => null);
37
+ const headers = { ...(bundledDcH != null ? { "dc.h": bundledDcH } : {}), ...(args.headers ?? {}) };
38
+ // -m4-single-only is passed by the cc1 wrapper; here the language/codegen knobs.
39
+ // Default to -O2, but let a user-supplied -O<level> win (gcc honors the LAST -O, so a
40
+ // default appended AFTER the user's would clobber it — only add -O2 if absent).
41
+ const userOpts = args.cc1Options ?? [];
42
+ const hasOpt = userOpts.some((o) => /^-O/.test(o));
43
+ const cc1Options = [...(hasOpt ? [] : ["-O2"]), ...userOpts, "-ffreestanding", "-fno-builtin", "-Wall"];
44
+ const sources = args.sources ?? (args.source != null ? { "main.c": args.source } : {});
45
+ let log = "";
46
+
47
+ /** @type {Record<string, Uint8Array>} */
48
+ const userObjs = {};
49
+ for (const cName of Object.keys(sources).filter((n) => /\.c$/i.test(n))) {
50
+ const cc = await runCc1sh({ source: sources[cName], headers, options: cc1Options });
51
+ log += `--- cc1 (${cName}) ---\n${cc.log || "(ok)"}\n`;
52
+ if (cc.exitCode !== 0 || !cc.asmSource)
53
+ return { ok: false, binary: null, log, exitCode: cc.exitCode || 1, stage: `cc1 (${cName})`, ...(cc.crash ? { crash: cc.crash } : {}) };
54
+ const as = await runShAs({ source: cc.asmSource });
55
+ log += `--- as (${cName}) ---\n${as.log || "(ok)"}\n`;
56
+ if (as.exitCode !== 0 || !as.object)
57
+ return { ok: false, binary: null, log, exitCode: as.exitCode || 1, stage: `as (${cName})`, ...(as.crash ? { crash: as.crash } : {}) };
58
+ userObjs[cName.replace(/\.c$/i, ".o")] = as.object;
59
+ }
60
+ // raw .s sources too
61
+ for (const sName of Object.keys(sources).filter((n) => /\.(s|asm)$/i.test(n))) {
62
+ const as = await runShAs({ source: sources[sName] });
63
+ log += `--- as (${sName}) ---\n${as.log || "(ok)"}\n`;
64
+ if (as.exitCode !== 0 || !as.object)
65
+ return { ok: false, binary: null, log, exitCode: as.exitCode || 1, stage: `as (${sName})`, ...(as.crash ? { crash: as.crash } : {}) };
66
+ userObjs[sName.replace(/\.(s|asm)$/i, ".o")] = as.object;
67
+ }
68
+
69
+ // crt0 (stack + .bss clear + main()).
70
+ const crt0Src = await readFile(path.join(LIB, "dc-crt0.s"), "utf-8");
71
+ const crt0As = await runShAs({ source: crt0Src });
72
+ log += `--- as (dc-crt0.s) ---\n${crt0As.log || "(ok)"}\n`;
73
+ if (crt0As.exitCode !== 0 || !crt0As.object)
74
+ return { ok: false, binary: null, log, exitCode: crt0As.exitCode || 1, stage: "as (crt0)", ...(crt0As.crash ? { crash: crt0As.crash } : {}) };
75
+
76
+ // link: crt0 + user objects + newlib (libc/libm) + libgcc.
77
+ const linkScript = await readFile(path.join(LIB, "dc.ld"), "utf-8");
78
+ const [libc, libm, libgcc] = await Promise.all([
79
+ readFile(path.join(LIB, "libc.a")),
80
+ readFile(path.join(LIB, "libm.a")),
81
+ readFile(path.join(LIB, "libgcc.a")),
82
+ ]);
83
+ const ld = await runShLd({
84
+ objects: { "crt0.o": crt0As.object, ...userObjs },
85
+ linkScript,
86
+ archives: {
87
+ "libc.a": new Uint8Array(libc),
88
+ "libm.a": new Uint8Array(libm),
89
+ "libgcc.a": new Uint8Array(libgcc),
90
+ },
91
+ libraries: ["c", "m", "gcc"],
92
+ libraryPaths: ["/work"],
93
+ options: ["--no-warn-rwx-segments"],
94
+ });
95
+ log += `--- ld ---\n${ld.log || "(ok)"}\n`;
96
+ if (ld.exitCode !== 0 || !ld.elf)
97
+ return { ok: false, binary: null, log, exitCode: ld.exitCode || 1, stage: "ld", ...(ld.crash ? { crash: ld.crash } : {}) };
98
+
99
+ // The ELF IS the deliverable — reios boots it directly.
100
+ return { ok: true, binary: ld.elf, log, exitCode: 0, stage: "done", ...(ld.map ? { symbols: ld.map } : {}) };
101
+ }
@@ -0,0 +1,122 @@
1
+ // sh-elf-gcc — WASM toolchain wrappers for Dreamcast (SH-4) C builds.
2
+ //
3
+ // Pipeline (mirrors mips-elf-gcc/gcc.js — gcc-the-driver can't fork/exec under
4
+ // emscripten, so we orchestrate cc1 → as → ld → objcopy through callMain):
5
+ // runCc1sh({source, headers, options}) → SH assembly (.s)
6
+ // runShAs({source, includes}) → .o ELF object
7
+ // runShLd({objects, linkScript, ...}) → linked .elf (+ map)
8
+ // runShObjcopy({elf}) → raw .bin
9
+ //
10
+ // The Dreamcast SH-4 is little-endian, m4-single-only FP. Single endianness — no
11
+ // EL/EB split like MIPS. The staged tool stems keep the createMips* EXPORT_NAMEs
12
+ // the build script reused, but the glue filenames are sh-elf-*.
13
+
14
+ import { fileURLToPath } from "node:url";
15
+ import { existsSync } from "node:fs";
16
+ import path from "node:path";
17
+
18
+ import { runIsolated, textFile, binaryFile, getOutputBytes, getOutputText } from "../_worker/run.js";
19
+
20
+ const __filename = fileURLToPath(import.meta.url);
21
+ const __dirname = path.dirname(__filename);
22
+
23
+ // The WASM ships in romdev-toolchain-sh-gcc; fall back to the in-tree dev copy.
24
+ function resolveShGlue(file) {
25
+ try {
26
+ const u = import.meta.resolve("romdev-toolchain-sh-gcc");
27
+ const p = path.join(path.dirname(fileURLToPath(u)), "wasm", file);
28
+ if (existsSync(p)) return p;
29
+ } catch { /* not resolvable — fall through */ }
30
+ const local = path.join(__dirname, "wasm", file);
31
+ if (existsSync(local)) return local;
32
+ throw new Error(`sh-elf-gcc WASM (${file}) not found — build it with scripts/build-sh-wasm-tools.sh`);
33
+ }
34
+ const _glue = {};
35
+ const shGlue = (file) => (_glue[file] ??= resolveShGlue(file));
36
+
37
+ // SH-4 little-endian, m4-single-only FP. cc1 wants -ml; as wants -little --isa=sh4.
38
+ const CC1_ARCH = ["-ml", "-m4-single-only"];
39
+ const AS_ARCH = ["-little", "--isa=sh4"];
40
+
41
+ // ── cc1 — SH gcc C frontend, source → assembly ───────────────────────
42
+ export async function runCc1sh(args) {
43
+ const { source, options = [] } = args;
44
+ const headers = args.headers ?? {};
45
+ const inputFiles = [textFile("/work/main.c", source)];
46
+ for (const [name, content] of Object.entries(headers)) inputFiles.push(textFile("/work/" + name, content));
47
+ const argv = [
48
+ ...CC1_ARCH,
49
+ "-iquote", "/work", "-I", "/work",
50
+ ...options,
51
+ "/work/main.c", "-o", "/work/main.s",
52
+ ];
53
+ const r = await runIsolated({
54
+ gluePath: shGlue("cc1.mjs"),
55
+ argv, inputFiles,
56
+ outputFiles: [{ vfsPath: "/work/main.s", encoding: "utf8" }],
57
+ });
58
+ return { log: r.log, exitCode: r.exitCode, asmSource: getOutputText(r, "/work/main.s") || null,
59
+ ...(r.crash ? { crash: r.crash, stage: "crash" } : {}) };
60
+ }
61
+
62
+ // ── sh-elf-as — GNU assembler, .s → .o ───────────────────────────────
63
+ export async function runShAs(args) {
64
+ const { source, options = [] } = args;
65
+ const includes = args.includes ?? {};
66
+ const binaryIncludes = args.binaryIncludes ?? {};
67
+ const inputFiles = [textFile("/work/main.s", source)];
68
+ for (const [name, content] of Object.entries(includes)) inputFiles.push(textFile("/work/" + name, content));
69
+ for (const [name, bytes] of Object.entries(binaryIncludes)) inputFiles.push(binaryFile("/work/" + name, bytes));
70
+ const argv = [...AS_ARCH, "-I", "/work", ...options, "/work/main.s", "-o", "/work/main.o"];
71
+ const r = await runIsolated({
72
+ gluePath: shGlue("sh-elf-as.mjs"),
73
+ argv, inputFiles,
74
+ outputFiles: [{ vfsPath: "/work/main.o", encoding: "base64" }],
75
+ });
76
+ return { log: r.log, exitCode: r.exitCode, object: getOutputBytes(r, "/work/main.o"),
77
+ ...(r.crash ? { crash: r.crash, stage: "crash" } : {}) };
78
+ }
79
+
80
+ // ── sh-elf-ld — GNU linker, .o + linker script → .elf ────────────────
81
+ export async function runShLd(args) {
82
+ const { objects, linkScript, libraries = [], libraryPaths = [], options = [] } = args;
83
+ const archives = args.archives ?? {};
84
+ const inputFiles = [textFile("/work/link.ld", linkScript)];
85
+ for (const [name, bytes] of Object.entries(objects)) inputFiles.push(binaryFile("/work/" + name, bytes));
86
+ for (const [name, bytes] of Object.entries(archives)) inputFiles.push(binaryFile("/work/" + name, bytes));
87
+ const argv = [
88
+ "-EL",
89
+ "-T", "/work/link.ld",
90
+ "-o", "/work/main.elf",
91
+ "-Map=/work/main.map",
92
+ ...libraryPaths.flatMap((p) => ["-L", p]),
93
+ ...Object.keys(objects).map((n) => "/work/" + n),
94
+ ...libraries.map((l) => `-l${l}`),
95
+ ...options,
96
+ ];
97
+ const r = await runIsolated({
98
+ gluePath: shGlue("sh-elf-ld.mjs"),
99
+ argv, inputFiles,
100
+ outputFiles: [
101
+ { vfsPath: "/work/main.elf", encoding: "base64" },
102
+ { vfsPath: "/work/main.map", encoding: "utf8" },
103
+ ],
104
+ });
105
+ return { log: r.log, exitCode: r.exitCode, elf: getOutputBytes(r, "/work/main.elf"),
106
+ map: getOutputText(r, "/work/main.map") || null,
107
+ ...(r.crash ? { crash: r.crash, stage: "crash" } : {}) };
108
+ }
109
+
110
+ // ── sh-elf-objcopy — ELF → raw .bin ──────────────────────────────────
111
+ export async function runShObjcopy(args) {
112
+ const { elf, options = [] } = args;
113
+ const inputFiles = [binaryFile("/work/main.elf", elf)];
114
+ const argv = ["-O", "binary", ...options, "/work/main.elf", "/work/main.bin"];
115
+ const r = await runIsolated({
116
+ gluePath: shGlue("sh-elf-objcopy.mjs"),
117
+ argv, inputFiles,
118
+ outputFiles: [{ vfsPath: "/work/main.bin", encoding: "base64" }],
119
+ });
120
+ return { log: r.log, exitCode: r.exitCode, binary: getOutputBytes(r, "/work/main.bin"),
121
+ ...(r.crash ? { crash: r.crash, stage: "crash" } : {}) };
122
+ }