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.
Files changed (110) hide show
  1. package/AGENTS.md +19 -16
  2. package/CHANGELOG.md +175 -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/platforms/gametank/lib/gt/audio/acp_image.bin +0 -0
  28. package/src/platforms/gametank/lib/gt/audio/audio_coprocessor.c +55 -0
  29. package/src/platforms/gametank/lib/gt/audio/audio_coprocessor.h +34 -0
  30. package/src/platforms/gametank/lib/gt/audio/audio_fw.asm +223 -0
  31. package/src/platforms/gametank/lib/gt/audio/audio_fw_blob.s +8 -0
  32. package/src/platforms/gametank/lib/gt/audio/instruments.c +79 -0
  33. package/src/platforms/gametank/lib/gt/audio/instruments.h +25 -0
  34. package/src/platforms/gametank/lib/gt/audio/music.c +370 -0
  35. package/src/platforms/gametank/lib/gt/audio/music.h +35 -0
  36. package/src/platforms/gametank/lib/gt/audio/note_numbers.h +132 -0
  37. package/src/platforms/gametank/lib/gt/audio/scaled_sines.raw +0 -0
  38. package/src/platforms/gametank/lib/gt/audio/sine_256_-63_63.bin +0 -0
  39. package/src/platforms/gametank/lib/gt/banking.c +29 -0
  40. package/src/platforms/gametank/lib/gt/banking.h +8 -0
  41. package/src/platforms/gametank/lib/gt/banking2.s +41 -0
  42. package/src/platforms/gametank/lib/gt/crt0.s +102 -0
  43. package/src/platforms/gametank/lib/gt/draw_logo.s +55 -0
  44. package/src/platforms/gametank/lib/gt/feature/persist/persist.c +103 -0
  45. package/src/platforms/gametank/lib/gt/feature/persist/persist.h +13 -0
  46. package/src/platforms/gametank/lib/gt/feature/random/random.c +40 -0
  47. package/src/platforms/gametank/lib/gt/feature/random/random.h +18 -0
  48. package/src/platforms/gametank/lib/gt/feature/text/text.c +111 -0
  49. package/src/platforms/gametank/lib/gt/feature/text/text.h +25 -0
  50. package/src/platforms/gametank/lib/gt/gametank.c +12 -0
  51. package/src/platforms/gametank/lib/gt/gametank.h +80 -0
  52. package/src/platforms/gametank/lib/gt/gametank_logo.inc +393 -0
  53. package/src/platforms/gametank/lib/gt/gen/assets/assets_index.h +9 -0
  54. package/src/platforms/gametank/lib/gt/gen/assets/sdk_default.h +5 -0
  55. package/src/platforms/gametank/lib/gt/gen/bank_nums.h +11 -0
  56. package/src/platforms/gametank/lib/gt/gen/modules_enabled.h +4 -0
  57. package/src/platforms/gametank/lib/gt/gen/modules_enabled.inc +4 -0
  58. package/src/platforms/gametank/lib/gt/gfx/draw_direct.c +132 -0
  59. package/src/platforms/gametank/lib/gt/gfx/draw_direct.h +75 -0
  60. package/src/platforms/gametank/lib/gt/gfx/draw_queue.c +177 -0
  61. package/src/platforms/gametank/lib/gt/gfx/draw_queue.h +35 -0
  62. package/src/platforms/gametank/lib/gt/gfx/draw_util.s +157 -0
  63. package/src/platforms/gametank/lib/gt/gfx/gfx_sys.c +43 -0
  64. package/src/platforms/gametank/lib/gt/gfx/gfx_sys.h +46 -0
  65. package/src/platforms/gametank/lib/gt/gfx/sprites.c +216 -0
  66. package/src/platforms/gametank/lib/gt/gfx/sprites.h +41 -0
  67. package/src/platforms/gametank/lib/gt/init.c +9 -0
  68. package/src/platforms/gametank/lib/gt/input.c +26 -0
  69. package/src/platforms/gametank/lib/gt/input.h +20 -0
  70. package/src/platforms/gametank/lib/gt/interrupt.s +67 -0
  71. package/src/platforms/gametank/lib/gt/vectors.s +14 -0
  72. package/src/platforms/gametank/lib/gt/wait.s +53 -0
  73. package/src/playtest/playtest.js +174 -26
  74. package/src/playtest/resampler/build.sh +19 -0
  75. package/src/playtest/resampler/index.mjs +75 -0
  76. package/src/playtest/resampler/resampler.c +129 -0
  77. package/src/playtest/resampler/resampler.mjs +2 -0
  78. package/src/playtest/resampler/resampler.wasm +0 -0
  79. package/src/toolchains/arm-none-eabi-gcc/gcc.js +39 -188
  80. package/src/toolchains/asar/asar.js +10 -15
  81. package/src/toolchains/cc65/cc65.js +82 -92
  82. package/src/toolchains/cc65/da65.js +12 -17
  83. package/src/toolchains/cc65/preset-resolver.js +13 -2
  84. package/src/toolchains/cc65/presets/gametank/gametank.h +80 -0
  85. package/src/toolchains/cc65/presets/gametank/sdk.cfg +32 -0
  86. package/src/toolchains/cc65/presets/gametank/sdk.crt0.s +61 -0
  87. package/src/toolchains/cc65/presets/gametank/sdk.vectors.s +11 -0
  88. package/src/toolchains/cc65/presets/gametank/single-bank.cfg +28 -0
  89. package/src/toolchains/cc65/presets/gametank/single-bank.crt0.s +71 -0
  90. package/src/toolchains/cc65/presets/gametank/single-bank.vectors.s +12 -0
  91. package/src/toolchains/common/c-build.js +109 -0
  92. package/src/toolchains/common/gcc-toolchain.js +164 -0
  93. package/src/toolchains/common/wasm-tool.js +101 -0
  94. package/src/toolchains/dasm/dasm.js +12 -18
  95. package/src/toolchains/gba-c/gba-c.js +253 -305
  96. package/src/toolchains/genesis-c/genesis-c.js +196 -225
  97. package/src/toolchains/index.js +58 -4
  98. package/src/toolchains/m68k-elf-gcc/gcc.js +39 -202
  99. package/src/toolchains/mips-c/mips-c.js +68 -78
  100. package/src/toolchains/mips-elf-gcc/gcc.js +55 -118
  101. package/src/toolchains/rgbds/rgbds.js +7 -19
  102. package/src/toolchains/sdcc/sdcc.js +35 -26
  103. package/src/toolchains/sh-c/sh-c.js +44 -52
  104. package/src/toolchains/sh-elf-gcc/gcc.js +55 -110
  105. package/src/toolchains/sjasm/sjasm.js +10 -14
  106. package/src/toolchains/snes-c/snes-c.js +125 -150
  107. package/src/toolchains/tcc816/tcc816.js +10 -15
  108. package/src/toolchains/vasm68k/vasm68k.js +11 -16
  109. package/src/toolchains/wladx/wladx.js +5 -17
  110. package/src/toolchains/z80/binutils.js +5 -11
