From 8876382a25808db202fbf635dfa0a32c629b02c4 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 27 Jan 2026 01:36:55 +0000 Subject: [PATCH] fix: prevent WebGPU setIndexBuffer crash after extended gameplay The Three.js render would go dark after ~7 minutes of gameplay due to WebGPU failing to set the index buffer on GPURenderPassEncoder. Root cause: When GPUIndirectRenderer cloned geometry attributes, the cloned buffers weren't immediately marked as needing update. WebGPU would lazily create GPU buffers, potentially sharing them with the source geometry. When UnitRenderer disposed inactive instanced meshes after INACTIVE_MESH_CLEANUP_FRAMES, the shared GPU buffers would be invalidated, causing the cloned geometry to reference invalid buffers. Fix: 1. Mark cloned attributes and index buffers as needsUpdate=true in GPUIndirectRenderer.createIndirectMesh() to force fresh GPU buffer creation 2. Add per-frame validation in updateMeshVisibility() to catch disposed geometry before rendering 3. Call updateMeshVisibility() every frame in UnitRenderer after culling to ensure invalid meshes are hidden before the render pass https://claude.ai/code/session_01E36JDY27fQMf5EzB3KMZD4 --- src/rendering/UnitRenderer.ts | 6 +++++ src/rendering/compute/GPUIndirectRenderer.ts | 25 ++++++++++++++++---- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/rendering/UnitRenderer.ts b/src/rendering/UnitRenderer.ts index 92d71bb2..8382a86a 100644 --- a/src/rendering/UnitRenderer.ts +++ b/src/rendering/UnitRenderer.ts @@ -911,6 +911,12 @@ export class UnitRenderer { } this.cullingService.performCulling(this.camera); + // Update mesh visibility (and validate) after culling, before rendering + // This catches any disposed geometry before it causes WebGPU crashes + if (this.gpuIndirectRenderer && this.gpuIndirectInitialized) { + this.gpuIndirectRenderer.updateMeshVisibility(); + } + // Log culling status periodically (every 300 frames ~5 seconds) if (this.frameCount % 300 === 1) { const stats = this.getGPURenderingStats(); diff --git a/src/rendering/compute/GPUIndirectRenderer.ts b/src/rendering/compute/GPUIndirectRenderer.ts index e4c01ba9..5fa3b496 100644 --- a/src/rendering/compute/GPUIndirectRenderer.ts +++ b/src/rendering/compute/GPUIndirectRenderer.ts @@ -181,13 +181,20 @@ export class GPUIndirectRenderer { // Clone attributes from source geometry to avoid sharing disposal lifecycle. // Setting by reference causes WebGPU "setIndexBuffer" errors when the source // geometry is disposed elsewhere while this geometry is still in use. + // CRITICAL: Mark cloned attributes as needsUpdate to force WebGPU to create + // fresh GPU buffers. Without this, WebGPU may lazily share buffers with the + // source geometry, which become invalid when the source is disposed. for (const name of Object.keys(geometry.attributes)) { - instancedGeometry.setAttribute(name, geometry.attributes[name].clone()); + const clonedAttr = geometry.attributes[name].clone(); + clonedAttr.needsUpdate = true; + instancedGeometry.setAttribute(name, clonedAttr); } // Clone index if present - critical to avoid shared buffer disposal issues if (geometry.index) { - instancedGeometry.setIndex(geometry.index.clone()); + const clonedIndex = geometry.index.clone(); + clonedIndex.needsUpdate = true; + instancedGeometry.setIndex(clonedIndex); } // Create per-instance position offset attribute (vec3) @@ -463,10 +470,20 @@ export class GPUIndirectRenderer { /** * Update mesh visibility based on instance counts * Hides meshes with 0 instances to avoid GPU overhead - * Also hides invalid meshes to prevent rendering crashes + * Also validates and hides invalid meshes to prevent rendering crashes */ updateMeshVisibility(): void { - for (const [, data] of this.indirectMeshes) { + for (const [key, data] of this.indirectMeshes) { + // Quick validation check every frame to catch disposed geometry before render + if (data.isValid) { + const index = data.geometry.index; + // Check if index buffer was disposed (array becomes null after dispose) + if (data.indexCount > 0 && (!index || !index.array)) { + data.isValid = false; + debugShaders.warn(`[GPUIndirectRenderer] Mesh geometry invalidated during visibility update: ${key}`); + } + } + // Never show invalid meshes if (!data.isValid) { data.mesh.visible = false;