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.
- package/AGENTS.md +19 -16
- package/CHANGELOG.md +175 -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/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/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
|
@@ -1,216 +1,67 @@
|
|
|
1
1
|
// arm-none-eabi-gcc — WASM toolchain wrappers for GBA C builds.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
3
|
+
// The full pipeline:
|
|
4
|
+
// runCc1arm({source, headers, options}) → ARM assembly text (.s)
|
|
5
|
+
// runArmAs({source, includes}) → .o ELF object
|
|
6
|
+
// runArmLd({objects, linkScript}) → linked .elf (+ map)
|
|
7
|
+
// runArmObjcopy({elf}) → raw .gba ROM
|
|
6
8
|
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// runArmLd({objects, linkScript}) → linked .elf
|
|
11
|
-
// runArmObjcopy({elf}) → raw .gba ROM
|
|
9
|
+
// 0.81.0: the 4 stages come from the shared makeGccToolchain() factory
|
|
10
|
+
// (common/gcc-toolchain.js); this file is just the ARM config + thin re-exports
|
|
11
|
+
// so existing call sites keep their runArm*/runCc1arm names.
|
|
12
12
|
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
|
|
13
|
+
// The WASM glue ships in romdev-platform-gba (155MB, incl. the 135MB cc1-arm);
|
|
14
|
+
// resolution is lazy + memoized, so booting never loads it unless a GBA C ROM
|
|
15
|
+
// is actually built. ARMv4T (arm7tdmi), thumb-interwork.
|
|
17
16
|
import { fileURLToPath } from "node:url";
|
|
18
|
-
import { existsSync } from "node:fs";
|
|
19
17
|
import path from "node:path";
|
|
20
18
|
|
|
21
|
-
import {
|
|
19
|
+
import { makeGccToolchain } from "../common/gcc-toolchain.js";
|
|
22
20
|
|
|
23
|
-
const
|
|
24
|
-
const __dirname = path.dirname(__filename);
|
|
21
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
25
22
|
|
|
26
|
-
|
|
27
|
-
// the only consumer). Resolve each tool's glue from that package; fall back to
|
|
28
|
-
// a local copy under src/ if present (transition / dev). emcc emits ESM
|
|
29
|
-
// (EXPORT_ES6=1) so the glue uses .mjs extensions. The package is a hard dep
|
|
30
|
-
// of romdev.
|
|
31
|
-
function resolveArmGlue(file) {
|
|
32
|
-
try {
|
|
33
|
-
const u = import.meta.resolve("romdev-platform-gba");
|
|
34
|
-
const p = path.join(path.dirname(fileURLToPath(u)), "wasm", file);
|
|
35
|
-
if (existsSync(p)) return p;
|
|
36
|
-
} catch { /* package not resolvable — fall through to local */ }
|
|
37
|
-
const local = path.join(__dirname, "wasm", file);
|
|
38
|
-
if (existsSync(local)) return local;
|
|
39
|
-
throw new Error(`arm-none-eabi-gcc WASM (${file}) not found — install romdev-platform-gba`);
|
|
40
|
-
}
|
|
23
|
+
const ARM_FLAGS = ["-mcpu=arm7tdmi", "-mthumb-interwork"];
|
|
41
24
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
25
|
+
const { runCc1, runAs, runLd, runObjcopy } = makeGccToolchain({
|
|
26
|
+
pkg: "romdev-platform-gba",
|
|
27
|
+
localDir: __dirname,
|
|
28
|
+
label: "arm-none-eabi-gcc",
|
|
29
|
+
glue: {
|
|
30
|
+
cc1: "cc1-arm.mjs",
|
|
31
|
+
as: "arm-none-eabi-as.mjs",
|
|
32
|
+
ld: "arm-none-eabi-ld.mjs",
|
|
33
|
+
objcopy: "arm-none-eabi-objcopy.mjs",
|
|
34
|
+
},
|
|
35
|
+
cc1Flags: ARM_FLAGS,
|
|
36
|
+
asFlags: ARM_FLAGS,
|
|
37
|
+
ldScriptName: "gba.ld",
|
|
38
|
+
outputName: "main.gba",
|
|
39
|
+
});
|
|
47
40
|
|
|
48
41
|
/**
|
|
49
42
|
* Compile a C source to ARM assembly via cc1.
|
|
50
|
-
*
|
|
51
|
-
* @param {Object} args
|
|
52
|
-
* @param {string} args.source main C source text
|
|
53
|
-
* @param {Record<string, string>} [args.headers] virtual headers visible via -I/work
|
|
54
|
-
* @param {string[]} [args.options] extra cc1 flags
|
|
43
|
+
* @param {{source:string, headers?:Record<string,string>, options?:string[]}} args
|
|
55
44
|
* @returns {Promise<{log:string, exitCode:number, asmSource:string|null, crash?:any}>}
|
|
56
45
|
*/
|
|
57
|
-
export
|
|
58
|
-
const { source, options = [] } = args;
|
|
59
|
-
const headers = args.headers ?? {};
|
|
60
|
-
/** @type {import("../_worker/run.js").InputFile[]} */
|
|
61
|
-
const inputFiles = [textFile("/work/main.c", source)];
|
|
62
|
-
for (const [name, content] of Object.entries(headers)) {
|
|
63
|
-
inputFiles.push(textFile("/work/" + name, content));
|
|
64
|
-
}
|
|
65
|
-
const argv = [
|
|
66
|
-
// GBA = ARM7TDMI. Thumb-interwork allows mixed ARM/Thumb code.
|
|
67
|
-
"-mcpu=arm7tdmi",
|
|
68
|
-
"-mthumb-interwork",
|
|
69
|
-
"-iquote", "/work",
|
|
70
|
-
"-I", "/work",
|
|
71
|
-
...options,
|
|
72
|
-
"/work/main.c",
|
|
73
|
-
"-o", "/work/main.s",
|
|
74
|
-
];
|
|
75
|
-
const r = await runIsolated({
|
|
76
|
-
gluePath: armGlue("cc1-arm.mjs"),
|
|
77
|
-
argv,
|
|
78
|
-
inputFiles,
|
|
79
|
-
outputFiles: [{ vfsPath: "/work/main.s", encoding: "utf8" }],
|
|
80
|
-
});
|
|
81
|
-
return {
|
|
82
|
-
log: r.log,
|
|
83
|
-
exitCode: r.exitCode,
|
|
84
|
-
asmSource: getOutputText(r, "/work/main.s") || null,
|
|
85
|
-
...(r.crash ? { crash: r.crash, stage: "crash" } : {}),
|
|
86
|
-
};
|
|
87
|
-
}
|
|
46
|
+
export const runCc1arm = runCc1;
|
|
88
47
|
|
|
89
48
|
/**
|
|
90
49
|
* Assemble ARM assembly with arm-none-eabi-as.
|
|
91
|
-
*
|
|
92
|
-
* @param {Object} args
|
|
93
|
-
* @param {string} args.source assembly source text
|
|
94
|
-
* @param {Record<string, string>} [args.includes]
|
|
95
|
-
* @param {Record<string, Uint8Array>} [args.binaryIncludes]
|
|
96
|
-
* @param {string[]} [args.options]
|
|
50
|
+
* @param {{source:string, includes?:Record<string,string>, binaryIncludes?:Record<string,Uint8Array>, options?:string[]}} args
|
|
97
51
|
* @returns {Promise<{log:string, exitCode:number, object:Uint8Array|null, crash?:any}>}
|
|
98
52
|
*/
|
|
99
|
-
export
|
|
100
|
-
const { source, options = [] } = args;
|
|
101
|
-
const includes = args.includes ?? {};
|
|
102
|
-
const binaryIncludes = args.binaryIncludes ?? {};
|
|
103
|
-
/** @type {import("../_worker/run.js").InputFile[]} */
|
|
104
|
-
const inputFiles = [textFile("/work/main.s", source)];
|
|
105
|
-
for (const [name, content] of Object.entries(includes)) {
|
|
106
|
-
inputFiles.push(textFile("/work/" + name, content));
|
|
107
|
-
}
|
|
108
|
-
for (const [name, bytes] of Object.entries(binaryIncludes)) {
|
|
109
|
-
inputFiles.push(binaryFile("/work/" + name, bytes));
|
|
110
|
-
}
|
|
111
|
-
const argv = [
|
|
112
|
-
"-mcpu=arm7tdmi",
|
|
113
|
-
"-mthumb-interwork",
|
|
114
|
-
"-I", "/work",
|
|
115
|
-
...options,
|
|
116
|
-
"/work/main.s",
|
|
117
|
-
"-o", "/work/main.o",
|
|
118
|
-
];
|
|
119
|
-
const r = await runIsolated({
|
|
120
|
-
gluePath: armGlue("arm-none-eabi-as.mjs"),
|
|
121
|
-
argv,
|
|
122
|
-
inputFiles,
|
|
123
|
-
outputFiles: [{ vfsPath: "/work/main.o", encoding: "base64" }],
|
|
124
|
-
});
|
|
125
|
-
return {
|
|
126
|
-
log: r.log,
|
|
127
|
-
exitCode: r.exitCode,
|
|
128
|
-
object: getOutputBytes(r, "/work/main.o"),
|
|
129
|
-
...(r.crash ? { crash: r.crash, stage: "crash" } : {}),
|
|
130
|
-
};
|
|
131
|
-
}
|
|
53
|
+
export const runArmAs = runAs;
|
|
132
54
|
|
|
133
55
|
/**
|
|
134
|
-
* Link ARM object files into an ELF executable.
|
|
135
|
-
*
|
|
136
|
-
* @
|
|
137
|
-
* @param {Record<string, Uint8Array>} args.objects
|
|
138
|
-
* @param {string} args.linkScript linker script contents (lnkscript / gba.ld)
|
|
139
|
-
* @param {string[]} [args.libraryPaths]
|
|
140
|
-
* @param {string[]} [args.libraries]
|
|
141
|
-
* @param {Record<string, Uint8Array>} [args.archives]
|
|
142
|
-
* @param {string[]} [args.options]
|
|
143
|
-
* @returns {Promise<{log:string, exitCode:number, elf:Uint8Array|null, crash?:any}>}
|
|
56
|
+
* Link ARM 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}>}
|
|
144
59
|
*/
|
|
145
|
-
export
|
|
146
|
-
const { objects, linkScript, libraries = [], libraryPaths = [], options = [] } = args;
|
|
147
|
-
const archives = args.archives ?? {};
|
|
148
|
-
/** @type {import("../_worker/run.js").InputFile[]} */
|
|
149
|
-
const inputFiles = [textFile("/work/gba.ld", linkScript)];
|
|
150
|
-
for (const [name, bytes] of Object.entries(objects)) {
|
|
151
|
-
inputFiles.push(binaryFile("/work/" + name, bytes));
|
|
152
|
-
}
|
|
153
|
-
for (const [name, bytes] of Object.entries(archives)) {
|
|
154
|
-
inputFiles.push(binaryFile("/work/" + name, bytes));
|
|
155
|
-
}
|
|
156
|
-
const argv = [
|
|
157
|
-
"-T", "/work/gba.ld",
|
|
158
|
-
"-o", "/work/main.elf",
|
|
159
|
-
// Emit a GNU ld map so symbols({op:'resolve'/'lookup'/'list'/'map'}) can turn
|
|
160
|
-
// a C global's name into an address (parsed by gnu-ld-map.js) — same as the
|
|
161
|
-
// m68k/Genesis path. Without this the GBA build returns no symbol table.
|
|
162
|
-
"-Map=/work/main.map",
|
|
163
|
-
...libraryPaths.flatMap((p) => ["-L", p]),
|
|
164
|
-
...Object.keys(objects).map((n) => "/work/" + n),
|
|
165
|
-
...libraries.map((l) => `-l${l}`),
|
|
166
|
-
...options,
|
|
167
|
-
];
|
|
168
|
-
const r = await runIsolated({
|
|
169
|
-
gluePath: armGlue("arm-none-eabi-ld.mjs"),
|
|
170
|
-
argv,
|
|
171
|
-
inputFiles,
|
|
172
|
-
outputFiles: [
|
|
173
|
-
{ vfsPath: "/work/main.elf", encoding: "base64" },
|
|
174
|
-
{ vfsPath: "/work/main.map", encoding: "utf8" },
|
|
175
|
-
],
|
|
176
|
-
});
|
|
177
|
-
return {
|
|
178
|
-
log: r.log,
|
|
179
|
-
exitCode: r.exitCode,
|
|
180
|
-
elf: getOutputBytes(r, "/work/main.elf"),
|
|
181
|
-
map: getOutputText(r, "/work/main.map") || null,
|
|
182
|
-
...(r.crash ? { crash: r.crash, stage: "crash" } : {}),
|
|
183
|
-
};
|
|
184
|
-
}
|
|
60
|
+
export const runArmLd = runLd;
|
|
185
61
|
|
|
186
62
|
/**
|
|
187
63
|
* Strip an ELF down to a raw .gba ROM.
|
|
188
|
-
*
|
|
189
|
-
* @param {Object} args
|
|
190
|
-
* @param {Uint8Array} args.elf
|
|
191
|
-
* @param {string[]} [args.options]
|
|
64
|
+
* @param {{elf:Uint8Array, options?:string[]}} args
|
|
192
65
|
* @returns {Promise<{log:string, exitCode:number, binary:Uint8Array|null, crash?:any}>}
|
|
193
66
|
*/
|
|
194
|
-
export
|
|
195
|
-
const { elf, options = [] } = args;
|
|
196
|
-
/** @type {import("../_worker/run.js").InputFile[]} */
|
|
197
|
-
const inputFiles = [binaryFile("/work/main.elf", elf)];
|
|
198
|
-
const argv = [
|
|
199
|
-
"-O", "binary",
|
|
200
|
-
...options,
|
|
201
|
-
"/work/main.elf",
|
|
202
|
-
"/work/main.gba",
|
|
203
|
-
];
|
|
204
|
-
const r = await runIsolated({
|
|
205
|
-
gluePath: armGlue("arm-none-eabi-objcopy.mjs"),
|
|
206
|
-
argv,
|
|
207
|
-
inputFiles,
|
|
208
|
-
outputFiles: [{ vfsPath: "/work/main.gba", encoding: "base64" }],
|
|
209
|
-
});
|
|
210
|
-
return {
|
|
211
|
-
log: r.log,
|
|
212
|
-
exitCode: r.exitCode,
|
|
213
|
-
binary: getOutputBytes(r, "/work/main.gba"),
|
|
214
|
-
...(r.crash ? { crash: r.crash, stage: "crash" } : {}),
|
|
215
|
-
};
|
|
216
|
-
}
|
|
67
|
+
export const runArmObjcopy = runObjcopy;
|
|
@@ -5,30 +5,25 @@
|
|
|
5
5
|
// directives.
|
|
6
6
|
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
|
-
import { existsSync } from "node:fs";
|
|
9
8
|
import path from "node:path";
|
|
10
9
|
|
|
11
10
|
import { runIsolated, textFile, binaryFile, getOutputBytes, getOutputText } from "../_worker/run.js";
|
|
11
|
+
import { resolveGlueFile } from "../common/wasm-tool.js";
|
|
12
12
|
|
|
13
13
|
const __filename = fileURLToPath(import.meta.url);
|
|
14
14
|
const __dirname = path.dirname(__filename);
|
|
15
15
|
|
|
16
16
|
// asar's WASM ships in romdev-platform-snes (the SNES platform is the only
|
|
17
|
-
// consumer)
|
|
18
|
-
//
|
|
19
|
-
function resolveAsarGlue() {
|
|
20
|
-
try {
|
|
21
|
-
const u = import.meta.resolve("romdev-platform-snes");
|
|
22
|
-
const p = path.join(path.dirname(fileURLToPath(u)), "wasm", "asar.js");
|
|
23
|
-
if (existsSync(p)) return p;
|
|
24
|
-
} catch { /* package not resolvable — fall through to local */ }
|
|
25
|
-
const local = path.join(__dirname, "wasm", "asar.js");
|
|
26
|
-
if (existsSync(local)) return local;
|
|
27
|
-
throw new Error("asar WASM not found — install romdev-platform-snes");
|
|
28
|
-
}
|
|
29
|
-
// Lazy + memoized: resolve only on the first asar (SNES asm) build, not at boot.
|
|
17
|
+
// consumer); a local src/ copy is the dev fallback. Lazy + memoized: resolve
|
|
18
|
+
// only on the first asar (SNES asm) build, not at boot.
|
|
30
19
|
let _gluePath;
|
|
31
|
-
const gluePath = () =>
|
|
20
|
+
const gluePath = () =>
|
|
21
|
+
(_gluePath ??= resolveGlueFile({
|
|
22
|
+
pkg: "romdev-platform-snes",
|
|
23
|
+
file: "asar.js",
|
|
24
|
+
localDir: __dirname,
|
|
25
|
+
label: "asar",
|
|
26
|
+
}));
|
|
32
27
|
|
|
33
28
|
/**
|
|
34
29
|
* Assemble an asar source.
|
|
@@ -15,6 +15,9 @@ import { fileURLToPath } from "node:url";
|
|
|
15
15
|
import { existsSync } from "node:fs";
|
|
16
16
|
import path from "node:path";
|
|
17
17
|
|
|
18
|
+
import { resolveToolBaseDir } from "../common/wasm-tool.js";
|
|
19
|
+
import { CBuild, BuildError } from "../common/c-build.js";
|
|
20
|
+
|
|
18
21
|
const __filename = fileURLToPath(import.meta.url);
|
|
19
22
|
const __dirname = path.dirname(__filename);
|
|
20
23
|
|
|
@@ -23,21 +26,18 @@ const __dirname = path.dirname(__filename);
|
|
|
23
26
|
// needs to link a target, so they must come from the same place as the WASM.
|
|
24
27
|
// Resolve the package's base dir once; fall back to a local copy under src/
|
|
25
28
|
// if present (transition / dev). The package is a hard dep of romdev.
|
|
26
|
-
function resolveCc65BaseDir() {
|
|
27
|
-
try {
|
|
28
|
-
const u = import.meta.resolve("romdev-toolchain-cc65");
|
|
29
|
-
const dir = path.dirname(fileURLToPath(u));
|
|
30
|
-
if (existsSync(path.join(dir, "wasm", "cc65.js"))) return dir;
|
|
31
|
-
} catch { /* package not resolvable — fall through to local */ }
|
|
32
|
-
if (existsSync(path.join(__dirname, "wasm", "cc65.js"))) return __dirname;
|
|
33
|
-
throw new Error("cc65 WASM not found — install romdev-toolchain-cc65");
|
|
34
|
-
}
|
|
35
29
|
// Lazy + memoized: resolve (and possibly throw "not installed") only on the
|
|
36
30
|
// first cc65 build (NES/C64/Atari7800/Lynx), not at module load — so booting
|
|
37
31
|
// the server never touches this package unless cc65 is actually used. Resolve
|
|
38
32
|
// the base dir once; derive the wasm + share dirs from it on demand.
|
|
39
33
|
let _cc65Base;
|
|
40
|
-
const cc65Base = () =>
|
|
34
|
+
const cc65Base = () =>
|
|
35
|
+
(_cc65Base ??= resolveToolBaseDir({
|
|
36
|
+
pkg: "romdev-toolchain-cc65",
|
|
37
|
+
sentinel: "wasm/cc65.js",
|
|
38
|
+
localDir: __dirname,
|
|
39
|
+
label: "cc65",
|
|
40
|
+
}));
|
|
41
41
|
|
|
42
42
|
/** True if the cc65 build toolchain WASM (cc65 + ld65, in romdev-toolchain-cc65)
|
|
43
43
|
* is installed/resolvable, without throwing — for the catalog(status) capability
|
|
@@ -45,7 +45,7 @@ const cc65Base = () => (_cc65Base ??= resolveCc65BaseDir());
|
|
|
45
45
|
* reflects ld65 (the linker) availability. */
|
|
46
46
|
export function cc65Available() {
|
|
47
47
|
try {
|
|
48
|
-
const dir =
|
|
48
|
+
const dir = cc65Base();
|
|
49
49
|
return existsSync(path.join(dir, "wasm", "cc65.js")) && existsSync(path.join(dir, "wasm", "ld65.js"));
|
|
50
50
|
} catch { return false; }
|
|
51
51
|
}
|
|
@@ -275,63 +275,57 @@ export async function buildC(args) {
|
|
|
275
275
|
// and these are pure upside. (NOTE: cc65 errors on an unknown -W name, so this
|
|
276
276
|
// list is verified valid against the bundled cc65.)
|
|
277
277
|
const ccWarn = ["-W", "unused-var,unused-func,unused-label,const-comparison,struct-param,pointer-sign"];
|
|
278
|
-
|
|
279
|
-
|
|
278
|
+
// ccOptions/caOptions = caller-supplied extra flags (e.g. GameTank's `--cpu 65c02`
|
|
279
|
+
// for the `-t none` path where the CPU isn't implied by a built-in cc65 target).
|
|
280
|
+
const ccOpts = [...(args.debug ? ["-g"] : []), ...ccWarn, ...(args.ccOptions ?? [])];
|
|
281
|
+
const caOpts = [...(args.debug ? ["-g"] : []), ...(args.caOptions ?? [])];
|
|
280
282
|
const sources = normalizeSources(args, "main.c");
|
|
281
283
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
if (cc.exitCode !== 0 || !cc.asmSource) {
|
|
301
|
-
return { binary: null, log, exitCode: cc.exitCode || 1, stage: "cc65" };
|
|
284
|
+
const cb = new CBuild();
|
|
285
|
+
try {
|
|
286
|
+
/** @type {Record<string, Uint8Array>} */
|
|
287
|
+
const objects = {};
|
|
288
|
+
for (const [name, src] of Object.entries(sources)) {
|
|
289
|
+
const ext = path.extname(name).toLowerCase();
|
|
290
|
+
let asmSource;
|
|
291
|
+
let asmName;
|
|
292
|
+
if (ext === ".s" || ext === ".asm") {
|
|
293
|
+
asmSource = src;
|
|
294
|
+
asmName = name;
|
|
295
|
+
} else {
|
|
296
|
+
// failure stage is bare "cc65" (no name) but the log header carries the name.
|
|
297
|
+
const cc = await cb.stage("cc65",
|
|
298
|
+
() => runCc65({ source: src, headers: args.headers, target: args.target, options: ccOpts }),
|
|
299
|
+
(r) => r.asmSource, { logName: `cc65 (${name})` });
|
|
300
|
+
asmSource = cc.asmSource;
|
|
301
|
+
asmName = name.replace(/\.(c|h)$/i, ".s");
|
|
302
302
|
}
|
|
303
|
-
|
|
304
|
-
|
|
303
|
+
const ca = await cb.stage("ca65",
|
|
304
|
+
() => runCa65({ source: asmSource, includes: args.asmIncludes, binaryIncludes: args.binaryIncludes, target: args.target, options: caOpts }),
|
|
305
|
+
(r) => r.object, { logName: `ca65 (${asmName})` });
|
|
306
|
+
objects[asmName.replace(/\.s$/, ".o")] = ca.object;
|
|
305
307
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
308
|
+
// ld65 is NOT throw-on-fail here: it returns its own success/failure inline with
|
|
309
|
+
// dbg + ramUsage, so keep it as a plain call (the cc65 contract has no `ok` field).
|
|
310
|
+
const ld = await runLd65({
|
|
311
|
+
objects,
|
|
310
312
|
target: args.target,
|
|
311
|
-
|
|
313
|
+
debug: args.debug,
|
|
314
|
+
linkerConfig: args.linkerConfig,
|
|
312
315
|
});
|
|
313
|
-
log +=
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
316
|
+
cb.log += "--- ld65 ---\n" + ld.log; // exact: no trailing newline (matches pre-refactor)
|
|
317
|
+
return {
|
|
318
|
+
binary: ld.binary,
|
|
319
|
+
dbg: ld.dbg,
|
|
320
|
+
log: cb.log,
|
|
321
|
+
exitCode: ld.exitCode,
|
|
322
|
+
ramUsage: parseRamUsage(ld.map),
|
|
323
|
+
stage: ld.exitCode === 0 ? "done" : "ld65",
|
|
324
|
+
};
|
|
325
|
+
} catch (e) {
|
|
326
|
+
if (e instanceof BuildError) return e.fields(); // cc65 shape: no `ok` field
|
|
327
|
+
throw e;
|
|
319
328
|
}
|
|
320
|
-
const ld = await runLd65({
|
|
321
|
-
objects,
|
|
322
|
-
target: args.target,
|
|
323
|
-
debug: args.debug,
|
|
324
|
-
linkerConfig: args.linkerConfig,
|
|
325
|
-
});
|
|
326
|
-
log += "--- ld65 ---\n" + ld.log;
|
|
327
|
-
return {
|
|
328
|
-
binary: ld.binary,
|
|
329
|
-
dbg: ld.dbg,
|
|
330
|
-
log,
|
|
331
|
-
exitCode: ld.exitCode,
|
|
332
|
-
ramUsage: parseRamUsage(ld.map),
|
|
333
|
-
stage: ld.exitCode === 0 ? "done" : "ld65",
|
|
334
|
-
};
|
|
335
329
|
}
|
|
336
330
|
|
|
337
331
|
/**
|
|
@@ -351,42 +345,38 @@ export async function buildC(args) {
|
|
|
351
345
|
* @param {string} [args.linkerConfig] custom ld65 .cfg
|
|
352
346
|
*/
|
|
353
347
|
export async function buildAsm(args) {
|
|
354
|
-
const caOpts = args.debug ? ["-g"] : [];
|
|
348
|
+
const caOpts = [...(args.debug ? ["-g"] : []), ...(args.caOptions ?? [])];
|
|
355
349
|
const sources = normalizeSources(args, "main.s");
|
|
356
350
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
const
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
351
|
+
const cb = new CBuild();
|
|
352
|
+
try {
|
|
353
|
+
/** @type {Record<string, Uint8Array>} */
|
|
354
|
+
const objects = {};
|
|
355
|
+
for (const [name, src] of Object.entries(sources)) {
|
|
356
|
+
const ca = await cb.stage("ca65",
|
|
357
|
+
() => runCa65({ source: src, includes: args.includes, binaryIncludes: args.binaryIncludes, target: args.target, options: caOpts }),
|
|
358
|
+
(r) => r.object, { logName: `ca65 (${name})` });
|
|
359
|
+
objects[name.replace(/\.(s|asm)$/i, ".o")] = ca.object;
|
|
360
|
+
}
|
|
361
|
+
const ld = await runLd65({
|
|
362
|
+
objects,
|
|
365
363
|
target: args.target,
|
|
366
|
-
|
|
364
|
+
debug: args.debug,
|
|
365
|
+
linkerConfig: args.linkerConfig,
|
|
367
366
|
});
|
|
368
|
-
log +=
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
367
|
+
cb.log += "--- ld65 ---\n" + ld.log;
|
|
368
|
+
return {
|
|
369
|
+
binary: ld.binary,
|
|
370
|
+
dbg: ld.dbg,
|
|
371
|
+
log: cb.log,
|
|
372
|
+
exitCode: ld.exitCode,
|
|
373
|
+
ramUsage: parseRamUsage(ld.map),
|
|
374
|
+
stage: ld.exitCode === 0 ? "done" : "ld65",
|
|
375
|
+
};
|
|
376
|
+
} catch (e) {
|
|
377
|
+
if (e instanceof BuildError) return e.fields(); // cc65 shape: no `ok` field
|
|
378
|
+
throw e;
|
|
374
379
|
}
|
|
375
|
-
const ld = await runLd65({
|
|
376
|
-
objects,
|
|
377
|
-
target: args.target,
|
|
378
|
-
debug: args.debug,
|
|
379
|
-
linkerConfig: args.linkerConfig,
|
|
380
|
-
});
|
|
381
|
-
log += "--- ld65 ---\n" + ld.log;
|
|
382
|
-
return {
|
|
383
|
-
binary: ld.binary,
|
|
384
|
-
dbg: ld.dbg,
|
|
385
|
-
log,
|
|
386
|
-
exitCode: ld.exitCode,
|
|
387
|
-
ramUsage: parseRamUsage(ld.map),
|
|
388
|
-
stage: ld.exitCode === 0 ? "done" : "ld65",
|
|
389
|
-
};
|
|
390
380
|
}
|
|
391
381
|
|
|
392
382
|
/**
|
|
@@ -3,36 +3,31 @@
|
|
|
3
3
|
// Run with --cpu 6502 (default). For SNES/65816 use --cpu 65816.
|
|
4
4
|
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import { existsSync } from "node:fs";
|
|
7
6
|
import path from "node:path";
|
|
8
7
|
|
|
9
8
|
import { runIsolated, textFile, binaryFile } from "../_worker/run.js";
|
|
9
|
+
import { resolveGlueFile } from "../common/wasm-tool.js";
|
|
10
10
|
|
|
11
11
|
const __filename = fileURLToPath(import.meta.url);
|
|
12
12
|
const __dirname = path.dirname(__filename);
|
|
13
13
|
|
|
14
|
-
// da65's WASM ships in romdev-toolchain-cc65 (alongside cc65 / ca65 / ld65)
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
function resolveDa65Glue() {
|
|
18
|
-
try {
|
|
19
|
-
const u = import.meta.resolve("romdev-toolchain-cc65");
|
|
20
|
-
const p = path.join(path.dirname(fileURLToPath(u)), "wasm", "da65.js");
|
|
21
|
-
if (existsSync(p)) return p;
|
|
22
|
-
} catch { /* package not resolvable — fall through to local */ }
|
|
23
|
-
const local = path.join(__dirname, "wasm", "da65.js");
|
|
24
|
-
if (existsSync(local)) return local;
|
|
25
|
-
throw new Error("da65 WASM not found — install romdev-toolchain-cc65");
|
|
26
|
-
}
|
|
27
|
-
// Lazy + memoized: resolve only on the first da65 disassembly, not at boot.
|
|
14
|
+
// da65's WASM ships in romdev-toolchain-cc65 (alongside cc65 / ca65 / ld65); a
|
|
15
|
+
// local src/ copy is the dev fallback. Lazy + memoized: resolve only on the
|
|
16
|
+
// first da65 disassembly, not at boot.
|
|
28
17
|
let _glue;
|
|
29
|
-
const glue = () =>
|
|
18
|
+
const glue = () =>
|
|
19
|
+
(_glue ??= resolveGlueFile({
|
|
20
|
+
pkg: "romdev-toolchain-cc65",
|
|
21
|
+
file: "da65.js",
|
|
22
|
+
localDir: __dirname,
|
|
23
|
+
label: "da65",
|
|
24
|
+
}));
|
|
30
25
|
|
|
31
26
|
/** True if the da65 WASM (romdev-toolchain-cc65) is installed/resolvable, without
|
|
32
27
|
* throwing — for capability probes (catalog status) that must not crash when the
|
|
33
28
|
* toolchain is absent. */
|
|
34
29
|
export function da65Available() {
|
|
35
|
-
try { return !!
|
|
30
|
+
try { return !!glue(); } catch { return false; }
|
|
36
31
|
}
|
|
37
32
|
|
|
38
33
|
/**
|
|
@@ -40,12 +40,23 @@ export async function resolveLinkerConfig(platform, arg) {
|
|
|
40
40
|
);
|
|
41
41
|
}
|
|
42
42
|
const supportSources = {};
|
|
43
|
+
// Bundled headers a preset makes available to the C compile (e.g. GameTank's
|
|
44
|
+
// `gametank.h` register defs) — keyed by their REAL name so `#include "gametank.h"`
|
|
45
|
+
// resolves, NOT renamed like the `.s` support sources. A bundled header is named
|
|
46
|
+
// `<name>.h` (no `<preset>.` prefix needed); the `.s`/`.asm` files follow the
|
|
47
|
+
// support-source convention.
|
|
48
|
+
/** @type {Record<string,string>} */
|
|
49
|
+
const headers = {};
|
|
43
50
|
try {
|
|
44
51
|
const entries = await readdir(presetDir);
|
|
45
52
|
const prefix = `${preset}.`;
|
|
46
53
|
for (const f of entries) {
|
|
47
|
-
if (!f.startsWith(prefix) || f === `${preset}.cfg`) continue;
|
|
48
54
|
const ext = path.extname(f).toLowerCase();
|
|
55
|
+
if (ext === ".h") {
|
|
56
|
+
headers[f] = await readFile(path.join(presetDir, f), "utf-8");
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (!f.startsWith(prefix) || f === `${preset}.cfg`) continue;
|
|
49
60
|
if (ext === ".s" || ext === ".asm") {
|
|
50
61
|
const contents = await readFile(path.join(presetDir, f), "utf-8");
|
|
51
62
|
const suffix = f.slice(prefix.length);
|
|
@@ -55,5 +66,5 @@ export async function resolveLinkerConfig(platform, arg) {
|
|
|
55
66
|
} catch {
|
|
56
67
|
// No support files.
|
|
57
68
|
}
|
|
58
|
-
return { cfg, supportSources };
|
|
69
|
+
return { cfg, supportSources, headers };
|
|
59
70
|
}
|