perf(client): reuse chunk mesh geometry instead of dispose/recreate cycle#27
Open
RZDESIGN wants to merge 2 commits intohytopiagg:mainfrom
Open
perf(client): reuse chunk mesh geometry instead of dispose/recreate cycle#27RZDESIGN wants to merge 2 commits intohytopiagg:mainfrom
RZDESIGN wants to merge 2 commits intohytopiagg:mainfrom
Conversation
added 2 commits
March 5, 2026 17:24
…ycle When a chunk batch mesh is updated (e.g. block placed/broken), the existing code disposes the old BufferGeometry and creates an entirely new one with fresh BufferAttribute objects. This forces Three.js to delete the old WebGL buffers and allocate new ones every time — even when the buffer sizes haven't changed. Swap the typed arrays on the existing BufferAttribute objects instead and mark them dirty. Three.js then uses gl.bufferSubData (fast path) when the byte-size is unchanged, and only recreates the buffer when the size actually differs. Made-with: Cursor
Three.js (v0.167.1) throws when a BufferAttribute's backing array is swapped to a different byteLength. Add a byteLength equality check in _swapAttribute and in the index-buffer path so we only reuse the existing WebGL buffer (fast bufferSubData path) when sizes match; otherwise fall back to creating a new BufferAttribute, which lets Three.js allocate a correctly-sized GPU buffer. Made-with: Cursor
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
ChunkMeshManager._createOrUpdateMesh()currently disposes the oldBufferGeometryand creates an entirely new one (with freshBufferAttributeobjects) every time a chunk batch mesh is updated — e.g. when a player breaks or places a block, or when light propagation triggers a rebuild.This forces Three.js to:
gl.deleteBuffer× 5-8 attributes + index)gl.createBuffer+gl.bufferData× 5-8 attributes + index)BufferGeometryand all itsBufferAttributeJS objectsThis PR eliminates that cycle by reusing the existing geometry — swapping the typed arrays on the existing
BufferAttributeobjects and marking them dirty.What changed
Single file:
client/src/chunks/ChunkMeshManager.tsThe
_createOrUpdateMeshmethod is split into two paths:BufferGeometry+BufferAttributeobjects normally.dispose()→new BufferGeometry(), the existing geometry'sBufferAttributeobjects are kept alive. Their.arrayproperty is swapped to the new typed array from the worker, and.needsUpdate = trueis set.Two small private helpers keep the update path clean:
_swapAttribute(geometry, name, data, itemSize)— swaps the array on an existing attribute, or creates the attribute if it doesn't exist yet._swapOptionalAttribute(geometry, name, data, itemSize)— same as above, but also handles the case where the attribute should be removed (e.g. a chunk that previously hadlightLeveldata no longer does).The index buffer handles a special case: chunk indices can be
Uint16ArrayorUint32Arraydepending on vertex count. When the type matches, the array is swapped in-place. When it changes (e.g. chunk grows past 65535 vertices), a newBufferAttributeis created for the index only.After swapping,
boundingSphereandboundingBoxare explicitly nulled and recomputed from the new data. This is necessary becauseupdateAABB()skips recomputation whenboundingBoxis already present.How Three.js handles this under the hood
When a
BufferAttributehasneedsUpdate = true, Three.js'sWebGLAttributescompares the new array'sbyteLengthagainst the stored GPU buffer size:gl.bufferSubData(reuse existing buffer)gl.deleteBuffer+gl.createBuffer+gl.bufferDataIn a typical play session, the majority of chunk updates are block place/break operations that change only a few faces. The vertex/index count stays identical or changes by a small amount. This means most updates hit the fast
bufferSubDatapath.Impact
For a scene with ~50 loaded chunk batches where a player is actively building:
BufferGeometry+ 6-8BufferAttribute+ 6-8TypedArraywrappersThe biggest win is eliminating the paired
gl.deleteBuffer/gl.createBuffercalls. GPU drivers must synchronize to ensure the old buffer isn't still in use before deleting it, which can introduce micro-stalls in the rendering pipeline. Reusing the buffer viabufferSubDataavoids this entirely.Edge cases handled
_swapOptionalAttributeadds or removes attributes as needed (e.g.lightLevelfor lit vs non-lit chunks,foamLevelfor liquid meshes)= null) before recomputation to ensurecomputeBoundingSphere()andupdateAABB()use the new data_removeMeshstill callsgeometry.dispose()— WebGL buffers are properly cleaned up when a batch is removed from the worldFiles changed
client/src/chunks/ChunkMeshManager.ts_createOrUpdateMeshto reuse geometry on update. Added_swapAttributeand_swapOptionalAttributehelpers. Removeddispose()+ reassignment from the update path.Test plan
main