From 8d883ee2af8d7e1281e8f0bd12c8437ad149bafe Mon Sep 17 00:00:00 2001 From: connorsoohoo Date: Mon, 6 Jul 2026 00:39:57 -0700 Subject: [PATCH] viewer/nodes: fix WebGPU pipeline poisoning from missing uv2 on slabs and empty roofs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Slab surface/side meshes (split in buildSlabGeometry) and the empty-roof merged-mesh fallback shipped geometry without a uv2 attribute while their default catalog finishes (woodplank floor, roof shingles) carry an aoMap that samples uv2. three r184's WebGPU renderer crashes mid scene pass on the referenced-but-missing attribute, leaving the post-processing MRT render target bound — every pipeline compiled afterwards validates against the wrong 3-attachment framebuffer and fails ("Color target has no corresponding fragment stage output" → Invalid RenderPipeline → Invalid CommandBuffer on every submit), and post FX permanently falls back after retries. - slab/geometry.ts: emit uv2 (copy of uv) on the split top/side geometries, matching the ensureUv2Attribute convention. - roof-system.tsx: the children-less merged-roof fallback used BoxGeometry(0,0,0), which has uv but no uv2; use ensureRenderableGeometryAttributes(new BufferGeometry()) instead. - post-processing.tsx: reset the render target when a render pass throws so a future bad mesh costs one frame, not the whole session. Reproduced with apps/ifc-converter loading 07-revit-architectural.ifc (35 slabs + 2 segment-less roofs); clean console after the fix. Co-Authored-By: Claude Fable 5 --- packages/nodes/src/slab/geometry.ts | 9 ++++++++- .../viewer/src/components/viewer/post-processing.tsx | 6 ++++++ packages/viewer/src/systems/roof/roof-system.tsx | 6 ++++-- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/packages/nodes/src/slab/geometry.ts b/packages/nodes/src/slab/geometry.ts index 6b3c241db..b9ec1853f 100644 --- a/packages/nodes/src/slab/geometry.ts +++ b/packages/nodes/src/slab/geometry.ts @@ -122,7 +122,14 @@ function splitSlabFacesByFacing(geometry: BufferGeometry): { const build = (data: { pos: number[]; uv: number[] }) => { const geo = new BufferGeometry() geo.setAttribute('position', new Float32BufferAttribute(data.pos, 3)) - if (data.uv.length > 0) geo.setAttribute('uv', new Float32BufferAttribute(data.uv, 2)) + if (data.uv.length > 0) { + geo.setAttribute('uv', new Float32BufferAttribute(data.uv, 2)) + // Slot finishes with an aoMap (e.g. the default woodplank) sample uv2; + // a referenced-but-missing attribute crashes the WebGPU renderer mid + // scene pass, which leaks the MRT render target and poisons every + // pipeline built afterwards. + geo.setAttribute('uv2', new Float32BufferAttribute(data.uv.slice(), 2)) + } geo.computeVertexNormals() return geo } diff --git a/packages/viewer/src/components/viewer/post-processing.tsx b/packages/viewer/src/components/viewer/post-processing.tsx index 801b11742..44c05e3ca 100644 --- a/packages/viewer/src/components/viewer/post-processing.tsx +++ b/packages/viewer/src/components/viewer/post-processing.tsx @@ -598,6 +598,12 @@ const PostProcessingPasses = ({ } } catch (error) { hasPipelineErrorRef.current = true + // A mid-pass exception (e.g. thrown inside a PassNode's scene render) + // leaves the pass's MRT render target bound on the renderer. Every + // pipeline compiled afterwards then validates against that 3-attachment + // framebuffer and fails, poisoning the rebuilt pipeline and the fallback + // path alike. Restore the default target before retrying. + ;(renderer as any).setRenderTarget?.(null) console.error('[viewer/post-processing] Render pass failed.', { retryCount: retryCountRef.current, rendererCtor: (renderer as any).constructor?.name, diff --git a/packages/viewer/src/systems/roof/roof-system.tsx b/packages/viewer/src/systems/roof/roof-system.tsx index 3a65665a2..45c44afeb 100644 --- a/packages/viewer/src/systems/roof/roof-system.tsx +++ b/packages/viewer/src/systems/roof/roof-system.tsx @@ -497,8 +497,10 @@ function updateMergedRoofGeometry( if (children.length === 0) { mergedMesh.geometry.dispose() - // Keep a valid position attribute so Drei's BVH can index safely. - mergedMesh.geometry = new THREE.BoxGeometry(0, 0, 0) + // Keep a valid position attribute so Drei's BVH can index safely. Must + // also carry uv2 — the roof finish's aoMap samples it, and a missing + // attribute crashes the WebGPU renderer (BoxGeometry has no uv2). + mergedMesh.geometry = ensureRenderableGeometryAttributes(new THREE.BufferGeometry()) return }