feat(web): preload completed and load debug wasm modules on demand
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import type { ErrorReport } from "../../../protocol/error";
|
||||
import { parseSceneDelta, type SceneDelta } from "../../../protocol/scene-delta";
|
||||
import type { MeshGeometryBuffer, WebEngineEditCommand, WebEngineLODError, WebEngineLODLevelResult, WebEngineOpenResourceStatus, WebEngineRequest, WebEngineResponse, WebEngineResult, WebEngineStatus } from "../../../protocol/web-engine";
|
||||
import type { MeshGeometryBuffer, WebEngineEditCommand, WebEngineJsonCommand, WebEngineLODError, WebEngineLODLevelResult, WebEngineOpenResourceStatus, WebEngineRequest, WebEngineResponse, WebEngineResult, WebEngineStatus } from "../../../protocol/web-engine";
|
||||
import { parseSceneSnapshotIR } from "../../../protocol/scene-ir";
|
||||
import { parseEditorWorkflow, EditorWorkflowValidationError } from "../../../protocol/editor-workflow";
|
||||
import { parseLODManifest, type SimplifyResult } from "../../../protocol/simplify";
|
||||
import type { LODGenerationRequest } from "../../../protocol/lod";
|
||||
import { evaluateSkinSimplifyGate } from "../../../protocol/skin";
|
||||
@@ -36,6 +37,7 @@ import {
|
||||
sha256LibraryBytes,
|
||||
type LibraryAppendClosureIR,
|
||||
} from "../../../protocol/library-main-append";
|
||||
import { createWasmModuleLoader, type WasmModuleLoader } from "../engine-client/wasm-module-loader";
|
||||
|
||||
type WasmModule = {
|
||||
_malloc: (size: number) => number;
|
||||
@@ -106,6 +108,7 @@ let wasmFactory: WasmFactory | null = null;
|
||||
let wasmBinary: ArrayBuffer | null = null;
|
||||
let handle = 0;
|
||||
let initializationPromise: Promise<WebEngineStatus> | null = null;
|
||||
let wasmModuleLoader: WasmModuleLoader | null = null;
|
||||
let currentSnapshot: ReturnType<typeof parseSceneSnapshotIR> | null = null;
|
||||
let currentGeometryBuffers: MeshGeometryBuffer[] = [];
|
||||
let sourceBlendBuffer: ArrayBuffer | null = null;
|
||||
@@ -909,7 +912,7 @@ function nativeError(fallbackCode: ErrorReport["code"]): ErrorReport {
|
||||
const message = module && module._web_engine_last_error_message() !== 0
|
||||
? module.UTF8ToString(module._web_engine_last_error_message())
|
||||
: "WebEngine native call failed";
|
||||
const mappedCode: ErrorReport["code"] = code === -4 ? "BLEND_READ_FAILED" : code === -5 ? "BLEND_WRITE_FAILED" : fallbackCode;
|
||||
const mappedCode: ErrorReport["code"] = code === -4 ? "BLEND_READ_FAILED" : code === -5 ? "BLEND_WRITE_FAILED" : message.startsWith("GN_NODE_UNSUPPORTED") ? "CAPABILITY_MISSING" : fallbackCode;
|
||||
return report(mappedCode, message);
|
||||
}
|
||||
|
||||
@@ -918,7 +921,7 @@ function nativeErrorFor(target: WasmModule, fallbackCode: ErrorReport["code"]):
|
||||
const message = target._web_engine_last_error_message() !== 0
|
||||
? target.UTF8ToString(target._web_engine_last_error_message())
|
||||
: "WebEngine native call failed";
|
||||
const mappedCode: ErrorReport["code"] = code === -4 ? "BLEND_READ_FAILED" : code === -5 ? "BLEND_WRITE_FAILED" : fallbackCode;
|
||||
const mappedCode: ErrorReport["code"] = code === -4 ? "BLEND_READ_FAILED" : code === -5 ? "BLEND_WRITE_FAILED" : message.startsWith("GN_NODE_UNSUPPORTED") ? "CAPABILITY_MISSING" : fallbackCode;
|
||||
return report(mappedCode, message);
|
||||
}
|
||||
|
||||
@@ -950,12 +953,15 @@ async function initialize(): Promise<WebEngineStatus> {
|
||||
if (module) return status();
|
||||
if (!initializationPromise) {
|
||||
initializationPromise = (async () => {
|
||||
const moduleManifest = await fetch("/wasm-module-manifest.json", { cache: "force-cache" }).then(async (response) => {
|
||||
if (!response.ok) throw new Error(`wasm-module-manifest request failed: ${response.status}`);
|
||||
return response.json();
|
||||
});
|
||||
wasmModuleLoader = createWasmModuleLoader(moduleManifest, "single");
|
||||
await wasmModuleLoader.preloadCompleted();
|
||||
const imported = await import("../vendor/blender/web_engine.js") as unknown as { default: WasmFactory };
|
||||
const factory = imported.default;
|
||||
const binary = await fetch("/vendor/blender/web_engine.wasm?v=2").then((response) => {
|
||||
if (!response.ok) throw new Error(`web_engine.wasm request failed: ${response.status}`);
|
||||
return response.arrayBuffer();
|
||||
});
|
||||
const binary = await wasmModuleLoader.load("core-completed");
|
||||
const initializedModule = await factory({ wasmBinary: binary });
|
||||
const initializedHandle = initializedModule._web_engine_create();
|
||||
if (initializedHandle <= 0) throw new Error("WebEngine handle creation failed");
|
||||
@@ -967,6 +973,7 @@ async function initialize(): Promise<WebEngineStatus> {
|
||||
})().catch((error: unknown) => {
|
||||
module = null;
|
||||
handle = 0;
|
||||
wasmModuleLoader = null;
|
||||
throw error;
|
||||
}).finally(() => {
|
||||
initializationPromise = null;
|
||||
@@ -1017,8 +1024,24 @@ function callUnifiedJson<T>(cmd: string, params: Record<string, unknown> = {}, t
|
||||
}
|
||||
}
|
||||
|
||||
const READ_ONLY_JSON_COMMANDS: ReadonlySet<WebEngineJsonCommand> = new Set(["snapshot", "delta", "depsgraph", "memoryStats"]);
|
||||
const STATEFUL_JSON_COMMANDS: ReadonlySet<WebEngineJsonCommand> = new Set(["applyCommand", "undo", "redo"]);
|
||||
|
||||
function readSnapshot(): ReturnType<typeof parseSceneSnapshotIR> {
|
||||
return parseSceneSnapshotIR(callUnifiedJson<unknown>("snapshot"));
|
||||
const snapshot = parseSceneSnapshotIR(callUnifiedJson<unknown>("snapshot"));
|
||||
if (snapshot.editorWorkflowStatus === "AVAILABLE" && !snapshot.editorWorkflow) {
|
||||
throw report("EDITOR_LAYOUT_INVALID", "SceneIR editor workflow is marked AVAILABLE but has no payload");
|
||||
}
|
||||
if (snapshot.editorWorkflow) {
|
||||
try {
|
||||
parseEditorWorkflow(snapshot.editorWorkflow);
|
||||
}
|
||||
catch (error) {
|
||||
if (error instanceof EditorWorkflowValidationError) throw report(error.code, error.message);
|
||||
throw report("EDITOR_LAYOUT_INVALID", "SceneIR editor workflow validation failed");
|
||||
}
|
||||
}
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
function readDelta(): SceneDelta {
|
||||
@@ -1279,7 +1302,7 @@ function cloneNonMeshGeometryBuffers(chunks: readonly NonMeshGeometryChunk[]): N
|
||||
}
|
||||
|
||||
function baseResult(): WebEngineResult {
|
||||
return { status: status() };
|
||||
return { status: status(), wasmModules: wasmModuleLoader?.state() };
|
||||
}
|
||||
|
||||
function publishFullScene(scene: SceneReadResult): Pick<WebEngineResult, "snapshot" | "geometryBuffers" | "nonMeshGeometryBuffers"> {
|
||||
@@ -1399,6 +1422,13 @@ async function handleRequest(event: MessageEvent<WebEngineRequest>): Promise<voi
|
||||
await initialize();
|
||||
result = baseResult();
|
||||
break;
|
||||
case "loadWasmModule": {
|
||||
await initialize();
|
||||
if (!wasmModuleLoader) throw report("WASM_INIT_FAILED", "WASM module manifest is unavailable", true);
|
||||
await wasmModuleLoader.loadDebug(request.command.id);
|
||||
result = { ...baseResult(), wasmModules: wasmModuleLoader.state() };
|
||||
break;
|
||||
}
|
||||
case "openBlend": {
|
||||
const resource = { liveInputBytes: request.command.buffer.byteLength, liveNativeHandles: 0, liveStagingFiles: 0 };
|
||||
activeOpenResources.set(request.requestId, resource);
|
||||
@@ -1478,6 +1508,35 @@ async function handleRequest(event: MessageEvent<WebEngineRequest>): Promise<voi
|
||||
result = { status: status(), ...publishFullScene(scene) };
|
||||
}
|
||||
break;
|
||||
case "jsonCommand": {
|
||||
await initialize();
|
||||
if (!READ_ONLY_JSON_COMMANDS.has(request.command.cmd) && !STATEFUL_JSON_COMMANDS.has(request.command.cmd)) {
|
||||
throw report("INVALID_ARGUMENT", `Unsupported JSON command: ${request.command.cmd}`, true);
|
||||
}
|
||||
if (STATEFUL_JSON_COMMANDS.has(request.command.cmd) && request.command.cmd === "applyCommand" &&
|
||||
(!request.command.params || typeof request.command.params.command !== "object" || request.command.params.command === null)) {
|
||||
throw report("INVALID_ARGUMENT", "cmd json applyCommand requires params.command", true);
|
||||
}
|
||||
const jsonResult = callUnifiedJson<unknown>(request.command.cmd, request.command.params ?? {});
|
||||
if (STATEFUL_JSON_COMMANDS.has(request.command.cmd)) {
|
||||
const scene = await readSnapshotResult();
|
||||
currentSnapshot = scene.snapshot;
|
||||
result = {
|
||||
status: status(),
|
||||
json: jsonResult,
|
||||
snapshot: scene.snapshot,
|
||||
delta: readDelta(),
|
||||
geometryBuffers: scene.geometryBuffers,
|
||||
nonMeshGeometryBuffers: scene.nonMeshGeometryBuffers,
|
||||
};
|
||||
break;
|
||||
}
|
||||
result = {
|
||||
status: status(),
|
||||
json: jsonResult,
|
||||
};
|
||||
break;
|
||||
}
|
||||
case "delta":
|
||||
await initialize();
|
||||
result = { status: status(), delta: readDelta() };
|
||||
|
||||
Reference in New Issue
Block a user