romdevtools 0.71.1 → 0.84.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 (110) hide show
  1. package/AGENTS.md +19 -16
  2. package/CHANGELOG.md +175 -0
  3. package/README.md +5 -5
  4. package/examples/README.md +1 -0
  5. package/examples/gametank/templates/gt_draw.h +91 -0
  6. package/examples/gametank/templates/gt_hud.h +89 -0
  7. package/examples/gametank/templates/gt_palette.h +59 -0
  8. package/examples/gametank/templates/gt_sound.h +90 -0
  9. package/examples/gametank/templates/gt_sprites.h +53 -0
  10. package/examples/gametank/templates/platformer.c +318 -0
  11. package/examples/gametank/templates/puzzle.c +295 -0
  12. package/examples/gametank/templates/racing.c +139 -0
  13. package/examples/gametank/templates/shmup.c +277 -0
  14. package/examples/gametank/templates/sports.c +119 -0
  15. package/package.json +23 -22
  16. package/src/cores/capabilities.js +24 -0
  17. package/src/cores/registry.js +7 -1
  18. package/src/host/LibretroHost.js +90 -17
  19. package/src/host/callbacks.js +20 -2
  20. package/src/host/cpu-state.js +17 -0
  21. package/src/host/gametank-acp-state.js +53 -0
  22. package/src/http/skill-doc.js +10 -6
  23. package/src/mcp/tools/audio.js +1 -1
  24. package/src/mcp/tools/cart-parts.js +76 -0
  25. package/src/mcp/tools/lifecycle.js +1 -1
  26. package/src/mcp/tools/platform-tools.js +9 -1
  27. package/src/platforms/gametank/lib/gt/audio/acp_image.bin +0 -0
  28. package/src/platforms/gametank/lib/gt/audio/audio_coprocessor.c +55 -0
  29. package/src/platforms/gametank/lib/gt/audio/audio_coprocessor.h +34 -0
  30. package/src/platforms/gametank/lib/gt/audio/audio_fw.asm +223 -0
  31. package/src/platforms/gametank/lib/gt/audio/audio_fw_blob.s +8 -0
  32. package/src/platforms/gametank/lib/gt/audio/instruments.c +79 -0
  33. package/src/platforms/gametank/lib/gt/audio/instruments.h +25 -0
  34. package/src/platforms/gametank/lib/gt/audio/music.c +370 -0
  35. package/src/platforms/gametank/lib/gt/audio/music.h +35 -0
  36. package/src/platforms/gametank/lib/gt/audio/note_numbers.h +132 -0
  37. package/src/platforms/gametank/lib/gt/audio/scaled_sines.raw +0 -0
  38. package/src/platforms/gametank/lib/gt/audio/sine_256_-63_63.bin +0 -0
  39. package/src/platforms/gametank/lib/gt/banking.c +29 -0
  40. package/src/platforms/gametank/lib/gt/banking.h +8 -0
  41. package/src/platforms/gametank/lib/gt/banking2.s +41 -0
  42. package/src/platforms/gametank/lib/gt/crt0.s +102 -0
  43. package/src/platforms/gametank/lib/gt/draw_logo.s +55 -0
  44. package/src/platforms/gametank/lib/gt/feature/persist/persist.c +103 -0
  45. package/src/platforms/gametank/lib/gt/feature/persist/persist.h +13 -0
  46. package/src/platforms/gametank/lib/gt/feature/random/random.c +40 -0
  47. package/src/platforms/gametank/lib/gt/feature/random/random.h +18 -0
  48. package/src/platforms/gametank/lib/gt/feature/text/text.c +111 -0
  49. package/src/platforms/gametank/lib/gt/feature/text/text.h +25 -0
  50. package/src/platforms/gametank/lib/gt/gametank.c +12 -0
  51. package/src/platforms/gametank/lib/gt/gametank.h +80 -0
  52. package/src/platforms/gametank/lib/gt/gametank_logo.inc +393 -0
  53. package/src/platforms/gametank/lib/gt/gen/assets/assets_index.h +9 -0
  54. package/src/platforms/gametank/lib/gt/gen/assets/sdk_default.h +5 -0
  55. package/src/platforms/gametank/lib/gt/gen/bank_nums.h +11 -0
  56. package/src/platforms/gametank/lib/gt/gen/modules_enabled.h +4 -0
  57. package/src/platforms/gametank/lib/gt/gen/modules_enabled.inc +4 -0
  58. package/src/platforms/gametank/lib/gt/gfx/draw_direct.c +132 -0
  59. package/src/platforms/gametank/lib/gt/gfx/draw_direct.h +75 -0
  60. package/src/platforms/gametank/lib/gt/gfx/draw_queue.c +177 -0
  61. package/src/platforms/gametank/lib/gt/gfx/draw_queue.h +35 -0
  62. package/src/platforms/gametank/lib/gt/gfx/draw_util.s +157 -0
  63. package/src/platforms/gametank/lib/gt/gfx/gfx_sys.c +43 -0
  64. package/src/platforms/gametank/lib/gt/gfx/gfx_sys.h +46 -0
  65. package/src/platforms/gametank/lib/gt/gfx/sprites.c +216 -0
  66. package/src/platforms/gametank/lib/gt/gfx/sprites.h +41 -0
  67. package/src/platforms/gametank/lib/gt/init.c +9 -0
  68. package/src/platforms/gametank/lib/gt/input.c +26 -0
  69. package/src/platforms/gametank/lib/gt/input.h +20 -0
  70. package/src/platforms/gametank/lib/gt/interrupt.s +67 -0
  71. package/src/platforms/gametank/lib/gt/vectors.s +14 -0
  72. package/src/platforms/gametank/lib/gt/wait.s +53 -0
  73. package/src/playtest/playtest.js +174 -26
  74. package/src/playtest/resampler/build.sh +19 -0
  75. package/src/playtest/resampler/index.mjs +75 -0
  76. package/src/playtest/resampler/resampler.c +129 -0
  77. package/src/playtest/resampler/resampler.mjs +2 -0
  78. package/src/playtest/resampler/resampler.wasm +0 -0
  79. package/src/toolchains/arm-none-eabi-gcc/gcc.js +39 -188
  80. package/src/toolchains/asar/asar.js +10 -15
  81. package/src/toolchains/cc65/cc65.js +82 -92
  82. package/src/toolchains/cc65/da65.js +12 -17
  83. package/src/toolchains/cc65/preset-resolver.js +13 -2
  84. package/src/toolchains/cc65/presets/gametank/gametank.h +80 -0
  85. package/src/toolchains/cc65/presets/gametank/sdk.cfg +32 -0
  86. package/src/toolchains/cc65/presets/gametank/sdk.crt0.s +61 -0
  87. package/src/toolchains/cc65/presets/gametank/sdk.vectors.s +11 -0
  88. package/src/toolchains/cc65/presets/gametank/single-bank.cfg +28 -0
  89. package/src/toolchains/cc65/presets/gametank/single-bank.crt0.s +71 -0
  90. package/src/toolchains/cc65/presets/gametank/single-bank.vectors.s +12 -0
  91. package/src/toolchains/common/c-build.js +109 -0
  92. package/src/toolchains/common/gcc-toolchain.js +164 -0
  93. package/src/toolchains/common/wasm-tool.js +101 -0
  94. package/src/toolchains/dasm/dasm.js +12 -18
  95. package/src/toolchains/gba-c/gba-c.js +253 -305
  96. package/src/toolchains/genesis-c/genesis-c.js +196 -225
  97. package/src/toolchains/index.js +58 -4
  98. package/src/toolchains/m68k-elf-gcc/gcc.js +39 -202
  99. package/src/toolchains/mips-c/mips-c.js +68 -78
  100. package/src/toolchains/mips-elf-gcc/gcc.js +55 -118
  101. package/src/toolchains/rgbds/rgbds.js +7 -19
  102. package/src/toolchains/sdcc/sdcc.js +35 -26
  103. package/src/toolchains/sh-c/sh-c.js +44 -52
  104. package/src/toolchains/sh-elf-gcc/gcc.js +55 -110
  105. package/src/toolchains/sjasm/sjasm.js +10 -14
  106. package/src/toolchains/snes-c/snes-c.js +125 -150
  107. package/src/toolchains/tcc816/tcc816.js +10 -15
  108. package/src/toolchains/vasm68k/vasm68k.js +11 -16
  109. package/src/toolchains/wladx/wladx.js +5 -17
  110. package/src/toolchains/z80/binutils.js +5 -11
