wasmcart 0.2.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 +410 -0
- package/SPEC.md +477 -0
- package/bin/wasmcart-pack.js +257 -0
- package/docs/bind_framebuffer.md +275 -0
- package/docs/fetch.md +105 -0
- package/docs/gl-surface.md +111 -0
- package/docs/input.md +102 -0
- package/docs/networking.md +78 -0
- package/docs/porting.md +88 -0
- package/include/wc_cart.h +144 -0
- package/include/wc_fb.h +275 -0
- package/include/wc_gl.h +224 -0
- package/include/wc_gl_blit.h +129 -0
- package/include/wc_mat4.h +210 -0
- package/include/wc_math.h +116 -0
- package/include/wc_pcm_mixer.h +487 -0
- package/include/wc_vec3.h +80 -0
- package/index.js +3 -0
- package/package.json +55 -0
- package/src/CartHost.js +1713 -0
- package/src/CartHostWeb.js +1381 -0
- package/src/abi.js +94 -0
- package/src/cartWorker.js +201 -0
- package/src/cartWorkerWeb.js +170 -0
- package/src/webgl_imports.js +1483 -0
- package/web.js +3 -0
package/src/abi.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// wasmcart ABI v3 definitions (backward compatible with v1 and v2)
|
|
2
|
+
|
|
3
|
+
export const ABI_VERSION = 3;
|
|
4
|
+
export const MIN_ABI_VERSION = 1; // oldest version we still support
|
|
5
|
+
|
|
6
|
+
// Button bitmask positions (matches common gamepad layout)
|
|
7
|
+
export const BUTTON = {
|
|
8
|
+
A: 1 << 0,
|
|
9
|
+
B: 1 << 1,
|
|
10
|
+
X: 1 << 2,
|
|
11
|
+
Y: 1 << 3,
|
|
12
|
+
L: 1 << 4,
|
|
13
|
+
R: 1 << 5,
|
|
14
|
+
START: 1 << 6,
|
|
15
|
+
SELECT: 1 << 7,
|
|
16
|
+
UP: 1 << 8,
|
|
17
|
+
DOWN: 1 << 9,
|
|
18
|
+
LEFT: 1 << 10,
|
|
19
|
+
RIGHT: 1 << 11,
|
|
20
|
+
L3: 1 << 12,
|
|
21
|
+
R3: 1 << 13,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// WCPad struct layout (16 bytes per pad)
|
|
25
|
+
// u16 buttons
|
|
26
|
+
// i16 left_x, left_y, right_x, right_y
|
|
27
|
+
// u8 left_trigger, right_trigger, connected, _pad
|
|
28
|
+
export const PAD_SIZE = 16;
|
|
29
|
+
export const MAX_PADS = 4;
|
|
30
|
+
export const INPUT_REGION_SIZE = PAD_SIZE * MAX_PADS; // 64 bytes
|
|
31
|
+
|
|
32
|
+
// WCTime struct layout (20 bytes)
|
|
33
|
+
// f64 time_ms (offset 0)
|
|
34
|
+
// f64 delta_ms (offset 8)
|
|
35
|
+
// u32 frame (offset 16)
|
|
36
|
+
export const TIME_SIZE = 20;
|
|
37
|
+
|
|
38
|
+
// WCInfo struct field offsets (returned by wc_get_info)
|
|
39
|
+
// All fields are u32
|
|
40
|
+
export const INFO_FIELDS = {
|
|
41
|
+
VERSION: 0,
|
|
42
|
+
WIDTH: 4,
|
|
43
|
+
HEIGHT: 8,
|
|
44
|
+
FB_PTR: 12,
|
|
45
|
+
AUDIO_PTR: 16,
|
|
46
|
+
AUDIO_CAP: 20, // capacity in stereo frames
|
|
47
|
+
AUDIO_WRITE: 24, // cart's write cursor offset (pointer to u32 in cart memory)
|
|
48
|
+
INPUT_PTR: 28,
|
|
49
|
+
SAVE_PTR: 32,
|
|
50
|
+
SAVE_SIZE: 36,
|
|
51
|
+
TIME_PTR: 40,
|
|
52
|
+
HOST_INFO_PTR: 44, // pointer to wc_host_info_t (host writes before wc_init)
|
|
53
|
+
};
|
|
54
|
+
export const INFO_STRUCT_SIZE = 48;
|
|
55
|
+
|
|
56
|
+
// WCHostInfo struct layout (written by host before wc_init)
|
|
57
|
+
// All fields are u32
|
|
58
|
+
export const HOST_INFO_FIELDS = {
|
|
59
|
+
PREFERRED_WIDTH: 0,
|
|
60
|
+
PREFERRED_HEIGHT: 4,
|
|
61
|
+
HOST_FPS: 8,
|
|
62
|
+
AUDIO_SAMPLE_RATE: 12,
|
|
63
|
+
FLAGS: 16,
|
|
64
|
+
};
|
|
65
|
+
export const HOST_INFO_SIZE = 20;
|
|
66
|
+
|
|
67
|
+
// Cart info flags (wc_info_t.flags)
|
|
68
|
+
export const FLAG_AUDIO_F32 = 1 << 0; // audio ring buffer uses float32
|
|
69
|
+
export const FLAG_NET_WS = 1 << 1; // cart wants WebSocket imports
|
|
70
|
+
export const FLAG_NET_DC = 1 << 2; // cart wants data channel imports
|
|
71
|
+
export const FLAG_POINTER = 1 << 3; // cart wants pointer input
|
|
72
|
+
export const FLAG_KEYBOARD = 1 << 4; // cart wants raw keyboard input
|
|
73
|
+
|
|
74
|
+
// Extended info fields (v3) - byte offsets from wc_info_t start
|
|
75
|
+
export const INFO_FIELDS_V3 = {
|
|
76
|
+
POINTER_PTR: 56, // u32 index 14
|
|
77
|
+
KEYS_PTR: 60, // u32 index 15
|
|
78
|
+
GPU_API: 64, // u32 index 16 - 0=2D, 1=WebGL2/GLES3, 2=WebGPU, 3=Vulkan
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// GPU API values for wc_info_t.gpu_api
|
|
82
|
+
export const GPU_API_NONE = 0; // 2D framebuffer only
|
|
83
|
+
export const GPU_API_WEBGL2 = 1; // WebGL2 / GLES3
|
|
84
|
+
export const GPU_API_WEBGPU = 2; // reserved
|
|
85
|
+
export const GPU_API_VULKAN = 3; // reserved
|
|
86
|
+
|
|
87
|
+
// Pointer struct layout (8 bytes per pointer)
|
|
88
|
+
// i16 x, i16 y, u8 buttons, u8 active, u8[2] pad
|
|
89
|
+
export const POINTER_SIZE = 8;
|
|
90
|
+
export const MAX_POINTERS = 10;
|
|
91
|
+
export const POINTER_REGION_SIZE = POINTER_SIZE * MAX_POINTERS; // 80 bytes
|
|
92
|
+
|
|
93
|
+
// Keyboard state bitmask size
|
|
94
|
+
export const KEYS_STATE_SIZE = 32; // 256 bits = 32 bytes
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cartWorker.js - WASI threads worker entry point
|
|
3
|
+
*
|
|
4
|
+
* Runs in a Node.js worker_thread. Receives a compiled WebAssembly.Module
|
|
5
|
+
* and shared WebAssembly.Memory, instantiates the module, and calls
|
|
6
|
+
* wasi_thread_start(tid, start_arg).
|
|
7
|
+
*
|
|
8
|
+
* All pthread logic (mutexes, condvars, TLS) lives inside the WASM module
|
|
9
|
+
* via wasi-libc. This worker just provides the host-side imports.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { workerData, parentPort } from 'worker_threads';
|
|
13
|
+
import { openSync, readSync, closeSync, statSync } from 'fs';
|
|
14
|
+
import { inflateRawSync } from 'zlib';
|
|
15
|
+
|
|
16
|
+
const { module: wasmModule, memory, tid, startArg, assetConfig } = workerData;
|
|
17
|
+
|
|
18
|
+
// --- Asset access (worker opens its own fd for the .wasc file) ---
|
|
19
|
+
|
|
20
|
+
let assetIndex = null; // Map<path, entry>
|
|
21
|
+
let assetFd = null;
|
|
22
|
+
let assetBuf = null;
|
|
23
|
+
|
|
24
|
+
if (assetConfig) {
|
|
25
|
+
if (assetConfig.type === 'zip' && assetConfig.filePath) {
|
|
26
|
+
assetFd = openSync(assetConfig.filePath, 'r');
|
|
27
|
+
assetIndex = new Map(assetConfig.index);
|
|
28
|
+
} else if (assetConfig.type === 'buffer' && assetConfig.buffer) {
|
|
29
|
+
assetBuf = assetConfig.buffer;
|
|
30
|
+
assetIndex = new Map(assetConfig.index);
|
|
31
|
+
} else if (assetConfig.type === 'dir' && assetConfig.dir) {
|
|
32
|
+
// Directory-based dev mode - not supported in worker yet
|
|
33
|
+
// (would need readFileSync, rarely used with threaded carts)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function readZipEntry(fd, entry) {
|
|
38
|
+
const buf = Buffer.alloc(entry.compressedSize);
|
|
39
|
+
readSync(fd, buf, 0, entry.compressedSize, entry.dataOffset);
|
|
40
|
+
if (entry.compressionMethod === 0) return buf;
|
|
41
|
+
if (entry.compressionMethod === 8) return inflateRawSync(buf);
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function readZipEntryFromBuffer(buf, entry) {
|
|
46
|
+
const data = buf.slice(entry.dataOffset, entry.dataOffset + entry.compressedSize);
|
|
47
|
+
if (entry.compressionMethod === 0) return data;
|
|
48
|
+
if (entry.compressionMethod === 8) return inflateRawSync(data);
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function assetSize(pathPtr, pathLen) {
|
|
53
|
+
if (!assetIndex) return -1;
|
|
54
|
+
const path = new TextDecoder().decode(new Uint8Array(memory.buffer, pathPtr, pathLen));
|
|
55
|
+
const entry = assetIndex.get(path);
|
|
56
|
+
return entry ? entry.uncompressedSize : -1;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function loadAsset(pathPtr, pathLen, destPtr, maxSize) {
|
|
60
|
+
if (!assetIndex) return -1;
|
|
61
|
+
const path = new TextDecoder().decode(new Uint8Array(memory.buffer, pathPtr, pathLen));
|
|
62
|
+
const entry = assetIndex.get(path);
|
|
63
|
+
if (!entry) return -1;
|
|
64
|
+
if (entry.uncompressedSize > maxSize) return -1;
|
|
65
|
+
|
|
66
|
+
let data;
|
|
67
|
+
if (assetFd !== null) {
|
|
68
|
+
data = readZipEntry(assetFd, entry);
|
|
69
|
+
} else if (assetBuf) {
|
|
70
|
+
data = readZipEntryFromBuffer(assetBuf, entry);
|
|
71
|
+
} else {
|
|
72
|
+
return -1;
|
|
73
|
+
}
|
|
74
|
+
if (!data) return -1;
|
|
75
|
+
|
|
76
|
+
// Write into shared WASM memory
|
|
77
|
+
const dest = new Uint8Array(memory.buffer, destPtr, data.length);
|
|
78
|
+
dest.set(new Uint8Array(data.buffer || data, data.byteOffset, data.length));
|
|
79
|
+
return data.length;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// --- Build import object ---
|
|
83
|
+
|
|
84
|
+
const imports = {
|
|
85
|
+
env: {
|
|
86
|
+
memory,
|
|
87
|
+
wc_log: (ptr, len) => {
|
|
88
|
+
const bytes = new Uint8Array(memory.buffer).slice(ptr, ptr + len);
|
|
89
|
+
const text = new TextDecoder().decode(bytes);
|
|
90
|
+
console.log(`[cart:t${tid}]`, text);
|
|
91
|
+
},
|
|
92
|
+
wc_asset_size: assetSize,
|
|
93
|
+
wc_load_asset: loadAsset,
|
|
94
|
+
emscripten_notify_memory_growth: () => {},
|
|
95
|
+
emscripten_asm_const_int: () => 0,
|
|
96
|
+
emscripten_asm_const_double: () => 0.0,
|
|
97
|
+
emscripten_get_element_css_size: () => 0,
|
|
98
|
+
__syscall_getcwd: () => -1,
|
|
99
|
+
__syscall_getdents64: () => -1,
|
|
100
|
+
},
|
|
101
|
+
wasi_snapshot_preview1: {
|
|
102
|
+
fd_close: () => 0,
|
|
103
|
+
fd_write: (fd, iovs, iovs_len, nwritten_ptr) => {
|
|
104
|
+
try {
|
|
105
|
+
const view = new DataView(memory.buffer);
|
|
106
|
+
const u8 = new Uint8Array(memory.buffer);
|
|
107
|
+
let totalWritten = 0;
|
|
108
|
+
let text = '';
|
|
109
|
+
for (let i = 0; i < iovs_len; i++) {
|
|
110
|
+
const ptr = view.getUint32(iovs + i * 8, true);
|
|
111
|
+
const len = view.getUint32(iovs + i * 8 + 4, true);
|
|
112
|
+
if (len > 0) text += new TextDecoder().decode(u8.slice(ptr, ptr + len));
|
|
113
|
+
totalWritten += len;
|
|
114
|
+
}
|
|
115
|
+
if (text && (fd === 1 || fd === 2)) {
|
|
116
|
+
for (const line of text.split('\n')) {
|
|
117
|
+
if (line.length > 0) process.stderr.write(`[cart:t${tid}] ${line}\n`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (nwritten_ptr) view.setUint32(nwritten_ptr, totalWritten, true);
|
|
121
|
+
} catch {}
|
|
122
|
+
return 0;
|
|
123
|
+
},
|
|
124
|
+
fd_seek: () => 0,
|
|
125
|
+
fd_read: () => 0,
|
|
126
|
+
environ_get: () => 0,
|
|
127
|
+
environ_sizes_get: () => 0,
|
|
128
|
+
proc_exit: () => {},
|
|
129
|
+
clock_time_get: (id, precision, resultPtr) => {
|
|
130
|
+
try {
|
|
131
|
+
const ns = BigInt(Math.round(performance.now() * 1e6));
|
|
132
|
+
new DataView(memory.buffer).setBigUint64(resultPtr, ns, true);
|
|
133
|
+
} catch {}
|
|
134
|
+
return 0;
|
|
135
|
+
},
|
|
136
|
+
sched_yield: () => 0,
|
|
137
|
+
},
|
|
138
|
+
wasi: {
|
|
139
|
+
'thread-spawn': (arg) => {
|
|
140
|
+
// Nested thread spawning: request main thread to do it
|
|
141
|
+
// For now, use synchronous message exchange
|
|
142
|
+
// TODO: use Atomics.wait/notify for true sync if needed
|
|
143
|
+
parentPort.postMessage({ type: 'spawn', startArg: arg });
|
|
144
|
+
// Return -1 for now (nested spawn needs async handling)
|
|
145
|
+
// Full implementation would use Atomics.wait on a shared buffer
|
|
146
|
+
return -1;
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
// Provide GL stubs if the module imports GL functions (they must not be called from workers)
|
|
152
|
+
const moduleImports = WebAssembly.Module.imports(wasmModule);
|
|
153
|
+
for (const imp of moduleImports) {
|
|
154
|
+
if (imp.kind !== 'function') continue;
|
|
155
|
+
// GL functions in 'gl' module
|
|
156
|
+
if (imp.module === 'gl') {
|
|
157
|
+
if (!imports.gl) imports.gl = {};
|
|
158
|
+
imports.gl[imp.name] = () => {
|
|
159
|
+
throw new Error(`GL call ${imp.name}() not allowed from worker thread ${tid}`);
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
// GL functions in 'env' module (gl4es pattern)
|
|
163
|
+
if (imp.module === 'env' && (imp.name.startsWith('gl') || imp.name.startsWith('emscripten_gl'))) {
|
|
164
|
+
if (!(imp.name in imports.env)) {
|
|
165
|
+
imports.env[imp.name] = () => {
|
|
166
|
+
throw new Error(`GL call ${imp.name}() not allowed from worker thread ${tid}`);
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// Other unknown env functions - provide no-op stubs to avoid instantiation failure
|
|
171
|
+
if (imp.module === 'env' && !(imp.name in imports.env)) {
|
|
172
|
+
imports.env[imp.name] = () => 0;
|
|
173
|
+
}
|
|
174
|
+
// Unknown wasi_snapshot_preview1 functions - no-op stubs
|
|
175
|
+
if (imp.module === 'wasi_snapshot_preview1' && !(imp.name in imports.wasi_snapshot_preview1)) {
|
|
176
|
+
imports.wasi_snapshot_preview1[imp.name] = () => 0;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// --- Instantiate and run ---
|
|
181
|
+
|
|
182
|
+
async function run() {
|
|
183
|
+
const instance = await WebAssembly.instantiate(wasmModule, imports);
|
|
184
|
+
|
|
185
|
+
// Call the thread entry point
|
|
186
|
+
instance.exports.wasi_thread_start(tid, startArg);
|
|
187
|
+
|
|
188
|
+
// Thread function returned - clean up and notify main thread
|
|
189
|
+
if (assetFd !== null) {
|
|
190
|
+
try { closeSync(assetFd); } catch {}
|
|
191
|
+
}
|
|
192
|
+
parentPort.postMessage({ type: 'exit', tid });
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
run().catch(err => {
|
|
196
|
+
console.error(`[thread ${tid}] fatal:`, err.message);
|
|
197
|
+
if (assetFd !== null) {
|
|
198
|
+
try { closeSync(assetFd); } catch {}
|
|
199
|
+
}
|
|
200
|
+
parentPort.postMessage({ type: 'exit', tid, error: err.message });
|
|
201
|
+
});
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cartWorkerWeb.js - WASI threads worker for browser
|
|
3
|
+
*
|
|
4
|
+
* Receives a compiled WebAssembly.Module and shared WebAssembly.Memory
|
|
5
|
+
* via postMessage, instantiates the module, and calls
|
|
6
|
+
* wasi_thread_start(tid, start_arg).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
let inflateSync = null;
|
|
10
|
+
|
|
11
|
+
let memory;
|
|
12
|
+
let tid;
|
|
13
|
+
let assetBuf = null;
|
|
14
|
+
let assetIndex = null;
|
|
15
|
+
|
|
16
|
+
function readZipEntryFromBuffer(buf, entry) {
|
|
17
|
+
const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
18
|
+
const nameLen = view.getUint16(entry.localHeaderOffset + 26, true);
|
|
19
|
+
const extraLen = view.getUint16(entry.localHeaderOffset + 28, true);
|
|
20
|
+
const dataOffset = entry.localHeaderOffset + 30 + nameLen + extraLen;
|
|
21
|
+
|
|
22
|
+
const compressedData = buf.subarray(dataOffset, dataOffset + entry.compressedSize);
|
|
23
|
+
|
|
24
|
+
if (entry.compressionMethod === 0) return compressedData;
|
|
25
|
+
if (entry.compressionMethod === 8) {
|
|
26
|
+
if (!inflateSync) throw new Error('fflate not loaded - cannot decompress asset');
|
|
27
|
+
return inflateSync(compressedData);
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function assetSize(pathPtr, pathLen) {
|
|
33
|
+
if (!assetIndex) return -1;
|
|
34
|
+
const path = new TextDecoder().decode(new Uint8Array(memory.buffer, pathPtr, pathLen));
|
|
35
|
+
const entry = assetIndex.get(path);
|
|
36
|
+
return entry ? entry.uncompressedSize : -1;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function loadAsset(pathPtr, pathLen, destPtr, maxSize) {
|
|
40
|
+
if (!assetIndex) return -1;
|
|
41
|
+
const path = new TextDecoder().decode(new Uint8Array(memory.buffer, pathPtr, pathLen));
|
|
42
|
+
const entry = assetIndex.get(path);
|
|
43
|
+
if (!entry) return -1;
|
|
44
|
+
if (entry.uncompressedSize > maxSize) return -1;
|
|
45
|
+
|
|
46
|
+
let data;
|
|
47
|
+
if (assetBuf) {
|
|
48
|
+
data = readZipEntryFromBuffer(assetBuf, entry);
|
|
49
|
+
} else {
|
|
50
|
+
return -1;
|
|
51
|
+
}
|
|
52
|
+
if (!data) return -1;
|
|
53
|
+
|
|
54
|
+
const dest = new Uint8Array(memory.buffer, destPtr, data.length);
|
|
55
|
+
dest.set(data.subarray ? data.subarray(0, data.length) : new Uint8Array(data, 0, data.length));
|
|
56
|
+
return data.length;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
self.onmessage = async function(e) {
|
|
60
|
+
const { module: wasmModule, memory: sharedMemory, tid: threadId, startArg, assetConfig } = e.data;
|
|
61
|
+
memory = sharedMemory;
|
|
62
|
+
tid = threadId;
|
|
63
|
+
|
|
64
|
+
// Set up asset access
|
|
65
|
+
if (assetConfig && assetConfig.type === 'buffer' && assetConfig.buffer) {
|
|
66
|
+
assetBuf = new Uint8Array(assetConfig.buffer);
|
|
67
|
+
assetIndex = new Map(assetConfig.index);
|
|
68
|
+
// Load fflate dynamically for asset decompression
|
|
69
|
+
try {
|
|
70
|
+
const fflate = await import('fflate');
|
|
71
|
+
inflateSync = fflate.inflateSync;
|
|
72
|
+
} catch (e) {
|
|
73
|
+
console.warn(`[thread] fflate not available - compressed assets will fail`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const imports = {
|
|
78
|
+
env: {
|
|
79
|
+
memory,
|
|
80
|
+
wc_log: (ptr, len) => {
|
|
81
|
+
const bytes = new Uint8Array(memory.buffer).slice(ptr, ptr + len);
|
|
82
|
+
const text = new TextDecoder().decode(bytes);
|
|
83
|
+
console.warn(`[cart:t${tid}]`, text);
|
|
84
|
+
},
|
|
85
|
+
wc_asset_size: assetSize,
|
|
86
|
+
wc_load_asset: loadAsset,
|
|
87
|
+
emscripten_notify_memory_growth: () => {},
|
|
88
|
+
emscripten_asm_const_int: () => 0,
|
|
89
|
+
emscripten_asm_const_double: () => 0.0,
|
|
90
|
+
emscripten_get_element_css_size: () => 0,
|
|
91
|
+
__syscall_getcwd: () => -1,
|
|
92
|
+
__syscall_getdents64: () => -1,
|
|
93
|
+
},
|
|
94
|
+
wasi_snapshot_preview1: {
|
|
95
|
+
fd_close: () => 0,
|
|
96
|
+
fd_write: (fd, iovs, iovs_len, nwritten_ptr) => {
|
|
97
|
+
try {
|
|
98
|
+
const view = new DataView(memory.buffer);
|
|
99
|
+
const u8 = new Uint8Array(memory.buffer);
|
|
100
|
+
let totalWritten = 0;
|
|
101
|
+
let text = '';
|
|
102
|
+
for (let i = 0; i < iovs_len; i++) {
|
|
103
|
+
const ptr = view.getUint32(iovs + i * 8, true);
|
|
104
|
+
const len = view.getUint32(iovs + i * 8 + 4, true);
|
|
105
|
+
if (len > 0) text += new TextDecoder().decode(u8.slice(ptr, ptr + len));
|
|
106
|
+
totalWritten += len;
|
|
107
|
+
}
|
|
108
|
+
if (text && (fd === 1 || fd === 2)) {
|
|
109
|
+
console.warn(`[cart:t${tid}]`, text);
|
|
110
|
+
}
|
|
111
|
+
if (nwritten_ptr) view.setUint32(nwritten_ptr, totalWritten, true);
|
|
112
|
+
} catch {}
|
|
113
|
+
return 0;
|
|
114
|
+
},
|
|
115
|
+
fd_seek: () => 0,
|
|
116
|
+
fd_read: () => 0,
|
|
117
|
+
environ_get: () => 0,
|
|
118
|
+
environ_sizes_get: () => 0,
|
|
119
|
+
proc_exit: () => {},
|
|
120
|
+
clock_time_get: (id, precision, resultPtr) => {
|
|
121
|
+
try {
|
|
122
|
+
const ns = BigInt(Math.round(performance.now() * 1e6));
|
|
123
|
+
new DataView(memory.buffer).setBigUint64(resultPtr, ns, true);
|
|
124
|
+
} catch {}
|
|
125
|
+
return 0;
|
|
126
|
+
},
|
|
127
|
+
sched_yield: () => 0,
|
|
128
|
+
},
|
|
129
|
+
wasi: {
|
|
130
|
+
'thread-spawn': (arg) => {
|
|
131
|
+
self.postMessage({ type: 'spawn', startArg: arg });
|
|
132
|
+
return -1;
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// Stub GL and unknown imports
|
|
138
|
+
const moduleImports = WebAssembly.Module.imports(wasmModule);
|
|
139
|
+
for (const imp of moduleImports) {
|
|
140
|
+
if (imp.kind !== 'function') continue;
|
|
141
|
+
if (imp.module === 'gl') {
|
|
142
|
+
if (!imports.gl) imports.gl = {};
|
|
143
|
+
imports.gl[imp.name] = () => {
|
|
144
|
+
throw new Error(`GL call ${imp.name}() not allowed from worker thread ${tid}`);
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
if (imp.module === 'env' && (imp.name.startsWith('gl') || imp.name.startsWith('emscripten_gl'))) {
|
|
148
|
+
if (!(imp.name in imports.env)) {
|
|
149
|
+
imports.env[imp.name] = () => {
|
|
150
|
+
throw new Error(`GL call ${imp.name}() not allowed from worker thread ${tid}`);
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (imp.module === 'env' && !(imp.name in imports.env)) {
|
|
155
|
+
imports.env[imp.name] = () => 0;
|
|
156
|
+
}
|
|
157
|
+
if (imp.module === 'wasi_snapshot_preview1' && !(imp.name in imports.wasi_snapshot_preview1)) {
|
|
158
|
+
imports.wasi_snapshot_preview1[imp.name] = () => 0;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
const instance = await WebAssembly.instantiate(wasmModule, imports);
|
|
164
|
+
instance.exports.wasi_thread_start(tid, startArg);
|
|
165
|
+
self.postMessage({ type: 'exit', tid });
|
|
166
|
+
} catch (err) {
|
|
167
|
+
console.error(`[thread ${tid}] fatal:`, err.message);
|
|
168
|
+
self.postMessage({ type: 'exit', tid, error: err.message });
|
|
169
|
+
}
|
|
170
|
+
};
|