romdevtools 0.71.0 → 0.84.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/AGENTS.md +19 -16
  2. package/CHANGELOG.md +196 -0
  3. package/README.md +5 -5
  4. package/examples/README.md +1 -0
  5. package/examples/gametank/templates/gt_draw.h +91 -0
  6. package/examples/gametank/templates/gt_hud.h +89 -0
  7. package/examples/gametank/templates/gt_palette.h +59 -0
  8. package/examples/gametank/templates/gt_sound.h +90 -0
  9. package/examples/gametank/templates/gt_sprites.h +53 -0
  10. package/examples/gametank/templates/platformer.c +318 -0
  11. package/examples/gametank/templates/puzzle.c +295 -0
  12. package/examples/gametank/templates/racing.c +139 -0
  13. package/examples/gametank/templates/shmup.c +277 -0
  14. package/examples/gametank/templates/sports.c +119 -0
  15. package/package.json +23 -22
  16. package/src/cores/capabilities.js +24 -0
  17. package/src/cores/registry.js +7 -1
  18. package/src/host/LibretroHost.js +90 -17
  19. package/src/host/callbacks.js +20 -2
  20. package/src/host/cpu-state.js +17 -0
  21. package/src/host/gametank-acp-state.js +53 -0
  22. package/src/http/skill-doc.js +10 -6
  23. package/src/mcp/tools/audio.js +1 -1
  24. package/src/mcp/tools/cart-parts.js +76 -0
  25. package/src/mcp/tools/lifecycle.js +1 -1
  26. package/src/mcp/tools/platform-tools.js +9 -1
  27. package/src/mcp/tools/platforms.js +46 -10
  28. package/src/mcp/tools/toolchain.js +35 -10
  29. package/src/platforms/gametank/lib/gt/audio/acp_image.bin +0 -0
  30. package/src/platforms/gametank/lib/gt/audio/audio_coprocessor.c +55 -0
  31. package/src/platforms/gametank/lib/gt/audio/audio_coprocessor.h +34 -0
  32. package/src/platforms/gametank/lib/gt/audio/audio_fw.asm +223 -0
  33. package/src/platforms/gametank/lib/gt/audio/audio_fw_blob.s +8 -0
  34. package/src/platforms/gametank/lib/gt/audio/instruments.c +79 -0
  35. package/src/platforms/gametank/lib/gt/audio/instruments.h +25 -0
  36. package/src/platforms/gametank/lib/gt/audio/music.c +370 -0
  37. package/src/platforms/gametank/lib/gt/audio/music.h +35 -0
  38. package/src/platforms/gametank/lib/gt/audio/note_numbers.h +132 -0
  39. package/src/platforms/gametank/lib/gt/audio/scaled_sines.raw +0 -0
  40. package/src/platforms/gametank/lib/gt/audio/sine_256_-63_63.bin +0 -0
  41. package/src/platforms/gametank/lib/gt/banking.c +29 -0
  42. package/src/platforms/gametank/lib/gt/banking.h +8 -0
  43. package/src/platforms/gametank/lib/gt/banking2.s +41 -0
  44. package/src/platforms/gametank/lib/gt/crt0.s +102 -0
  45. package/src/platforms/gametank/lib/gt/draw_logo.s +55 -0
  46. package/src/platforms/gametank/lib/gt/feature/persist/persist.c +103 -0
  47. package/src/platforms/gametank/lib/gt/feature/persist/persist.h +13 -0
  48. package/src/platforms/gametank/lib/gt/feature/random/random.c +40 -0
  49. package/src/platforms/gametank/lib/gt/feature/random/random.h +18 -0
  50. package/src/platforms/gametank/lib/gt/feature/text/text.c +111 -0
  51. package/src/platforms/gametank/lib/gt/feature/text/text.h +25 -0
  52. package/src/platforms/gametank/lib/gt/gametank.c +12 -0
  53. package/src/platforms/gametank/lib/gt/gametank.h +80 -0
  54. package/src/platforms/gametank/lib/gt/gametank_logo.inc +393 -0
  55. package/src/platforms/gametank/lib/gt/gen/assets/assets_index.h +9 -0
  56. package/src/platforms/gametank/lib/gt/gen/assets/sdk_default.h +5 -0
  57. package/src/platforms/gametank/lib/gt/gen/bank_nums.h +11 -0
  58. package/src/platforms/gametank/lib/gt/gen/modules_enabled.h +4 -0
  59. package/src/platforms/gametank/lib/gt/gen/modules_enabled.inc +4 -0
  60. package/src/platforms/gametank/lib/gt/gfx/draw_direct.c +132 -0
  61. package/src/platforms/gametank/lib/gt/gfx/draw_direct.h +75 -0
  62. package/src/platforms/gametank/lib/gt/gfx/draw_queue.c +177 -0
  63. package/src/platforms/gametank/lib/gt/gfx/draw_queue.h +35 -0
  64. package/src/platforms/gametank/lib/gt/gfx/draw_util.s +157 -0
  65. package/src/platforms/gametank/lib/gt/gfx/gfx_sys.c +43 -0
  66. package/src/platforms/gametank/lib/gt/gfx/gfx_sys.h +46 -0
  67. package/src/platforms/gametank/lib/gt/gfx/sprites.c +216 -0
  68. package/src/platforms/gametank/lib/gt/gfx/sprites.h +41 -0
  69. package/src/platforms/gametank/lib/gt/init.c +9 -0
  70. package/src/platforms/gametank/lib/gt/input.c +26 -0
  71. package/src/platforms/gametank/lib/gt/input.h +20 -0
  72. package/src/platforms/gametank/lib/gt/interrupt.s +67 -0
  73. package/src/platforms/gametank/lib/gt/vectors.s +14 -0
  74. package/src/platforms/gametank/lib/gt/wait.s +53 -0
  75. package/src/platforms/gba/MENTAL_MODEL.md +22 -1
  76. package/src/playtest/playtest.js +174 -26
  77. package/src/playtest/resampler/build.sh +19 -0
  78. package/src/playtest/resampler/index.mjs +75 -0
  79. package/src/playtest/resampler/resampler.c +129 -0
  80. package/src/playtest/resampler/resampler.mjs +2 -0
  81. package/src/playtest/resampler/resampler.wasm +0 -0
  82. package/src/toolchains/arm-none-eabi-gcc/gcc.js +39 -188
  83. package/src/toolchains/asar/asar.js +10 -15
  84. package/src/toolchains/cc65/cc65.js +82 -92
  85. package/src/toolchains/cc65/da65.js +12 -17
  86. package/src/toolchains/cc65/preset-resolver.js +13 -2
  87. package/src/toolchains/cc65/presets/gametank/gametank.h +80 -0
  88. package/src/toolchains/cc65/presets/gametank/sdk.cfg +32 -0
  89. package/src/toolchains/cc65/presets/gametank/sdk.crt0.s +61 -0
  90. package/src/toolchains/cc65/presets/gametank/sdk.vectors.s +11 -0
  91. package/src/toolchains/cc65/presets/gametank/single-bank.cfg +28 -0
  92. package/src/toolchains/cc65/presets/gametank/single-bank.crt0.s +71 -0
  93. package/src/toolchains/cc65/presets/gametank/single-bank.vectors.s +12 -0
  94. package/src/toolchains/common/c-build.js +109 -0
  95. package/src/toolchains/common/gcc-toolchain.js +164 -0
  96. package/src/toolchains/common/wasm-tool.js +101 -0
  97. package/src/toolchains/dasm/dasm.js +12 -18
  98. package/src/toolchains/gba-c/gba-c.js +253 -305
  99. package/src/toolchains/genesis-c/genesis-c.js +196 -225
  100. package/src/toolchains/index.js +58 -4
  101. package/src/toolchains/m68k-elf-gcc/gcc.js +39 -202
  102. package/src/toolchains/mips-c/mips-c.js +68 -78
  103. package/src/toolchains/mips-elf-gcc/gcc.js +55 -118
  104. package/src/toolchains/rgbds/rgbds.js +7 -19
  105. package/src/toolchains/sdcc/sdcc.js +35 -26
  106. package/src/toolchains/sh-c/sh-c.js +44 -52
  107. package/src/toolchains/sh-elf-gcc/gcc.js +55 -110
  108. package/src/toolchains/sjasm/sjasm.js +10 -14
  109. package/src/toolchains/snes-c/snes-c.js +125 -150
  110. package/src/toolchains/tcc816/tcc816.js +10 -15
  111. package/src/toolchains/vasm68k/vasm68k.js +11 -16
  112. package/src/toolchains/wladx/wladx.js +5 -17
  113. package/src/toolchains/z80/binutils.js +5 -11