@@ -2,229 +2,66 @@
2
2
  //
3
3
  // The full pipeline:
4
4
  // runCc1m68k({source, headers, options}) → m68k assembly text (.s)
5
- // runM68kAs({asmSource, includes}) → .o ELF object
6
- // runM68kLd({objects, linkScript}) → linked .elf
7
- // runM68kObjcopy({elf}) → raw .bin Genesis ROM
5
+ // runM68kAs({source, includes}) → .o ELF object
6
+ // runM68kLd({objects, linkScript}) → linked .elf (+ map)
7
+ // runM68kObjcopy({elf}) → raw .bin Genesis ROM
8
8
  //
9
- // Each step runs as a WASM module through our worker pool (R12
10
- // subprocess isolation), same pattern as wladx.js / tcc816.js. The JS
11
- // glue replaces gcc's normal driver gcc-the-driver spawns cc1/as/ld
12
- // as subprocesses via fork/exec, which emscripten can't do. We don't
13
- // need it: this file orchestrates the same steps through callMain.
14
-
9
+ // 0.81.0: the 4 stages are now produced by the shared makeGccToolchain() factory
10
+ // (common/gcc-toolchain.js) every arch's cc1/as/ld/objcopy share the same
11
+ // input-marshalling + runIsolated + output-decoding; this file is just the m68k
12
+ // CONFIG (glue names, arch flags, link-script name, ROM extension) + thin named
13
+ // re-exports so existing call sites keep their runM68k* names.
14
+ //
15
+ // The WASM glue ships in romdev-toolchain-m68k-gcc (20MB); resolution is lazy +
16
+ // memoized, so booting the server never loads it unless a Genesis C ROM is built.
15
17
  import { fileURLToPath } from "node:url";
