Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/rendering/UnitRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
25 changes: 21 additions & 4 deletions src/rendering/compute/GPUIndirectRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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;
Expand Down