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
18 changes: 11 additions & 7 deletions src/rendering/BuildingRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,12 +237,12 @@ export class BuildingRenderer {
}
this.buildingMeshes.clear();

// Also clear instanced groups
// NOTE: Do NOT dispose geometry here - it's shared with the asset cache.
// Disposing shared geometry causes WebGPU "setIndexBuffer" errors.
// Clear instanced groups
for (const group of this.instancedGroups.values()) {
this.scene.remove(group.mesh);
// Only dispose materials (they may be cloned)
// Dispose geometry (now safe since we clone it during creation)
group.mesh.geometry.dispose();
// Dispose materials
if (group.mesh.material instanceof THREE.Material) {
group.mesh.material.dispose();
} else if (Array.isArray(group.mesh.material)) {
Expand Down Expand Up @@ -280,7 +280,10 @@ export class BuildingRenderer {

baseMesh.traverse((child) => {
if (child instanceof THREE.Mesh && !geometry) {
geometry = child.geometry;
// Clone geometry to avoid sharing disposal lifecycle with asset cache.
// Without cloning, disposing this mesh would invalidate GPU buffers
// still used by other meshes, causing WebGPU "setIndexBuffer" crashes.
geometry = child.geometry.clone();
material = child.material;
// Get the world scale of this mesh (includes parent scales from normalization)
child.getWorldScale(modelScale);
Expand Down Expand Up @@ -2409,10 +2412,11 @@ export class BuildingRenderer {
this.buildingMeshes.clear();

// Dispose instanced groups
// NOTE: Do NOT dispose geometry - it's shared with the asset cache
for (const group of this.instancedGroups.values()) {
this.scene.remove(group.mesh);
// Only dispose materials
// Dispose geometry (now safe since we clone it during creation)
group.mesh.geometry.dispose();
// Dispose materials
if (group.mesh.material instanceof THREE.Material) {
group.mesh.material.dispose();
} else if (Array.isArray(group.mesh.material)) {
Expand Down
5 changes: 4 additions & 1 deletion src/rendering/ResourceRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,10 @@ export class ResourceRenderer {
// Extract geometry, material, and transform info from first mesh
baseMesh.traverse((child: THREE.Object3D) => {
if (child instanceof THREE.Mesh && !geometry) {
geometry = child.geometry;
// Clone geometry to avoid sharing disposal lifecycle with asset cache.
// Without cloning, disposing this mesh would invalidate GPU buffers
// still used by other meshes, causing WebGPU "setIndexBuffer" crashes.
geometry = child.geometry.clone();
material = child.material;
// Get world quaternion to extract X/Z rotations that stand the model upright
child.getWorldQuaternion(tempQuat);
Expand Down
21 changes: 13 additions & 8 deletions src/rendering/UnitRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -716,7 +716,10 @@ export class UnitRenderer {

baseMesh.traverse((child) => {
if (child instanceof THREE.Mesh && !geometry) {
geometry = child.geometry;
// Clone geometry to avoid sharing disposal lifecycle with asset cache.
// Without cloning, disposing this mesh would invalidate GPU buffers
// still used by other meshes, causing WebGPU "setIndexBuffer" crashes.
geometry = child.geometry.clone();
material = child.material;
// Get the mesh's world position Y - this is lost when extracting geometry
const worldPos = new THREE.Vector3();
Expand Down Expand Up @@ -1177,7 +1180,9 @@ export class UnitRenderer {
this.scene.remove(group.mesh);
// Dispose velocity buffer attributes
disposeInstancedVelocity(group.mesh);
// Only dispose materials (geometry is shared with asset cache)
// Dispose geometry (now safe since we clone it during creation)
group.mesh.geometry.dispose();
// Dispose materials
if (group.mesh.material instanceof THREE.Material) {
group.mesh.material.dispose();
} else if (Array.isArray(group.mesh.material)) {
Expand Down Expand Up @@ -1242,14 +1247,13 @@ export class UnitRenderer {
debugAssets.log('[UnitRenderer] Refreshing all unit meshes...');

// Clear instanced groups
// NOTE: Do NOT dispose geometry here - it's shared with the asset cache.
// Disposing shared geometry causes WebGPU "setIndexBuffer" errors when
// other meshes try to use the now-invalid GPU buffer.
for (const group of this.instancedGroups.values()) {
this.scene.remove(group.mesh);
// Dispose velocity buffer attributes to prevent memory leak
disposeInstancedVelocity(group.mesh);
// Only dispose materials (they are cloned per-instance group)
// Dispose geometry (now safe since we clone it during creation)
group.mesh.geometry.dispose();
// Dispose materials
if (group.mesh.material instanceof THREE.Material) {
group.mesh.material.dispose();
} else if (Array.isArray(group.mesh.material)) {
Expand Down Expand Up @@ -1323,12 +1327,13 @@ export class UnitRenderer {
this.selectionRingRenderer.dispose();
this.teamMarkerGeometry.dispose();

// NOTE: Do NOT dispose geometry - it's shared with the asset cache
for (const group of this.instancedGroups.values()) {
this.scene.remove(group.mesh);
// Dispose velocity buffer attributes to prevent memory leak
disposeInstancedVelocity(group.mesh);
// Only dispose materials
// Dispose geometry (now safe since we clone it during creation)
group.mesh.geometry.dispose();
// Dispose materials
if (group.mesh.material instanceof THREE.Material) {
group.mesh.material.dispose();
} else if (Array.isArray(group.mesh.material)) {
Expand Down