16
- import { existsSync } from "node:fs";
17
18
  import path from "node:path";
18
19
 
19
- import { runIsolated, textFile, binaryFile, getOutputBytes, getOutputText } from "../_worker/run.js";
20
-
21
- const __filename = fileURLToPath(import.meta.url);
22
- const __dirname = path.dirname(__filename);
20
+ import { makeGccToolchain } from "../common/gcc-toolchain.js";
23
21
 
24
- // m68k-elf-gcc's WASM ships in romdev-toolchain-m68k-gcc. Resolve each tool's
25
- // glue from that package; fall back to a local copy under src/ if present
26
- // (transition / dev). emcc's `EXPORT_ES6=1` output is ESM (uses
27
- // import.meta.url + dynamic require), so the glue uses .mjs extensions. The
28
- // package is a hard dep of romdev.
29
- function resolveM68kGlue(file) {
30
- try {
31
- const u = import.meta.resolve("romdev-toolchain-m68k-gcc");
32
- const p = path.join(path.dirname(fileURLToPath(u)), "wasm", file);
33
- if (existsSync(p)) return p;
34
- } catch { /* package not resolvable — fall through to local */ }
35
- const local = path.join(__dirname, "wasm", file);
36
- if (existsSync(local)) return local;
37
- throw new Error(`m68k-elf-gcc WASM (${file}) not found — install romdev-toolchain-m68k-gcc`);
38
- }
22
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
39
23
 
40
- // Lazy + memoized per tool: resolve only on the first Genesis-C build that uses
41
- // each tool, not at module load — so booting the server never touches this
42
- // (20MB) package unless a Genesis C ROM is actually built.
43
- const _glue = {};
44
- const m68kGlue = (file) => (_glue[file] ??= resolveM68kGlue(file));
45
-
46
- // ── cc1 — m68k gcc C frontend, source → assembly ─────────────────────
47
- //
48
- // Canonical gcc invocation pattern (what `gcc -S` does internally):
49
- // cc1 -quiet <source.c> -mcpu=68000 -O<n> -o <out.s>
50
- //
51
- // We strip the -quiet so the agent sees diagnostics, and add a fixed
52
- // `-mcpu=68000` since Genesis is always 68000 (no 68020+).
24
+ const { runCc1, runAs, runLd, runObjcopy } = makeGccToolchain({
25
+ pkg: "romdev-toolchain-m68k-gcc",
26
+ localDir: __dirname,
27
+ label: "m68k-elf-gcc",
28
+ glue: {
29
+ cc1: "cc1-m68k.mjs",
30
+ as: "m68k-elf-as.mjs",
31
+ ld: "m68k-elf-ld.mjs",
32
+ objcopy: "m68k-elf-objcopy.mjs",
33
+ },
34
+ // Genesis is always a bare 68000 (no 68020+).
35
+ cc1Flags: ["-mcpu=68000"],
36
+ asFlags: ["-march=68000"],
37
+ ldScriptName: "genesis.ld",
38
+ outputName: "main.bin",
39
+ });
53
40
 
54
41
  /**
55
42
  * Compile a C source to m68k assembly via cc1.
56
- *
57
- * @param {Object} args
58
- * @param {string} args.source main C source text
59
- * @param {Record<string, string>} [args.headers] virtual headers visible via -I/work
60
- * @param {string[]} [args.options] extra cc1 flags (e.g. ['-O2', '-Wall'])
43
+ * @param {{source:string, headers?:Record<string,string>, options?:string[]}} args
61
44
  * @returns {Promise<{log:string, exitCode:number, asmSource:string|null, crash?:any}>}
62
45
  */