@@ -6,8 +6,10 @@ import {
6
6
  RETRO_PIXEL_FORMAT_0RGB1555,
7
7
  RETRO_PIXEL_FORMAT_RGB565,
8
8
  RETRO_PIXEL_FORMAT_XRGB8888,
9
+ ROMDEV_PIXEL_FORMAT_RGBA8888,
9
10
  } from "../host/retroConstants.js";
10
11
  import { log } from "../mcp/log.js";
12
+ import { initResampler, resampleS16Stereo } from "./resampler/index.mjs";
11
13
  import path from "node:path";
12
14
  import { existsSync, writeFileSync, renameSync, mkdirSync } from "node:fs";
13
15
  import { execFile } from "node:child_process";
@@ -203,10 +205,39 @@ const SDL_BUTTON_TO_LIBRETRO_BIT = {
203
205
  back: 2, // SELECT
204
206
  guide: 2,
205
207
  start: 3,
206
- leftShoulder: 10,
207
- rightShoulder: 11,
208
- leftStick: 14,
209
- rightStick: 15,
208
+ leftShoulder: 10, // RETRO L
209
+ rightShoulder: 11, // RETRO R
210
+ leftStick: 14, // RETRO L3
211
+ rightStick: 15, // RETRO R3
212
+ // NOTE: L2/R2 (bits 12/13) are ANALOG triggers in libretro — node-sdl exposes
213
+ // them as axes (leftTrigger/rightTrigger), not buttons, so they're read from
214
+ // inst.axes below, not here.
215
+ };
216
+
217
+ // N64-specific pad map. parallel_n64's RetroPad layout (its digital_cbuttons_map)
218
+ // is NOT the generic NES/SNES one — RETRO B="N64 A", RETRO Y="N64 B", RETRO X/A/L/R
219
+ // are the four C-buttons, RETRO Select="N64 L", RETRO R2="N64 R", RETRO L2="N64 Z".
220
+ // The N64's Z/L/R are DIGITAL buttons (its analog triggers don't exist), so on a
221
+ // modern pad they go on the SHOULDER buttons, NOT the analog triggers (which idle
222
+ // half-pressed and would stick). Layout for a standard X360-style pad:
223
+ // Xbox A (bottom) = N64 A (accelerate)
224
+ // Xbox B (right) = N64 B (brake)
225
+ // Xbox X (left) = N64 Z (FIRE ITEM) ← a free face button, easy to reach
226
+ // Xbox Y (top) = N64 B (alias, so either right/top brakes)
227
+ // L shoulder = N64 L
228
+ // R shoulder = N64 R (hop / drift)
229
+ // right stick = the four C-buttons (in readControllerInto)
230
+ // left stick/dpad = N64 analog stick (via the dpad→ANALOG synth in callbacks.js)
231
+ const SDL_BUTTON_TO_LIBRETRO_BIT_N64 = {
232
+ dpadUp: 4, dpadDown: 5, dpadLeft: 6, dpadRight: 7, // N64 d-pad (literal)
233
+ a: 0, // Xbox A (bottom) → RETRO B = N64 A (accelerate)
234
+ b: 1, // Xbox B (right) → RETRO Y = N64 B (brake)
235
+ y: 1, // Xbox Y (top) → RETRO Y = N64 B (alias)
236
+ x: 12, // Xbox X (left) → RETRO L2 = N64 Z (FIRE ITEM)
237
+ start: 3, // Start
238
+ leftShoulder: 2, // L shoulder → RETRO Select = N64 L
239
+ rightShoulder: 13, // R shoulder → RETRO R2 = N64 R (hop/drift)
240
+ // C-buttons (RETRO X/A/L/R) come from the RIGHT STICK in readControllerInto.
210
241
  };
