img2num 0.0.0 → 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.
@@ -0,0 +1,243 @@
1
+ /**
2
+ * @file wasmWorker.js
3
+ * @description
4
+ * Worker for calling WASM functions (Img2Num module) with proper memory handling.
5
+ *
6
+ * Notes:
7
+ * - WASM exposes its linear memory via typed array views such as HEAP32 (Int32) or HEAPU8 (byte).
8
+ * - When adding a new TypedArray type:
9
+ * 1. Add the corresponding HEAP view to EXPORTED_RUNTIME_METHODS in the Emscripten CMakeLists.txt.
10
+ * 2. Allocate memory with `_malloc`.
11
+ * 3. Copy data into the appropriate HEAP view. HEAP views are important because they allow the raw data to be read correctly.
12
+ * 4. After the call, read data back from the same HEAP view.
13
+ * 5. Free the allocated memory. This is important! We are using C-style memory since the code interfaces with the C ABI, so there is no GC.
14
+ * - This ensures JavaScript arrays correctly map to WASM memory.
15
+ *
16
+ * Important:
17
+ * - The `args` object passed to all convenience wrapper functions must match the order
18
+ * of the C++ function parameters exactly. For example:
19
+ * ```js
20
+ * args = { a: 1, b: 2 };
21
+ * ```
22
+ * corresponds to
23
+ * ```cpp
24
+ * int add(int a, int b);
25
+ * ```
26
+ *
27
+ * - Async functions:
28
+ * WebGPU operations inside WASM can be asynchronous. Use Emscripten Asyncify
29
+ * (via `ccall` with `{ async: true }`) to properly pause and resume execution.
30
+ *
31
+ * @example
32
+ * self.postMessage({
33
+ * id: 1,
34
+ * funcName: "bilateral_filter_gpu",
35
+ * args: { input: myArray },
36
+ * bufferKeys: [{ key: "input", type: "Uint8Array" }],
37
+ * returnType: "void"
38
+ * });
39
+ */
40
+
41
+ import createImg2NumModule from "@wasm/index.js";
42
+
43
+ let wasmModule;
44
+
45
+ /**
46
+ * Promise that resolves when WASM module is ready.
47
+ * @type {Promise<void>}
48
+ */
49
+ const readyPromise = createImg2NumModule().then((mod) => {
50
+ wasmModule = mod;
51
+ });
52
+
53
+ // --------------------
54
+ // WASM Type Handlers
55
+ // --------------------
56
+ /**
57
+ * Handlers for allocating, reading, and freeing different WASM types.
58
+ * @type {Record<string, {alloc: Function, read: Function}>}
59
+ */
60
+ const WASM_TYPES = {
61
+ void: {
62
+ alloc: () => null,
63
+ read: () => undefined,
64
+ },
65
+
66
+ Int32Array: {
67
+ /**
68
+ * Allocate an Int32Array in WASM memory.
69
+ * @param {Int32Array} arr
70
+ * @returns {number} Pointer to allocated memory
71
+ */
72
+ alloc: (arr) => {
73
+ const ptr = wasmModule._malloc(arr.byteLength);
74
+ wasmModule.HEAP32.set(arr, ptr >> 2);
75
+ return ptr;
76
+ },
77
+
78
+ /**
79
+ * Read an Int32Array from WASM memory.
80
+ * @param {number} ptr
81
+ * @param {number} len
82
+ * @returns {Int32Array}
83
+ */
84
+ read: (ptr, len) => new Int32Array(wasmModule.HEAP32.buffer, ptr, len).slice(),
85
+ },
86
+
87
+ Uint8Array: {
88
+ alloc: (arr) => {
89
+ const ptr = wasmModule._malloc(arr.byteLength);
90
+ wasmModule.HEAPU8.set(arr, ptr);
91
+ return ptr;
92
+ },
93
+ read: (ptr, len) => wasmModule.HEAPU8.slice(ptr, ptr + len),
94
+ },
95
+
96
+ Uint8ClampedArray: {
97
+ alloc: (arr) => {
98
+ const ptr = wasmModule._malloc(arr.byteLength);
99
+ wasmModule.HEAPU8.set(arr, ptr);
100
+ return ptr;
101
+ },
102
+ read: (ptr, len) => new Uint8ClampedArray(wasmModule.HEAPU8.slice(ptr, ptr + len)),
103
+ },
104
+
105
+ string: {
106
+ /**
107
+ * Allocate a string in WASM memory.
108
+ * @param {string} str
109
+ * @returns {number} Pointer to allocated memory
110
+ */
111
+ alloc: (str) => {
112
+ const len = wasmModule.lengthBytesUTF8(str) + 1;
113
+ const ptr = wasmModule._malloc(len);
114
+ wasmModule.stringToUTF8(str, ptr, len);
115
+ return ptr;
116
+ },
117
+
118
+ /**
119
+ * Read a string from WASM memory.
120
+ * @param {number} ptr
121
+ * @returns {string|null}
122
+ */
123
+ read: (ptr) => (ptr ? wasmModule.UTF8ToString(ptr) : null),
124
+ },
125
+ };
126
+
127
+ // --------------------
128
+ // Internal helper
129
+ // --------------------
130
+ /**
131
+ * Call a WASM function via ccall, handling asyncify automatically.
132
+ * @param {string} funcName
133
+ * @param {Map<string, number>} argsMap - Map of argument names to WASM pointers or numbers
134
+ * @param {string} returnType - WASM return type (e.g., 'void', 'Int32Array', 'string')
135
+ * @returns {Promise<number>} Result pointer or numeric return value
136
+ */
137
+ async function callWasm(funcName, argsMap, returnType) {
138
+ const argTypes = Array(argsMap.size).fill("number");
139
+ const retType = returnType !== "void" ? "number" : null;
140
+
141
+ return await wasmModule.ccall(funcName, retType, argTypes, [...argsMap.values()], { async: true });
142
+ }
143
+
144
+ // --------------------
145
+ // Worker message handler
146
+ // --------------------
147
+ /**
148
+ * Handle messages from main thread.
149
+ * Expects `data` to contain:
150
+ * - id: unique message ID
151
+ * - funcName: WASM export to call
152
+ * - args: object of input arguments to WASM export
153
+ * - bufferKeys: array of {key, type} defining memory buffers for WASM export args - JS doesn't have pointers, so we must do this
154
+ * - returnType: expected return type of the WASM export
155
+ * @param {MessageEvent} event
156
+ */
157
+
158
+ async function handleMessage(data) {
159
+ await readyPromise;
160
+
161
+ const { id, funcName, args, bufferKeys, returnType } = data;
162
+
163
+ // These are freed in the finally block
164
+ const pointers = new Map();
165
+
166
+ try {
167
+ // -------- Validation --------
168
+ if (!funcName) throw new Error("Missing funcName");
169
+ if (!args) throw new Error("Missing args");
170
+ if (!bufferKeys) throw new Error("Missing bufferKeys");
171
+ if (!returnType) throw new Error("Missing returnType");
172
+
173
+ const argsMap = new Map(Object.entries(args));
174
+
175
+ // -------- Allocate buffers --------
176
+ for (const { key, type } of bufferKeys) {
177
+ const handler = WASM_TYPES[type];
178
+ if (!handler) throw new Error(`Unsupported type: ${type}`);
179
+
180
+ const val = argsMap.get(key);
181
+ const ptr = handler.alloc(val);
182
+
183
+ pointers.set(key, { ptr, type, length: val?.length });
184
+ argsMap.set(key, ptr);
185
+ }
186
+
187
+ // -------- Call WASM --------
188
+ let result = await callWasm(funcName, argsMap, returnType);
189
+
190
+ // -------- Read outputs --------
191
+ /** @type {Record<string, any>} */
192
+ const output = Object.create(null);
193
+ for (const { key, type } of bufferKeys) {
194
+ const { ptr, length } = pointers.get(key);
195
+ output[key] = WASM_TYPES[type].read(ptr, length);
196
+ }
197
+
198
+ // -------- Handle return --------
199
+ let returnValue = result;
200
+
201
+ if (returnType !== "void") {
202
+ returnValue = WASM_TYPES[returnType].read(result);
203
+ }
204
+
205
+ if (returnType === "string" && result) {
206
+ wasmModule._free(result);
207
+ }
208
+
209
+ globalThis.postMessage({ id, output, returnValue });
210
+ } catch (error) {
211
+ globalThis.postMessage({ id, error: error.message });
212
+ } finally {
213
+ // -------- Cleanup --------
214
+ for (const { ptr } of pointers.values()) {
215
+ wasmModule._free(ptr);
216
+ }
217
+ }
218
+ }
219
+
220
+ if (__TARGET__ === "node") {
221
+ const { parentPort } = await import("node:worker_threads");
222
+ const { initWebGPU, destroyWebGPU } = await import("../target/node/webgpu.js");
223
+
224
+ try {
225
+ await initWebGPU();
226
+ } catch (err) {
227
+ console.error(`[Img2Num node/worker.js] Error: ${err}`);
228
+ }
229
+
230
+ // 2. FIX THE TYPO: Polyfill globalThis.postMessage so handleMessage can call it natively!
231
+ globalThis.postMessage = (data) => parentPort.postMessage(data);
232
+
233
+ // 3. Listen for incoming messages from the console app main thread
234
+ // (Node passes the raw payload directly, no nested event wrapper needed)
235
+ parentPort.on("message", async (data) => {
236
+ await handleMessage(data);
237
+ await destroyWebGPU();
238
+ parentPort.close();
239
+ });
240
+ } else {
241
+ // Browser Worker setup: Standard event-unwrapping listener
242
+ globalThis.onmessage = ({ data }) => handleMessage(data);
243
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "ESNext",
5
+ "allowJs": true,
6
+ "checkJs": false,
7
+ "outDir": "dist",
8
+ "rootDir": ".",
9
+ "moduleResolution": "bundler",
10
+ "esModuleInterop": true,
11
+ // 1. CRITICAL: Tells TypeScript to stop checking external dependency declarations.
12
+ // This immediately squashes type collisions originating inside node_modules.
13
+ "skipLibCheck": true,
14
+
15
+ // 2. ISOLATION: Explicitly restrict libraries to standard Core ECMAScript features.
16
+ // By omitting "DOM" here, you prevent TypeScript from loading its built-in WebGPU types.
17
+ "lib": ["ESNext"]
18
+ },
19
+ "include": ["*.js", "src/*.js", "src/target/**/*.js", "src/workers/*.js"],
20
+ "typedocOptions": {
21
+ "exclude": ["node_modules/", "dist/"]
22
+ }
23
+ }
package/vite.config.js ADDED
@@ -0,0 +1,74 @@
1
+ import { defineConfig } from "vite";
2
+ import path from "path";
3
+
4
+ export default defineConfig(() => {
5
+ const __TARGET__ = process.env.TARGET ?? "browser";
6
+ const isNode = __TARGET__ === "node";
7
+
8
+ return {
9
+ base: "./",
10
+ plugins: [],
11
+
12
+ // NATIVE COPIER: Copies index.wasm into dist/node/ automatically
13
+ publicDir: isNode ? "build-wasm" : "src/workers",
14
+
15
+ build: {
16
+ base: "./",
17
+ outDir: `dist/${__TARGET__}`,
18
+ assetsDir: ".",
19
+ emptyOutDir: true,
20
+
21
+ // 1. CRITICAL: This tells the production bundler to preserve
22
+ // native Node subsystems (like 'module' and 'fs') instead of shimming them for the web.
23
+ ssr: isNode,
24
+ ...(isNode && { target: "node18" }),
25
+
26
+ minify: !isNode,
27
+
28
+ lib: isNode
29
+ ? {
30
+ entry: {
31
+ img2num: "src/index.js",
32
+ wasmWorker: "src/workers/wasmWorker.js",
33
+ },
34
+ formats: ["es"],
35
+ fileName: (format, entryName) => `${entryName}.js`,
36
+ }
37
+ : {
38
+ entry: "src/index.js",
39
+ formats: ["es"],
40
+ },
41
+
42
+ rollupOptions: {
43
+ external: isNode ? ["node:worker_threads", "worker_threads", "node:path", "path", "node:url", "url", "node:webgpu", "webgpu", "node:module", "module"] : [],
44
+
45
+ output: isNode
46
+ ? {
47
+ manualChunks(id) {
48
+ if (id.includes("build-wasm")) {
49
+ return "wasmWorker";
50
+ }
51
+ },
52
+ assetFileNames: "[name][extname]",
53
+ }
54
+ : {},
55
+ },
56
+ },
57
+
58
+ define: {
59
+ __TARGET__: JSON.stringify(__TARGET__),
60
+ },
61
+
62
+ resolve: {
63
+ alias: {
64
+ "@__TARGET__": path.resolve(__dirname, `./src/target/${__TARGET__}`),
65
+ "@wasm": path.resolve(__dirname, "./build-wasm"),
66
+ "@workers": path.resolve(__dirname, "./src/workers"),
67
+ },
68
+ },
69
+
70
+ worker: {
71
+ format: "es",
72
+ },
73
+ };
74
+ });