romdevtools 0.56.1 → 0.71.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +338 -0
- package/README.md +9 -7
- package/examples/dreamcast/hello/main.c +24 -0
- package/examples/dreamcast/platformer/main.c +31 -0
- package/examples/dreamcast/puzzle/main.c +44 -0
- package/examples/dreamcast/racing/main.c +39 -0
- package/examples/dreamcast/shmup/main.c +50 -0
- package/examples/dreamcast/sports/main.c +39 -0
- package/package.json +5 -3
- package/src/analysis/analyze.js +60 -5
- package/src/analysis/decompile.js +3 -0
- package/src/analysis/rizin.js +3 -1
- package/src/cores/capabilities.js +43 -7
- package/src/cores/registry.js +13 -8
- package/src/host/LibretroGL.js +26 -23
- package/src/host/LibretroHost.js +302 -24
- package/src/host/callbacks.js +72 -1
- package/src/host/coreLoader.js +17 -4
- package/src/host/cpu-state.js +32 -0
- package/src/host/dc-aica-state.js +67 -0
- package/src/mcp/tools/audio.js +1 -1
- package/src/mcp/tools/disasm.js +1 -1
- package/src/mcp/tools/index.js +1 -1
- package/src/mcp/tools/platform-docs.js +1 -1
- package/src/mcp/tools/platform-tools.js +9 -1
- package/src/mcp/tools/project.js +16 -0
- package/src/mcp/tools/toolchain.js +115 -10
- package/src/platforms/dreamcast/MENTAL_MODEL.md +87 -0
- package/src/platforms/dreamcast/TROUBLESHOOTING.md +55 -0
- package/src/platforms/dreamcast/UPSTREAM_SOURCES.md +57 -0
- package/src/platforms/n64/MENTAL_MODEL.md +84 -0
- package/src/platforms/n64/TROUBLESHOOTING.md +60 -0
- package/src/platforms/n64/UPSTREAM_SOURCES.md +52 -0
- package/src/platforms/n64/lib/c/n64.c +181 -80
- package/src/platforms/ps1/MENTAL_MODEL.md +85 -0
- package/src/platforms/ps1/TROUBLESHOOTING.md +55 -0
- package/src/platforms/ps1/UPSTREAM_SOURCES.md +54 -0
- package/src/platforms/snes/TROUBLESHOOTING.md +10 -0
- package/src/toolchains/asar/asar.js +84 -14
- package/src/toolchains/index.js +30 -0
- package/src/toolchains/mips-c/mips-c.js +35 -1
- package/src/toolchains/sh-c/lib/dc-crt0.s +23 -0
- package/src/toolchains/sh-c/lib/dc.h +152 -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 +104 -0
- package/src/toolchains/sh-elf-gcc/gcc.js +122 -0
|
@@ -0,0 +1,104 @@
|
|
|
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 -O1, NOT -O2: the sh-elf cc1.wasm build has an -O2-only pass that aborts
|
|
40
|
+
// ("memory access out of bounds" during "Assembling functions") on common control
|
|
41
|
+
// flow — e.g. an infinite loop that mutates locals through both `if`/`else` branches.
|
|
42
|
+
// -O1 dodges it entirely and is plenty for DC homebrew. A user-supplied -O<level>
|
|
43
|
+
// still wins (gcc honors the LAST -O, so only add a default when none is present).
|
|
44
|
+
const userOpts = args.cc1Options ?? [];
|
|
45
|
+
const hasOpt = userOpts.some((o) => /^-O/.test(o));
|
|
46
|
+
const cc1Options = [...(hasOpt ? [] : ["-O1"]), ...userOpts, "-ffreestanding", "-fno-builtin", "-Wall"];
|
|
47
|
+
const sources = args.sources ?? (args.source != null ? { "main.c": args.source } : {});
|
|
48
|
+
let log = "";
|
|
49
|
+
|
|
50
|
+
/** @type {Record<string, Uint8Array>} */
|
|
51
|
+
const userObjs = {};
|
|
52
|
+
for (const cName of Object.keys(sources).filter((n) => /\.c$/i.test(n))) {
|
|
53
|
+
const cc = await runCc1sh({ source: sources[cName], headers, options: cc1Options });
|
|
54
|
+
log += `--- cc1 (${cName}) ---\n${cc.log || "(ok)"}\n`;
|
|
55
|
+
if (cc.exitCode !== 0 || !cc.asmSource)
|
|
56
|
+
return { ok: false, binary: null, log, exitCode: cc.exitCode || 1, stage: `cc1 (${cName})`, ...(cc.crash ? { crash: cc.crash } : {}) };
|
|
57
|
+
const as = await runShAs({ source: cc.asmSource });
|
|
58
|
+
log += `--- as (${cName}) ---\n${as.log || "(ok)"}\n`;
|
|
59
|
+
if (as.exitCode !== 0 || !as.object)
|
|
60
|
+
return { ok: false, binary: null, log, exitCode: as.exitCode || 1, stage: `as (${cName})`, ...(as.crash ? { crash: as.crash } : {}) };
|
|
61
|
+
userObjs[cName.replace(/\.c$/i, ".o")] = as.object;
|
|
62
|
+
}
|
|
63
|
+
// raw .s sources too
|
|
64
|
+
for (const sName of Object.keys(sources).filter((n) => /\.(s|asm)$/i.test(n))) {
|
|
65
|
+
const as = await runShAs({ source: sources[sName] });
|
|
66
|
+
log += `--- as (${sName}) ---\n${as.log || "(ok)"}\n`;
|
|
67
|
+
if (as.exitCode !== 0 || !as.object)
|
|
68
|
+
return { ok: false, binary: null, log, exitCode: as.exitCode || 1, stage: `as (${sName})`, ...(as.crash ? { crash: as.crash } : {}) };
|
|
69
|
+
userObjs[sName.replace(/\.(s|asm)$/i, ".o")] = as.object;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// crt0 (stack + .bss clear + main()).
|
|
73
|
+
const crt0Src = await readFile(path.join(LIB, "dc-crt0.s"), "utf-8");
|
|
74
|
+
const crt0As = await runShAs({ source: crt0Src });
|
|
75
|
+
log += `--- as (dc-crt0.s) ---\n${crt0As.log || "(ok)"}\n`;
|
|
76
|
+
if (crt0As.exitCode !== 0 || !crt0As.object)
|
|
77
|
+
return { ok: false, binary: null, log, exitCode: crt0As.exitCode || 1, stage: "as (crt0)", ...(crt0As.crash ? { crash: crt0As.crash } : {}) };
|
|
78
|
+
|
|
79
|
+
// link: crt0 + user objects + newlib (libc/libm) + libgcc.
|
|
80
|
+
const linkScript = await readFile(path.join(LIB, "dc.ld"), "utf-8");
|
|
81
|
+
const [libc, libm, libgcc] = await Promise.all([
|
|
82
|
+
readFile(path.join(LIB, "libc.a")),
|
|
83
|
+
readFile(path.join(LIB, "libm.a")),
|
|
84
|
+
readFile(path.join(LIB, "libgcc.a")),
|
|
85
|
+
]);
|
|
86
|
+
const ld = await runShLd({
|
|
87
|
+
objects: { "crt0.o": crt0As.object, ...userObjs },
|
|
88
|
+
linkScript,
|
|
89
|
+
archives: {
|
|
90
|
+
"libc.a": new Uint8Array(libc),
|
|
91
|
+
"libm.a": new Uint8Array(libm),
|
|
92
|
+
"libgcc.a": new Uint8Array(libgcc),
|
|
93
|
+
},
|
|
94
|
+
libraries: ["c", "m", "gcc"],
|
|
95
|
+
libraryPaths: ["/work"],
|
|
96
|
+
options: ["--no-warn-rwx-segments"],
|
|
97
|
+
});
|
|
98
|
+
log += `--- ld ---\n${ld.log || "(ok)"}\n`;
|
|
99
|
+
if (ld.exitCode !== 0 || !ld.elf)
|
|
100
|
+
return { ok: false, binary: null, log, exitCode: ld.exitCode || 1, stage: "ld", ...(ld.crash ? { crash: ld.crash } : {}) };
|
|
101
|
+
|
|
102
|
+
// The ELF IS the deliverable — reios boots it directly.
|
|
103
|
+
return { ok: true, binary: ld.elf, log, exitCode: 0, stage: "done", ...(ld.map ? { symbols: ld.map } : {}) };
|
|
104
|
+
}
|
|
@@ -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
|
+
}
|