63
- export async function runCc1m68k(args) {
64
- const { source, options = [] } = args;
65
- const headers = args.headers ?? {};
66
- /** @type {import("../_worker/run.js").InputFile[]} */
67
- const inputFiles = [textFile("/work/main.c", source)];
68
- for (const [name, content] of Object.entries(headers)) {
69
- inputFiles.push(textFile("/work/" + name, content));
70
- }
71
- const argv = [
72
- "-mcpu=68000",
73
- "-iquote", "/work",
74
- "-I", "/work", // also handle #include <...> form (SGDK headers etc.)
75
- ...options,
76
- "/work/main.c",
77
- "-o", "/work/main.s",
78
- ];
79
- const r = await runIsolated({
80
- gluePath: m68kGlue("cc1-m68k.mjs"),
81
- argv,
82
- inputFiles,
83
- outputFiles: [{ vfsPath: "/work/main.s", encoding: "utf8" }],
84
- });
85
- return {
86
- log: r.log,
87
- exitCode: r.exitCode,
88
- asmSource: getOutputText(r, "/work/main.s") || null,
89
- ...(r.crash ? { crash: r.crash, stage: "crash" } : {}),
90
- };
91
- }
92
-
93
- // ── m68k-elf-as — GNU assembler, .s → .o ─────────────────────────────
46
+ export const runCc1m68k = runCc1;
94
47
 
95
48
  /**
96
49
  * Assemble m68k assembly with m68k-elf-as.
97
- *
98
- * @param {Object} args
99
- * @param {string} args.source assembly source text
100
- * @param {Record<string, string>} [args.includes] sibling .s/.inc files (text)
101
- * @param {Record<string, Uint8Array>} [args.binaryIncludes] sibling binary files
102
- * the .s `.incbin`s — typically a rom_header.bin or similar pre-computed blob.
103
- * Mounted into MEMFS verbatim at the given path.
104
- * @param {string[]} [args.options]
50
+ * @param {{source:string, includes?:Record<string,string>, binaryIncludes?:Record<string,Uint8Array>, options?:string[]}} args
105
51
  * @returns {Promise<{log:string, exitCode:number, object:Uint8Array|null, crash?:any}>}
106
52
  */
107
- export async function runM68kAs(args) {
108
- const { source, options = [] } = args;
109
- const includes = args.includes ?? {};
110
- const binaryIncludes = args.binaryIncludes ?? {};
111
- /** @type {import("../_worker/run.js").InputFile[]} */
112
- const inputFiles = [textFile("/work/main.s", source)];
113
- for (const [name, content] of Object.entries(includes)) {
114
- inputFiles.push(textFile("/work/" + name, content));
115
- }
116
- for (const [name, bytes] of Object.entries(binaryIncludes)) {
117
- inputFiles.push(binaryFile("/work/" + name, bytes));
118
- }
119
- const argv = [
120
- "-march=68000",
121
- "-I", "/work",
122
- ...options,
123
- "/work/main.s",
124
- "-o", "/work/main.o",
125
- ];
126
- const r = await runIsolated({
127
- gluePath: m68kGlue("m68k-elf-as.mjs"),
128
- argv,
129
- inputFiles,
130
- outputFiles: [{ vfsPath: "/work/main.o", encoding: "base64" }],
131
- });
132
- return {
133
- log: r.log,
134
- exitCode: r.exitCode,
135
- object: getOutputBytes(r, "/work/main.o"),
136
- ...(r.crash ? { crash: r.crash, stage: "crash" } : {}),
137
- };
138
- }
139
-
140
- // ── m68k-elf-ld — GNU linker, .o + linker script → .elf ──────────────
53
+ export const runM68kAs = runAs;
141
54
 
142
55
  /**
143
- * Link m68k object files into an ELF executable.
144
- *
145
- * @param {Object} args
146
- * @param {Record<string, Uint8Array>} args.objects name → .o bytes
147
- * @param {string} args.linkScript ld linker script contents (genesis.ld)
148
- * @param {string[]} [args.libraryPaths] paths to search for archives
149
- * @param {string[]} [args.libraries] -l<name> archives (e.g. ['gcc', 'c', 'md'])
150
- * @param {Record<string, Uint8Array>} [args.archives] additional .a archives to ship
151
- * into MEMFS (e.g. {'libgcc.a': bytes, 'libmd.a': bytes})
152
- * @param {string[]} [args.options] extra ld flags
153
- * @returns {Promise<{log:string, exitCode:number, elf:Uint8Array|null, crash?:any}>}
56
+ * Link m68k object files into an ELF executable (+ linker map).
57
+ * @param {{objects:Record<string,Uint8Array>, linkScript:string, libraries?:string[], libraryPaths?:string[], archives?:Record<string,Uint8Array>, options?:string[]}} args
58
+ * @returns {Promise<{log:string, exitCode:number, elf:Uint8Array|null, map:string|null, crash?:any}>}
154
59
  */
