romdevtools 0.71.0 → 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.
- package/AGENTS.md +19 -16
- package/CHANGELOG.md +196 -0
- package/README.md +5 -5
- package/examples/README.md +1 -0
- package/examples/gametank/templates/gt_draw.h +91 -0
- package/examples/gametank/templates/gt_hud.h +89 -0
- package/examples/gametank/templates/gt_palette.h +59 -0
- package/examples/gametank/templates/gt_sound.h +90 -0
- package/examples/gametank/templates/gt_sprites.h +53 -0
- package/examples/gametank/templates/platformer.c +318 -0
- package/examples/gametank/templates/puzzle.c +295 -0
- package/examples/gametank/templates/racing.c +139 -0
- package/examples/gametank/templates/shmup.c +277 -0
- package/examples/gametank/templates/sports.c +119 -0
- package/package.json +23 -22
- package/src/cores/capabilities.js +24 -0
- package/src/cores/registry.js +7 -1
- package/src/host/LibretroHost.js +90 -17
- package/src/host/callbacks.js +20 -2
- package/src/host/cpu-state.js +17 -0
- package/src/host/gametank-acp-state.js +53 -0
- package/src/http/skill-doc.js +10 -6
- package/src/mcp/tools/audio.js +1 -1
- package/src/mcp/tools/cart-parts.js +76 -0
- package/src/mcp/tools/lifecycle.js +1 -1
- package/src/mcp/tools/platform-tools.js +9 -1
- package/src/mcp/tools/platforms.js +46 -10
- package/src/mcp/tools/toolchain.js +35 -10
- package/src/platforms/gametank/lib/gt/audio/acp_image.bin +0 -0
- package/src/platforms/gametank/lib/gt/audio/audio_coprocessor.c +55 -0
- package/src/platforms/gametank/lib/gt/audio/audio_coprocessor.h +34 -0
- package/src/platforms/gametank/lib/gt/audio/audio_fw.asm +223 -0
- package/src/platforms/gametank/lib/gt/audio/audio_fw_blob.s +8 -0
- package/src/platforms/gametank/lib/gt/audio/instruments.c +79 -0
- package/src/platforms/gametank/lib/gt/audio/instruments.h +25 -0
- package/src/platforms/gametank/lib/gt/audio/music.c +370 -0
- package/src/platforms/gametank/lib/gt/audio/music.h +35 -0
- package/src/platforms/gametank/lib/gt/audio/note_numbers.h +132 -0
- package/src/platforms/gametank/lib/gt/audio/scaled_sines.raw +0 -0
- package/src/platforms/gametank/lib/gt/audio/sine_256_-63_63.bin +0 -0
- package/src/platforms/gametank/lib/gt/banking.c +29 -0
- package/src/platforms/gametank/lib/gt/banking.h +8 -0
- package/src/platforms/gametank/lib/gt/banking2.s +41 -0
- package/src/platforms/gametank/lib/gt/crt0.s +102 -0
- package/src/platforms/gametank/lib/gt/draw_logo.s +55 -0
- package/src/platforms/gametank/lib/gt/feature/persist/persist.c +103 -0
- package/src/platforms/gametank/lib/gt/feature/persist/persist.h +13 -0
- package/src/platforms/gametank/lib/gt/feature/random/random.c +40 -0
- package/src/platforms/gametank/lib/gt/feature/random/random.h +18 -0
- package/src/platforms/gametank/lib/gt/feature/text/text.c +111 -0
- package/src/platforms/gametank/lib/gt/feature/text/text.h +25 -0
- package/src/platforms/gametank/lib/gt/gametank.c +12 -0
- package/src/platforms/gametank/lib/gt/gametank.h +80 -0
- package/src/platforms/gametank/lib/gt/gametank_logo.inc +393 -0
- package/src/platforms/gametank/lib/gt/gen/assets/assets_index.h +9 -0
- package/src/platforms/gametank/lib/gt/gen/assets/sdk_default.h +5 -0
- package/src/platforms/gametank/lib/gt/gen/bank_nums.h +11 -0
- package/src/platforms/gametank/lib/gt/gen/modules_enabled.h +4 -0
- package/src/platforms/gametank/lib/gt/gen/modules_enabled.inc +4 -0
- package/src/platforms/gametank/lib/gt/gfx/draw_direct.c +132 -0
- package/src/platforms/gametank/lib/gt/gfx/draw_direct.h +75 -0
- package/src/platforms/gametank/lib/gt/gfx/draw_queue.c +177 -0
- package/src/platforms/gametank/lib/gt/gfx/draw_queue.h +35 -0
- package/src/platforms/gametank/lib/gt/gfx/draw_util.s +157 -0
- package/src/platforms/gametank/lib/gt/gfx/gfx_sys.c +43 -0
- package/src/platforms/gametank/lib/gt/gfx/gfx_sys.h +46 -0
- package/src/platforms/gametank/lib/gt/gfx/sprites.c +216 -0
- package/src/platforms/gametank/lib/gt/gfx/sprites.h +41 -0
- package/src/platforms/gametank/lib/gt/init.c +9 -0
- package/src/platforms/gametank/lib/gt/input.c +26 -0
- package/src/platforms/gametank/lib/gt/input.h +20 -0
- package/src/platforms/gametank/lib/gt/interrupt.s +67 -0
- package/src/platforms/gametank/lib/gt/vectors.s +14 -0
- package/src/platforms/gametank/lib/gt/wait.s +53 -0
- package/src/platforms/gba/MENTAL_MODEL.md +22 -1
- package/src/playtest/playtest.js +174 -26
- package/src/playtest/resampler/build.sh +19 -0
- package/src/playtest/resampler/index.mjs +75 -0
- package/src/playtest/resampler/resampler.c +129 -0
- package/src/playtest/resampler/resampler.mjs +2 -0
- package/src/playtest/resampler/resampler.wasm +0 -0
- package/src/toolchains/arm-none-eabi-gcc/gcc.js +39 -188
- package/src/toolchains/asar/asar.js +10 -15
- package/src/toolchains/cc65/cc65.js +82 -92
- package/src/toolchains/cc65/da65.js +12 -17
- package/src/toolchains/cc65/preset-resolver.js +13 -2
- package/src/toolchains/cc65/presets/gametank/gametank.h +80 -0
- package/src/toolchains/cc65/presets/gametank/sdk.cfg +32 -0
- package/src/toolchains/cc65/presets/gametank/sdk.crt0.s +61 -0
- package/src/toolchains/cc65/presets/gametank/sdk.vectors.s +11 -0
- package/src/toolchains/cc65/presets/gametank/single-bank.cfg +28 -0
- package/src/toolchains/cc65/presets/gametank/single-bank.crt0.s +71 -0
- package/src/toolchains/cc65/presets/gametank/single-bank.vectors.s +12 -0
- package/src/toolchains/common/c-build.js +109 -0
- package/src/toolchains/common/gcc-toolchain.js +164 -0
- package/src/toolchains/common/wasm-tool.js +101 -0
- package/src/toolchains/dasm/dasm.js +12 -18
- package/src/toolchains/gba-c/gba-c.js +253 -305
- package/src/toolchains/genesis-c/genesis-c.js +196 -225
- package/src/toolchains/index.js +58 -4
- package/src/toolchains/m68k-elf-gcc/gcc.js +39 -202
- package/src/toolchains/mips-c/mips-c.js +68 -78
- package/src/toolchains/mips-elf-gcc/gcc.js +55 -118
- package/src/toolchains/rgbds/rgbds.js +7 -19
- package/src/toolchains/sdcc/sdcc.js +35 -26
- package/src/toolchains/sh-c/sh-c.js +44 -52
- package/src/toolchains/sh-elf-gcc/gcc.js +55 -110
- package/src/toolchains/sjasm/sjasm.js +10 -14
- package/src/toolchains/snes-c/snes-c.js +125 -150
- package/src/toolchains/tcc816/tcc816.js +10 -15
- package/src/toolchains/vasm68k/vasm68k.js +11 -16
- package/src/toolchains/wladx/wladx.js +5 -17
- package/src/toolchains/z80/binutils.js +5 -11
|
@@ -2,229 +2,66 @@
|
|
|
2
2
|
//
|
|
3
3
|
// The full pipeline:
|
|
4
4
|
// runCc1m68k({source, headers, options}) → m68k assembly text (.s)
|
|
5
|
-
// runM68kAs({
|
|
6
|
-
// runM68kLd({objects, linkScript})
|
|
7
|
-
// runM68kObjcopy({elf})
|
|
5
|
+
// runM68kAs({source, includes}) → .o ELF object
|
|
6
|
+
// runM68kLd({objects, linkScript}) → linked .elf (+ map)
|
|
7
|
+
// runM68kObjcopy({elf}) → raw .bin Genesis ROM
|
|
8
8
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
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
|
|
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
|
-
* @
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
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
|
-
//
|
|
11
|
-
// (-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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;
|