Skip to content

Commit 6f991aa

Browse files
authored
Merge pull request #18 from Kitware/refactor-wasm-loader
feat: Refactor src directory
2 parents 1843e4d + 2027a84 commit 6f991aa

15 files changed

+800
-472
lines changed

src/core/blobURL.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/**
2+
* Create a blob URL from provided buffer and mime type
3+
* @param {ArrayBufferLike} buffer
4+
* @param {string} mimeType
5+
* @returns {string} blob URL
6+
*/
7+
export function createBlobURL(buffer, mimeType) {
8+
return URL.createObjectURL(new Blob([buffer], { type: mimeType }));
9+
}
10+
11+
/**
12+
* Dispose a blob URL
13+
* @param {string} url
14+
*/
15+
export function disposeBlobURL(url) {
16+
// URL.revokeObjectURL is a no-op for non-blob URLs
17+
URL.revokeObjectURL(url);
18+
}

src/core/configManager.js

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { EXECUTION_MODES, MIME_TYPES, RENDERING_BACKENDS } from "./constants";
2+
import { createBlobURL } from "./blobURL";
3+
import { stripLeadingDotSlash } from "./stringOps";
4+
5+
/**
6+
* Validate provided config
7+
* @param {object} config
8+
*/
9+
export function validateConfig(config) {
10+
const validRenderings = Object.values(RENDERING_BACKENDS);
11+
const validExecModes = Object.values(EXECUTION_MODES);
12+
if (config.rendering !== undefined && !validRenderings.includes(config.rendering)) {
13+
throw new Error(`Invalid rendering backend: ${config.rendering}. Valid options are: ${validRenderings.join(', ')}`);
14+
}
15+
if (config.exec !== undefined && !validExecModes.includes(config.exec)) {
16+
throw new Error(`Invalid execution mode: ${config.exec}. Valid options are: ${validExecModes.join(', ')}`);
17+
}
18+
}
19+
20+
/**
21+
* Normalize provided config based on constraints. WebGPU requires async execution.
22+
* @param {object} config
23+
* @returns {object}
24+
*/
25+
export function normalizeConfig(config) {
26+
if (config?.rendering === RENDERING_BACKENDS.WEBGPU) {
27+
if (config?.exec !== EXECUTION_MODES.ASYNC) {
28+
console.warn('WebGPU rendering requires async execution mode. Switching exec to "async".');
29+
return { ...config, exec: EXECUTION_MODES.ASYNC };
30+
}
31+
}
32+
return config;
33+
}
34+
35+
/**
36+
* Check if two configs are the same. Only rendering and exec are compared.
37+
* @param {object} config1
38+
* @param {object} config2
39+
* @returns {boolean}
40+
*/
41+
export function isSameConfig(config1, config2) {
42+
return config1.rendering === config2.rendering && config1.exec === config2.exec;
43+
}
44+
45+
/**
46+
* Create Emscripten config from provided config
47+
* @param {object} config
48+
* @param {{name: string, buffer: ArrayBufferLike}} wasmFile
49+
* @returns {object}
50+
*/
51+
export function createEmscriptenConfig(config, wasmFile) {
52+
const emscriptenConfig = {};
53+
let wasmURL = null;
54+
if (wasmFile !== null && wasmFile !== undefined) {
55+
emscriptenConfig.locateFile = (fileName) => {
56+
const normalizedFileName = stripLeadingDotSlash(fileName);
57+
const normalizedTargetName = stripLeadingDotSlash(wasmFile.name);
58+
if (normalizedFileName === normalizedTargetName) {
59+
wasmURL = createBlobURL(wasmFile.buffer, MIME_TYPES.WASM);
60+
return wasmURL;
61+
}
62+
// Resolve other files relative to this module's URL, not the page URL.
63+
return new URL(fileName, import.meta.url).href;
64+
};
65+
}
66+
let userOnRuntimeInitialized = config?.onRuntimeInitialized;
67+
emscriptenConfig.onRuntimeInitialized = () => {
68+
if (typeof userOnRuntimeInitialized === 'function') {
69+
userOnRuntimeInitialized();
70+
}
71+
// Free the object URL after runtime is initialized
72+
if (wasmURL !== null) {
73+
URL.revokeObjectURL(wasmURL);
74+
}
75+
};
76+
if (config?.rendering === RENDERING_BACKENDS.WEBGPU) {
77+
const userPreRun = Array.isArray(config?.preRun)
78+
? config.preRun
79+
: (config?.preRun ? [config.preRun] : []);
80+
emscriptenConfig.preRun = [(module) => {
81+
module.ENV.VTK_GRAPHICS_BACKEND = 'WEBGPU';
82+
},
83+
...userPreRun];
84+
}
85+
return emscriptenConfig;
86+
}

src/core/constants.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
export const RENDERING_BACKENDS = Object.freeze({
2+
WEBGL: 'webgl',
3+
WEBGPU: 'webgpu',
4+
});
5+
6+
export const EXECUTION_MODES = Object.freeze({
7+
SYNC: 'sync',
8+
ASYNC: 'async',
9+
});
10+
11+
export const DEFAULT_WASM_BASE_NAME = 'vtk';
12+
13+
export const DEFAULT_CONFIG = Object.freeze({
14+
rendering: RENDERING_BACKENDS.WEBGL,
15+
exec: EXECUTION_MODES.SYNC,
16+
});
17+
18+
export const WASM_FILE_EXTENSION = '.wasm';
19+
20+
export const MODULE_JS_FILE_EXTENSION = '.mjs';
21+
22+
export const MIME_TYPES = Object.freeze({
23+
WASM: 'application/wasm',
24+
JAVASCRIPT: 'application/javascript',
25+
});