155
- export async function runM68kLd(args) {
156
- const { objects, linkScript, libraries = [], libraryPaths = [], options = [] } = args;
157
- const archives = args.archives ?? {};
158
- /** @type {import("../_worker/run.js").InputFile[]} */
159
- const inputFiles = [textFile("/work/genesis.ld", linkScript)];
160
- for (const [name, bytes] of Object.entries(objects)) {
161
- inputFiles.push(binaryFile("/work/" + name, bytes));
162
- }
163
- for (const [name, bytes] of Object.entries(archives)) {
164
- inputFiles.push(binaryFile("/work/" + name, bytes));
165
- }
166
- const argv = [
167
- "-T", "/work/genesis.ld",
168
- "-o", "/work/main.elf",
169
- // Emit a linker map: it lists every symbol's final address (`0xADDR symbol`
170
- // style), which is how an agent finds where a RAM variable like `well[][]`
171
- // landed so it can writeMemory to it. Cheap to always produce.
172
- "-Map=/work/main.map",
173
- ...libraryPaths.flatMap((p) => ["-L", p]),
174
- ...Object.keys(objects).map((n) => "/work/" + n),
175
- ...libraries.map((l) => `-l${l}`),
176
- ...options,
177
- ];
178
- const r = await runIsolated({
179
- gluePath: m68kGlue("m68k-elf-ld.mjs"),
180
- argv,
181
- inputFiles,
182
- outputFiles: [
183
- { vfsPath: "/work/main.elf", encoding: "base64" },
184
- { vfsPath: "/work/main.map", encoding: "utf8" },
185
- ],
186
- });
187
- return {
188
- log: r.log,
189
- exitCode: r.exitCode,
190
- elf: getOutputBytes(r, "/work/main.elf"),
191
- // The map is present on a successful link; absent → "" (link failed, so
192
- // the caller is looking at exitCode/log anyway).
193
- map: getOutputText(r, "/work/main.map") || null,
194
- ...(r.crash ? { crash: r.crash, stage: "crash" } : {}),
195
- };
196
- }
197
-
198
- // ── m68k-elf-objcopy — extract raw Genesis ROM from ELF ──────────────
60
+ export const runM68kLd = runLd;
199
61
 
200
62
  /**
201
63
  * Strip an ELF down to a raw .bin (the Genesis ROM format).
202
- *
203
- * @param {Object} args
204
- * @param {Uint8Array} args.elf input ELF bytes
205
- * @param {string[]} [args.options] extra objcopy flags
64
+ * @param {{elf:Uint8Array, options?:string[]}} args
206
65
  * @returns {Promise<{log:string, exitCode:number, binary:Uint8Array|null, crash?:any}>}
207
66
  */
208
- export async function runM68kObjcopy(args) {
209
- const { elf, options = [] } = args;
210
- /** @type {import("../_worker/run.js").InputFile[]} */
211
- const inputFiles = [binaryFile("/work/main.elf", elf)];
212
- const argv = [
213
- "-O", "binary",
214
- ...options,
215
- "/work/main.elf",
216
- "/work/main.bin",
217
- ];
218
- const r = await runIsolated({
219
- gluePath: m68kGlue("m68k-elf-objcopy.mjs"),
220
- argv,
221
- inputFiles,
222
- outputFiles: [{ vfsPath: "/work/main.bin", encoding: "base64" }],
223
- });
224
- return {
225
- log: r.log,
226
- exitCode: r.exitCode,
227
- binary: getOutputBytes(r, "/work/main.bin"),
228
- ...(r.crash ? { crash: r.crash, stage: "crash" } : {}),
229
- };
230
- }
67
+ export const runM68kObjcopy = runObjcopy;
@@ -13,6 +13,7 @@ import { readFile } from "node:fs/promises";
13
13
  import path from "node:path";
14
14
 
15
15
  import { runCc1mips, runMipsAs, runMipsLd, runMipsObjcopy } from "../mips-elf-gcc/gcc.js";
16
+ import { CBuild, BuildError } from "../common/c-build.js";
16
17
 
17
18
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
18
19
  const LIB = path.join(__dirname, "lib");
@@ -101,89 +102,78 @@ export async function buildMipsC(args) {
101
102
  const callerHasC = (args.sources && (args.sources[`${helperName}.c`] != null));
102
103
  if (!callerHasC) autoHelperSrc = await readFile(cPath, "utf-8").catch(() => null);
103
104
  }
104
- let log = "";
105
+ const cb = new CBuild();
106
+ // bound stage runners (endian + opts fixed) so the per-source loops stay terse
107
+ const cc1 = (source) => runCc1mips({ source, headers, options: cc1Options, endian });
108
+ const as = (source) => runMipsAs({ source, endian });
105
109
 
