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.
- package/CHANGELOG.md +572 -0
- package/README.md +9 -7
- package/examples/dreamcast/hello/main.c +24 -0
- package/examples/n64/platformer/main.c +158 -0
- package/examples/n64/puzzle/main.c +117 -0
- package/examples/n64/racing/main.c +147 -0
- package/examples/n64/shmup/main.c +122 -0
- package/examples/n64/sports/main.c +127 -0
- package/examples/ps1/platformer/main.c +158 -0
- package/examples/ps1/puzzle/main.c +125 -0
- package/examples/ps1/racing/main.c +147 -0
- package/examples/ps1/shmup/main.c +192 -0
- package/examples/ps1/sports/main.c +127 -0
- package/examples/ps1/sprite_move/main.c +38 -0
- package/package.json +11 -2
- package/src/analysis/analyze.js +224 -29
- package/src/analysis/decompile.js +7 -0
- package/src/analysis/decompiler/sleigh/mips.ldefs +25 -0
- package/src/analysis/decompiler/sleigh/mips32.pspec +78 -0
- package/src/analysis/decompiler/sleigh/mips32be.cspec +107 -0
- package/src/analysis/decompiler/sleigh/mips32be.sla +211162 -0
- package/src/analysis/decompiler/sleigh/mips32le.cspec +106 -0
- package/src/analysis/decompiler/sleigh/mips32le.sla +210624 -0
- package/src/analysis/rizin.js +24 -1
- package/src/cores/capabilities.js +122 -2
- package/src/cores/registry.js +17 -0
- package/src/host/LibretroGL.js +273 -0
- package/src/host/LibretroGLBridge.js +836 -0
- package/src/host/LibretroHost.js +203 -3
- package/src/host/callbacks.js +22 -2
- package/src/host/coreLoader.js +65 -5
- package/src/host/cpu-state.js +27 -0
- package/src/host/framebuffer.js +14 -1
- package/src/host/glOptionalDep.js +60 -0
- package/src/host/n64-ai-state.js +43 -0
- package/src/host/ps1-spu-state.js +65 -0
- package/src/host/retroConstants.js +14 -0
- package/src/mcp/tools/disasm.js +2 -0
- package/src/mcp/tools/frame.js +18 -9
- package/src/mcp/tools/lifecycle.js +1 -1
- package/src/mcp/tools/platform-tools.js +20 -4
- package/src/mcp/tools/platforms.js +38 -4
- package/src/mcp/tools/project.js +116 -0
- package/src/mcp/tools/rendering-context.js +2 -1
- package/src/mcp/tools/toolchain.js +1 -1
- package/src/platforms/n64/lib/c/n64.c +196 -0
- package/src/platforms/n64/lib/c/n64.h +68 -0
- package/src/platforms/ps1/lib/c/psx.c +200 -0
- package/src/platforms/ps1/lib/c/psx.h +83 -0
- package/src/toolchains/index.js +65 -0
- package/src/toolchains/mips-c/lib/be/libc.a +0 -0
- package/src/toolchains/mips-c/lib/be/libgcc.a +0 -0
- package/src/toolchains/mips-c/lib/be/libm.a +0 -0
- package/src/toolchains/mips-c/lib/el/libc.a +0 -0
- package/src/toolchains/mips-c/lib/el/libm.a +0 -0
- package/src/toolchains/mips-c/lib/n64-crt0.s +21 -0
- package/src/toolchains/mips-c/lib/n64-ipl3.s +30 -0
- package/src/toolchains/mips-c/lib/n64.ld +15 -0
- package/src/toolchains/mips-c/lib/ps1-crt0.s +20 -0
- package/src/toolchains/mips-c/lib/ps1.ld +15 -0
- package/src/toolchains/mips-c/lib/softint.c +37 -0
- package/src/toolchains/mips-c/mips-c.js +155 -0
- package/src/toolchains/mips-elf-gcc/gcc.js +130 -0
- package/src/toolchains/sh-c/lib/dc-crt0.s +23 -0
- package/src/toolchains/sh-c/lib/dc.h +102 -0
- package/src/toolchains/sh-c/lib/dc.ld +11 -0
- package/src/toolchains/sh-c/lib/libc.a +0 -0
- package/src/toolchains/sh-c/lib/libgcc.a +0 -0
- package/src/toolchains/sh-c/lib/libm.a +0 -0
- package/src/toolchains/sh-c/sh-c.js +101 -0
- 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
|
+
}
|