From 2ffb965b670edc96bdcec25fa81536cc9d790227 Mon Sep 17 00:00:00 2001 From: mes123456 Date: Fri, 4 Sep 2026 11:17:27 +0800 Subject: [PATCH] feat(web): preload completed wasm and load debug modules on demand --- web/app/index.html | 2 + web/app/public/wasm-module-manifest.json | 54 ++++++++++++------- web/app/src/engine-client/WebEngineClient.ts | 11 ++++ .../src/engine-client/wasm-module-loader.ts | 27 ++++++---- web/app/src/workers/web-engine.worker.ts | 2 +- web/tests/e2e/wasm-module-loading.spec.ts | 28 ++++++++++ 6 files changed, 95 insertions(+), 29 deletions(-) create mode 100644 web/tests/e2e/wasm-module-loading.spec.ts 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); +});