106
- /** @type {Record<string, Uint8Array>} */
107
- const userObjs = {};
108
- for (const cName of Object.keys(sources).filter((n) => /\.c$/i.test(n))) {
109
- const cc = await runCc1mips({ source: sources[cName], headers, options: cc1Options, endian });
110
- log += `--- cc1 (${cName}) ---\n${cc.log || "(ok)"}\n`;
111
- if (cc.exitCode !== 0 || !cc.asmSource) return { ok: false, binary: null, log, exitCode: cc.exitCode || 1, stage: `cc1 (${cName})`, ...(cc.crash ? { crash: cc.crash } : {}) };
112
- const as = await runMipsAs({ source: cc.asmSource, endian });
113
- log += `--- as (${cName}) ---\n${as.log || "(ok)"}\n`;
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 } : {}) };
115
- userObjs[cName.replace(/\.c$/i, ".o")] = as.object;
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
- }
110
+ try {
111
+ /** @type {Record<string, Uint8Array>} */
112
+ const userObjs = {};
113
+ for (const cName of Object.keys(sources).filter((n) => /\.c$/i.test(n))) {
114
+ const cc = await cb.stage(`cc1 (${cName})`, () => cc1(sources[cName]), (r) => r.asmSource);
115
+ const ao = await cb.stage(`as (${cName})`, () => as(cc.asmSource), (r) => r.object);
116
+ userObjs[cName.replace(/\.c$/i, ".o")] = ao.object;
117
+ }
118
+ // Auto-bundled helper .c (n64.c / psx.c) compiled with the SAME headers so it can
119
+ // see its own header, linked alongside the user objects. Skipped when the caller
120
+ // supplied their own helper .c (callerHasC) above.
121
+ if (autoHelperSrc != null) {
122
+ const cc = await cb.stage(`cc1 (${helperName}.c bundled)`, () => cc1(autoHelperSrc), (r) => r.asmSource);
123
+ const ao = await cb.stage(`as (${helperName}.c bundled)`, () => as(cc.asmSource), (r) => r.object);
124
+ userObjs[`${helperName}.o`] = ao.object;
125
+ }
129
126
 
130
- // raw .s sources too
131
- for (const sName of Object.keys(sources).filter((n) => /\.(s|asm)$/i.test(n))) {
132
- const as = await runMipsAs({ source: sources[sName], endian });
133
- log += `--- as (${sName}) ---\n${as.log || "(ok)"}\n`;
134
- if (as.exitCode !== 0 || !as.object) return { ok: false, binary: null, log, exitCode: as.exitCode || 1, stage: `as (${sName})`, ...(as.crash ? { crash: as.crash } : {}) };
135
- userObjs[sName.replace(/\.(s|asm)$/i, ".o")] = as.object;
136
- }
127
+ // raw .s sources too
128
+ for (const sName of Object.keys(sources).filter((n) => /\.(s|asm)$/i.test(n))) {
129
+ const ao = await cb.stage(`as (${sName})`, () => as(sources[sName]), (r) => r.object);
130
+ userObjs[sName.replace(/\.(s|asm)$/i, ".o")] = ao.object;
131
+ }
137
132
 
138
- // crt0 (per platform)
139
- const crt0Name = platform === "ps1" ? "ps1-crt0.s" : "n64-crt0.s";
140
- const crt0Src = await readFile(path.join(LIB, crt0Name), "utf-8");
141
- const crt0As = await runMipsAs({ source: crt0Src, endian });
142
- log += `--- as (${crt0Name}) ---\n${crt0As.log || "(ok)"}\n`;
143
- if (crt0As.exitCode !== 0 || !crt0As.object) return { ok: false, binary: null, log, exitCode: crt0As.exitCode || 1, stage: "as (crt0)", ...(crt0As.crash ? { crash: crt0As.crash } : {}) };
133
+ // crt0 (per platform)
134
+ const crt0Name = platform === "ps1" ? "ps1-crt0.s" : "n64-crt0.s";
135
+ const crt0Src = await readFile(path.join(LIB, crt0Name), "utf-8");
136
+ const crt0As = await cb.stage(`as (${crt0Name})`, () => as(crt0Src), (r) => r.object);
144
137
 
145
- // softint.c — the few libgcc helpers (64-bit divide/mod) in plain C, so the
146
- // link doesn't need an endian-specific libgcc.a (the EL libgcc isn't bundled).
147
- const softSrc = await readFile(path.join(LIB, "softint.c"), "utf-8");
148
- const softCc = await runCc1mips({ source: softSrc, options: cc1Options, endian });
149
- const softAs = softCc.asmSource ? await runMipsAs({ source: softCc.asmSource, endian }) : { exitCode: 1 };
150
- if (softCc.exitCode !== 0 || softAs.exitCode !== 0 || !softAs.object) {
151
- return { ok: false, binary: null, log: log + (softCc.log || "") + (softAs.log || ""), exitCode: 1, stage: "softint" };
152
- }
138
+ // softint.c — the few libgcc helpers (64-bit divide/mod) in plain C, so the
139
+ // link doesn't need an endian-specific libgcc.a (the EL libgcc isn't bundled).
140
+ const softSrc = await readFile(path.join(LIB, "softint.c"), "utf-8");
141
+ const softCc = await cb.stage("cc1 (softint.c)", () => cc1(softSrc), (r) => r.asmSource);
142
+ const softAs = await cb.stage("as (softint.c)", () => as(softCc.asmSource), (r) => r.object);
153
143
 
154
- // link
155
- const ldName = platform === "ps1" ? "ps1.ld" : "n64.ld";
156
- const linkScript = await readFile(path.join(LIB, ldName), "utf-8");
157
- // newlib + libgcc are endian-specific: el/ (PS1 little) vs be/ (N64 big). libgcc
158
- // is only bundled for be/ — softint.c covers the EL case, so libgcc is optional.
159
- const libDir = path.join(LIB, endian === "little" ? "el" : "be");
160
- const [libc, libm] = await Promise.all([
161
- readFile(path.join(libDir, "libc.a")), readFile(path.join(libDir, "libm.a")),
162
- ]);
163
- const archives = { "libc.a": new Uint8Array(libc), "libm.a": new Uint8Array(libm) };
164
- const libraries = ["c", "m"];
165
- try {
166
- const libgcc = await readFile(path.join(libDir, "libgcc.a"));
167
- archives["libgcc.a"] = new Uint8Array(libgcc);
168
- libraries.unshift("gcc");
169
- } catch { /* no endian libgcc — softint.c provides the needed helpers */ }
170
- const ld = await runMipsLd({
171
- objects: { "crt0.o": crt0As.object, "softint.o": softAs.object, ...userObjs },
172
- linkScript, endian,
173
- archives,
174
- libraries,
175
- libraryPaths: ["/work"],
176
- options: ["--no-warn-rwx-segments"],
177
- });
178
- log += `--- ld ---\n${ld.log || "(ok)"}\n`;
179
- if (ld.exitCode !== 0 || !ld.elf) return { ok: false, binary: null, log, exitCode: ld.exitCode || 1, stage: "ld", ...(ld.crash ? { crash: ld.crash } : {}) };
144
+ // link
145
+ const ldName = platform === "ps1" ? "ps1.ld" : "n64.ld";
146
+ const linkScript = await readFile(path.join(LIB, ldName), "utf-8");
147
+ // newlib + libgcc are endian-specific: el/ (PS1 little) vs be/ (N64 big). libgcc
148
+ // is only bundled for be/ — softint.c covers the EL case, so libgcc is optional.
149
+ const libDir = path.join(LIB, endian === "little" ? "el" : "be");
150
+ const [libc, libm] = await Promise.all([
151
+ readFile(path.join(libDir, "libc.a")), readFile(path.join(libDir, "libm.a")),
152
+ ]);
153
+ const archives = { "libc.a": new Uint8Array(libc), "libm.a": new Uint8Array(libm) };
154
+ const libraries = ["c", "m"];
155
+ try {
156
+ const libgcc = await readFile(path.join(libDir, "libgcc.a"));
157
+ archives["libgcc.a"] = new Uint8Array(libgcc);
158
+ libraries.unshift("gcc");
159
+ } catch { /* no endian libgcc — softint.c provides the needed helpers */ }
160
+ const ld = await cb.stage("ld", () => runMipsLd({
161
+ objects: { "crt0.o": crt0As.object, "softint.o": softAs.object, ...userObjs },
162
+ linkScript, endian,
163
+ archives,
164
+ libraries,
165
+ libraryPaths: ["/work"],
166
+ options: ["--no-warn-rwx-segments"],
167
+ }), (r) => r.elf);
180
168
 
181
- const oc = await runMipsObjcopy({ elf: ld.elf });
182
- log += `--- objcopy ---\n${oc.log || "(ok)"}\n`;
183
- if (oc.exitCode !== 0 || !oc.binary) return { ok: false, binary: null, log, exitCode: oc.exitCode || 1, stage: "objcopy", ...(oc.crash ? { crash: oc.crash } : {}) };
169
+ const oc = await cb.stage("objcopy", () => runMipsObjcopy({ elf: ld.elf }), (r) => r.binary);
184
170
 
185
- const binary = platform === "ps1" ? wrapPsExe(oc.binary)
186
- : platform === "n64" ? wrapN64Rom(oc.binary)
187
- : oc.binary;
188
- return { ok: true, binary, log, exitCode: 0, stage: "done", ...(ld.map ? { symbols: ld.map } : {}) };
171
+ const binary = platform === "ps1" ? wrapPsExe(oc.binary)
172
+ : platform === "n64" ? wrapN64Rom(oc.binary)
173
+ : oc.binary;
174
+ return { ok: true, binary, log: cb.log, exitCode: 0, stage: "done", ...(ld.map ? { symbols: ld.map } : {}) };
175
+ } catch (e) {
176
+ if (e instanceof BuildError) return e.toResult();
177
+ throw e;
178
+ }
189
179
  }
