diff --git a/BOLT'S JOURNAL - PERFORMANCE LEARNINGS.md b/BOLT'S JOURNAL - PERFORMANCE LEARNINGS.md index f687441..609417f 100644 --- a/BOLT'S JOURNAL - PERFORMANCE LEARNINGS.md +++ b/BOLT'S JOURNAL - PERFORMANCE LEARNINGS.md @@ -1,2 +1,6 @@ - Direct matrix array manipulation (`instanceMatrix.array`) bypasses expensive Object3D composition and matrix allocations, significantly improving rendering batcher update loops. ## 2024-04-09 - TSL and GC Performance Rules\n**Learning:** In Three.js, TSL math nodes are generally faster and preferred over updating uniforms via JS every frame for performance optimization. For Three.js InstancedMesh objects, colors must be updated via `.setColorAt()`. Modifying the material directly will incorrectly affect all instances. In Candy World, collision detection handled in JavaScript becomes a severe bottleneck at >500 entities. AssemblyScript/WASM handles 2000+ entities efficiently.\n**Action:** Use TSL math nodes instead of JS uniforms whenever possible. Always use `.setColorAt()` for InstancedMesh colors. Use WASM for heavy collision detection. + +## 2024-05-XX - Zero-Allocation Matrix Batching +**Learning:** Calling `Object3D.updateMatrix()` and `mesh.setMatrixAt()` inside update loops or batch generation code causes significant CPU overhead and garbage collection (GC) spikes because they instantiate intermediate objects and allocate arrays under the hood. +**Action:** For all `InstancedMesh` batchers, construct `Matrix4` locally using zero-allocation scratch variables (`_scratchMatrix.compose(pos, quat, scale)`) and copy the result directly to the underlying buffer memory using `_scratchMatrix.toArray(mesh.instanceMatrix.array, index * 16)`. Always follow up with `mesh.instanceMatrix.needsUpdate = true`. diff --git a/src/compute/gpu-foliage-animator.ts b/src/compute/gpu-foliage-animator.ts index 9bc3951..2e289b3 100644 --- a/src/compute/gpu-foliage-animator.ts +++ b/src/compute/gpu-foliage-animator.ts @@ -711,7 +711,8 @@ export async function updateInstancedMeshFromAnimator( } dummy.updateMatrix(); - mesh.setMatrixAt(i, dummy.matrix); + // ⚡ OPTIMIZATION: Write directly to instanceMatrix array instead of updateMatrix + setMatrixAt + dummy.matrix.toArray(mesh.instanceMatrix.array, (i) * 16); } mesh.instanceMatrix.needsUpdate = true; diff --git a/src/compute/gpu-particle-system.ts b/src/compute/gpu-particle-system.ts index 687d69a..98ba816 100644 --- a/src/compute/gpu-particle-system.ts +++ b/src/compute/gpu-particle-system.ts @@ -432,7 +432,8 @@ export class GPUParticleSystem { this.dummy.scale.set(scale, scale, scale); this.dummy.updateMatrix(); - this.particleMesh.setMatrixAt(i, this.dummy.matrix); + // ⚡ OPTIMIZATION: Write directly to instanceMatrix array instead of updateMatrix + setMatrixAt + this.dummy.matrix.toArray(this.particleMesh.instanceMatrix.array, (i) * 16); } this.particleMesh.instanceMatrix.needsUpdate = true; diff --git a/src/foliage/cave.ts b/src/foliage/cave.ts index b96e361..10a6e05 100644 --- a/src/foliage/cave.ts +++ b/src/foliage/cave.ts @@ -218,7 +218,8 @@ export function createCaveEntrance(options: CaveOptions = {}): THREE.Group { _scratchObj.scale.set(s * 0.5, s, s * 0.5); _scratchObj.updateMatrix(); - formationsMesh.setMatrixAt(i, _scratchObj.matrix); + // ⚡ OPTIMIZATION: Write directly to instanceMatrix array instead of updateMatrix + setMatrixAt + _scratchObj.matrix.toArray(formationsMesh.instanceMatrix.array, (i) * 16); } group.add(formationsMesh); diff --git a/src/foliage/cloud-batcher.ts b/src/foliage/cloud-batcher.ts index ead831a..1fe4e2c 100644 --- a/src/foliage/cloud-batcher.ts +++ b/src/foliage/cloud-batcher.ts @@ -287,7 +287,8 @@ export class CloudBatcher { for (let i = 0; i < count; i++) { // Global = CloudWorld * PuffLocal _scratchMat.multiplyMatrices(worldMat, puffs[i]); - this.mesh.setMatrixAt(start + i, _scratchMat); + // ⚡ OPTIMIZATION: Write directly to instanceMatrix array instead of updateMatrix + setMatrixAt + _scratchMat.toArray(this.mesh.instanceMatrix.array, (start + i) * 16); } } diff --git a/src/foliage/dandelion-batcher.ts b/src/foliage/dandelion-batcher.ts index 038daad..6613b6e 100644 --- a/src/foliage/dandelion-batcher.ts +++ b/src/foliage/dandelion-batcher.ts @@ -257,7 +257,8 @@ export class DandelionBatcher { _scratchScale.setScalar(scale); _scratchMat.scale(_scratchScale); - this.mesh!.setMatrixAt(i, _scratchMat); + // ⚡ OPTIMIZATION: Write directly to instanceMatrix array instead of updateMatrix + setMatrixAt + _scratchMat.toArray(this.mesh!.instanceMatrix.array, (i) * 16); this.mesh!.instanceMatrix.needsUpdate = true; this.mesh!.count = this.count; @@ -273,7 +274,8 @@ export class DandelionBatcher { this.dummy.scale.set(0, 0, 0); this.dummy.updateMatrix(); - this.mesh.setMatrixAt(batchIndex, this.dummy.matrix); + // ⚡ OPTIMIZATION: Write directly to instanceMatrix array instead of updateMatrix + setMatrixAt + this.dummy.matrix.toArray(this.mesh.instanceMatrix.array, (batchIndex) * 16); this.mesh.instanceMatrix.needsUpdate = true; console.log(`[DandelionBatcher] Harvested dandelion #${batchIndex}`); diff --git a/src/foliage/flower-batcher.ts b/src/foliage/flower-batcher.ts index 2c0455c..ec975a8 100644 --- a/src/foliage/flower-batcher.ts +++ b/src/foliage/flower-batcher.ts @@ -318,7 +318,8 @@ export class FlowerBatcher { if (index >= max) return; - mesh.setMatrixAt(index, matrix); + // ⚡ OPTIMIZATION: Write directly to instanceMatrix array instead of updateMatrix + setMatrixAt + matrix.toArray(mesh.instanceMatrix.array, (index) * 16); if (color && mesh.instanceColor) { mesh.setColorAt(index, color); } diff --git a/src/foliage/glowing-flower-batcher.ts b/src/foliage/glowing-flower-batcher.ts index 138c073..64f41ac 100644 --- a/src/foliage/glowing-flower-batcher.ts +++ b/src/foliage/glowing-flower-batcher.ts @@ -211,7 +211,8 @@ export class GlowingFlowerBatcher { _scratchScale.set(0.05, stemHeight, 0.05); _scratchMat.makeScale(_scratchScale.x, _scratchScale.y, _scratchScale.z); _scratchMat.premultiply(baseMatrix); - this.stemMesh!.setMatrixAt(i, _scratchMat); + // ⚡ OPTIMIZATION: Write directly to instanceMatrix array instead of updateMatrix + setMatrixAt + _scratchMat.toArray(this.stemMesh!.instanceMatrix.array, (i) * 16); // Head Transform (At top of stem) // ⚡ OPTIMIZATION: Re-use scratch variable to avoid GC spikes @@ -221,7 +222,8 @@ export class GlowingFlowerBatcher { _scratchMat2.scale(_scratchScale); _scratchMat2.premultiply(baseMatrix); const headWorld = _scratchMat2; - this.headMesh!.setMatrixAt(i, headWorld); + // ⚡ OPTIMIZATION: Write directly to instanceMatrix array instead of updateMatrix + setMatrixAt + headWorld.toArray(this.headMesh!.instanceMatrix.array, (i) * 16); // Wash Transform (At top of stem) // ⚡ OPTIMIZATION: Re-use scratch variable to avoid GC spikes @@ -231,7 +233,8 @@ export class GlowingFlowerBatcher { _scratchMat3.scale(_scratchScale); _scratchMat3.premultiply(baseMatrix); const washWorld = _scratchMat3; - this.washMesh!.setMatrixAt(i, washWorld); + // ⚡ OPTIMIZATION: Write directly to instanceMatrix array instead of updateMatrix + setMatrixAt + washWorld.toArray(this.washMesh!.instanceMatrix.array, (i) * 16); // Color if (typeof color === 'number') _scratchColor.setHex(color); diff --git a/src/foliage/grass.ts b/src/foliage/grass.ts index 73802f8..bd2b822 100644 --- a/src/foliage/grass.ts +++ b/src/foliage/grass.ts @@ -151,7 +151,8 @@ export function addGrassInstance(x: number, y: number, z: number) { dummy.scale.set(s, s, s); dummy.updateMatrix(); - mesh.setMatrixAt(index, dummy.matrix); + // ⚡ OPTIMIZATION: Write directly to instanceMatrix array instead of updateMatrix + setMatrixAt + dummy.matrix.toArray(mesh.instanceMatrix.array, (index) * 16); mesh.count++; mesh.instanceMatrix.needsUpdate = true; } diff --git a/src/foliage/lantern-batcher.ts b/src/foliage/lantern-batcher.ts index 5f56ffb..fa88fa6 100644 --- a/src/foliage/lantern-batcher.ts +++ b/src/foliage/lantern-batcher.ts @@ -311,10 +311,12 @@ export class LanternBatcher { dummy.updateMatrix(); // Stem - this.stemMesh!.setMatrixAt(i, dummy.matrix); + // ⚡ OPTIMIZATION: Write directly to instanceMatrix array instead of updateMatrix + setMatrixAt + dummy.matrix.toArray(this.stemMesh!.instanceMatrix.array, (i) * 16); // Top - this.topMesh!.setMatrixAt(i, dummy.matrix); + // ⚡ OPTIMIZATION: Write directly to instanceMatrix array instead of updateMatrix + setMatrixAt + dummy.matrix.toArray(this.topMesh!.instanceMatrix.array, (i) * 16); // Params const height = options.height || 2.5; diff --git a/src/foliage/lod.ts b/src/foliage/lod.ts index f3a316d..c002c55 100644 --- a/src/foliage/lod.ts +++ b/src/foliage/lod.ts @@ -618,7 +618,8 @@ export class FoliageLODManager { mesh.count = count; for (let i = 0; i < count; i++) { - mesh.setMatrixAt(i, matrices[i]); + // ⚡ OPTIMIZATION: Write directly to instanceMatrix array instead of updateMatrix + setMatrixAt + matrices[i].toArray(mesh.instanceMatrix.array, (i) * 16); mesh.setColorAt(i, colors[i]); } diff --git a/src/foliage/mushroom-batcher.ts b/src/foliage/mushroom-batcher.ts index 0055e11..3a09f1a 100644 --- a/src/foliage/mushroom-batcher.ts +++ b/src/foliage/mushroom-batcher.ts @@ -629,7 +629,8 @@ export class MushroomBatcher { // 1. Set Matrix dummy.updateMatrix(); - this.mesh!.setMatrixAt(i, dummy.matrix); + // ⚡ OPTIMIZATION: Write directly to instanceMatrix array instead of updateMatrix + setMatrixAt + dummy.matrix.toArray(this.mesh!.instanceMatrix.array, (i) * 16); // PALETTE: Set Color // Default to Red (0xFF6B6B) if no note color provided @@ -692,7 +693,8 @@ export class MushroomBatcher { // A. Copy Attributes from Last to Removed // Matrix this.mesh!.getMatrixAt(lastIndex, _scratchMatrix); - this.mesh!.setMatrixAt(indexToRemove, _scratchMatrix); + // ⚡ OPTIMIZATION: Write directly to instanceMatrix array instead of updateMatrix + setMatrixAt + _scratchMatrix.toArray(this.mesh!.instanceMatrix.array, (indexToRemove) * 16); // Color this.mesh!.getColorAt(lastIndex, _scratchColor); diff --git a/src/foliage/simple-flower-batcher.ts b/src/foliage/simple-flower-batcher.ts index 7c16812..8c966b2 100644 --- a/src/foliage/simple-flower-batcher.ts +++ b/src/foliage/simple-flower-batcher.ts @@ -296,7 +296,8 @@ export class SimpleFlowerBatcher { _scratchScale.set(0.05, stemHeight, 0.05); _scratchMat.makeScale(_scratchScale.x, _scratchScale.y, _scratchScale.z); _scratchMat.premultiply(baseMatrix); // Apply World Transform - this.stemMesh!.setMatrixAt(i, _scratchMat); + // ⚡ OPTIMIZATION: Write directly to instanceMatrix array instead of updateMatrix + setMatrixAt + _scratchMat.toArray(this.stemMesh!.instanceMatrix.array, (i) * 16); // Head Transform (At top of stem) // Translation(0, stemHeight, 0) relative to Base. @@ -306,7 +307,8 @@ export class SimpleFlowerBatcher { const headWorld = _scratchMat2; // No clone, avoid GC spike // Petals - this.petalMesh!.setMatrixAt(i, headWorld); + // ⚡ OPTIMIZATION: Write directly to instanceMatrix array instead of updateMatrix + setMatrixAt + headWorld.toArray(this.petalMesh!.instanceMatrix.array, (i) * 16); // Color if (typeof color === 'number') _scratchColor.setHex(color); @@ -317,20 +319,24 @@ export class SimpleFlowerBatcher { // Center: Scale(0.1) _scratchMat.makeScale(0.1, 0.1, 0.1); _scratchMat.premultiply(headWorld); - this.centerMesh!.setMatrixAt(i, _scratchMat); + // ⚡ OPTIMIZATION: Write directly to instanceMatrix array instead of updateMatrix + setMatrixAt + _scratchMat.toArray(this.centerMesh!.instanceMatrix.array, (i) * 16); // Stamens: No extra scale needed (baked in geometry), just head transform - this.stamenMesh!.setMatrixAt(i, headWorld); + // ⚡ OPTIMIZATION: Write directly to instanceMatrix array instead of updateMatrix + setMatrixAt + headWorld.toArray(this.stamenMesh!.instanceMatrix.array, (i) * 16); // Beam: Random chance if (Math.random() > 0.5) { _scratchMat.makeScale(0.1, 1.0, 0.1); _scratchMat.premultiply(headWorld); - this.beamMesh!.setMatrixAt(i, _scratchMat); + // ⚡ OPTIMIZATION: Write directly to instanceMatrix array instead of updateMatrix + setMatrixAt + _scratchMat.toArray(this.beamMesh!.instanceMatrix.array, (i) * 16); } else { _scratchMat.makeScale(0, 0, 0); _scratchMat.premultiply(headWorld); - this.beamMesh!.setMatrixAt(i, _scratchMat); + // ⚡ OPTIMIZATION: Write directly to instanceMatrix array instead of updateMatrix + setMatrixAt + _scratchMat.toArray(this.beamMesh!.instanceMatrix.array, (i) * 16); } this.beamMesh!.setColorAt(i, _scratchColor); diff --git a/src/foliage/waterfall-batcher.ts b/src/foliage/waterfall-batcher.ts index 538b6a3..81a6ba4 100644 --- a/src/foliage/waterfall-batcher.ts +++ b/src/foliage/waterfall-batcher.ts @@ -247,7 +247,8 @@ export class WaterfallBatcher { _scratchPos.y -= height * 0.5; _scratchMatrix.compose(_scratchPos, _scratchQuat, _scratchScale); - this.mesh!.setMatrixAt(index, _scratchMatrix); + // ⚡ OPTIMIZATION: Write directly to instanceMatrix array instead of updateMatrix + setMatrixAt + _scratchMatrix.toArray(this.mesh!.instanceMatrix.array, (index) * 16); this.mesh!.instanceMatrix.needsUpdate = true; // 2. Setup Splashes (8 per waterfall) @@ -276,7 +277,8 @@ export class WaterfallBatcher { // Initialize matrix to identity (needed for rendering, even if positionNode overrides) _scratchMatrix.identity(); - this.splashMesh!.setMatrixAt(si, _scratchMatrix); + // ⚡ OPTIMIZATION: Write directly to instanceMatrix array instead of updateMatrix + setMatrixAt + _scratchMatrix.toArray(this.splashMesh!.instanceMatrix.array, (si) * 16); } this.splashOrigin!.needsUpdate = true; @@ -298,7 +300,8 @@ export class WaterfallBatcher { if (indexToRemove !== lastIndex) { // Swap Column this.mesh!.getMatrixAt(lastIndex, _scratchMatrix); - this.mesh!.setMatrixAt(indexToRemove, _scratchMatrix); + // ⚡ OPTIMIZATION: Write directly to instanceMatrix array instead of updateMatrix + setMatrixAt + _scratchMatrix.toArray(this.mesh!.instanceMatrix.array, (indexToRemove) * 16); // Swap Splashes (Block of 8) const srcStart = lastIndex * SPLASHES_PER_WATERFALL; @@ -364,7 +367,8 @@ export class WaterfallBatcher { _scratchScale.z = _scratchScale.x * thicknessScale; _scratchMatrix.compose(_scratchPos, _scratchQuat, _scratchScale); - this.mesh!.setMatrixAt(index, _scratchMatrix); + // ⚡ OPTIMIZATION: Write directly to instanceMatrix array instead of updateMatrix + setMatrixAt + _scratchMatrix.toArray(this.mesh!.instanceMatrix.array, (index) * 16); this.mesh!.instanceMatrix.needsUpdate = true; } } diff --git a/src/gameplay/jitter-mines.ts b/src/gameplay/jitter-mines.ts index 1548050..a91ab4d 100644 --- a/src/gameplay/jitter-mines.ts +++ b/src/gameplay/jitter-mines.ts @@ -159,7 +159,8 @@ class JitterMineSystem { _scratchDummy.scale.setScalar(1); _scratchDummy.rotation.set(0,0,0); _scratchDummy.updateMatrix(); - this.mesh.setMatrixAt(i, _scratchDummy.matrix); + // ⚡ OPTIMIZATION: Write directly to instanceMatrix array instead of updateMatrix + setMatrixAt + _scratchDummy.matrix.toArray(this.mesh.instanceMatrix.array, (i) * 16); } } diff --git a/src/gameplay/rainbow-blaster.ts b/src/gameplay/rainbow-blaster.ts index 5b31e1f..2f7e667 100644 --- a/src/gameplay/rainbow-blaster.ts +++ b/src/gameplay/rainbow-blaster.ts @@ -127,7 +127,8 @@ class ProjectilePool { // WebGPU TSL multiplies custom positionNode output by instanceMatrix const identityMatrix = new THREE.Matrix4(); for (let i = 0; i < MAX_PROJECTILES; i++) { - this.mesh.setMatrixAt(i, identityMatrix); + // ⚡ OPTIMIZATION: Write directly to instanceMatrix array instead of updateMatrix + setMatrixAt + identityMatrix.toArray(this.mesh.instanceMatrix.array, (i) * 16); } this.mesh.instanceMatrix.needsUpdate = true; diff --git a/src/systems/glitch-grenade.ts b/src/systems/glitch-grenade.ts index d5e7373..56a8ef9 100644 --- a/src/systems/glitch-grenade.ts +++ b/src/systems/glitch-grenade.ts @@ -88,7 +88,8 @@ class GlitchGrenadeSystem { // ⚡ OPTIMIZATION: Write a pure identity matrix into the instanceMatrix buffer const identityMatrix = new THREE.Matrix4(); for (let i = 0; i < MAX_GRENADES; i++) { - this.mesh.setMatrixAt(i, identityMatrix); + // ⚡ OPTIMIZATION: Write directly to instanceMatrix array instead of updateMatrix + setMatrixAt + identityMatrix.toArray(this.mesh.instanceMatrix.array, (i) * 16); } this.mesh.instanceMatrix.needsUpdate = true; diff --git a/src/systems/physics/physics.ts b/src/systems/physics/physics.ts index c409ef4..6b573f2 100644 --- a/src/systems/physics/physics.ts +++ b/src/systems/physics/physics.ts @@ -478,7 +478,8 @@ function checkHarmonyOrbs() { harmonyOrbSystem.dummy.position.set(0, -9999, 0); harmonyOrbSystem.dummy.scale.setScalar(0); harmonyOrbSystem.dummy.updateMatrix(); - harmonyOrbSystem.mesh.setMatrixAt(i, harmonyOrbSystem.dummy.matrix); + // ⚡ OPTIMIZATION: Write directly to instanceMatrix array instead of updateMatrix + setMatrixAt + harmonyOrbSystem.dummy.matrix.toArray(harmonyOrbSystem.mesh.instanceMatrix.array, (i) * 16); harmonyOrbSystem.mesh.instanceMatrix.needsUpdate = true; // Visuals & Logic