diff --git a/docs/status/RE_PBR_RENDER_20261112.md b/docs/status/RE_PBR_RENDER_20261112.md index 12b596f21..9a4ac3d68 100644 --- a/docs/status/RE_PBR_RENDER_20261112.md +++ b/docs/status/RE_PBR_RENDER_20261112.md @@ -110,7 +110,7 @@ HQ 2048(10.8 MB 库,67,633,152 bytes 解码);`check-re-pbr-limits.mjs` | 部署工具 | `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 秒) | +| 首次初始化 | 154–193 秒(两次实测;gzip wasm 单次下载;`init` 超时已从 30 秒提高到 300 秒) | | 部署证据 | `/home/mes123456/workinf_Blender_Wasm/tests/golden/RE-PBR-RENDER-001/deployed-app.png` | 部署期间修复的发布问题: @@ -122,6 +122,9 @@ HQ 2048(10.8 MB 库,67,633,152 bytes 解码);`check-re-pbr-limits.mjs` `init` 超时改为 300 秒。 3. 241MB wasm 直接传输在服务器上行带宽下超过 300 秒;已在 nginx 58014 server 块启用 gzip (仅 `application/wasm` 等文本类型,视频不压缩),压缩后 79.4MB。 +4. 慢速首载时,启动自动新建 General 工程的模板下载会在用户打开 studio 之后应用,覆盖用户场景 + (实测 studio 已 6 对象,几秒后又回到 General 3 对象)。已在文件选择/教案载入时设置 + `startupTemplateCancelled`,并在 `newProject(true)` 的模板下载后再次检查;用户打开优先于启动模板。 ## 验证结果 @@ -139,8 +142,9 @@ 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`) | +| 线上首次加载 | `init` 192.8 s(范围 154–193 s);wasm 仅 1 次 200(`Content-Encoding: gzip`,79.4MB);无 pageerror/console error | +| 线上启动竞态 | init 完成后**立即**打开 studio,6 对象保持 10 秒以上不被启动 General 覆盖;8 秒内进入 `Engine: SceneIR r1 (6 objects)` | +| 线上场景流程 | studio 6 对象 → `猴头.glb` 7 对象(`Engine: GLB imported r2`,Suzanne 1225 顶点/969 面) | | 线上 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/web/app/src/app/App.tsx b/web/app/src/app/App.tsx index f18c112fe..07635dbb9 100644 --- a/web/app/src/app/App.tsx +++ b/web/app/src/app/App.tsx @@ -3939,6 +3939,7 @@ export function App({localOnly=false,course}:{localOnly?:boolean;course?:Availab },[]); const desktopHandoffLoaded=useRef(false); const wasmStartupLoaded=useRef(false); + const startupTemplateCancelled=useRef(false); const textDraftDirty=useRef(false); const [projectActionLocked,setProjectActionLocked]=useState(false); const textDraftAreas=useRef(new Set()); @@ -5432,6 +5433,7 @@ export function App({localOnly=false,course}:{localOnly?:boolean;course?:Availab const openTutorialScene = async (url: string, fileName: string): Promise => { try { + startupTemplateCancelled.current = true; const response = await fetch(url, { cache: "no-cache" }); if (!response.ok) return false; const file = new File([await response.arrayBuffer()], fileName, { type: "application/x-blender" }); @@ -7193,7 +7195,8 @@ export function App({localOnly=false,course}:{localOnly?:boolean;course?:Availab window.addEventListener('message',onNativeFullscreenRequest); return()=>window.removeEventListener('message',onNativeFullscreenRequest); },[toggleBrowserFullscreen]); - const newProject=async()=>{if(headerPending)return; + const newProject=async(startup=false)=>{if(headerPending)return; + if(startup&&startupTemplateCancelled.current)return; /* A user open in flight supersedes the startup template, but a brief autosave * checkpoint must not suppress project creation entirely: wait for the lock, * then skip only if another open already replaced the project meanwhile. */ @@ -7202,6 +7205,9 @@ export function App({localOnly=false,course}:{localOnly?:boolean;course?:Availab if(projectIdRef.current!==startProject)return; setHeaderPending(true);try{ const response=await fetch('/templates/general.blend');if(!response.ok)throw new Error('NEW_PROJECT_TEMPLATE_MISSING'); + // The template fetch can outlive a user open started after the lock check. + if(startup&&startupTemplateCancelled.current)return; + if(projectIdRef.current!==startProject)return; if(!await openBlendFile(new File([await response.arrayBuffer()],'Untitled.blend')))return; const client=webClientRef.current;if(!client)throw new Error('ENGINE_NOT_READY'); const current=await client.snapshot();if(!current.snapshot.nodes.some(node=>node.name==='Cube'))throw new Error('NEW_PROJECT_OPEN_FAILED'); @@ -7830,7 +7836,7 @@ else: },[wasmStatus]); useEffect(()=>{ if(wasmStartupLoaded.current||new URLSearchParams(location.search).has('desktopHandoff')||!wasmStatus.startsWith('WASM ABI: ready'))return; - wasmStartupLoaded.current=true;void newProject(); + wasmStartupLoaded.current=true;void newProject(true); },[wasmStatus,localOnly]); const applyNativeTimelineEdit = async (edit: NativeTimelineEdit, areaId?: string): Promise => { @@ -8078,7 +8084,7 @@ else: ({label:item.name,checked:item.id===nativeWorkspace?.id,disabled:!item.writable,action:()=>void selectNativeWorkspace(item.id)})):BLENDER_WORKSPACE_IDS.map(id=>({label:id,checked:id===workspace,action:()=>dispatchUI({type:'switchWorkspace',workspaceId:id})}))}/>
{(snapshot?.scenes?.length??0)>1?:{activeSceneFromSnapshot(snapshot)?.name??"Scene"}}ViewLayer{workerFaultTestMode ? : null}
- route.format).join(",")} onChange={(event) => { const file = event.target.files?.[0]; if (file) { const gate = gateIOFormatFileSelection(file.name, IO_FORMAT_UI_REGISTRY); if (gate.status === "BLOCKED") setEngineStatus(recordDiagnostic("ENGINE", "IO_FORMAT_UNSUPPORTED", gate, { fileName: file.name })); else if (file.name.toLowerCase().endsWith(".blend")) void openBlendFile(file); else { const route = IO_FORMAT_UI_REGISTRY.importRoutes.find((candidate) => candidate.execution === "LOCAL" && candidate.extensions.some((extension) => file.name.toLowerCase().endsWith(extension))); if (route) { if (localOnly && route.format === "OBJ") openOBJImport(file); else void importSceneFile(file, route.format); } else setEngineStatus(recordDiagnostic("ENGINE", "IO_FORMAT_UNSUPPORTED", { code: "IO_FORMAT_UNSUPPORTED", message: "No native WebEngine importer is bound for this format" }, { fileName: file.name })); } } event.target.value = ""; }} /> + route.format).join(",")} onChange={(event) => { const file = event.target.files?.[0]; if (file) { startupTemplateCancelled.current = true; const gate = gateIOFormatFileSelection(file.name, IO_FORMAT_UI_REGISTRY); if (gate.status === "BLOCKED") setEngineStatus(recordDiagnostic("ENGINE", "IO_FORMAT_UNSUPPORTED", gate, { fileName: file.name })); else if (file.name.toLowerCase().endsWith(".blend")) void openBlendFile(file); else { const route = IO_FORMAT_UI_REGISTRY.importRoutes.find((candidate) => candidate.execution === "LOCAL" && candidate.extensions.some((extension) => file.name.toLowerCase().endsWith(extension))); if (route) { if (localOnly && route.format === "OBJ") openOBJImport(file); else void importSceneFile(file, route.format); } else setEngineStatus(recordDiagnostic("ENGINE", "IO_FORMAT_UNSUPPORTED", { code: "IO_FORMAT_UNSUPPORTED", message: "No native WebEngine importer is bound for this format" }, { fileName: file.name })); } } event.target.value = ""; }} /> {workerFault ?
{workerFault.source === "engine" ? "Engine Worker" : "Storage Worker"} stopped; current project list and scene are retained.{workerRecoveryStatus === "FAILED" ? Recovery failed; retry is safe. : null}
: null} {recentProjectIssues.length > 0 ?
最近项目中有 {recentProjectIssues.length} 个条目无法验证;当前场景不会被删除。
{recentProjectIssues.map((issue) =>
{issue.project.displayName}: {recentProjectIssueMessage(issue.code)}
)}
: null}