diff --git a/web/app/index.html b/web/app/index.html
index fea40c68f..107950131 100644
--- a/web/app/index.html
+++ b/web/app/index.html
@@ -5,6 +5,8 @@
Web Blender Modeler V1
+
+
diff --git a/web/app/public/wasm-module-manifest.json b/web/app/public/wasm-module-manifest.json
index fe6c8344c..5c6a81bb6 100644
--- a/web/app/public/wasm-module-manifest.json
+++ b/web/app/public/wasm-module-manifest.json
@@ -3,26 +3,9 @@
"releaseId": "blender-wasm-0.1.0-rc.1",
"modules": [
{
- "id": "core-completed",
+ "id": "core-runtime-completed",
"lifecycle": "complete",
"preload": true,
- "resources": {
- "single": {
- "fileName": "web_engine.wasm",
- "url": "/vendor/blender/web_engine.wasm",
- "sha256": "66fc59cba55a8c17d398b74d12ea4a9f09746c0f30c40f579c44aa34956e3072"
- },
- "pthread": {
- "fileName": "web_engine.wasm",
- "url": "/vendor/blender/pthread/web_engine.wasm",
- "sha256": "f2a26a9221b5d4d5e710463a89e2d93faf9b2d8a9076a455c5ce1a0e52d88b5f"
- }
- }
- },
- {
- "id": "debug-incremental",
- "lifecycle": "debug",
- "preload": false,
"resources": {
"single": {
"fileName": "web_engine.wasm",
@@ -35,6 +18,41 @@
"sha256": "f2a26a9221b5d4d5e710463a89e2d93faf9b2d8a9076a455c5ce1a0e52d88b5f"
}
}
+ },
+ {
+ "id": "mesh-editing-completed",
+ "lifecycle": "complete",
+ "preload": true,
+ "resources": {
+ "single": { "fileName": "web_engine.wasm", "url": "/vendor/blender/single/web_engine.wasm", "sha256": "7f3a50f1d41b2ac0e6366e84caaabefc3cdfc47c055d5ea459abec654da6fcc0" },
+ "pthread": { "fileName": "web_engine.wasm", "url": "/vendor/blender/pthread/web_engine.wasm", "sha256": "f2a26a9221b5d4d5e710463a89e2d93faf9b2d8a9076a455c5ce1a0e52d88b5f" }
+ }
+ },
+ {
+ "id": "reader-capabilities-completed",
+ "lifecycle": "complete",
+ "preload": true,
+ "resources": {
+ "single": { "fileName": "web_engine.wasm", "url": "/vendor/blender/single/web_engine.wasm", "sha256": "7f3a50f1d41b2ac0e6366e84caaabefc3cdfc47c055d5ea459abec654da6fcc0" },
+ "pthread": { "fileName": "web_engine.wasm", "url": "/vendor/blender/pthread/web_engine.wasm", "sha256": "f2a26a9221b5d4d5e710463a89e2d93faf9b2d8a9076a455c5ce1a0e52d88b5f" }
+ }
+ },
+ {
+ "id": "debug-incremental",
+ "lifecycle": "debug",
+ "preload": false,
+ "resources": {
+ "single": {
+ "fileName": "web_engine.wasm",
+ "url": "/vendor/blender/web_engine.wasm",
+ "sha256": "66fc59cba55a8c17d398b74d12ea4a9f09746c0f30c40f579c44aa34956e3072"
+ },
+ "pthread": {
+ "fileName": "web_engine.wasm",
+ "url": "/vendor/blender/pthread/web_engine.wasm",
+ "sha256": "f2a26a9221b5d4d5e710463a89e2d93faf9b2d8a9076a455c5ce1a0e52d88b5f"
+ }
+ }
}
]
}
diff --git a/web/app/src/engine-client/WebEngineClient.ts b/web/app/src/engine-client/WebEngineClient.ts
index a34edc4a4..f05dcfb18 100644
--- a/web/app/src/engine-client/WebEngineClient.ts
+++ b/web/app/src/engine-client/WebEngineClient.ts
@@ -83,6 +83,17 @@ export class WebEngineClient {
return (await this.request({ type: "init" })).status;
}
+ async shutdown(): Promise {
+ if (!this.worker) return;
+ try {
+ await this.request({ type: "shutdown" });
+ }
+ finally {
+ this.worker?.terminate();
+ this.worker = null;
+ }
+ }
+
/** Load one debug WASM module without replacing the completed engine module. */
async loadDebugWasmModule(id: string): Promise> {
const result = await this.request({ type: "loadWasmModule", id });
diff --git a/web/app/src/engine-client/wasm-module-loader.ts b/web/app/src/engine-client/wasm-module-loader.ts
index 0719c0f6b..e908835cd 100644
--- a/web/app/src/engine-client/wasm-module-loader.ts
+++ b/web/app/src/engine-client/wasm-module-loader.ts
@@ -37,20 +37,27 @@ export function createWasmModuleLoader(
const bytes = new Map();
const statuses = new Map(manifest.modules.map((entry) => [entry.id, { id: entry.id, lifecycle: entry.lifecycle, status: "pending", byteLength: 0 }]));
const loading = new Map>();
+ const markLoaded = (entry: WasmModuleEntry, value: ArrayBuffer): ArrayBuffer => {
+ statuses.set(entry.id, { id: entry.id, lifecycle: entry.lifecycle, status: "loaded", byteLength: value.byteLength });
+ return value;
+ };
const load = (entry: WasmModuleEntry): Promise => {
- const existing = bytes.get(entry.id);
- if (existing) return Promise.resolve(existing);
- const active = loading.get(entry.id);
- if (active) return active;
- const promise = fetchVerified(entry.resources[variant], fetcher).then((value) => {
- bytes.set(entry.id, value);
- statuses.set(entry.id, { id: entry.id, lifecycle: entry.lifecycle, status: "loaded", byteLength: value.byteLength });
- return value;
+ const resource = entry.resources[variant];
+ const existing = bytes.get(resource.url);
+ if (existing) return Promise.resolve(markLoaded(entry, existing));
+ const active = loading.get(resource.url);
+ if (active) return active.then((value) => markLoaded(entry, value), (error: unknown) => {
+ statuses.set(entry.id, { id: entry.id, lifecycle: entry.lifecycle, status: "failed", byteLength: 0, error: error instanceof Error ? error.message : String(error) });
+ throw error;
+ });
+ const promise = fetchVerified(resource, fetcher).then((value) => {
+ bytes.set(resource.url, value);
+ return markLoaded(entry, value);
}).catch((error: unknown) => {
statuses.set(entry.id, { id: entry.id, lifecycle: entry.lifecycle, status: "failed", byteLength: 0, error: error instanceof Error ? error.message : String(error) });
throw error;
- }).finally(() => loading.delete(entry.id));
- loading.set(entry.id, promise);
+ }).finally(() => loading.delete(resource.url));
+ loading.set(resource.url, promise);
return promise;
};
return {
diff --git a/web/app/src/workers/web-engine.worker.ts b/web/app/src/workers/web-engine.worker.ts
index c3dba4071..3de260925 100644
--- a/web/app/src/workers/web-engine.worker.ts
+++ b/web/app/src/workers/web-engine.worker.ts
@@ -961,7 +961,7 @@ async function initialize(): Promise {
await wasmModuleLoader.preloadCompleted();
const imported = await import("../vendor/blender/web_engine.js") as unknown as { default: WasmFactory };
const factory = imported.default;
- const binary = await wasmModuleLoader.load("core-completed");
+ const binary = await wasmModuleLoader.loadDebug("debug-incremental");
const initializedModule = await factory({ wasmBinary: binary });
const initializedHandle = initializedModule._web_engine_create();
if (initializedHandle <= 0) throw new Error("WebEngine handle creation failed");
diff --git a/web/tests/e2e/wasm-module-loading.spec.ts b/web/tests/e2e/wasm-module-loading.spec.ts
new file mode 100644
index 000000000..ea6ad73f2
--- /dev/null
+++ b/web/tests/e2e/wasm-module-loading.spec.ts
@@ -0,0 +1,28 @@
+import { expect, test } from "@playwright/test";
+
+test("preloads completed WASM and loads the debug module independently", async ({ page }) => {
+ await page.goto("/");
+ const result = await page.evaluate(async () => {
+ const { WebEngineClient } = await import("/src/engine-client/WebEngineClient.ts");
+ const client = new WebEngineClient({ timeoutMs: 60_000 });
+ try {
+ await client.init();
+ const before = await client.snapshot();
+ const pending = before.status;
+ const loaded = await client.loadDebugWasmModule("debug-incremental");
+ return {
+ pending,
+ loaded,
+ debug: loaded.find((module) => module.id === "debug-incremental"),
+ completed: loaded.filter((module) => module.lifecycle === "complete"),
+ };
+ }
+ finally { await client.shutdown(); }
+ });
+ expect(result.pending.ready).toBe(true);
+ expect(result.completed.length).toBeGreaterThan(0);
+ expect(result.completed.every((module) => module.status === "loaded")).toBe(true);
+ expect(result.debug?.status).toBe("loaded");
+ expect(result.debug?.lifecycle).toBe("debug");
+ expect(result.debug?.byteLength).toBeGreaterThan(0);
+});