fix(viewer): defer geometry disposal to avoid WebGPU dispose race (Sentry MONOREPO-EDITOR-DK/EG/EH)#468
fix(viewer): defer geometry disposal to avoid WebGPU dispose race (Sentry MONOREPO-EDITOR-DK/EG/EH)#468anton-pascal wants to merge 2 commits into
Conversation
…ntry MONOREPO-EDITOR-DK/EG/EH)
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
…priority system Bugbot flagged that RoofSystem's flushGeometryDisposals() runs after GeometrySystem's within the same frame, so geometries queued by an earlier rebuild system could be disposed before the WebGPU render pass — reintroducing the dispose race deferral is meant to prevent. Move the flush into a dedicated GeometryDisposalFlushSystem that runs in useFrame at priority -1000, ahead of every rebuild system regardless of mount order. Remove the inline flush calls from GeometrySystem and RoofSystem so the queue only ever holds geometries dropped on a previous frame (already released by the renderer). Update deferred-dispose docs to note the single-caller contract. Addresses Cursor Bugbot review on PR #468 (Sentry MONOREPO-EDITOR-DK/EG/EH).
|
Good catch by Bugbot — confirmed and fixed in cb566be. Root cause: Fix: Introduced a dedicated Viewer package typechecks clean and Biome passes on the touched files. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit cb566be. Configure here.
| queue before ANY rebuild system runs this frame, guaranteeing only | ||
| geometries dropped on a previous frame (already released by the | ||
| renderer) are disposed. See lib/deferred-dispose.ts. */} | ||
| <GeometryDisposalFlushSystem /> |
There was a problem hiding this comment.
Flush unmount strands disposal queue
Medium Severity
The GeometryDisposalFlushSystem is a React component that drains the module-global pendingGeometryDisposals queue via useFrame. If this component unmounts (e.g., after a scene error or Canvas unmount), the useFrame callback stops, leaving the global queue undrained. This causes GPU resources for queued geometries to leak, persisting until a full page reload.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit cb566be. Configure here.


What
Defensive fix for a Three.js WebGPU dispose race that is generating the top Sentry error in the editor: MONOREPO-EDITOR-DK (4453 events), plus EG (110) and EH (94) — all the same root cause.
The race
Inside the per-frame rebuild loops of
GeometrySystemandRoofSystem, an outgoing live geometry (one currently attached to a rendered mesh) was disposed synchronously mid-useFrame:geometry-system.tsx→disposeChildren()(mesh.geometry.dispose()on swapped-out children)roof-system.tsx→updateRoofSegmentGeometry()(mesh.geometry.dispose()before reassign)roof-system.tsx→ theBoxGeometryplaceholder swaproof-system.tsx→updateMergedRoofGeometry()(mergedMesh.geometry.dispose()on the two reassign paths)On the WebGPU backend the renderer still holds a
RenderObjectreferencing that geometry for the frame in flight.BufferGeometry.dispose()synchronously dispatches thedisposeevent; three's WebGPUonDisposehandler then callsRenderObject.getAttributes(), which reads.id/.countoff attributes that have already been freed →(three@0.184.0, @react-three/fiber@9.6.1)
A
try/catcharound.dispose()cannot help — the throw happens inside three's own async event dispatch, not in our call frame.The fix — defensive deferral, behavior identical
New tiny helper
packages/viewer/src/lib/deferred-dispose.ts:queueGeometryDispose(geometry)— queue an outgoing live geometry instead of freeing it now.flushGeometryDisposals()— drain the queue (wrapped in try/catch), called once at the start of each system'suseFrame, before any rebuild work.The renderer releases its
RenderObjectfor a geometry once it's no longer drawn (i.e. after the frame in which we swapped in the replacement). So we hand the outgoing geometry to the queue and free it at the top of the next frame, after that render has completed. Every geometry is still disposed — just one frame later, after the renderer let go of it. No geometry leaks, no rendering-behavior change.Only the live-mesh dispose sites were changed. Transient CSG intermediates (brushes, temporary merge/subtract results that never enter the scene graph) are left disposing synchronously — the renderer never held a
RenderObjectfor them, so there's no race.Testing
bunx tsc --build packages/viewer/tsconfig.json→ exit 0 (clean).bun test .../geometry-system.test.ts→ 1 pass.biome linton all three touched files → clean.Scope
Addresses Sentry MONOREPO-EDITOR-DK / EG / EH. Defensive-only; does not redesign the rebuild pipeline. Do not merge without review.
Note
Medium Risk
Touches core viewer frame ordering and geometry lifecycle for all parametric/roof rebuilds; timing-sensitive but narrowly scoped to dispose sites on rendered meshes with documented ordering constraints.
Overview
Fixes a WebGPU dispose race where synchronous
BufferGeometry.dispose()on meshes still referenced by the renderer duringuseFramerebuilds triggeredCannot read properties of undefined (reading 'id')(top Sentry errors DK/EG/EH).Adds
deferred-dispose.tswithqueueGeometryDisposeandflushGeometryDisposals, plusGeometryDisposalFlushSystemmounted in the viewer atuseFramepriority -1000 so the queue drains before any rebuild system runs each frame.GeometrySystemandRoofSystemnow queue outgoing live mesh geometries instead of disposing them mid-frame; transient CSG intermediates still dispose synchronously.Geometries are freed one frame later, after the render pass that swapped them out—same cleanup behavior, no pipeline redesign.
Reviewed by Cursor Bugbot for commit cb566be. Bugbot is set up for automated code reviews on this repo. Configure here.