game-koi 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +60 -0
- package/dist/index.d.ts +67 -0
- package/dist/index.js +188 -0
- package/dist/worklet.d.ts +1 -0
- package/dist/worklet.js +69 -0
- package/package.json +45 -0
- package/wasm/game_koi_web.d.ts +92 -0
- package/wasm/game_koi_web.js +341 -0
- package/wasm/game_koi_web_bg.wasm +0 -0
- package/wasm/game_koi_web_bg.wasm.d.ts +19 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Khôi Tran
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# game-koi
|
|
2
|
+
|
|
3
|
+
A Game Boy (DMG) emulator, compiled to WebAssembly, wrapped in a small TypeScript API
|
|
4
|
+
that owns the canvas rendering, audio, and pacing loop for you.
|
|
5
|
+
|
|
6
|
+
```
|
|
7
|
+
npm install game-koi
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## Usage
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { GameKoi } from "game-koi";
|
|
14
|
+
|
|
15
|
+
const canvas = document.querySelector("canvas")!;
|
|
16
|
+
const romBytes = new Uint8Array(await (await fetch("/game.gb")).arrayBuffer());
|
|
17
|
+
|
|
18
|
+
// Call from inside a user gesture (e.g. a click handler) — browsers suspend a
|
|
19
|
+
// freshly created AudioContext otherwise.
|
|
20
|
+
button.addEventListener("click", async () => {
|
|
21
|
+
const koi = await GameKoi.create({ canvas, rom: romBytes });
|
|
22
|
+
|
|
23
|
+
// koi.pause(); koi.resume(); koi.dispose();
|
|
24
|
+
// koi.press("a"); koi.release("a");
|
|
25
|
+
});
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
That's the whole integration: no separate files to serve for the audio worklet, no
|
|
29
|
+
manual wasm memory or canvas bookkeeping, and the built-in keyboard layout (arrow
|
|
30
|
+
keys, Z/X, Enter, right Shift) is wired up automatically. Pass `keymap: null` to
|
|
31
|
+
`create()` to opt out of that and drive `press`/`release` yourself — from a gamepad,
|
|
32
|
+
touch controls, or your own key bindings.
|
|
33
|
+
|
|
34
|
+
## API
|
|
35
|
+
|
|
36
|
+
- `GameKoi.create(options)` — builds and starts a running emulator.
|
|
37
|
+
- `canvas: HTMLCanvasElement` — resized to 160x144 and drawn into every frame.
|
|
38
|
+
- `rom: Uint8Array` — the `.gb` file's bytes.
|
|
39
|
+
- `keymap?: Record<string, Button> | null` — maps `KeyboardEvent.code` to a
|
|
40
|
+
button; `null` disables built-in keyboard handling.
|
|
41
|
+
- `targetBuffer?: number` — audio samples to keep queued ahead. Default `1600`.
|
|
42
|
+
- `maxFramesPerWake?: number` — cap on frames emulated per wake-up, so a
|
|
43
|
+
backgrounded tab can't return to a freeze. Default `4`.
|
|
44
|
+
- `koi.loadRom(rom)` — swaps in a new ROM, keeping the same canvas and audio setup.
|
|
45
|
+
- `koi.press(button)` / `koi.release(button)` — `Button` is one of `"up"`, `"down"`,
|
|
46
|
+
`"left"`, `"right"`, `"a"`, `"b"`, `"start"`, `"select"`.
|
|
47
|
+
- `koi.pause()` / `koi.resume()`.
|
|
48
|
+
- `koi.dispose()` — stops the loop and tears down the audio graph. Call this before
|
|
49
|
+
dropping a `GameKoi` instance, or the AudioContext and its worklet leak.
|
|
50
|
+
|
|
51
|
+
## Notes
|
|
52
|
+
|
|
53
|
+
- Ships as an ES module with bundled `.d.ts` types; works with any bundler that
|
|
54
|
+
understands wasm as a fetchable asset (Vite, webpack 5, esbuild, Next.js) as well
|
|
55
|
+
as directly in a browser via `<script type="module">`, served over `http(s)://`
|
|
56
|
+
(not `file://` — ES modules and `AudioWorklet` both require an origin).
|
|
57
|
+
- This package is the browser build of
|
|
58
|
+
[game-koi](https://github.com/KeeTraxx/game-koi), a from-scratch Game Boy emulator
|
|
59
|
+
written in Rust. The core emulation has no audio/video device of its own; this
|
|
60
|
+
package is what connects it to a web page.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* game-koi — a Game Boy (DMG) emulator, compiled to WebAssembly.
|
|
3
|
+
*
|
|
4
|
+
* `GameKoi` is the whole integration surface: point it at a canvas and hand it a
|
|
5
|
+
* ROM's bytes, and it owns wasm setup, the AudioWorklet, and the render/audio pacing
|
|
6
|
+
* loop. Nothing else needs to be served or configured by the page — the wasm binary
|
|
7
|
+
* ships as a normal ES module asset next to this file, and the audio worklet is
|
|
8
|
+
* inlined as a Blob URL.
|
|
9
|
+
*/
|
|
10
|
+
export type Button = "up" | "down" | "left" | "right" | "a" | "b" | "start" | "select";
|
|
11
|
+
export interface GameKoiOptions {
|
|
12
|
+
/** Canvas the emulator draws into. Resized to the Game Boy's 160x144. */
|
|
13
|
+
canvas: HTMLCanvasElement;
|
|
14
|
+
/** The ROM image — a `.gb` file's bytes. */
|
|
15
|
+
rom: Uint8Array;
|
|
16
|
+
/**
|
|
17
|
+
* Maps `KeyboardEvent.code` to a button. Pass `null` to disable built-in keyboard
|
|
18
|
+
* handling entirely and drive `press`/`release` yourself. Defaults to the desktop
|
|
19
|
+
* build's layout: arrow keys, Z/X, Enter, right Shift.
|
|
20
|
+
*/
|
|
21
|
+
keymap?: Record<string, Button> | null;
|
|
22
|
+
/** Samples to keep queued ahead of playback. Default 1600 (~2 frames at 48kHz). */
|
|
23
|
+
targetBuffer?: number;
|
|
24
|
+
/**
|
|
25
|
+
* Safety cap on frames emulated per wake-up. Without this, a backgrounded tab
|
|
26
|
+
* (where `requestAnimationFrame` stops firing) would return to a large deficit and
|
|
27
|
+
* freeze the page catching up. Default 4.
|
|
28
|
+
*/
|
|
29
|
+
maxFramesPerWake?: number;
|
|
30
|
+
}
|
|
31
|
+
/** A running Game Boy, attached to a canvas and driving an AudioWorklet. */
|
|
32
|
+
export declare class GameKoi {
|
|
33
|
+
private readonly ctx;
|
|
34
|
+
private readonly audioContext;
|
|
35
|
+
private readonly worklet;
|
|
36
|
+
private readonly wasmMemory;
|
|
37
|
+
private readonly imageData;
|
|
38
|
+
private readonly keymap;
|
|
39
|
+
private readonly targetBuffer;
|
|
40
|
+
private readonly maxFramesPerWake;
|
|
41
|
+
private emulator;
|
|
42
|
+
private buffered;
|
|
43
|
+
private running;
|
|
44
|
+
private rafHandle;
|
|
45
|
+
private keydownListener?;
|
|
46
|
+
private keyupListener?;
|
|
47
|
+
private constructor();
|
|
48
|
+
/**
|
|
49
|
+
* Builds and starts a running emulator.
|
|
50
|
+
*
|
|
51
|
+
* Must be called from inside a user gesture (a click or keypress handler) —
|
|
52
|
+
* browsers suspend a freshly created `AudioContext` otherwise.
|
|
53
|
+
*/
|
|
54
|
+
static create(options: GameKoiOptions): Promise<GameKoi>;
|
|
55
|
+
/** Swaps in a new ROM, keeping the same canvas and audio graph. */
|
|
56
|
+
loadRom(rom: Uint8Array): void;
|
|
57
|
+
press(button: Button): void;
|
|
58
|
+
release(button: Button): void;
|
|
59
|
+
pause(): void;
|
|
60
|
+
resume(): void;
|
|
61
|
+
/** Stops the loop, releases wasm memory, and tears down the audio graph. */
|
|
62
|
+
dispose(): void;
|
|
63
|
+
private loop;
|
|
64
|
+
private drawFrame;
|
|
65
|
+
private attachKeyboard;
|
|
66
|
+
private detachKeyboard;
|
|
67
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* game-koi — a Game Boy (DMG) emulator, compiled to WebAssembly.
|
|
3
|
+
*
|
|
4
|
+
* `GameKoi` is the whole integration surface: point it at a canvas and hand it a
|
|
5
|
+
* ROM's bytes, and it owns wasm setup, the AudioWorklet, and the render/audio pacing
|
|
6
|
+
* loop. Nothing else needs to be served or configured by the page — the wasm binary
|
|
7
|
+
* ships as a normal ES module asset next to this file, and the audio worklet is
|
|
8
|
+
* inlined as a Blob URL.
|
|
9
|
+
*/
|
|
10
|
+
import init, { Emulator } from "../wasm/game_koi_web.js";
|
|
11
|
+
import { WORKLET_SOURCE } from "./worklet.js";
|
|
12
|
+
const DEFAULT_KEYMAP = {
|
|
13
|
+
ArrowRight: "right",
|
|
14
|
+
ArrowLeft: "left",
|
|
15
|
+
ArrowUp: "up",
|
|
16
|
+
ArrowDown: "down",
|
|
17
|
+
KeyZ: "a",
|
|
18
|
+
KeyX: "b",
|
|
19
|
+
Enter: "start",
|
|
20
|
+
ShiftRight: "select",
|
|
21
|
+
};
|
|
22
|
+
// The wasm module only needs instantiating once per page, no matter how many
|
|
23
|
+
// `GameKoi` instances are created.
|
|
24
|
+
let wasmReady = null;
|
|
25
|
+
function ensureWasm() {
|
|
26
|
+
if (!wasmReady)
|
|
27
|
+
wasmReady = init();
|
|
28
|
+
return wasmReady;
|
|
29
|
+
}
|
|
30
|
+
/** A running Game Boy, attached to a canvas and driving an AudioWorklet. */
|
|
31
|
+
export class GameKoi {
|
|
32
|
+
ctx;
|
|
33
|
+
audioContext;
|
|
34
|
+
worklet;
|
|
35
|
+
wasmMemory;
|
|
36
|
+
imageData;
|
|
37
|
+
keymap;
|
|
38
|
+
targetBuffer;
|
|
39
|
+
maxFramesPerWake;
|
|
40
|
+
emulator;
|
|
41
|
+
buffered = 0;
|
|
42
|
+
running = false;
|
|
43
|
+
rafHandle = 0;
|
|
44
|
+
keydownListener;
|
|
45
|
+
keyupListener;
|
|
46
|
+
constructor(emulator, wasmMemory, ctx, audioContext, worklet, keymap, targetBuffer, maxFramesPerWake) {
|
|
47
|
+
this.emulator = emulator;
|
|
48
|
+
this.wasmMemory = wasmMemory;
|
|
49
|
+
this.ctx = ctx;
|
|
50
|
+
this.audioContext = audioContext;
|
|
51
|
+
this.worklet = worklet;
|
|
52
|
+
this.keymap = keymap;
|
|
53
|
+
this.targetBuffer = targetBuffer;
|
|
54
|
+
this.maxFramesPerWake = maxFramesPerWake;
|
|
55
|
+
this.imageData = ctx.createImageData(Emulator.width(), Emulator.height());
|
|
56
|
+
this.worklet.port.onmessage = (event) => {
|
|
57
|
+
this.buffered = event.data.buffered;
|
|
58
|
+
};
|
|
59
|
+
if (keymap)
|
|
60
|
+
this.attachKeyboard();
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Builds and starts a running emulator.
|
|
64
|
+
*
|
|
65
|
+
* Must be called from inside a user gesture (a click or keypress handler) —
|
|
66
|
+
* browsers suspend a freshly created `AudioContext` otherwise.
|
|
67
|
+
*/
|
|
68
|
+
static async create(options) {
|
|
69
|
+
const wasm = await ensureWasm();
|
|
70
|
+
const canvas = options.canvas;
|
|
71
|
+
canvas.width = Emulator.width();
|
|
72
|
+
canvas.height = Emulator.height();
|
|
73
|
+
const ctx = canvas.getContext("2d");
|
|
74
|
+
if (!ctx)
|
|
75
|
+
throw new Error("canvas 2D context unavailable");
|
|
76
|
+
const audioContext = new AudioContext();
|
|
77
|
+
const blobUrl = URL.createObjectURL(new Blob([WORKLET_SOURCE], { type: "application/javascript" }));
|
|
78
|
+
try {
|
|
79
|
+
await audioContext.audioWorklet.addModule(blobUrl);
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
URL.revokeObjectURL(blobUrl);
|
|
83
|
+
}
|
|
84
|
+
const worklet = new AudioWorkletNode(audioContext, "koi-processor", {
|
|
85
|
+
outputChannelCount: [2],
|
|
86
|
+
});
|
|
87
|
+
worklet.connect(audioContext.destination);
|
|
88
|
+
const emulator = new Emulator(options.rom, audioContext.sampleRate);
|
|
89
|
+
const keymap = options.keymap === null ? null : options.keymap ?? DEFAULT_KEYMAP;
|
|
90
|
+
const koi = new GameKoi(emulator, wasm.memory, ctx, audioContext, worklet, keymap, options.targetBuffer ?? 1600, options.maxFramesPerWake ?? 4);
|
|
91
|
+
koi.running = true;
|
|
92
|
+
koi.loop();
|
|
93
|
+
return koi;
|
|
94
|
+
}
|
|
95
|
+
/** Swaps in a new ROM, keeping the same canvas and audio graph. */
|
|
96
|
+
loadRom(rom) {
|
|
97
|
+
this.emulator.free();
|
|
98
|
+
this.emulator = new Emulator(rom, this.audioContext.sampleRate);
|
|
99
|
+
}
|
|
100
|
+
press(button) {
|
|
101
|
+
this.emulator.press(button);
|
|
102
|
+
}
|
|
103
|
+
release(button) {
|
|
104
|
+
this.emulator.release(button);
|
|
105
|
+
}
|
|
106
|
+
pause() {
|
|
107
|
+
this.running = false;
|
|
108
|
+
cancelAnimationFrame(this.rafHandle);
|
|
109
|
+
void this.audioContext.suspend();
|
|
110
|
+
}
|
|
111
|
+
resume() {
|
|
112
|
+
if (this.running)
|
|
113
|
+
return;
|
|
114
|
+
this.running = true;
|
|
115
|
+
void this.audioContext.resume();
|
|
116
|
+
this.loop();
|
|
117
|
+
}
|
|
118
|
+
/** Stops the loop, releases wasm memory, and tears down the audio graph. */
|
|
119
|
+
dispose() {
|
|
120
|
+
this.running = false;
|
|
121
|
+
cancelAnimationFrame(this.rafHandle);
|
|
122
|
+
this.detachKeyboard();
|
|
123
|
+
this.emulator.free();
|
|
124
|
+
this.worklet.disconnect();
|
|
125
|
+
void this.audioContext.close();
|
|
126
|
+
}
|
|
127
|
+
loop = () => {
|
|
128
|
+
if (!this.running)
|
|
129
|
+
return;
|
|
130
|
+
// Emulate however many frames the audio buffer is short by, capped. Audio wins
|
|
131
|
+
// when the display's refresh rate and the Game Boy's 59.73Hz disagree, since a
|
|
132
|
+
// dry buffer is audible and a repeated frame is not.
|
|
133
|
+
let frames = 0;
|
|
134
|
+
while (this.buffered < this.targetBuffer && frames < this.maxFramesPerWake) {
|
|
135
|
+
this.emulator.run_frame();
|
|
136
|
+
const samples = this.emulator.take_samples();
|
|
137
|
+
if (samples.length > 0) {
|
|
138
|
+
const count = samples.length / 2;
|
|
139
|
+
const left = new Float32Array(count);
|
|
140
|
+
const right = new Float32Array(count);
|
|
141
|
+
for (let i = 0; i < count; i++) {
|
|
142
|
+
left[i] = samples[i * 2];
|
|
143
|
+
right[i] = samples[i * 2 + 1];
|
|
144
|
+
}
|
|
145
|
+
// Transfer rather than copy: these are throwaway buffers.
|
|
146
|
+
this.worklet.port.postMessage({ left, right }, [left.buffer, right.buffer]);
|
|
147
|
+
this.buffered += count;
|
|
148
|
+
}
|
|
149
|
+
frames++;
|
|
150
|
+
}
|
|
151
|
+
if (frames > 0)
|
|
152
|
+
this.drawFrame();
|
|
153
|
+
this.rafHandle = requestAnimationFrame(this.loop);
|
|
154
|
+
};
|
|
155
|
+
drawFrame() {
|
|
156
|
+
// Read `wasmMemory.buffer` fresh rather than caching it: any wasm allocation can
|
|
157
|
+
// grow linear memory, which allocates a new ArrayBuffer and silently detaches
|
|
158
|
+
// views over the old one. Re-reading it here means a grown buffer is picked up
|
|
159
|
+
// automatically instead of producing a zero-length view.
|
|
160
|
+
const bytes = new Uint8ClampedArray(this.wasmMemory.buffer, this.emulator.frame_ptr(), this.emulator.frame_len());
|
|
161
|
+
this.imageData.data.set(bytes);
|
|
162
|
+
this.ctx.putImageData(this.imageData, 0, 0);
|
|
163
|
+
}
|
|
164
|
+
attachKeyboard() {
|
|
165
|
+
this.keydownListener = (event) => {
|
|
166
|
+
const button = this.keymap?.[event.code];
|
|
167
|
+
if (button) {
|
|
168
|
+
this.press(button);
|
|
169
|
+
event.preventDefault();
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
this.keyupListener = (event) => {
|
|
173
|
+
const button = this.keymap?.[event.code];
|
|
174
|
+
if (button) {
|
|
175
|
+
this.release(button);
|
|
176
|
+
event.preventDefault();
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
addEventListener("keydown", this.keydownListener);
|
|
180
|
+
addEventListener("keyup", this.keyupListener);
|
|
181
|
+
}
|
|
182
|
+
detachKeyboard() {
|
|
183
|
+
if (this.keydownListener)
|
|
184
|
+
removeEventListener("keydown", this.keydownListener);
|
|
185
|
+
if (this.keyupListener)
|
|
186
|
+
removeEventListener("keyup", this.keyupListener);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const WORKLET_SOURCE = "\n const CAPACITY = 48000; // ~0.5s at 48kHz stereo: rides out a slow main thread.\n\n class KoiProcessor extends AudioWorkletProcessor {\n constructor() {\n super();\n this.left = new Float32Array(CAPACITY);\n this.right = new Float32Array(CAPACITY);\n this.readIndex = 0;\n this.writeIndex = 0;\n\n this.port.onmessage = (event) => {\n const { left, right } = event.data;\n for (let i = 0; i < left.length; i++) {\n const next = (this.writeIndex + 1) % CAPACITY;\n // Full: drop the oldest rather than the newest, so audio does not fall\n // permanently further behind the picture.\n if (next === this.readIndex) {\n this.readIndex = (this.readIndex + 1) % CAPACITY;\n }\n this.left[this.writeIndex] = left[i];\n this.right[this.writeIndex] = right[i];\n this.writeIndex = next;\n }\n };\n }\n\n available() {\n return (this.writeIndex - this.readIndex + CAPACITY) % CAPACITY;\n }\n\n process(_inputs, outputs) {\n const output = outputs[0];\n const outL = output[0];\n const outR = output.length > 1 ? output[1] : output[0];\n\n for (let i = 0; i < outL.length; i++) {\n if (this.readIndex === this.writeIndex) {\n // Silence on underrun. Holding the last sample turns a click into a buzz,\n // which is worse.\n outL[i] = 0;\n outR[i] = 0;\n } else {\n outL[i] = this.left[this.readIndex];\n outR[i] = this.right[this.readIndex];\n this.readIndex = (this.readIndex + 1) % CAPACITY;\n }\n }\n\n // The main thread paces itself off this number, so it goes back every block.\n this.port.postMessage({ buffered: this.available() });\n return true;\n }\n }\n\n registerProcessor(\"koi-processor\", KoiProcessor);\n";
|
package/dist/worklet.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// The audio side of the pacing clock. This runs on the browser's audio thread, woken
|
|
2
|
+
// on a schedule the sound hardware drives rather than the display. It owns a ring
|
|
3
|
+
// buffer that the main thread fills; all it does is drain it and report how full it
|
|
4
|
+
// is.
|
|
5
|
+
//
|
|
6
|
+
// Why a ring and not a message per frame: postMessage allocates and is queued behind
|
|
7
|
+
// whatever else the audio thread has to do, and this callback has a hard deadline. A
|
|
8
|
+
// pre-allocated ring written by one thread and read by another needs no allocation and
|
|
9
|
+
// no lock for a single-producer/single-consumer pair like this one.
|
|
10
|
+
//
|
|
11
|
+
// Kept as a source string, not a separate .js file, so `GameKoi` can register it via a
|
|
12
|
+
// Blob URL — a consumer never needs to serve this file or point a bundler at it.
|
|
13
|
+
export const WORKLET_SOURCE = `
|
|
14
|
+
const CAPACITY = 48000; // ~0.5s at 48kHz stereo: rides out a slow main thread.
|
|
15
|
+
|
|
16
|
+
class KoiProcessor extends AudioWorkletProcessor {
|
|
17
|
+
constructor() {
|
|
18
|
+
super();
|
|
19
|
+
this.left = new Float32Array(CAPACITY);
|
|
20
|
+
this.right = new Float32Array(CAPACITY);
|
|
21
|
+
this.readIndex = 0;
|
|
22
|
+
this.writeIndex = 0;
|
|
23
|
+
|
|
24
|
+
this.port.onmessage = (event) => {
|
|
25
|
+
const { left, right } = event.data;
|
|
26
|
+
for (let i = 0; i < left.length; i++) {
|
|
27
|
+
const next = (this.writeIndex + 1) % CAPACITY;
|
|
28
|
+
// Full: drop the oldest rather than the newest, so audio does not fall
|
|
29
|
+
// permanently further behind the picture.
|
|
30
|
+
if (next === this.readIndex) {
|
|
31
|
+
this.readIndex = (this.readIndex + 1) % CAPACITY;
|
|
32
|
+
}
|
|
33
|
+
this.left[this.writeIndex] = left[i];
|
|
34
|
+
this.right[this.writeIndex] = right[i];
|
|
35
|
+
this.writeIndex = next;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
available() {
|
|
41
|
+
return (this.writeIndex - this.readIndex + CAPACITY) % CAPACITY;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
process(_inputs, outputs) {
|
|
45
|
+
const output = outputs[0];
|
|
46
|
+
const outL = output[0];
|
|
47
|
+
const outR = output.length > 1 ? output[1] : output[0];
|
|
48
|
+
|
|
49
|
+
for (let i = 0; i < outL.length; i++) {
|
|
50
|
+
if (this.readIndex === this.writeIndex) {
|
|
51
|
+
// Silence on underrun. Holding the last sample turns a click into a buzz,
|
|
52
|
+
// which is worse.
|
|
53
|
+
outL[i] = 0;
|
|
54
|
+
outR[i] = 0;
|
|
55
|
+
} else {
|
|
56
|
+
outL[i] = this.left[this.readIndex];
|
|
57
|
+
outR[i] = this.right[this.readIndex];
|
|
58
|
+
this.readIndex = (this.readIndex + 1) % CAPACITY;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// The main thread paces itself off this number, so it goes back every block.
|
|
63
|
+
this.port.postMessage({ buffered: this.available() });
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
registerProcessor("koi-processor", KoiProcessor);
|
|
69
|
+
`;
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "game-koi",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A Game Boy (DMG) emulator, compiled to WebAssembly, with a drop-in canvas + audio wrapper",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"module": "dist/index.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"wasm",
|
|
18
|
+
"LICENSE"
|
|
19
|
+
],
|
|
20
|
+
"sideEffects": false,
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc -p tsconfig.json",
|
|
23
|
+
"prepublishOnly": "npm run build"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"gameboy",
|
|
27
|
+
"game boy",
|
|
28
|
+
"dmg",
|
|
29
|
+
"emulator",
|
|
30
|
+
"wasm",
|
|
31
|
+
"webassembly"
|
|
32
|
+
],
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/KeeTraxx/game-koi.git",
|
|
37
|
+
"directory": "crates/game-koi-web/js"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=18"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"typescript": "^5.6.0"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/* tslint:disable */
|
|
2
|
+
/* eslint-disable */
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A running Game Boy, owned by JavaScript.
|
|
6
|
+
*/
|
|
7
|
+
export class Emulator {
|
|
8
|
+
free(): void;
|
|
9
|
+
[Symbol.dispose](): void;
|
|
10
|
+
frame_len(): number;
|
|
11
|
+
/**
|
|
12
|
+
* Pointer to the RGBA framebuffer inside wasm memory.
|
|
13
|
+
*
|
|
14
|
+
* Paired with [`Emulator::frame_len`], this lets the page build a view over wasm
|
|
15
|
+
* memory once and reuse it. Any call that grows wasm memory can detach that view,
|
|
16
|
+
* so re-read both after loading a new ROM.
|
|
17
|
+
*/
|
|
18
|
+
frame_ptr(): number;
|
|
19
|
+
static height(): number;
|
|
20
|
+
/**
|
|
21
|
+
* Builds a machine from a ROM image.
|
|
22
|
+
*
|
|
23
|
+
* Fails if the header will not parse or the cartridge uses a mapper the core does
|
|
24
|
+
* not implement — the same typed failure the desktop build gets, flattened to a
|
|
25
|
+
* string because that is what crosses into JavaScript.
|
|
26
|
+
*/
|
|
27
|
+
constructor(rom: Uint8Array, sample_rate: number);
|
|
28
|
+
/**
|
|
29
|
+
* Presses a button, named as the page sees it ("a", "start", "dpad-up", ...).
|
|
30
|
+
*
|
|
31
|
+
* Unknown names are ignored rather than an error: a key the page does not map is
|
|
32
|
+
* not a failure worth unwinding across the boundary for.
|
|
33
|
+
*/
|
|
34
|
+
press(button: string): void;
|
|
35
|
+
release(button: string): void;
|
|
36
|
+
/**
|
|
37
|
+
* Runs until the PPU finishes a frame.
|
|
38
|
+
*
|
|
39
|
+
* The cycle budget is the same safety valve the desktop frontend uses: a ROM that
|
|
40
|
+
* somehow never completes a frame would otherwise wedge the browser tab.
|
|
41
|
+
*/
|
|
42
|
+
run_frame(): void;
|
|
43
|
+
/**
|
|
44
|
+
* Takes the audio produced since the last call, interleaved as L,R,L,R.
|
|
45
|
+
*/
|
|
46
|
+
take_samples(): Float32Array;
|
|
47
|
+
static width(): number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
|
|
51
|
+
|
|
52
|
+
export interface InitOutput {
|
|
53
|
+
readonly memory: WebAssembly.Memory;
|
|
54
|
+
readonly __wbg_emulator_free: (a: number, b: number) => void;
|
|
55
|
+
readonly emulator_frame_len: (a: number) => number;
|
|
56
|
+
readonly emulator_frame_ptr: (a: number) => number;
|
|
57
|
+
readonly emulator_height: () => number;
|
|
58
|
+
readonly emulator_new: (a: number, b: number, c: number) => [number, number, number];
|
|
59
|
+
readonly emulator_press: (a: number, b: number, c: number) => void;
|
|
60
|
+
readonly emulator_release: (a: number, b: number, c: number) => void;
|
|
61
|
+
readonly emulator_run_frame: (a: number) => void;
|
|
62
|
+
readonly emulator_take_samples: (a: number) => [number, number];
|
|
63
|
+
readonly emulator_width: () => number;
|
|
64
|
+
readonly __wbindgen_externrefs: WebAssembly.Table;
|
|
65
|
+
readonly __wbindgen_malloc: (a: number, b: number) => number;
|
|
66
|
+
readonly __externref_table_dealloc: (a: number) => void;
|
|
67
|
+
readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
|
|
68
|
+
readonly __wbindgen_free: (a: number, b: number, c: number) => void;
|
|
69
|
+
readonly __wbindgen_start: () => void;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export type SyncInitInput = BufferSource | WebAssembly.Module;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Instantiates the given `module`, which can either be bytes or
|
|
76
|
+
* a precompiled `WebAssembly.Module`.
|
|
77
|
+
*
|
|
78
|
+
* @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated.
|
|
79
|
+
*
|
|
80
|
+
* @returns {InitOutput}
|
|
81
|
+
*/
|
|
82
|
+
export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* If `module_or_path` is {RequestInfo} or {URL}, makes a request and
|
|
86
|
+
* for everything else, calls `WebAssembly.instantiate` directly.
|
|
87
|
+
*
|
|
88
|
+
* @param {{ module_or_path: InitInput | Promise<InitInput> }} module_or_path - Passing `InitInput` directly is deprecated.
|
|
89
|
+
*
|
|
90
|
+
* @returns {Promise<InitOutput>}
|
|
91
|
+
*/
|
|
92
|
+
export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise<InitInput> } | InitInput | Promise<InitInput>): Promise<InitOutput>;
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
/* @ts-self-types="./game_koi_web.d.ts" */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A running Game Boy, owned by JavaScript.
|
|
5
|
+
*/
|
|
6
|
+
export class Emulator {
|
|
7
|
+
__destroy_into_raw() {
|
|
8
|
+
const ptr = this.__wbg_ptr;
|
|
9
|
+
this.__wbg_ptr = 0;
|
|
10
|
+
EmulatorFinalization.unregister(this);
|
|
11
|
+
return ptr;
|
|
12
|
+
}
|
|
13
|
+
free() {
|
|
14
|
+
const ptr = this.__destroy_into_raw();
|
|
15
|
+
wasm.__wbg_emulator_free(ptr, 0);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* @returns {number}
|
|
19
|
+
*/
|
|
20
|
+
frame_len() {
|
|
21
|
+
const ret = wasm.emulator_frame_len(this.__wbg_ptr);
|
|
22
|
+
return ret >>> 0;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Pointer to the RGBA framebuffer inside wasm memory.
|
|
26
|
+
*
|
|
27
|
+
* Paired with [`Emulator::frame_len`], this lets the page build a view over wasm
|
|
28
|
+
* memory once and reuse it. Any call that grows wasm memory can detach that view,
|
|
29
|
+
* so re-read both after loading a new ROM.
|
|
30
|
+
* @returns {number}
|
|
31
|
+
*/
|
|
32
|
+
frame_ptr() {
|
|
33
|
+
const ret = wasm.emulator_frame_ptr(this.__wbg_ptr);
|
|
34
|
+
return ret >>> 0;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* @returns {number}
|
|
38
|
+
*/
|
|
39
|
+
static height() {
|
|
40
|
+
const ret = wasm.emulator_height();
|
|
41
|
+
return ret >>> 0;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Builds a machine from a ROM image.
|
|
45
|
+
*
|
|
46
|
+
* Fails if the header will not parse or the cartridge uses a mapper the core does
|
|
47
|
+
* not implement — the same typed failure the desktop build gets, flattened to a
|
|
48
|
+
* string because that is what crosses into JavaScript.
|
|
49
|
+
* @param {Uint8Array} rom
|
|
50
|
+
* @param {number} sample_rate
|
|
51
|
+
*/
|
|
52
|
+
constructor(rom, sample_rate) {
|
|
53
|
+
const ptr0 = passArray8ToWasm0(rom, wasm.__wbindgen_malloc);
|
|
54
|
+
const len0 = WASM_VECTOR_LEN;
|
|
55
|
+
const ret = wasm.emulator_new(ptr0, len0, sample_rate);
|
|
56
|
+
if (ret[2]) {
|
|
57
|
+
throw takeFromExternrefTable0(ret[1]);
|
|
58
|
+
}
|
|
59
|
+
this.__wbg_ptr = ret[0];
|
|
60
|
+
EmulatorFinalization.register(this, this.__wbg_ptr, this);
|
|
61
|
+
return this;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Presses a button, named as the page sees it ("a", "start", "dpad-up", ...).
|
|
65
|
+
*
|
|
66
|
+
* Unknown names are ignored rather than an error: a key the page does not map is
|
|
67
|
+
* not a failure worth unwinding across the boundary for.
|
|
68
|
+
* @param {string} button
|
|
69
|
+
*/
|
|
70
|
+
press(button) {
|
|
71
|
+
const ptr0 = passStringToWasm0(button, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
|
72
|
+
const len0 = WASM_VECTOR_LEN;
|
|
73
|
+
wasm.emulator_press(this.__wbg_ptr, ptr0, len0);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* @param {string} button
|
|
77
|
+
*/
|
|
78
|
+
release(button) {
|
|
79
|
+
const ptr0 = passStringToWasm0(button, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
|
80
|
+
const len0 = WASM_VECTOR_LEN;
|
|
81
|
+
wasm.emulator_release(this.__wbg_ptr, ptr0, len0);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Runs until the PPU finishes a frame.
|
|
85
|
+
*
|
|
86
|
+
* The cycle budget is the same safety valve the desktop frontend uses: a ROM that
|
|
87
|
+
* somehow never completes a frame would otherwise wedge the browser tab.
|
|
88
|
+
*/
|
|
89
|
+
run_frame() {
|
|
90
|
+
wasm.emulator_run_frame(this.__wbg_ptr);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Takes the audio produced since the last call, interleaved as L,R,L,R.
|
|
94
|
+
* @returns {Float32Array}
|
|
95
|
+
*/
|
|
96
|
+
take_samples() {
|
|
97
|
+
const ret = wasm.emulator_take_samples(this.__wbg_ptr);
|
|
98
|
+
var v1 = getArrayF32FromWasm0(ret[0], ret[1]).slice();
|
|
99
|
+
wasm.__wbindgen_free(ret[0], ret[1] * 4, 4);
|
|
100
|
+
return v1;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* @returns {number}
|
|
104
|
+
*/
|
|
105
|
+
static width() {
|
|
106
|
+
const ret = wasm.emulator_width();
|
|
107
|
+
return ret >>> 0;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (Symbol.dispose) Emulator.prototype[Symbol.dispose] = Emulator.prototype.free;
|
|
111
|
+
function __wbg_get_imports() {
|
|
112
|
+
const import0 = {
|
|
113
|
+
__proto__: null,
|
|
114
|
+
__wbg_Error_67e7344beaa85059: function(arg0, arg1) {
|
|
115
|
+
const ret = Error(getStringFromWasm0(arg0, arg1));
|
|
116
|
+
return ret;
|
|
117
|
+
},
|
|
118
|
+
__wbg___wbindgen_throw_5d9e815e6fdf150f: function(arg0, arg1) {
|
|
119
|
+
throw new Error(getStringFromWasm0(arg0, arg1));
|
|
120
|
+
},
|
|
121
|
+
__wbindgen_init_externref_table: function() {
|
|
122
|
+
const table = wasm.__wbindgen_externrefs;
|
|
123
|
+
const offset = table.grow(4);
|
|
124
|
+
table.set(0, undefined);
|
|
125
|
+
table.set(offset + 0, undefined);
|
|
126
|
+
table.set(offset + 1, null);
|
|
127
|
+
table.set(offset + 2, true);
|
|
128
|
+
table.set(offset + 3, false);
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
return {
|
|
132
|
+
__proto__: null,
|
|
133
|
+
"./game_koi_web_bg.js": import0,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const EmulatorFinalization = (typeof FinalizationRegistry === 'undefined')
|
|
138
|
+
? { register: () => {}, unregister: () => {} }
|
|
139
|
+
: new FinalizationRegistry(ptr => wasm.__wbg_emulator_free(ptr, 1));
|
|
140
|
+
|
|
141
|
+
function getArrayF32FromWasm0(ptr, len) {
|
|
142
|
+
ptr = ptr >>> 0;
|
|
143
|
+
return getFloat32ArrayMemory0().subarray(ptr / 4, ptr / 4 + len);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
let cachedFloat32ArrayMemory0 = null;
|
|
147
|
+
function getFloat32ArrayMemory0() {
|
|
148
|
+
if (cachedFloat32ArrayMemory0 === null || cachedFloat32ArrayMemory0.byteLength === 0) {
|
|
149
|
+
cachedFloat32ArrayMemory0 = new Float32Array(wasm.memory.buffer);
|
|
150
|
+
}
|
|
151
|
+
return cachedFloat32ArrayMemory0;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function getStringFromWasm0(ptr, len) {
|
|
155
|
+
return decodeText(ptr >>> 0, len);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
let cachedUint8ArrayMemory0 = null;
|
|
159
|
+
function getUint8ArrayMemory0() {
|
|
160
|
+
if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) {
|
|
161
|
+
cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer);
|
|
162
|
+
}
|
|
163
|
+
return cachedUint8ArrayMemory0;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function passArray8ToWasm0(arg, malloc) {
|
|
167
|
+
const ptr = malloc(arg.length * 1, 1) >>> 0;
|
|
168
|
+
getUint8ArrayMemory0().set(arg, ptr / 1);
|
|
169
|
+
WASM_VECTOR_LEN = arg.length;
|
|
170
|
+
return ptr;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function passStringToWasm0(arg, malloc, realloc) {
|
|
174
|
+
if (realloc === undefined) {
|
|
175
|
+
const buf = cachedTextEncoder.encode(arg);
|
|
176
|
+
const ptr = malloc(buf.length, 1) >>> 0;
|
|
177
|
+
getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf);
|
|
178
|
+
WASM_VECTOR_LEN = buf.length;
|
|
179
|
+
return ptr;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
let len = arg.length;
|
|
183
|
+
let ptr = malloc(len, 1) >>> 0;
|
|
184
|
+
|
|
185
|
+
const mem = getUint8ArrayMemory0();
|
|
186
|
+
|
|
187
|
+
let offset = 0;
|
|
188
|
+
|
|
189
|
+
for (; offset < len; offset++) {
|
|
190
|
+
const code = arg.charCodeAt(offset);
|
|
191
|
+
if (code > 0x7F) break;
|
|
192
|
+
mem[ptr + offset] = code;
|
|
193
|
+
}
|
|
194
|
+
if (offset !== len) {
|
|
195
|
+
if (offset !== 0) {
|
|
196
|
+
arg = arg.slice(offset);
|
|
197
|
+
}
|
|
198
|
+
ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0;
|
|
199
|
+
const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len);
|
|
200
|
+
const ret = cachedTextEncoder.encodeInto(arg, view);
|
|
201
|
+
|
|
202
|
+
offset += ret.written;
|
|
203
|
+
ptr = realloc(ptr, len, offset, 1) >>> 0;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
WASM_VECTOR_LEN = offset;
|
|
207
|
+
return ptr;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function takeFromExternrefTable0(idx) {
|
|
211
|
+
const value = wasm.__wbindgen_externrefs.get(idx);
|
|
212
|
+
wasm.__externref_table_dealloc(idx);
|
|
213
|
+
return value;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
|
|
217
|
+
cachedTextDecoder.decode();
|
|
218
|
+
const MAX_SAFARI_DECODE_BYTES = 2146435072;
|
|
219
|
+
let numBytesDecoded = 0;
|
|
220
|
+
function decodeText(ptr, len) {
|
|
221
|
+
numBytesDecoded += len;
|
|
222
|
+
if (numBytesDecoded >= MAX_SAFARI_DECODE_BYTES) {
|
|
223
|
+
cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
|
|
224
|
+
cachedTextDecoder.decode();
|
|
225
|
+
numBytesDecoded = len;
|
|
226
|
+
}
|
|
227
|
+
return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const cachedTextEncoder = new TextEncoder();
|
|
231
|
+
|
|
232
|
+
if (!('encodeInto' in cachedTextEncoder)) {
|
|
233
|
+
cachedTextEncoder.encodeInto = function (arg, view) {
|
|
234
|
+
const buf = cachedTextEncoder.encode(arg);
|
|
235
|
+
view.set(buf);
|
|
236
|
+
return {
|
|
237
|
+
read: arg.length,
|
|
238
|
+
written: buf.length
|
|
239
|
+
};
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
let WASM_VECTOR_LEN = 0;
|
|
244
|
+
|
|
245
|
+
let wasmModule, wasmInstance, wasm;
|
|
246
|
+
function __wbg_finalize_init(instance, module) {
|
|
247
|
+
wasmInstance = instance;
|
|
248
|
+
wasm = instance.exports;
|
|
249
|
+
wasmModule = module;
|
|
250
|
+
cachedFloat32ArrayMemory0 = null;
|
|
251
|
+
cachedUint8ArrayMemory0 = null;
|
|
252
|
+
wasm.__wbindgen_start();
|
|
253
|
+
return wasm;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async function __wbg_load(module, imports) {
|
|
257
|
+
if (typeof Response === 'function' && module instanceof Response) {
|
|
258
|
+
if (!module.ok) {
|
|
259
|
+
throw new Error(`failed to fetch Wasm: ${module.status} ${module.statusText} fetching '${module.url}'`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (typeof WebAssembly.instantiateStreaming === 'function') {
|
|
263
|
+
try {
|
|
264
|
+
return await WebAssembly.instantiateStreaming(module, imports);
|
|
265
|
+
} catch (e) {
|
|
266
|
+
const validResponse = expectedResponseType(module.type);
|
|
267
|
+
|
|
268
|
+
if (validResponse && module.headers.get('Content-Type') !== 'application/wasm') {
|
|
269
|
+
console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e);
|
|
270
|
+
|
|
271
|
+
} else { throw e; }
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const bytes = await module.arrayBuffer();
|
|
276
|
+
return await WebAssembly.instantiate(bytes, imports);
|
|
277
|
+
} else {
|
|
278
|
+
const instance = await WebAssembly.instantiate(module, imports);
|
|
279
|
+
|
|
280
|
+
if (instance instanceof WebAssembly.Instance) {
|
|
281
|
+
return { instance, module };
|
|
282
|
+
} else {
|
|
283
|
+
return instance;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function expectedResponseType(type) {
|
|
288
|
+
switch (type) {
|
|
289
|
+
case 'basic': case 'cors': case 'default': return true;
|
|
290
|
+
}
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function initSync(module) {
|
|
296
|
+
if (wasm !== undefined) return wasm;
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
if (module !== undefined) {
|
|
300
|
+
if (Object.getPrototypeOf(module) === Object.prototype) {
|
|
301
|
+
({module} = module)
|
|
302
|
+
} else {
|
|
303
|
+
console.warn('using deprecated parameters for `initSync()`; pass a single object instead')
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const imports = __wbg_get_imports();
|
|
308
|
+
if (!(module instanceof WebAssembly.Module)) {
|
|
309
|
+
module = new WebAssembly.Module(module);
|
|
310
|
+
}
|
|
311
|
+
const instance = new WebAssembly.Instance(module, imports);
|
|
312
|
+
return __wbg_finalize_init(instance, module);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async function __wbg_init(module_or_path) {
|
|
316
|
+
if (wasm !== undefined) return wasm;
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
if (module_or_path !== undefined) {
|
|
320
|
+
if (Object.getPrototypeOf(module_or_path) === Object.prototype) {
|
|
321
|
+
({module_or_path} = module_or_path)
|
|
322
|
+
} else {
|
|
323
|
+
console.warn('using deprecated parameters for the initialization function; pass a single object instead')
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (module_or_path === undefined) {
|
|
328
|
+
module_or_path = new URL('game_koi_web_bg.wasm', import.meta.url);
|
|
329
|
+
}
|
|
330
|
+
const imports = __wbg_get_imports();
|
|
331
|
+
|
|
332
|
+
if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) {
|
|
333
|
+
module_or_path = fetch(module_or_path);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const { instance, module } = await __wbg_load(await module_or_path, imports);
|
|
337
|
+
|
|
338
|
+
return __wbg_finalize_init(instance, module);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export { initSync, __wbg_init as default };
|
|
Binary file
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/* tslint:disable */
|
|
2
|
+
/* eslint-disable */
|
|
3
|
+
export const memory: WebAssembly.Memory;
|
|
4
|
+
export const __wbg_emulator_free: (a: number, b: number) => void;
|
|
5
|
+
export const emulator_frame_len: (a: number) => number;
|
|
6
|
+
export const emulator_frame_ptr: (a: number) => number;
|
|
7
|
+
export const emulator_height: () => number;
|
|
8
|
+
export const emulator_new: (a: number, b: number, c: number) => [number, number, number];
|
|
9
|
+
export const emulator_press: (a: number, b: number, c: number) => void;
|
|
10
|
+
export const emulator_release: (a: number, b: number, c: number) => void;
|
|
11
|
+
export const emulator_run_frame: (a: number) => void;
|
|
12
|
+
export const emulator_take_samples: (a: number) => [number, number];
|
|
13
|
+
export const emulator_width: () => number;
|
|
14
|
+
export const __wbindgen_externrefs: WebAssembly.Table;
|
|
15
|
+
export const __wbindgen_malloc: (a: number, b: number) => number;
|
|
16
|
+
export const __externref_table_dealloc: (a: number) => void;
|
|
17
|
+
export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
|
|
18
|
+
export const __wbindgen_free: (a: number, b: number, c: number) => void;
|
|
19
|
+
export const __wbindgen_start: () => void;
|