src/core/future.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/**
2+
* Create a future that returns
3+
* @returns { promise, resolve, reject }
4+
*/
5+
export function createFuture() {
6+
let resolve, reject;
7+
const promise = new Promise((res, rej) => {
8+
resolve = res;
9+
reject = rej;
10+
});
11+
return { promise, resolve, reject };
12+
}

src/core/gzipBundle.js

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import untar from "js-untar";
2+
import { stripLeadingDotSlash } from "./stringOps.js";
3+
import { createFuture } from "./future.js";
4+
import { MODULE_JS_FILE_EXTENSION, WASM_FILE_EXTENSION } from "./constants.js";
5+
6+
/**
7+
* Check if provided URL points to a gzip bundle
8+
* @param {string} url
9+
* @returns {boolean}
10+
*/
11+
export function isGzipBundle(url) {
12+
return typeof url === "string" && url.endsWith(".gz");
13+
}
14+
15+
/**
16+
* Fetch gzip bundle from provided URL
17+
* @param {string} url
18+
* @returns {Promise<ArrayBuffer>} The decompressed tar archive contents from the gzip bundle.
19+
*/
20+
export function fetchGzipBundle(url) {
21+
const { promise, resolve, reject } = createFuture();
22+
fetch(url).then((response) => {
23+
if (response.ok) {
24+
const decompressedStream = response.body.pipeThrough(new DecompressionStream('gzip'));
25+
const decompressionResponse = new Response(decompressedStream);
26+
decompressionResponse
27+
.blob()
28+
.then((blob) => blob.arrayBuffer())
29+
.then(resolve)
30+
.catch(reject);
31+
}
32+
else {
33+
reject(new Error(`Could not fetch gzip bundle from ${url} - response status: ${response.status}`));
34+
}
35+
}).catch(reject);
36+
return promise;
37+
}
38+
39+
/**
40+
* Extract the JavaScript and WebAssembly files from gzip bundle.
41+
* @param {ArrayBuffer} contents
42+
* @param {object} config
43+
* @param {string} wasmBaseName
44+
* @returns {Promise<{js: {name: string, buffer: ArrayBufferLike}, wasm: {name: string, buffer: ArrayBufferLike}}>}
45+
*/
46+
export function extractFilesFromGzipBundle(contents, config, wasmBaseName) {
47+
const { promise, resolve, reject } = createFuture();
48+
untar(contents)
49+
.then((files) => {
50+
const execModeSuffix = config?.exec === "async" ? "Async" : "";
51+
const jsFileMatch = `${wasmBaseName}WebAssembly${execModeSuffix}${MODULE_JS_FILE_EXTENSION}`;
52+
const wasmFileMatch = `${wasmBaseName}WebAssembly${execModeSuffix}${WASM_FILE_EXTENSION}`;
53+
const jsFile = files.find((file) => stripLeadingDotSlash(file.name) === jsFileMatch);
54+
const wasmFile = files.find((file) => stripLeadingDotSlash(file.name) === wasmFileMatch);
55+
if (jsFile === undefined || wasmFile === undefined) {
56+
reject(new Error(`Could not find expected files ${jsFileMatch} and ${wasmFileMatch} in the gzip bundle`));
57+
}
58+
else {
59+
resolve({ js: jsFile, wasm: wasmFile });
60+
}
61+
})
62+
.catch(reject);
63+
return promise;
64+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/**
2+
* Converts a C++ style name (PascalCase) to a JavaScript style name (camelCase).
3+
* @param {string} cxxName
4+
* @returns {string}
5+
*/
6+
export function toJsName(cxxName) {
7+
const jsName = `${cxxName.charAt(0).toLowerCase()}${cxxName.slice(1)}`;
8+
// console.log("c2j", cxxName, "=>", jsName);
9+
return jsName;
10+
}
11+
12+
/**
13+
* Converts a JavaScript style name (camelCase) to a C++ style name (PascalCase).
14+
* @param {string} jsName
15+
* @returns {string}
16+
*/
17+
export function toCxxName(jsName) {
18+
const cxxName = `${jsName.charAt(0).toUpperCase()}${jsName.slice(1)}`;
19+
// console.log("j2c", jsName, "=>", cxxName);
20+
return cxxName;
21+
}
22+
23+
/**
24+
* Convert JS-style keyword arguments to C++-style keyword arguments.
25+
* @param {object} kwArgs
26+
* @returns {object}
27+
*/
28+
export function toCxxKeys(kwArgs) {
29+
const wrapped = {};
30+
Object.entries(kwArgs).forEach(([k, v]) => {
31+
wrapped[toCxxName(k)] = v;
32+
});
33+
return wrapped;
34+
}
35+
36+
/**
37+
* Convert C++-style keyword arguments to JS-style keyword arguments.
38+
* @param {object} kwArgs
39+
* @returns {object}
40+
*/
41+
export function toJsKeys(kwArgs) {
42+
const wrapped = {};
43+
Object.entries(kwArgs).forEach(([k, v]) => {
44+
wrapped[toJsName(k)] = v;
45+
});
46+
return wrapped;
47+
}

0 commit comments

Comments
 (0)