211
242
 
212
243
  // Analog stick → dpad direction (for games that only read dpad)
@@ -479,12 +510,22 @@ export async function playtest(args) {
479
510
  }
480
511
 
481
512
  // Open the window.
513
+ //
514
+ // HW-render cores (n64/ps1/dreamcast) already own a GL context via native-gles
515
+ // (the EGL pbuffer the core renders its RDP/GPU into; we glReadPixels it to CPU
516
+ // pixels). If the SDL window ALSO requests an accelerated (GL) renderer, node-sdl
517
+ // calls glXMakeCurrent on the same X display and the two GL contexts collide →
518
+ // `X Error BadAccess (GLX X_GLXMakeCurrent)`, crashing the process. The window only
519
+ // ever presents CPU pixels (window.render(..., "rgba32", rgba)), so it does NOT need
520
+ // its own GL context — open a SOFTWARE-blit window for HW-render cores to avoid the
521
+ // context fight. Software cores keep the accelerated path (faster upscale blit).
522
+ const hwRenderCore = !!host.hwRender;
482
523
  const window = sdl.video.createWindow({
483
524
  title,
484
525
  width: winInitW,
485
526
  height: winInitH,
486
527
  resizable: true,
487
- accelerated: true,
528
+ accelerated: hwRenderCore ? false : true,
488
529
  vsync: false,
489
530
  });
490
531
  log.debug(`[playtest] window opened: ${winInitW}x${winInitH}, fb=${fbWidth}x${fbHeight}, aspect=${aspectMode}`);