@@ -1,130 +1,67 @@
1
1
  // mips-elf-gcc — WASM toolchain wrappers for N64 / PS1 C builds.
2
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
3
+ // The full pipeline:
4
+ // runCc1mips({source, headers, options, endian})MIPS assembly text (.s)
5
+ // runMipsAs({source, includes, endian}) .o ELF object
6
+ // runMipsLd({objects, linkScript, endian}) linked .elf (+ map)
7
+ // runMipsObjcopy({elf}) raw .bin
9
8
  //
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
-
9
+ // 0.81.0: the 4 stages come from the shared makeGccToolchain() factory
10
+ // (common/gcc-toolchain.js); this file is just the MIPS config + thin re-exports.
11
+ // Unlike the other arches, MIPS is bi-endian (N64 big, PS1 little) — its cc1 and
12
+ // as/ld want DIFFERENT endian flag spellings (cc1: -mel/-meb; as/ld: -EL/-EB), so
13
+ // the flags are functions of `endian` (default "big" for N64). MIPS32, ABI o32, -G0.
14
+ //
15
+ // WASM glue ships in romdev-toolchain-mips-gcc; resolution is lazy + memoized.
13
16
  import { fileURLToPath } from "node:url";
14
- import { existsSync } from "node:fs";
15
17
  import path from "node:path";
