romdevtools 0.71.1 → 0.84.1
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 +184 -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 +24 -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/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
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
import { runSjasm, runBintos } from "../sjasm/sjasm.js";
|
|
36
36
|
import { packAr } from "../common/ar.js";
|
|
37
37
|
import { resolveSdkArchive } from "../common/sdk-cache.js";
|
|
38
|
+
import { CBuild, BuildError } from "../common/c-build.js";
|
|
38
39
|
|
|
39
40
|
const __filename = fileURLToPath(import.meta.url);
|
|
40
41
|
const __dirname = path.dirname(__filename);
|
|
@@ -254,7 +255,7 @@ export async function buildGenesisC(args) {
|
|
|
254
255
|
* 7. objcopy -O binary → final .bin
|
|
255
256
|
*/
|
|
256
257
|
async function buildWithSgdk({ sources, headers, binaryIncludes, cc1Options, rebuildSdk = false, writeSeed = false }) {
|
|
257
|
-
|
|
258
|
+
const cb = new CBuild();
|
|
258
259
|
// SGDK_GCC define + freestanding-style flags must be in cc1 invocations
|
|
259
260
|
const sgdkCc1Options = [
|
|
260
261
|
"-DSGDK_GCC",
|
|
@@ -280,154 +281,135 @@ async function buildWithSgdk({ sources, headers, binaryIncludes, cc1Options, reb
|
|
|
280
281
|
const sgdkHeaders = await loadSgdkHeaders();
|
|
281
282
|
const tccHeaders = { ...headers, ...sgdkHeaders };
|
|
282
283
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
}
|
|
297
|
-
const as = await runM68kAs({ source: cc.asmSource });
|
|
298
|
-
log += `--- as (${cName} → .o) ---\n` + (as.log || "(ok)") + "\n";
|
|
299
|
-
if (as.exitCode !== 0 || !as.object) {
|
|
300
|
-
return { ok: false, binary: null, log, exitCode: as.exitCode || 1, stage: `as (${cName})`, runtime: "sgdk", ...(as.crash ? { crash: as.crash } : {}) };
|
|
284
|
+
try {
|
|
285
|
+
// ── Stage B: compile + assemble each user .c source ──
|
|
286
|
+
/** @type {Record<string, Uint8Array>} */
|
|
287
|
+
const userObjs = {};
|
|
288
|
+
const cFiles = Object.keys(sources).filter((n) => /\.c$/i.test(n));
|
|
289
|
+
for (const cName of cFiles) {
|
|
290
|
+
const cc = await cb.stage(`cc1 (${cName})`,
|
|
291
|
+
() => runCc1m68k({ source: sources[cName], headers: tccHeaders, options: userCc1Options }),
|
|
292
|
+
(r) => r.asmSource);
|
|
293
|
+
const as = await cb.stage(`as (${cName})`,
|
|
294
|
+
() => runM68kAs({ source: cc.asmSource }),
|
|
295
|
+
(r) => r.object, { logName: `as (${cName} → .o)` });
|
|
296
|
+
userObjs[cName.replace(/\.c$/i, ".o")] = as.object;
|
|
301
297
|
}
|
|
302
|
-
userObjs[cName.replace(/\.c$/i, ".o")] = as.object;
|
|
303
|
-
}
|
|
304
298
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
299
|
+
// ── Stage B': assemble user .s sibling files directly ──
|
|
300
|
+
// User .s files may .incbin sibling binary blobs (e.g. xgm2 music) — pass
|
|
301
|
+
// the same binaryIncludes map to each so the assembler can mount them.
|
|
302
|
+
const asmFiles = Object.keys(sources).filter((n) => /\.(s|asm)$/i.test(n));
|
|
303
|
+
for (const asmName of asmFiles) {
|
|
304
|
+
const as = await cb.stage(`as (${asmName})`,
|
|
305
|
+
() => runM68kAs({ source: sources[asmName], binaryIncludes }),
|
|
306
|
+
(r) => r.object);
|
|
307
|
+
userObjs[asmName.replace(/\.(s|asm)$/i, ".o")] = as.object;
|
|
314
308
|
}
|
|
315
|
-
userObjs[asmName.replace(/\.(s|asm)$/i, ".o")] = as.object;
|
|
316
|
-
}
|
|
317
309
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
//
|
|
356
|
-
//
|
|
357
|
-
//
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
310
|
+
// ── Stage C: build rom_header.bin from SGDK's rom_header.c ──
|
|
311
|
+
const romHeaderC = await readFile(path.join(SGDK_LIB_DIR, "rom_header.c"), "utf-8");
|
|
312
|
+
const rhCc = await cb.stage("cc1 (rom_header)",
|
|
313
|
+
() => runCc1m68k({ source: romHeaderC, headers: tccHeaders, options: sgdkCc1Options }),
|
|
314
|
+
(r) => r.asmSource, { logName: "cc1 (rom_header.c)" });
|
|
315
|
+
const rhAs = await cb.stage("as (rom_header)",
|
|
316
|
+
() => runM68kAs({ source: rhCc.asmSource }),
|
|
317
|
+
(r) => r.object, { logName: "as (rom_header.s)" });
|
|
318
|
+
// Objcopy → raw header bin (256 bytes)
|
|
319
|
+
const rhObjcopy = await cb.stage("objcopy (rom_header)",
|
|
320
|
+
() => runM68kObjcopy({ elf: rhAs.object }),
|
|
321
|
+
(r) => r.binary, { logName: "objcopy (rom_header.o → .bin)" });
|
|
322
|
+
|
|
323
|
+
// ── Stage D: assemble sega.s with the just-built rom_header.bin as a sibling ──
|
|
324
|
+
// sega.s `.incbin "out/rom_header.bin"` — we mount it at /work/out/rom_header.bin
|
|
325
|
+
// via the worker's binaryFile facility. runM68kAs's includes map only handles text,
|
|
326
|
+
// so we need to extend the as call to accept binary siblings.
|
|
327
|
+
//
|
|
328
|
+
// Note: we use `sega.preprocessed.s` (the cpp-expanded form). The raw sega.s
|
|
329
|
+
// uses `#include <task_cst.h>` + `#define`d constants, which SGDK normally
|
|
330
|
+
// expands via gcc's `-x assembler-with-cpp` driver mode. Our `runM68kAs`
|
|
331
|
+
// wrapper calls `as` directly without cpp, so we ship the preprocessed
|
|
332
|
+
// version as a build artifact (~7 KB) instead of running cpp at every build.
|
|
333
|
+
const segaSrc = await readFile(path.join(SGDK_LIB_DIR, "sega.preprocessed.s"), "utf-8");
|
|
334
|
+
const segaAs = await cb.stage("as (sega.s)",
|
|
335
|
+
() => runM68kAs({
|
|
336
|
+
source: segaSrc,
|
|
337
|
+
binaryIncludes: { "out/rom_header.bin": rhObjcopy.binary },
|
|
338
|
+
// SGDK assembles sega.s with the same -DSGDK_GCC etc. flags as C — pass them
|
|
339
|
+
// via -Wa,--register-prefix-optional,--bitwise-or implicitly via cc1's driver.
|
|
340
|
+
// Our as wrapper takes raw flags; the equivalent here is just --bitwise-or.
|
|
341
|
+
options: ["--register-prefix-optional", "--bitwise-or"],
|
|
342
|
+
}),
|
|
343
|
+
(r) => r.object);
|
|
344
|
+
|
|
345
|
+
// ── Stage D2: compile the SGDK runtime FROM SOURCE → libmd.a ──
|
|
346
|
+
// (Z80 drivers via sjasm+bintos, all SGDK .c via m68k-gcc, libres + .s
|
|
347
|
+
// assembled, packed into an archive. No prebuilt libmd.a black box.)
|
|
348
|
+
// Seed by default (compiling SGDK from source is ~18s); rebuildSdk:true
|
|
349
|
+
// recompiles; an edit to the vendored SGDK source without the flag is flagged.
|
|
350
|
+
const sdkWarnings = [];
|
|
351
|
+
const sgdkRes = await resolveSdkArchive({
|
|
352
|
+
name: "SGDK",
|
|
353
|
+
sources: await readSgdkSources(),
|
|
354
|
+
seedPath: path.join(SGDK_LIB_DIR, "libmd.seed.a"),
|
|
355
|
+
seedHashPath: path.join(SGDK_LIB_DIR, "libmd.seed.hash"),
|
|
356
|
+
rebuild: rebuildSdk, writeSeed,
|
|
357
|
+
compileFromSource: async () => {
|
|
358
|
+
const r = await compileSgdkRuntime({ ...tccHeaders }, sgdkCc1Options);
|
|
359
|
+
return r.ok ? { ok: true, archive: r.libmd } : r;
|
|
360
|
+
},
|
|
361
|
+
});
|
|
362
|
+
if (!sgdkRes.ok) {
|
|
363
|
+
// Original appended sgdkRes.log raw (no newline normalization) to the log.
|
|
364
|
+
cb.log += (sgdkRes.log || "");
|
|
365
|
+
throw new BuildError({ stage: `sgdk runtime: ${sgdkRes.stage}`, exitCode: 1, log: cb.log });
|
|
366
|
+
}
|
|
367
|
+
if (sgdkRes.sdkEditIgnored) sdkWarnings.push(sgdkRes.sdkEditIgnored);
|
|
368
|
+
cb.log += `--- SGDK runtime ${sgdkRes.fromSource ? "compiled from source" : "from prebuilt seed"} ---\n`;
|
|
369
|
+
|
|
370
|
+
// ── Stage E: link everything ──
|
|
371
|
+
const mdLd = await readFile(path.join(SGDK_LIB_DIR, "md.ld"), "utf-8");
|
|
372
|
+
const [libgcc, libc, libm] = await Promise.all([
|
|
373
|
+
readFile(path.join(MINIMAL_LIB_DIR, "libgcc.a")),
|
|
374
|
+
readFile(path.join(MINIMAL_LIB_DIR, "libc.a")),
|
|
375
|
+
readFile(path.join(MINIMAL_LIB_DIR, "libm.a")),
|
|
376
|
+
]);
|
|
377
|
+
|
|
378
|
+
const ld = await cb.stage("ld",
|
|
379
|
+
() => runM68kLd({
|
|
380
|
+
objects: { "sega.o": segaAs.object, ...userObjs },
|
|
381
|
+
linkScript: mdLd,
|
|
382
|
+
archives: {
|
|
383
|
+
"libmd.a": sgdkRes.archive,
|
|
384
|
+
"libgcc.a": new Uint8Array(libgcc),
|
|
385
|
+
"libc.a": new Uint8Array(libc),
|
|
386
|
+
"libm.a": new Uint8Array(libm),
|
|
387
|
+
},
|
|
388
|
+
libraries: ["md", "gcc", "c"],
|
|
389
|
+
libraryPaths: ["/work"],
|
|
390
|
+
options: ["--no-warn-rwx-segments"],
|
|
391
|
+
}),
|
|
392
|
+
(r) => r.elf);
|
|
393
|
+
|
|
394
|
+
// ── Stage F: extract raw ROM ──
|
|
395
|
+
const objcopy = await cb.stage("objcopy",
|
|
396
|
+
() => runM68kObjcopy({ elf: ld.elf }),
|
|
397
|
+
(r) => r.binary);
|
|
398
|
+
|
|
399
|
+
return {
|
|
400
|
+
ok: true,
|
|
401
|
+
binary: objcopy.binary,
|
|
402
|
+
log: cb.log,
|
|
403
|
+
exitCode: 0,
|
|
404
|
+
stage: "done",
|
|
405
|
+
runtime: "sgdk",
|
|
406
|
+
...(ld.map ? { symbols: ld.map } : {}),
|
|
407
|
+
...(sdkWarnings.length ? { sdkEditIgnored: sdkWarnings } : {}),
|
|
408
|
+
};
|
|
409
|
+
} catch (e) {
|
|
410
|
+
if (e instanceof BuildError) return e.toResult({ runtime: "sgdk" });
|
|
411
|
+
throw e;
|
|
419
412
|
}
|
|
420
|
-
|
|
421
|
-
return {
|
|
422
|
-
ok: true,
|
|
423
|
-
binary: objcopy.binary,
|
|
424
|
-
log,
|
|
425
|
-
exitCode: 0,
|
|
426
|
-
stage: "done",
|
|
427
|
-
runtime: "sgdk",
|
|
428
|
-
...(ld.map ? { symbols: ld.map } : {}),
|
|
429
|
-
...(sdkWarnings.length ? { sdkEditIgnored: sdkWarnings } : {}),
|
|
430
|
-
};
|
|
431
413
|
}
|
|
432
414
|
|
|
433
415
|
/**
|
|
@@ -472,93 +454,82 @@ export function finalizeGenesisRom(bin) {
|
|
|
472
454
|
*/
|
|
473
455
|
async function buildMinimal(args) {
|
|
474
456
|
const { sources, headers, binaryIncludes, cc1Options } = args;
|
|
475
|
-
|
|
457
|
+
const cb = new CBuild();
|
|
476
458
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
const
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
// ── Stage 2: assemble that .s with m68k-elf-as ────────────────
|
|
494
|
-
const as = await runM68kAs({ source: cc.asmSource });
|
|
495
|
-
log += `--- as (${cName} → .o) ---\n` + (as.log || "(ok)") + "\n";
|
|
496
|
-
if (as.exitCode !== 0 || !as.object) {
|
|
497
|
-
return { ok: false, binary: null, log, exitCode: as.exitCode || 1, stage: `as (${cName})`, runtime: "minimal", ...(as.crash ? { crash: as.crash } : {}) };
|
|
459
|
+
try {
|
|
460
|
+
// ── Stage 1: compile each .c file via cc1 → .s ─────────────────
|
|
461
|
+
/** @type {Record<string, Uint8Array>} */
|
|
462
|
+
const userObjs = {};
|
|
463
|
+
// User .c gets warnings on (minimal path has no SDK to flood). See buildWithSgdk.
|
|
464
|
+
const userCc1Options = [...cc1Options, "-Wall", "-Wextra", "-Wno-unused-parameter"];
|
|
465
|
+
const cFiles = Object.keys(sources).filter((n) => /\.c$/i.test(n));
|
|
466
|
+
for (const cName of cFiles) {
|
|
467
|
+
const cc = await cb.stage(`cc1 (${cName})`,
|
|
468
|
+
() => runCc1m68k({ source: sources[cName], headers, options: userCc1Options }),
|
|
469
|
+
(r) => r.asmSource);
|
|
470
|
+
// ── Stage 2: assemble that .s with m68k-elf-as ────────────────
|
|
471
|
+
const as = await cb.stage(`as (${cName})`,
|
|
472
|
+
() => runM68kAs({ source: cc.asmSource }),
|
|
473
|
+
(r) => r.object, { logName: `as (${cName} → .o)` });
|
|
474
|
+
userObjs[cName.replace(/\.c$/i, ".o")] = as.object;
|
|
498
475
|
}
|
|
499
|
-
userObjs[cName.replace(/\.c$/i, ".o")] = as.object;
|
|
500
|
-
}
|
|
501
476
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
477
|
+
// ── Stage 2b: assemble each .s file directly ───────────────────
|
|
478
|
+
const asmFiles = Object.keys(sources).filter((n) => /\.(s|asm)$/i.test(n));
|
|
479
|
+
for (const asmName of asmFiles) {
|
|
480
|
+
const as = await cb.stage(`as (${asmName})`,
|
|
481
|
+
() => runM68kAs({ source: sources[asmName], binaryIncludes }),
|
|
482
|
+
(r) => r.object);
|
|
483
|
+
userObjs[asmName.replace(/\.(s|asm)$/i, ".o")] = as.object;
|
|
509
484
|
}
|
|
510
|
-
userObjs[asmName.replace(/\.(s|asm)$/i, ".o")] = as.object;
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
// ── Stage 3: assemble the bundled sega.s crt0 ───────────────────
|
|
514
|
-
const sega = await readFile(path.join(MINIMAL_LIB_DIR, "sega.s"), "utf-8");
|
|
515
|
-
const segaAs = await runM68kAs({ source: sega });
|
|
516
|
-
log += "--- as (sega.s crt0) ---\n" + (segaAs.log || "(ok)") + "\n";
|
|
517
|
-
if (segaAs.exitCode !== 0 || !segaAs.object) {
|
|
518
|
-
return { ok: false, binary: null, log, exitCode: segaAs.exitCode || 1, stage: "as (sega.s)", runtime: "minimal", ...(segaAs.crash ? { crash: segaAs.crash } : {}) };
|
|
519
|
-
}
|
|
520
485
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
486
|
+
// ── Stage 3: assemble the bundled sega.s crt0 ───────────────────
|
|
487
|
+
const sega = await readFile(path.join(MINIMAL_LIB_DIR, "sega.s"), "utf-8");
|
|
488
|
+
const segaAs = await cb.stage("as (sega.s)",
|
|
489
|
+
() => runM68kAs({ source: sega }),
|
|
490
|
+
(r) => r.object, { logName: "as (sega.s crt0)" });
|
|
491
|
+
|
|
492
|
+
// ── Stage 4: link everything ────────────────────────────────────
|
|
493
|
+
const linkScript = await readFile(path.join(MINIMAL_LIB_DIR, "genesis.ld"), "utf-8");
|
|
494
|
+
const [libgcc, libc, libm] = await Promise.all([
|
|
495
|
+
readFile(path.join(MINIMAL_LIB_DIR, "libgcc.a")),
|
|
496
|
+
readFile(path.join(MINIMAL_LIB_DIR, "libc.a")),
|
|
497
|
+
readFile(path.join(MINIMAL_LIB_DIR, "libm.a")),
|
|
498
|
+
]);
|
|
499
|
+
|
|
500
|
+
const ld = await cb.stage("ld",
|
|
501
|
+
() => runM68kLd({
|
|
502
|
+
objects: { "sega.o": segaAs.object, ...userObjs },
|
|
503
|
+
linkScript,
|
|
504
|
+
archives: {
|
|
505
|
+
"libgcc.a": new Uint8Array(libgcc),
|
|
506
|
+
"libc.a": new Uint8Array(libc),
|
|
507
|
+
"libm.a": new Uint8Array(libm),
|
|
508
|
+
},
|
|
509
|
+
libraries: ["c", "gcc", "m"],
|
|
510
|
+
libraryPaths: ["/work"],
|
|
511
|
+
options: ["--no-warn-rwx-segments"],
|
|
512
|
+
}),
|
|
513
|
+
(r) => r.elf);
|
|
514
|
+
|
|
515
|
+
// ── Stage 5: extract raw binary ─────────────────────────────────
|
|
516
|
+
const objcopy = await cb.stage("objcopy",
|
|
517
|
+
() => runM68kObjcopy({ elf: ld.elf }),
|
|
518
|
+
(r) => r.binary);
|
|
519
|
+
|
|
520
|
+
return {
|
|
521
|
+
ok: true,
|
|
522
|
+
binary: objcopy.binary,
|
|
523
|
+
log: cb.log,
|
|
524
|
+
exitCode: 0,
|
|
525
|
+
stage: "done",
|
|
526
|
+
runtime: "minimal",
|
|
527
|
+
...(ld.map ? { symbols: ld.map } : {}),
|
|
528
|
+
};
|
|
529
|
+
} catch (e) {
|
|
530
|
+
if (e instanceof BuildError) return e.toResult({ runtime: "minimal" });
|
|
531
|
+
throw e;
|
|
544
532
|
}
|
|
545
|
-
|
|
546
|
-
// ── Stage 5: extract raw binary ─────────────────────────────────
|
|
547
|
-
const objcopy = await runM68kObjcopy({ elf: ld.elf });
|
|
548
|
-
log += "--- objcopy ---\n" + (objcopy.log || "(ok)") + "\n";
|
|
549
|
-
if (objcopy.exitCode !== 0 || !objcopy.binary) {
|
|
550
|
-
return { ok: false, binary: null, log, exitCode: objcopy.exitCode || 1, stage: "objcopy", runtime: "minimal", ...(objcopy.crash ? { crash: objcopy.crash } : {}) };
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
return {
|
|
554
|
-
ok: true,
|
|
555
|
-
binary: objcopy.binary,
|
|
556
|
-
log,
|
|
557
|
-
exitCode: 0,
|
|
558
|
-
stage: "done",
|
|
559
|
-
runtime: "minimal",
|
|
560
|
-
...(ld.map ? { symbols: ld.map } : {}),
|
|
561
|
-
};
|
|
562
533
|
}
|
|
563
534
|
|
|
564
535
|
/** @param {{source?:string, sources?:Record<string,string>}} args */
|
package/src/toolchains/index.js
CHANGED
|
@@ -17,6 +17,23 @@ const CC65_TARGET = {
|
|
|
17
17
|
atari7800: "atari7800",
|
|
18
18
|
lynx: "lynx",
|
|
19
19
|
pce: "pce",
|
|
20
|
+
// GameTank has no built-in cc65 target — it links bare via `-t none` + the
|
|
21
|
+
// bundled single-bank preset cfg/crt0/vectors (presets/gametank/).
|
|
22
|
+
gametank: "none",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** cc65 platforms whose CPU is NOT implied by a built-in target. The built-in
|
|
26
|
+
* targets (lynx/pce/…) set the CPU themselves; `-t none` defaults to plain 6502,
|
|
27
|
+
* so GameTank (a real W65C02S) MUST be told `--cpu 65c02` for cc65 codegen +
|
|
28
|
+
* `--cpu W65C02` for ca65, or it silently emits 6502 code on a 65C02. */
|
|
29
|
+
const CC65_CPU = {
|
|
30
|
+
gametank: { cc: "65c02", ca: "W65C02" },
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** Exact ROM sizes for cc65 platforms whose mapper is size-keyed (no header).
|
|
34
|
+
* GameTank detects EEPROM32K by a flat 32 KB image — pad to exactly 32768. */
|
|
35
|
+
const CC65_ROM_SIZE = {
|
|
36
|
+
gametank: 32768,
|
|
20
37
|
};
|
|
21
38
|
|
|
22
39
|
/**
|
|
@@ -81,6 +98,10 @@ const LANGUAGE_TOOLCHAIN = {
|
|
|
81
98
|
c: { toolchain: "cc65", available: true, note: "C for PC Engine via cc65's huc6280 target — crt0 + pce.lib (VDC/VCE/PSG/joypad helpers) auto-linked. #include <pce.h>." },
|
|
82
99
|
asm: { toolchain: "cc65", available: true },
|
|
83
100
|
},
|
|
101
|
+
gametank: {
|
|
102
|
+
c: { toolchain: "cc65", available: true, note: "C for GameTank (Clyde Shaffer's open W65C02S console) via cc65 `--cpu 65c02` + a bundled single-bank 32KB preset (crt0 + linker cfg + vectors), pass linkerConfig:'single-bank'. #include \"gametank.h\" for the blitter/DMA/banking/gamepad registers ($4000-$4007 blitter, $2007 dma_flags, $2005 bank_reg, $2008/9 gamepads). Bare path — color-fill the 128x128 framebuffer via the blitter; the full SDK gfx/audio/text runtime + 2MB multi-bank flash pipeline is not bundled (Tier B, deferred). Output is a flat 32KB .gtr (EEPROM32K, size-keyed mapper)." },
|
|
103
|
+
asm: { toolchain: "cc65", available: true },
|
|
104
|
+
},
|
|
84
105
|
msx: {
|
|
85
106
|
c: { toolchain: "sdcc", available: true, note: "C for MSX via SDCC's z80 port. Cartridge ROM with the 'AB' header + INIT pointer at $4000; boots on C-BIOS (open MSX BIOS). MSX2 by default (runs MSX1 carts too)." },
|
|
86
107
|
asm: { toolchain: "sdcc", available: true },
|
|
@@ -114,6 +135,7 @@ const PLATFORM_DEFAULT_LANGUAGE = {
|
|
|
114
135
|
c64: "c",
|
|
115
136
|
atari7800: "c",
|
|
116
137
|
lynx: "c",
|
|
138
|
+
gametank: "c",
|
|
117
139
|
gb: "c",
|
|
118
140
|
gbc: "c",
|
|
119
141
|
snes: "c",
|
|
@@ -425,24 +447,56 @@ export async function buildForPlatform(args) {
|
|
|
425
447
|
// the build pipeline does NOT auto-select presets based on source
|
|
426
448
|
// contents — every byte that compiles must be visible to the caller.
|
|
427
449
|
const { resolveLinkerConfig } = await import("./cc65/preset-resolver.js");
|
|
428
|
-
const { cfg, supportSources } = await resolveLinkerConfig(args.platform, args.linkerConfig);
|
|
450
|
+
const { cfg, supportSources, headers: presetHeaders } = await resolveLinkerConfig(args.platform, args.linkerConfig);
|
|
429
451
|
|
|
430
452
|
// Splice in any preset support sources (crt0, etc.). User sources win
|
|
431
453
|
// if there's a name collision, but the preset uses `_preset_*` so it
|
|
432
454
|
// wouldn't normally collide.
|
|
433
455
|
sources = { ...supportSources, ...sources };
|
|
434
456
|
|
|
457
|
+
// A preset can bundle headers (e.g. GameTank's gametank.h) so `#include
|
|
458
|
+
// "gametank.h"` just works; caller-supplied headers win on collision.
|
|
459
|
+
const mergedHeaders = { ...(presetHeaders ?? {}), ...(args.includes ?? {}) };
|
|
460
|
+
|
|
461
|
+
// CPU override for `-t none` platforms whose CPU isn't implied by the target
|
|
462
|
+
// (GameTank → cc65 `--cpu 65c02`, ca65 `--cpu W65C02`).
|
|
463
|
+
const cpu = CC65_CPU[args.platform];
|
|
464
|
+
const ccCpuOpts = cpu ? ["--cpu", cpu.cc] : [];
|
|
465
|
+
const caCpuOpts = cpu ? ["--cpu", cpu.ca] : [];
|
|
466
|
+
|
|
435
467
|
const builder = anyC ? buildC : buildAsm;
|
|
436
468
|
const r = await builder({
|
|
437
469
|
sources,
|
|
438
470
|
target: cc65Target,
|
|
439
|
-
[anyC ? "headers" : "includes"]: args.includes,
|
|
471
|
+
[anyC ? "headers" : "includes"]: anyC ? mergedHeaders : args.includes,
|
|
472
|
+
// ca65 .include files (e.g. an SDK's modules_enabled.inc) — distinct from C
|
|
473
|
+
// headers; the C path's per-TU ca65 pass needs them, and they weren't being
|
|
474
|
+
// forwarded. A platform with a multi-file asm-backed SDK (GameTank) needs this.
|
|
475
|
+
...(args.asmIncludes ? { asmIncludes: args.asmIncludes } : {}),
|
|
440
476
|
binaryIncludes: args.binaryIncludes,
|
|
441
477
|
linkerConfig: cfg,
|
|
478
|
+
ccOptions: ccCpuOpts,
|
|
479
|
+
caOptions: caCpuOpts,
|
|
442
480
|
});
|
|
481
|
+
|
|
482
|
+
// Size-keyed mappers (GameTank EEPROM32K): pad the flat image to the exact
|
|
483
|
+
// ROM size the core size-detects. Under-size or a few bytes over would fall
|
|
484
|
+
// into the UNKNOWN mapper path.
|
|
485
|
+
let binary = r.binary;
|
|
486
|
+
const romSize = CC65_ROM_SIZE[args.platform];
|
|
487
|
+
if (binary && r.exitCode === 0 && romSize && binary.length !== romSize) {
|
|
488
|
+
if (binary.length < romSize) {
|
|
489
|
+
const padded = new Uint8Array(romSize);
|
|
490
|
+
padded.set(binary);
|
|
491
|
+
binary = padded;
|
|
492
|
+
} else {
|
|
493
|
+
binary = binary.slice(0, romSize);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
443
497
|
return {
|
|
444
|
-
ok: r.exitCode === 0 &&
|
|
445
|
-
binary
|
|
498
|
+
ok: r.exitCode === 0 && binary !== null,
|
|
499
|
+
binary,
|
|
446
500
|
listing: "",
|
|
447
501
|
symbols: "",
|
|
448
502
|
log: r.log,
|