@@ -494,16 +535,38 @@ export async function playtest(args) {
494
535
  // Mismatched rates produce choppy/sped-up/cracking playback because the
495
536
  // SDL device consumes samples at the wrong rate, alternately starving
496
537
  // (clicks) and overflowing.
538
+ //
539
+ // EXCEPTION — very-low-rate cores (the GameTank ACP emits ~13983 Hz, 3x lower
540
+ // than anything else): SDL's device buffer granularity (thousands of samples)
541
+ // dwarfs a 60 fps core's ~233-sample-per-frame chunks at that rate, so the
542
+ // device starves between ticks = clicks and pops. (RetroArch avoids this with
543
+ // a sinc resampler + dynamic rate control to the device rate.) So for low
544
+ // rates we open the device at 48 kHz and LINEAR-RESAMPLE each chunk up to it,
545
+ // which makes the per-frame chunks big enough for clean playback.
497
546
  let audio = null;
498
547
  const coreSampleRate = Math.round(host.status.audioSampleRate ?? 48000);
548
+ const AUDIO_RESAMPLE_TO = 48000;
549
+ let needsResample = coreSampleRate > 0 && coreSampleRate < 24000;
550
+ // Load the WASM+SIMD resampler if this core needs upsampling. If it fails to
551
+ // load, fall back to opening the device at the native rate (better than no
552
+ // audio) — needsResample is forced off so we never call a missing resampler.
553
+ if (needsResample) {
554
+ const ready = await initResampler();
555
+ if (!ready) {
556
+ log.error("[playtest] resampler WASM failed to load — using native rate (audio may click)");
557
+ needsResample = false;
558
+ }
559
+ }
560
+ const deviceSampleRate = needsResample ? AUDIO_RESAMPLE_TO : coreSampleRate;
499
561
  try {
500
562
  audio = sdl.audio.openDevice({ type: "playback" }, {
501
563
  channels: 2,
502
- frequency: coreSampleRate,
564
+ frequency: deviceSampleRate,
503
565
  format: "s16",
504
566
  });
505
567
  audio.play();
506
- log.debug(`[playtest] audio: ${coreSampleRate} Hz, stereo, s16`);
568
+ log.debug(`[playtest] audio: ${deviceSampleRate} Hz, stereo, s16` +
569
+ (needsResample ? ` (resampled from core ${coreSampleRate} Hz)` : ""));
507
570
  } catch (e) {
508
571
  log.error("[playtest] audio init failed (continuing silent):", e.message);
509
572
  }
@@ -688,7 +751,15 @@ export async function playtest(args) {
688
751
 
689
752
  // Tick = one emulated frame + render + audio drain. Driven by setInterval
690
753
  // so the Node event loop stays free for MCP requests on the same host.
691
- const frameMs = 1000 / 60;
754
+ // Pace to the CORE's native refresh rate (status.coreFps), not a hardcoded 60: a
755
+ // 30fps title (Sonic Adventure on flycast) at a 60Hz tick gets double-ticked —
756
+ // wasting half the budget and, on the heavy interpreter-only DC core (23ms/frame,
757
+ // no JIT), falling behind every tick → the black-flash/glitch. At its real 30fps
758
+ // each frame gets a full 33ms tick, which the core can actually hit. Clamped so a
759
+ // bogus report can't run the window absurdly fast or slow.
760
+ const coreFps = openHost?.status?.coreFps;
761
+ const fps = (coreFps >= 20 && coreFps <= 120) ? coreFps : 60;
762
+ const frameMs = 1000 / fps;
692
763
 
693
764
  function tick() {
694
765
  if (!running || window.destroyed) { stop(); return; }
@@ -704,7 +775,12 @@ export async function playtest(args) {
704
775
  // the N-second cadence; serialize off the live host so it captures the human's
705
776
  // exact progress. Skipped while paused (nothing changed) and on the very first
706
777
  // ticks (let the core settle).
707
- if (checkpointPath && tickCount - lastCheckpointTick >= checkpointEverySec * 60 && !h.status.paused) {
778
+ // Auto-checkpoint serializes the WHOLE machine state cheap for 8/16-bit (KB,
779
+ // instant) but BRUTAL for the hwRender 3D cores (DC/N64 savestate ≈16MB, ~18ms
780
+ // to serialize), which would freeze the window for ~18ms every cadence on an
781
+ // already-slow core. Skip it entirely for hwRender — same call as the rewind
782
+ // buffer skip. (Eviction recovery matters less than a playable window here.)
783
+ if (!h.hwRender && checkpointPath && tickCount - lastCheckpointTick >= checkpointEverySec * 60 && !h.status.paused) {
708
784
  lastCheckpointTick = tickCount;
709
785
  writeCheckpoint(h, "auto");
710
786
  }
@@ -726,6 +802,7 @@ export async function playtest(args) {
726
802
  // window leaves it alone. Select+Start on any controller quits.
727
803
  let quit = false;
728
804
  const isC64 = h.status?.platform === "c64";
805
+ const isN64 = h.status?.platform === "n64";
729
806
  function readControllerInto(port, inst) {
730
807
  if (!inst) return;
731
808
  const btn = inst.buttons || {};
@@ -733,7 +810,8 @@ export async function playtest(args) {
733
810
  quit = true;
734
811
  return;
735
812
  }
736
- for (const [sdlName, bit] of Object.entries(SDL_BUTTON_TO_LIBRETRO_BIT)) {
813
+ const buttonMap = isN64 ? SDL_BUTTON_TO_LIBRETRO_BIT_N64 : SDL_BUTTON_TO_LIBRETRO_BIT;
814
+ for (const [sdlName, bit] of Object.entries(buttonMap)) {
737
815
  if (btn[sdlName]) port[bitToName(bit)] = true;
738
816
  }
739
817
  const axes = inst.axes || {};
@@ -743,6 +821,9 @@ export async function playtest(args) {
743
821
  else if (lx < -STICK_DEADZONE) port.left = true;
744
822
  if (ly > STICK_DEADZONE) port.down = true;
745
823
  else if (ly < -STICK_DEADZONE) port.up = true;
824
+ // NOTE: the analog triggers are NOT used for N64 — its Z/L/R are digital and
825
+ // map to the SHOULDER buttons (see SDL_BUTTON_TO_LIBRETRO_BIT_N64). (node-sdl's
826
+ // X360 trigger axes also idle at ~0.5, so reading them as buttons would stick.)
746
827
  // C64: the RIGHT stick selects the function keys (F1/F3/F5/F7) — the
747
828
  // Batocera/RetroDeck convention so a controller alone reaches the keyboard
748
829
  // keys C64 setup screens need. Emitted as virtual buttons the host's C64
@@ -755,6 +836,19 @@ export async function playtest(args) {
755
836
  if (rx < -STICK_DEADZONE) port.c64_f3 = true; // left → F3
756
837
  else if (rx > STICK_DEADZONE) port.c64_f5 = true; // right → F5
757
838
  }
839
+ // N64: the RIGHT stick drives the four C-buttons — the standard emulation
840
+ // convention so a modern dual-stick pad plays N64 naturally (left stick =
841
+ // analog stick via the d-pad synthesis in callbacks.js; right stick = C). The
842
+ // C-buttons land on libretro bits A/X/L/R, which parallel_n64 reads as
843
+ // C-Down/C-Up/C-Left/C-Right. Z is the left trigger (mapped above).
844
+ if (isN64) {
845
+ const rx = axes.rightStickX ?? 0;
846
+ const ry = axes.rightStickY ?? 0;
847
+ if (ry < -STICK_DEADZONE) port.x = true; // up → C-Up (RETRO X)
848
+ else if (ry > STICK_DEADZONE) port.a = true; // down → C-Down (RETRO A)
849
+ if (rx < -STICK_DEADZONE) port.l = true; // left → C-Left (RETRO L)
850
+ else if (rx > STICK_DEADZONE) port.r = true; // right → C-Right (RETRO R)
851
+ }
758
852
  }
759
853
 
760
854
  const port0 = {};
@@ -792,8 +886,14 @@ export async function playtest(args) {
792
886
  // either port)?
793
887
  const humanPressing = anyButtonHeld(port0) || anyButtonHeld(port1);
794
888
  humanInput.note(humanPressing, tickCount);
795
- // Capture snapshot before stepping so R can rewind to it later.
796
- if (h.status?.loaded) {
889
+ // Capture snapshot before stepping so R can rewind to it later. SKIP for
890
+ // hwRender cores (n64/ps1/dreamcast): their savestates are HUGE (N64 ≈16MB
891
+ // each — 600 frames would be ~9GB of RAM) and serializeState costs ~8ms/frame
892
+ // there, eating half the 16.6ms budget and starving the audio feed (the choppy
893
+ // playback). The R-key rewind is a nicety, not worth that on the 3D engines —
894
+ // pause + savestate still work for those. (Rewind buffer is playtest-only; it's
895
+ // NOT part of the debug ABI, so dropping it on these cores changes nothing else.)
896
+ if (h.status?.loaded && !h.hwRender) {
797
897
  try {
798
898
  const snap = h.serializeState();
799
899
  rewindBuffer.push(snap);
@@ -810,9 +910,39 @@ export async function playtest(args) {
810
910
  h.setInput({ ports: [port0, port1] });
811
911
  humanInputDirty = humanPressing;
812
912
  }
913
+ // AUDIO-PACED stepping, BUDGETED BY WALL-CLOCK. We catch the buffer up by
914
+ // stepping extra frames per tick to keep SDL's queue topped — but a fast core
915
+ // (n64 2.4ms/frame) and a SUB-REALTIME core (flycast DC ~60ms/frame) need very
916
+ // different burst sizes. A fixed MAX_STEPS frame-count cap is the trap: 8 frames
917
+ // is 19ms on n64 (fine) but 480ms on DC — which BLOCKS the Node event loop for
918
+ // half a second per tick, so the window can't repaint and audio drains dry →
919
+ // "breaks down, super choppy" death spiral that never recovers. The fix is to
920
+ // cap by TIME: keep stepping only while we're under a wall-clock budget (~1.5
921
+ // ticks). A sub-realtime core then runs steady-slow (audio underruns gracefully,
922
+ // a constant low pitch) instead of stuttering — and the loop ALWAYS yields the
923
+ // event loop promptly so the window stays responsive.
813
924
  let stepped = 0;
814
925
  try {
815
- stepped = h.stepFrames(1);
926
+ if (audio && deviceSampleRate > 0) {
927
+ const bps = deviceSampleRate * 4; // stereo s16
928
+ const TARGET_MS = 60; // keep ~60ms queued — drain sets the speed
929
+ const BUDGET_MS = frameMs * 1.5; // wall-clock ceiling for the whole burst
930
+ const burstStart = performance.now();
931
+ do {
932
+ stepped += h.stepFrames(1);
933
+ // Stop the instant we've spent our wall-clock budget — this is what keeps
934
+ // a slow core from freezing the loop. A single frame already over budget
935
+ // still steps once (progress), then we yield.
936
+ if (performance.now() - burstStart >= BUDGET_MS) break;
937
+ const qMs = ((audio.queued ?? 0) / bps) * 1000;
938
+ let ringMs = 0;
939
+ for (const b of h.state.audioRing) ringMs += (b.length / 2);
940
+ ringMs = (ringMs / deviceSampleRate) * 1000;
941
+ if (qMs + ringMs >= TARGET_MS) break;
942
+ } while (true);
943
+ } else {
944
+ stepped = h.stepFrames(1); // no audio device → plain 1 frame/tick
945
+ }
816
946
  } catch (e) {
817
947
  // A step error mid-swap (host being torn down/rebuilt) is transient —
818
948
  // skip this frame and let the next tick pick up the new host. Don't kill
@@ -879,17 +1009,16 @@ export async function playtest(args) {
879
1009
  }
880
1010
 
881
1011
  if (running && audio && h.state.audioRing.length > 0) {
882
- // SDL device tells us how many bytes are still queued (not yet played).
883
- // If we get >200ms ahead, the player got a stutter and is now
884
- // building latency drop new samples until the queue drains back
885
- // to ~50ms. This keeps audio synced to wall clock without underruns.
886
- // Wrap the whole block in try/catch stop() may have closed the
887
- // audio device between the running check and the access below.
1012
+ // Enqueue the real audio the core produced this tick. The AUDIO-PACED stepping
1013
+ // above already steps however many frames are needed to keep ~60ms queued, so
1014
+ // the buffer stays full from REAL samples no silence padding, no rate-deficit
1015
+ // starvation. SDL's steady drain at the device rate is what sets emulation speed.
1016
+ // We only stop enqueuing if latency runs away (>250ms) a safety valve, not
1017
+ // the normal path.
888
1018
  try {
889
- const bytesPerSecond = coreSampleRate * 2 /* ch */ * 2 /* s16 */;
890
- const queuedBytes = audio.queued ?? 0;
891
- const queuedMs = (queuedBytes / bytesPerSecond) * 1000;
892
- if (queuedMs < 200) {
1019
+ const bytesPerSecond = deviceSampleRate * 2 /* ch */ * 2 /* s16 */;
1020
+ const queuedMs = ((audio.queued ?? 0) / bytesPerSecond) * 1000;
1021
+ if (queuedMs < 250) {
893
1022
  let total = 0;
894
1023
  for (const buf of h.state.audioRing) total += buf.byteLength;
895
1024
  const merged = Buffer.alloc(total);
@@ -898,14 +1027,15 @@ export async function playtest(args) {
898
1027
  merged.set(new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength), off);
899
1028
  off += buf.byteLength;
900
1029
  }
901
- audio.enqueue(merged);
1030
+ audio.enqueue(needsResample
1031
+ ? resampleS16Stereo(merged, coreSampleRate, deviceSampleRate)
1032
+ : merged);
902
1033
  }
903
1034
  } catch (e) {
904
1035
  if (!e.message?.includes("closed")) {
905
1036
  log.error("[playtest] audio enqueue error:", e.message);
906
1037
  }
907
1038
  }
908
- // Always clear the ring — if we skipped enqueue we drop those samples.
909
1039
  h.state.audioRing.length = 0;
910
1040
  }
911
1041
  }
@@ -992,7 +1122,25 @@ function bitToName(bit) {
992
1122
  /** Convert libretro framebuffer (any pixel format) to RGBA32. */
993
1123
  function framebufferToRgba(f) {
994
1124
  const out = Buffer.alloc(f.width * f.height * 4);
995
- if (f.format === RETRO_PIXEL_FORMAT_XRGB8888) {
1125
+ if (f.format === ROMDEV_PIXEL_FORMAT_RGBA8888) {
1126
+ // HW-render readback (n64/ps1/dreamcast via native-gles): pixels are ALREADY
1127
+ // RGBA in row order. Copy straight through but FORCE alpha=255 — the GL render
1128
+ // target leaves alpha=0 (unused channel), which SDL would composite as fully
1129
+ // transparent → a black window. This is the on-screen analogue of the same
1130
+ // alpha fix framebufferToRgba() in framebuffer.js does for screenshots.
1131
+ for (let y = 0; y < f.height; y++) {
1132
+ const src = y * f.pitch;
1133
+ const dst = y * f.width * 4;
1134
+ for (let x = 0; x < f.width; x++) {
1135
+ const s = src + x * 4;
1136
+ const d = dst + x * 4;
1137
+ out[d + 0] = f.pixels[s + 0];
1138
+ out[d + 1] = f.pixels[s + 1];
1139
+ out[d + 2] = f.pixels[s + 2];
1140
+ out[d + 3] = 0xff;
1141
+ }
1142
+ }
1143
+ } else if (f.format === RETRO_PIXEL_FORMAT_XRGB8888) {
996
1144
  for (let y = 0; y < f.height; y++) {
997
1145
  const src = y * f.pitch;
998
1146
  const dst = y * f.width * 4;
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env bash
2
+ # Build the WASM+SIMD audio resampler for the playtest sink.
3
+ # Mirrors simdpipe's emcc style (-O3 -msimd128, MODULARIZE ES6). Single-thread,
4
+ # small heap (audio chunks are a few KB). Output: resampler.mjs + resampler.wasm.
5
+ set -euo pipefail
6
+ cd "$(dirname "$0")"
7
+
8
+ source "$HOME/code/mine/emsdk/emsdk_env.sh" >/dev/null 2>&1 || true
9
+
10
+ emcc resampler.c \
11
+ -O3 -msimd128 -ffast-math \
12
+ -s WASM=1 -s MODULARIZE=1 -s EXPORT_ES6=1 \
13
+ -s ENVIRONMENT=node,web,worker \
14
+ -s ALLOW_MEMORY_GROWTH=1 -s INITIAL_MEMORY=4194304 \
15
+ -s EXPORTED_RUNTIME_METHODS='["HEAP16","HEAPU8","cwrap","ccall"]' \
16
+ -s EXPORTED_FUNCTIONS='["_rs_alloc","_rs_free","_rs_resample","_malloc","_free"]' \
17
+ -o resampler.mjs
18
+
19
+ echo "built resampler.mjs + resampler.wasm ($(stat -c%s resampler.wasm 2>/dev/null || echo '?') bytes)"
@@ -0,0 +1,75 @@
1
+ /* ── resampler/index.mjs — JS wrapper for the WASM+SIMD audio resampler ────────
2
+ *
3
+ * Loads the WASM module once and exposes resampleS16Stereo(buf, src, dst) that
4
+ * resamples an interleaved S16LE stereo Node Buffer. Used by the playtest audio
5
+ * sink for low-rate cores (the GameTank ACP at ~13983 Hz) — see resampler.c for
6
+ * the why (libretro frontends resample; only GameTank is low enough to need it).
7
+ *
8
+ * The WASM scratch buffers are sized once to the largest chunk seen and reused.
9
+ */
10
+ import path from "node:path";
11
+ import { fileURLToPath } from "node:url";
12
+
13
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
14
+
15
+ let mod = null; // the emscripten Module
16
+ let inPtr = 0, inCap = 0; // input scratch (bytes)
17
+ let outPtr = 0, outCap = 0; // output scratch (bytes)
18
+
19
+ /** Lazily instantiate the WASM module. Returns true once ready, false if it
20
+ * failed to load (caller falls back to passing audio through unresampled). */
21
+ export async function initResampler() {
22
+ if (mod) return true;
23
+ try {
24
+ const factory = (await import(path.join(__dirname, "resampler.mjs"))).default;
25
+ mod = await factory();
26
+ return true;
27
+ } catch (e) {
28
+ mod = null;
29
+ return false;
30
+ }
31
+ }
32
+
33
+ function ensureCap(needIn, needOut) {
34
+ if (needIn > inCap) {
35
+ if (inPtr) mod._rs_free(inPtr);
36
+ inCap = needIn * 2; // grow with headroom
37
+ inPtr = mod._rs_alloc(inCap);
38
+ }
39
+ if (needOut > outCap) {
40
+ if (outPtr) mod._rs_free(outPtr);
41
+ outCap = needOut * 2;
42
+ outPtr = mod._rs_alloc(outCap);
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Resample an interleaved S16LE stereo Buffer from srcRate to dstRate using the
48
+ * WASM+SIMD core. Synchronous; initResampler() must have resolved true first.
49
+ * Falls back to returning the input unchanged if the module isn't loaded.
50
+ * @param {Buffer} buf interleaved S16LE stereo at srcRate
51
+ * @param {number} srcRate
52
+ * @param {number} dstRate
53
+ * @returns {Buffer} interleaved S16LE stereo at dstRate
54
+ */
55
+ export function resampleS16Stereo(buf, srcRate, dstRate) {
56
+ if (!mod || srcRate === dstRate || srcRate <= 0 || dstRate <= 0) return buf;
57
+ const inFrames = (buf.length / 4) | 0; // 4 bytes/stereo frame
58
+ if (inFrames < 2) return buf;
59
+ const maxOutFrames = Math.ceil(inFrames * (dstRate / srcRate)) + 2;
60
+
61
+ ensureCap(inFrames * 4, maxOutFrames * 4);
62
+
63
+ // copy input into WASM heap
64
+ mod.HEAPU8.set(new Uint8Array(buf.buffer, buf.byteOffset, buf.length), inPtr);
65
+
66
+ const outFrames = mod._rs_resample(
67
+ inPtr, inFrames, outPtr, maxOutFrames, srcRate, dstRate
68
+ );
69
+ if (outFrames <= 0) return buf;
70
+
71
+ // copy result out into a fresh Buffer (the audio device owns its own memory,
72
+ // and the WASM heap may move on the next call).
73
+ const outBytes = outFrames * 4;
74
+ return Buffer.from(new Uint8Array(mod.HEAPU8.buffer, outPtr, outBytes));
75
+ }
@@ -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;