diff --git a/docs/status/RE_PBR_RENDER_20261112.md b/docs/status/RE_PBR_RENDER_20261112.md index 0cdf6cad6..12b596f21 100644 --- a/docs/status/RE_PBR_RENDER_20261112.md +++ b/docs/status/RE_PBR_RENDER_20261112.md @@ -98,6 +98,31 @@ HQ 2048(10.8 MB 库,67,633,152 bytes 解码);`check-re-pbr-limits.mjs` - `web/tests/e2e/re-pbr-render-workflow.spec.ts` 仍按旧文案“打开图文教案”查找链接;面板现为 “打开图文/视频教案”。已改为正则 `/打开图文/`,主流程用例重跑通过(3.6 分钟)。 +## 生产部署(2026-11-12) + +| 项目 | 值 | +| --- | --- | +| 公网地址 | `https://49.232.250.35:58014/`(Let's Encrypt 证书,`curl` 校验 `ssl_verify_result=0`) | +| 入口 | nginx 58014 → `upstream blender_web_58014_backend 127.0.0.1:58114` | +| 服务 | `blender-web-58014.service`,www-data 运行 `deployment-http-server.mjs current/app` | +| 新 release | `/var/www/blender-web/releases/20261112-re-pbr-d5dd647`(`current` 指向) | +| 回滚 release | `/var/www/blender-web/releases/731af06aee6a2d2b61b902db9fd4b7bd5b04474e4689c48baf213fd5fcb3769f` | +| 部署工具 | `current/tools/web/deployment-http-server.mjs` + 8 个本地依赖模块(闭包 9 个),`current/docs/web/deployment-contract.json` | +| 静态产物 | `web/dist`(599MB;`assets/web_engine-*.wasm` 在服务器以符号链接指向 `vendor/blender/web_engine.wasm`,避免重复占盘) | +| wasm 传输 | nginx 58014 server 块启用 gzip(`application/wasm`);实测 241,949,523 → 79,395,379 bytes | +| 首次初始化 | 154 秒(gzip wasm 单次下载;`init` 超时已从 30 秒提高到 300 秒) | +| 部署证据 | `/home/mes123456/workinf_Blender_Wasm/tests/golden/RE-PBR-RENDER-001/deployed-app.png` | + +部署期间修复的发布问题: + +1. `web/app/public/engine-manifest.json` 的 single wasm SHA-256 仍是旧值 `551a86…`;主线程 `verifyWasmResource` + 会额外下载 241MB 并失败。已改为实际 `31550b…`,并把校验 fetch 改为 `force-cache`。 +2. 主线程重复校验与 worker 加载各下载一次 241MB;远程首次加载必然超过 30 秒 `init` 超时。 + 已移除主线程 wasm 字节重复下载(worker 的 `WasmModuleLoader` 仍做 SHA-256 校验), + `init` 超时改为 300 秒。 +3. 241MB wasm 直接传输在服务器上行带宽下超过 300 秒;已在 nginx 58014 server 块启用 gzip + (仅 `application/wasm` 等文本类型,视频不压缩),压缩后 79.4MB。 + ## 验证结果 | 检查 | 结果 | @@ -114,6 +139,10 @@ HQ 2048(10.8 MB 库,67,633,152 bytes 解码);`check-re-pbr-limits.mjs` | 生产构建 | `npm run build` PASS(typecheck + vite build;`web/dist` 含视频、字幕、poster、13 张章节截图、新 wasm) | | 教案页面冒烟(生产 dist + 部署服务) | 56 张图片全部解码、视频 667.04 s、VTT 13 cues、无 4xx | | 视频音频 | AAC 单声道 48 kHz;`volumedetect` mean −23.4 dB、max −2.6 dB(非静音) | +| 线上首次加载 | `init` 154 s;wasm 仅 1 次 200(`Content-Encoding: gzip`,79.4MB);无 pageerror/console error | +| 线上场景流程 | 启动 General 3 对象 → studio 6 对象 → `猴头.glb` 7 对象(`Engine: GLB imported r2`) | +| 线上 PBR 渲染 | 512×512,`data-texture-status=ready:1:0:`,产物写入 OPFS(截图见 `deployed-app.png`) | +| 线上教案 | 56 张图片全部解码、视频 `duration=667.04`、VTT 200 + 13 cues、0 个 4xx | ## 未验证 / 限制 diff --git a/tests/golden/RE-PBR-RENDER-001/deployed-app.png b/tests/golden/RE-PBR-RENDER-001/deployed-app.png new file mode 100644 index 000000000..0b71f61d8 Binary files /dev/null and b/tests/golden/RE-PBR-RENDER-001/deployed-app.png differ diff --git a/web/app/public/engine-manifest.json b/web/app/public/engine-manifest.json index 1f20c6610..0f0fb18d9 100644 --- a/web/app/public/engine-manifest.json +++ b/web/app/public/engine-manifest.json @@ -21,7 +21,7 @@ "wasm": { "fileName": "web_engine.wasm", "url": "/vendor/blender/web_engine.wasm", - "sha256": "551a86b11ec0c2a8d2097e9f6aa80beda1a041c46c437ead7eb98db97fcd47d8" + "sha256": "31550b374cc0127c9b5468cebe65be37b8558c1604a28e99ec73a34be96c13f3" } } }, diff --git a/web/app/src/app/App.tsx b/web/app/src/app/App.tsx index 83e4e205c..f18c112fe 100644 --- a/web/app/src/app/App.tsx +++ b/web/app/src/app/App.tsx @@ -43,7 +43,7 @@ import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from "r import { createPortal, flushSync } from "react-dom"; import { WebEngineClient, type BlendOpenResult } from "../engine-client/WebEngineClient"; import {runEmbeddedRender} from '../engine-client/embedded-render'; -import { loadWebEngineManifest, verifyWasmResource } from "../../../protocol/manifest"; +import { loadWebEngineManifest } from "../../../protocol/manifest"; import type { ProgressEvent } from "../../../protocol/progress"; import type { MaterialIR, SceneSnapshotIR } from "../../../protocol/scene-ir"; import { sequencerCommands } from "../../../protocol/sequencer"; @@ -6050,11 +6050,10 @@ export function App({localOnly=false,course}:{localOnly?:boolean;course?:Availab useEffect(() => { let mounted = true; void loadWebEngineManifest() - .then(async (manifest) => { - const requiredResource = "variants" in manifest - ? manifest.variants.find((variant) => variant.id === "single")?.resources.wasm - : manifest.wasm.find((resource) => resource.required); - if (requiredResource) await verifyWasmResource(requiredResource); + .then((manifest) => { + // The engine worker verifies the wasm bytes when it loads the module. + // Re-fetching the multi-hundred-MB wasm here doubles the first-load + // transfer and can exceed the remote init budget. if (mounted) setManifestStatus(`Manifest: verified r${manifest.protocolVersion}`); }) .catch((error: unknown) => { diff --git a/web/app/src/engine-client/WebEngineClient.ts b/web/app/src/engine-client/WebEngineClient.ts index 1b4855c36..f8ccee0ae 100644 --- a/web/app/src/engine-client/WebEngineClient.ts +++ b/web/app/src/engine-client/WebEngineClient.ts @@ -326,7 +326,9 @@ export class WebEngineClient { } async init(): Promise { - return (await this.request({ type: "init" })).status; + // The first load fetches and instantiates the multi-hundred-MB engine wasm. + // A 30 s budget only fits loopback; remote deployments need a longer first-run window. + return (await this.request({ type: "init" }, [], undefined, undefined, undefined, 300_000)).status; } async gpuUICapabilities(): Promise> { @@ -1249,6 +1251,7 @@ export class WebEngineClient { onProgress?: (progress: ProgressEvent) => void, signal?: AbortSignal, nativeInterrupt?: Int32Array, + timeoutMs = this.timeoutMs, ): Promise { if (this.shutdownTask && command.type !== "shutdown" && command.type !== "cancelOpen") return Promise.reject(this.report("INVALID_ARGUMENT", "SESSION_CLOSE_PENDING: native session persistence is in progress", true)); if (command.type === "openBlend" && signal?.aborted) return Promise.reject(this.report("OPEN_CANCELLED", "Blend open cancelled before dispatch", true)); @@ -1270,7 +1273,7 @@ export class WebEngineClient { if (nativeInterrupt && Atomics.load(nativeInterrupt, 0) === 1 && !interruptGraceUsed) { interruptGraceUsed = true; const pending = this.pending.get(requestId); - if (pending) pending.timer = setTimeout(onTimeout, Math.min(this.timeoutMs, 5000)); + if (pending) pending.timer = setTimeout(onTimeout, Math.min(timeoutMs, 5000)); return; } const pending = this.pending.get(requestId); @@ -1279,9 +1282,9 @@ export class WebEngineClient { pending.timedOut = true; pending.timeoutError = this.report("WORKER_TERMINATED", `WebEngine 请求超时: ${command.type}`, true); pending.cleanup?.(); pending.cleanup = undefined; pending.reject(pending.timeoutError!); - pending.timer = setTimeout(() => { if (this.pending.get(requestId) !== pending) return; this.pending.delete(requestId); this.retiredPending.set(requestId, pending); }, this.timeoutMs); + pending.timer = setTimeout(() => { if (this.pending.get(requestId) !== pending) return; this.pending.delete(requestId); this.retiredPending.set(requestId, pending); }, timeoutMs); }; - const timer = setTimeout(onTimeout, Math.max(this.timeoutMs,this.renderDeadline-Date.now())); + const timer = setTimeout(onTimeout, Math.max(timeoutMs,this.renderDeadline-Date.now())); const abort = (): void => { void this.request({ type: "cancelOpen", targetRequestId: requestId }) .catch(() => undefined); diff --git a/web/protocol/manifest.ts b/web/protocol/manifest.ts index c0bfe5b6d..309d88eed 100644 --- a/web/protocol/manifest.ts +++ b/web/protocol/manifest.ts @@ -262,7 +262,9 @@ export async function loadWebEngineManifest(url = "/engine-manifest.json"): Prom export async function verifyWasmResource( resource: Pick & Partial>, ): Promise { - const response = await fetch(resource.url, { cache: "no-store" }); + // Reuse the HTTP cache populated by the worker's module loader. A no-store + // verification fetch would download the multi-hundred-MB engine wasm twice. + const response = await fetch(resource.url, { cache: "force-cache" }); const resourceId = resource.id ?? resource.url; if (!response.ok) throw new Error(`WASM resource request failed: ${resourceId}`); const bytes = await response.arrayBuffer();