@@ -0,0 +1,129 @@
1
+ /* ── resampler.c — WASM+SIMD linear resampler for the romdev playtest audio sink ─
2
+ *
3
+ * Resamples interleaved S16 STEREO PCM from a source rate to a device rate.
4
+ *
5
+ * WHY this exists: the libretro CONTRACT is that a core declares its native audio
6
+ * rate in get_system_av_info and emits raw samples at that rate; the FRONTEND is
7
+ * responsible for resampling to the audio device (RetroArch does this in C with a
8
+ * sinc resampler). Every romdev core sits at 31–48 kHz EXCEPT the GameTank ACP at
9
+ * ~13983 Hz — 2.3x lower than the next core. At that rate SDL's fixed device
10
+ * buffer (4096 samples ≈ 293 ms) starves between 60 fps ticks that each feed only
11
+ * ~233 samples → clicks and pops. So the playtest sink opens the device at 48 kHz
12
+ * and resamples low-rate cores up to it. Doing that per-frame in JS is wasteful;
13
+ * this is the native-speed SIMD path (linear interp, 4 output frames/iteration).
14
+ *
15
+ * Build: see build.sh (emcc -O3 -msimd128). Exports rs_alloc/rs_free/rs_resample.
16
+ *
17
+ * Contract:
18
+ * rs_resample(inPtr, inFrames, outPtr, outCap, srcRate, dstRate) -> outFrames
19
+ * inPtr : int16_t* interleaved L,R,L,R… at srcRate (inFrames stereo frames)
20
+ * outPtr : int16_t* interleaved buffer with room for outCap stereo frames
21
+ * returns the number of stereo frames written (<= outCap).
22
+ * The caller sizes outCap >= ceil(inFrames * dstRate/srcRate) + 1.
23
+ */
24
+ #include <stdint.h>
25
+ #include <wasm_simd128.h>
26
+ #include <emscripten.h>
27
+
28
+ #define EXPORT EMSCRIPTEN_KEEPALIVE
29
+
30
+ /* scratch buffers live in the WASM heap, allocated from JS via rs_alloc. */
31
+ EXPORT void *rs_alloc(int bytes) { return __builtin_malloc((unsigned long)bytes); }
32
+ EXPORT void rs_free(void *p) { __builtin_free(p); }
33
+
34
+ /* Linear-resample interleaved S16 stereo. Returns stereo frames written.
35
+ *
36
+ * For output frame i: srcPos = i / ratio = i * srcRate/dstRate. We split srcPos
37
+ * into integer index i0 and fraction f in [0,1): out = in[i0]*(1-f) + in[i0+1]*f
38
+ * per channel. We vectorize across 4 consecutive output frames: compute their 4
39
+ * srcPos in f32x4, their 4 integer indices and 4 fractions, gather the 8 source
40
+ * samples (can't SIMD-gather in wasm128, so scalar gather) but do the lerp math in
41
+ * SIMD. The gather dominates, but keeping the arithmetic in f32x4 + a single
42
+ * saturating narrow per 4 frames is still a clear win over per-sample JS.
43
+ */
44
+ EXPORT int rs_resample(const int16_t *in, int inFrames,
45
+ int16_t *out, int outCap,
46
+ int srcRate, int dstRate) {
47
+ if (inFrames < 2 || srcRate <= 0 || dstRate <= 0) return 0;
48
+ if (srcRate == dstRate) {
49
+ int n = inFrames < outCap ? inFrames : outCap;
50
+ for (int i = 0; i < n * 2; i++) out[i] = in[i];
51
+ return n;
52
+ }
53
+
54
+ /* step = srcRate/dstRate in source frames per output frame (fixed math in f64). */
55
+ const double step = (double)srcRate / (double)dstRate;
56
+ long outFrames = (long)((double)inFrames * (double)dstRate / (double)srcRate);
57
+ if (outFrames > outCap) outFrames = outCap;
58
+ const int maxI0 = inFrames - 2; /* so i0+1 is valid */
59
+
60
+ long i = 0;
61
+ /* SIMD body: 4 output frames per iteration. */
62
+ for (; i + 4 <= outFrames; i += 4) {
63
+ /* source positions for the 4 frames */
64
+ double p0 = (double)(i + 0) * step;
65
+ double p1 = (double)(i + 1) * step;
66
+ double p2 = (double)(i + 2) * step;
67
+ double p3 = (double)(i + 3) * step;
68
+
69
+ int i0_0 = (int)p0, i0_1 = (int)p1, i0_2 = (int)p2, i0_3 = (int)p3;
70
+ if (i0_0 > maxI0) i0_0 = maxI0; if (i0_1 > maxI0) i0_1 = maxI0;
71
+ if (i0_2 > maxI0) i0_2 = maxI0; if (i0_3 > maxI0) i0_3 = maxI0;
72
+
73
+ /* fractions as f32x4 */
74
+ v128_t frac = wasm_f32x4_make((float)(p0 - i0_0), (float)(p1 - i0_1),
75
+ (float)(p2 - i0_2), (float)(p3 - i0_3));
76
+ v128_t inv = wasm_f32x4_sub(wasm_f32x4_splat(1.0f), frac);
77
+
78
+ /* gather the 8 source samples per channel (scalar — no wasm128 gather). */
79
+ v128_t l0 = wasm_f32x4_make((float)in[(i0_0*2)], (float)in[(i0_1*2)],
80
+ (float)in[(i0_2*2)], (float)in[(i0_3*2)]);
81
+ v128_t l1 = wasm_f32x4_make((float)in[(i0_0*2)+2], (float)in[(i0_1*2)+2],
82
+ (float)in[(i0_2*2)+2], (float)in[(i0_3*2)+2]);
83
+ v128_t r0 = wasm_f32x4_make((float)in[(i0_0*2)+1], (float)in[(i0_1*2)+1],
84
+ (float)in[(i0_2*2)+1], (float)in[(i0_3*2)+1]);
85
+ v128_t r1 = wasm_f32x4_make((float)in[(i0_0*2)+3], (float)in[(i0_1*2)+3],
86
+ (float)in[(i0_2*2)+3], (float)in[(i0_3*2)+3]);
87
+
88
+ /* lerp: l = l0*inv + l1*frac */
89
+ v128_t lo = wasm_f32x4_add(wasm_f32x4_mul(l0, inv), wasm_f32x4_mul(l1, frac));
90
+ v128_t ro = wasm_f32x4_add(wasm_f32x4_mul(r0, inv), wasm_f32x4_mul(r1, frac));
91
+
92
+ /* round to nearest, convert to i32, store interleaved. */
93
+ v128_t half = wasm_f32x4_splat(0.5f);
94
+ v128_t lneg = wasm_f32x4_lt(lo, wasm_f32x4_splat(0.0f));
95
+ v128_t rneg = wasm_f32x4_lt(ro, wasm_f32x4_splat(0.0f));
96
+ lo = wasm_f32x4_add(lo, wasm_v128_bitselect(wasm_f32x4_splat(-0.5f), half, lneg));
97
+ ro = wasm_f32x4_add(ro, wasm_v128_bitselect(wasm_f32x4_splat(-0.5f), half, rneg));
98
+ v128_t li = wasm_i32x4_trunc_sat_f32x4(lo);
99
+ v128_t ri = wasm_i32x4_trunc_sat_f32x4(ro);
100
+
101
+ int li_[4], ri_[4];
102
+ wasm_v128_store(li_, li);
103
+ wasm_v128_store(ri_, ri);
104
+ for (int k = 0; k < 4; k++) {
105
+ int lv = li_[k], rv = ri_[k];
106
+ if (lv > 32767) lv = 32767; else if (lv < -32768) lv = -32768;
107
+ if (rv > 32767) rv = 32767; else if (rv < -32768) rv = -32768;
108
+ out[(i + k) * 2] = (int16_t)lv;
109
+ out[(i + k) * 2 + 1] = (int16_t)rv;
110
+ }
111
+ }
112
+
113
+ /* scalar tail */
114
+ for (; i < outFrames; i++) {
115
+ double p = (double)i * step;
116
+ int i0 = (int)p; if (i0 > maxI0) i0 = maxI0;
117
+ float f = (float)(p - i0), invf = 1.0f - f;
118
+ float l = in[i0*2] * invf + in[(i0+1)*2] * f;
119
+ float r = in[i0*2+1] * invf + in[(i0+1)*2+1] * f;
120
+ int lv = (int)(l < 0 ? l - 0.5f : l + 0.5f);
121
+ int rv = (int)(r < 0 ? r - 0.5f : r + 0.5f);
122
+ if (lv > 32767) lv = 32767; else if (lv < -32768) lv = -32768;
123
+ if (rv > 32767) rv = 32767; else if (rv < -32768) rv = -32768;
124
+ out[i*2] = (int16_t)lv;
125
+ out[i*2+1] = (int16_t)rv;
126
+ }
127
+
128
+ return (int)outFrames;
129
+ }
@@ -0,0 +1,2 @@
1
+ async function Module(moduleArg={}){var moduleRtn;var Module=moduleArg;var ENVIRONMENT_IS_WEB=!!globalThis.window;var ENVIRONMENT_IS_WORKER=!!globalThis.WorkerGlobalScope;var ENVIRONMENT_IS_NODE=globalThis.process?.versions?.node&&globalThis.process?.type!="renderer";if(ENVIRONMENT_IS_NODE){const{createRequire}=await import("module");var require=createRequire(import.meta.url)}var arguments_=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};var _scriptName=import.meta.url;var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_NODE){var fs=require("fs");if(_scriptName.startsWith("file:")){scriptDirectory=require("path").dirname(require("url").fileURLToPath(_scriptName))+"/"}readBinary=filename=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename);return ret};readAsync=async(filename,binary=true)=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename,binary?undefined:"utf8");return ret};if(process.argv.length>1){thisProgram=process.argv[1].replace(/\\/g,"/")}arguments_=process.argv.slice(2);quit_=(status,toThrow)=>{process.exitCode=status;throw toThrow}}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){try{scriptDirectory=new URL(".",_scriptName).href}catch{}{if(ENVIRONMENT_IS_WORKER){readBinary=url=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return new Uint8Array(xhr.response)}}readAsync=async url=>{var response=await fetch(url,{credentials:"same-origin"});if(response.ok){return response.arrayBuffer()}throw new Error(response.status+" : "+response.url)}}}else{}var out=console.log.bind(console);var err=console.error.bind(console);var wasmBinary;var ABORT=false;var isFileURI=filename=>filename.startsWith("file://");var readyPromiseResolve,readyPromiseReject;var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;var HEAP64,HEAPU64;var runtimeInitialized=false;function updateMemoryViews(){var b=wasmMemory.buffer;HEAP8=new Int8Array(b);Module["HEAP16"]=HEAP16=new Int16Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);HEAPU16=new Uint16Array(b);HEAP32=new Int32Array(b);HEAPU32=new Uint32Array(b);HEAPF32=new Float32Array(b);HEAPF64=new Float64Array(b);HEAP64=new BigInt64Array(b);HEAPU64=new BigUint64Array(b)}function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="function")Module["preRun"]=[Module["preRun"]];while(Module["preRun"].length){addOnPreRun(Module["preRun"].shift())}}callRuntimeCallbacks(onPreRuns)}function initRuntime(){runtimeInitialized=true;wasmExports["c"]()}function postRun(){if(Module["postRun"]){if(typeof Module["postRun"]=="function")Module["postRun"]=[Module["postRun"]];while(Module["postRun"].length){addOnPostRun(Module["postRun"].shift())}}callRuntimeCallbacks(onPostRuns)}function abort(what){Module["onAbort"]?.(what);what="Aborted("+what+")";err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);readyPromiseReject?.(e);throw e}var wasmBinaryFile;function findWasmBinary(){if(Module["locateFile"]){return locateFile("resampler.wasm")}return new URL("resampler.wasm",import.meta.url).href}function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}async function getWasmBinary(binaryFile){if(!wasmBinary){try{var response=await readAsync(binaryFile);return new Uint8Array(response)}catch{}}return getBinarySync(binaryFile)}async function instantiateArrayBuffer(binaryFile,imports){try{var binary=await getWasmBinary(binaryFile);var instance=await WebAssembly.instantiate(binary,imports);return instance}catch(reason){err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)}}async function instantiateAsync(binary,binaryFile,imports){if(!binary&&!ENVIRONMENT_IS_NODE){try{var response=fetch(binaryFile,{credentials:"same-origin"});var instantiationResult=await WebAssembly.instantiateStreaming(response,imports);return instantiationResult}catch(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation")}}return instantiateArrayBuffer(binaryFile,imports)}function getWasmImports(){var imports={a:wasmImports};return imports}async function createWasm(){function receiveInstance(instance,module){wasmExports=instance.exports;assignWasmExports(wasmExports);updateMemoryViews();return wasmExports}function receiveInstantiationResult(result){return receiveInstance(result["instance"])}var info=getWasmImports();if(Module["instantiateWasm"]){return new Promise((resolve,reject)=>{Module["instantiateWasm"](info,(inst,mod)=>{resolve(receiveInstance(inst,mod))})})}wasmBinaryFile??=findWasmBinary();var result=await instantiateAsync(wasmBinary,wasmBinaryFile,info);var exports=receiveInstantiationResult(result);return exports}class ExitStatus{name="ExitStatus";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var onPostRuns=[];var addOnPostRun=cb=>onPostRuns.push(cb);var onPreRuns=[];var addOnPreRun=cb=>onPreRuns.push(cb);var noExitRuntime=true;var stackRestore=val=>__emscripten_stack_restore(val);var stackSave=()=>_emscripten_stack_get_current();var getHeapMax=()=>2147483648;var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var growMemory=size=>{var oldHeapSize=wasmMemory.buffer.byteLength;var pages=(size-oldHeapSize+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var getCFunc=ident=>{var func=Module["_"+ident];return func};var writeArrayToMemory=(array,buffer)=>{HEAP8.set(array,buffer)};var lengthBytesUTF8=str=>{var len=0;for(var i=0;i<str.length;++i){var c=str.charCodeAt(i);if(c<=127){len++}else if(c<=2047){len+=2}else if(c>=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i<str.length;++i){var u=str.codePointAt(i);if(u<=127){if(outIdx>=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63;i++}}heap[outIdx]=0;return outIdx-startIdx};var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);var stackAlloc=sz=>__emscripten_stack_alloc(sz);var stringToUTF8OnStack=str=>{var size=lengthBytesUTF8(str)+1;var ret=stackAlloc(size);stringToUTF8(str,ret,size);return ret};var UTF8Decoder=globalThis.TextDecoder&&new TextDecoder;var findStringEnd=(heapOrArray,idx,maxBytesToRead,ignoreNul)=>{var maxIdx=idx+maxBytesToRead;if(ignoreNul)return maxIdx;while(heapOrArray[idx]&&!(idx>=maxIdx))++idx;return idx};var UTF8ArrayToString=(heapOrArray,idx=0,maxBytesToRead,ignoreNul)=>{var endPtr=findStringEnd(heapOrArray,idx,maxBytesToRead,ignoreNul);if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx<endPtr){var u0=heapOrArray[idx++];if(!(u0&128)){str+=String.fromCharCode(u0);continue}var u1=heapOrArray[idx++]&63;if((u0&224)==192){str+=String.fromCharCode((u0&31)<<6|u1);continue}var u2=heapOrArray[idx++]&63;if((u0&240)==224){u0=(u0&15)<<12|u1<<6|u2}else{u0=(u0&7)<<18|u1<<12|u2<<6|heapOrArray[idx++]&63}if(u0<65536){str+=String.fromCharCode(u0)}else{var ch=u0-65536;str+=String.fromCharCode(55296|ch>>10,56320|ch&1023)}}return str};var UTF8ToString=(ptr,maxBytesToRead,ignoreNul)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead,ignoreNul):"";var ccall=(ident,returnType,argTypes,args,opts)=>{var toC={string:str=>{var ret=0;if(str!==null&&str!==undefined&&str!==0){ret=stringToUTF8OnStack(str)}return ret},array:arr=>{var ret=stackAlloc(arr.length);writeArrayToMemory(arr,ret);return ret}};function convertReturnValue(ret){if(returnType==="string"){return UTF8ToString(ret)}if(returnType==="boolean")return Boolean(ret);return ret}var func=getCFunc(ident);var cArgs=[];var stack=0;if(args){for(var i=0;i<args.length;i++){var converter=toC[argTypes[i]];if(converter){if(stack===0)stack=stackSave();cArgs[i]=converter(args[i])}else{cArgs[i]=args[i]}}}var ret=func(...cArgs);function onDone(ret){if(stack!==0)stackRestore(stack);return convertReturnValue(ret)}ret=onDone(ret);return ret};var cwrap=(ident,returnType,argTypes,opts)=>{var numericArgs=!argTypes||argTypes.every(type=>type==="number"||type==="boolean");var numericRet=returnType!=="string";if(numericRet&&numericArgs&&!opts){return getCFunc(ident)}return(...args)=>ccall(ident,returnType,argTypes,args,opts)};{if(Module["noExitRuntime"])noExitRuntime=Module["noExitRuntime"];if(Module["print"])out=Module["print"];if(Module["printErr"])err=Module["printErr"];if(Module["wasmBinary"])wasmBinary=Module["wasmBinary"];if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].shift()()}}}Module["ccall"]=ccall;Module["cwrap"]=cwrap;var _rs_alloc,_malloc,_rs_free,_free,_rs_resample,__emscripten_stack_restore,__emscripten_stack_alloc,_emscripten_stack_get_current,memory,__indirect_function_table,wasmMemory;function assignWasmExports(wasmExports){_rs_alloc=Module["_rs_alloc"]=wasmExports["d"];_malloc=Module["_malloc"]=wasmExports["e"];_rs_free=Module["_rs_free"]=wasmExports["f"];_free=Module["_free"]=wasmExports["g"];_rs_resample=Module["_rs_resample"]=wasmExports["h"];__emscripten_stack_restore=wasmExports["i"];__emscripten_stack_alloc=wasmExports["j"];_emscripten_stack_get_current=wasmExports["k"];memory=wasmMemory=wasmExports["b"];__indirect_function_table=wasmExports["__indirect_function_table"]}var wasmImports={a:_emscripten_resize_heap};function run(){preRun();function doRun(){Module["calledRun"]=true;if(ABORT)return;initRuntime();readyPromiseResolve?.(Module);Module["onRuntimeInitialized"]?.();postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(()=>{setTimeout(()=>Module["setStatus"](""),1);doRun()},1)}else{doRun()}}var wasmExports;wasmExports=await (createWasm());run();if(runtimeInitialized){moduleRtn=Module}else{moduleRtn=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject})}
2
+ ;return moduleRtn}export default Module;
@@ -1,216 +1,67 @@
1
1
  // arm-none-eabi-gcc — WASM toolchain wrappers for GBA C builds.
2
2
  //
3
- // Mirrors src/toolchains/m68k-elf-gcc/gcc.js. The GBA target uses
4
- // ARM7TDMI in Thumb-interwork mode by default (matches libgba's
5
- // expectations).
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
- // Pipeline:
8
- // runCc1arm({source, headers, options}) → ARM assembly text (.s)
9
- // runArmAs({asmSource, includes}) → .o ELF object
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
- // Each step runs as a WASM module through the worker pool (R12
14
- // crash isolation). The WASM tools live under wasm/ (cc1-arm.{mjs,wasm}
15
- // etc.). emcc emits ESM (EXPORT_ES6=1) so we use .mjs extensions.
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 { runIsolated, textFile, binaryFile, getOutputBytes, getOutputText } from "../_worker/run.js";
19
+ import { makeGccToolchain } from "../common/gcc-toolchain.js";
22
20
 
23
- const __filename = fileURLToPath(import.meta.url);
24
- const __dirname = path.dirname(__filename);
21
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
25
22
 
26
- // arm-none-eabi-gcc's WASM ships in romdev-platform-gba (the GBA platform is
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
- // Lazy + memoized per tool: resolve only on the first GBA-C build that uses
43
- // each tool, not at module load — so booting the server never touches this
44
- // (155MB, incl. the 135MB cc1-arm) package unless a GBA C ROM is actually built.
45
- const _glue = {};
46
- const armGlue = (file) => (_glue[file] ??= resolveArmGlue(file));
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 async function runCc1arm(args) {
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 async function runArmAs(args) {
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
- * @param {Object} args
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 async function runArmLd(args) {
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 async function runArmObjcopy(args) {
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). Resolve from that package; fall back to a local copy under src/
18
- // if present (transition / dev). The package is a hard dep of romdev.
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 = () => (_gluePath ??= resolveAsarGlue());
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.