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.
- package/CHANGELOG.md +43 -0
- package/dist/browser/__vite-browser-external-CSF28Lpk.js +1 -0
- package/dist/browser/img2num.js +179 -0
- package/dist/browser/wasmWorker-BVnzGxvB.js +2 -0
- package/{wasmWorker.js → dist/browser/wasmWorker.js} +31 -5
- package/dist/node/img2num.js +514 -0
- package/dist/node/index.js +2 -0
- package/dist/node/index.wasm +0 -0
- package/dist/node/wasmWorker-CpNpexYK.js +4471 -0
- package/dist/node/wasmWorker.js +198 -0
- package/dist/node/webgpu-BjVEVfI9.js +23 -0
- package/package.json +23 -13
- package/{safeWasmWrappers.js → src/safeWasmWrappers.js} +9 -7
- package/src/target/browser/worker.js +10 -0
- package/src/target/node/webgpu.js +35 -0
- package/src/target/node/worker.js +20 -0
- package/{wasmClient.js → src/wasmClient.js} +30 -15
- package/src/workers/wasmWorker.js +243 -0
- package/tsconfig.typedoc.json +23 -0
- package/vite.config.js +74 -0
- package/build-wasm/index.js +0 -14
- package/build-wasm/index.wasm +0 -0
- /package/{imageToUint8ClampedArray.js → src/imageToUint8ClampedArray.js} +0 -0
- /package/{index.js → src/index.js} +0 -0
|
@@ -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
|
+
});
|