16
18
 
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);
19
+ import { makeGccToolchain } from "../common/gcc-toolchain.js";
21
20
 
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));
21
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
35
22
 
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
- }
23
+ const { runCc1, runAs, runLd, runObjcopy } = makeGccToolchain({
24
+ pkg: "romdev-toolchain-mips-gcc",
25
+ localDir: __dirname,
26
+ label: "mips-elf-gcc",
27
+ glue: {
28
+ cc1: "cc1.mjs",
29
+ as: "mips-elf-as.mjs",
30
+ ld: "mips-elf-ld.mjs",
31
+ objcopy: "mips-elf-objcopy.mjs",
32
+ },
33
+ defaultEndian: "big",
34
+ cc1Flags: (endian) => [endian === "little" ? "-mel" : "-meb", "-mabi=32"],
35
+ asFlags: (endian) => [endian === "little" ? "-EL" : "-EB", "-mabi=32", "-G0"],
36
+ ldFlags: (endian) => [endian === "little" ? "-EL" : "-EB"],
37
+ ldScriptName: "link.ld",
38
+ outputName: "main.bin",
39
+ });
48
40
 
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
- }
41
+ /**
42
+ * Compile a C source to MIPS assembly via cc1.
43
+ * @param {{source:string, headers?:Record<string,string>, options?:string[], endian?:"big"|"little"}} args
44
+ * @returns {Promise<{log:string, exitCode:number, asmSource:string|null, crash?:any}>}
45
+ */
46
+ export const runCc1mips = runCc1;
69
47
 
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
- }
48
+ /**
49
+ * Assemble MIPS assembly with mips-elf-as.
50
+ * @param {{source:string, includes?:Record<string,string>, binaryIncludes?:Record<string,Uint8Array>, options?:string[], endian?:"big"|"little"}} args
51
+ * @returns {Promise<{log:string, exitCode:number, object:Uint8Array|null, crash?:any}>}
52
+ */
53
+ export const runMipsAs = runAs;
87
54
 
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
- }
55
+ /**
56
+ * Link MIPS object files into an ELF executable (+ linker map).
57
+ * @param {{objects:Record<string,Uint8Array>, linkScript:string, libraries?:string[], libraryPaths?:string[], archives?:Record<string,Uint8Array>, options?:string[], endian?:"big"|"little"}} args
58
+ * @returns {Promise<{log:string, exitCode:number, elf:Uint8Array|null, map:string|null, crash?:any}>}
59
+ */
60
+ export const runMipsLd = runLd;
117
61
 
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
- }
62
+ /**
63
+ * Strip an ELF down to a raw .bin.
64
+ * @param {{elf:Uint8Array, options?:string[]}} args
65
+ * @returns {Promise<{log:string, exitCode:number, binary:Uint8Array|null, crash?:any}>}
66
+ */
67
+ export const runMipsObjcopy = runObjcopy;