working geometry pool manager!

This commit is contained in:
Vitaly Turovsky 2025-07-20 08:21:15 +03:00
commit 9ee28ef62f
4 changed files with 389 additions and 70 deletions

View file

@ -20,7 +20,7 @@ export type WorldDataEmitterEvents = {
entityMoved: (data: any) => void
playerEntity: (data: any) => void
time: (data: number) => void
renderDistance: (viewDistance: number) => void
renderDistance: (viewDistance: number, keepChunksDistance: number) => void
blockEntities: (data: Record<string, any> | { blockEntities: Record<string, any> }) => void
markAsLoaded: (data: { x: number, z: number }) => void
unloadChunk: (data: { x: number, z: number }) => void
@ -79,7 +79,7 @@ export class WorldDataEmitter extends (EventEmitter as new () => TypedEmitter<Wo
updateViewDistance (viewDistance: number) {
this.viewDistance = viewDistance
this.emitter.emit('renderDistance', viewDistance)
this.emitter.emit('renderDistance', viewDistance, this.keepChunksDistance)
}
listenToBot (bot: typeof __type_bot) {

View file

@ -487,7 +487,6 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
this.allChunksFinished = true
this.allLoadedIn ??= Date.now() - this.initialChunkLoadWasStartedIn!
}
this.updateChunksStats()
}
changeHandSwingingState (isAnimationPlaying: boolean, isLeftHand: boolean): void { }

View file

@ -0,0 +1,298 @@
import * as THREE from 'three'
import { MesherGeometryOutput } from '../lib/mesher/shared'
export interface ChunkMeshPool {
mesh: THREE.Mesh
inUse: boolean
lastUsedTime: number
sectionKey?: string
}
export class ChunkMeshManager {
private readonly meshPool: ChunkMeshPool[] = []
private readonly activeSections = new Map<string, ChunkMeshPool>()
private poolSize: number
private maxPoolSize: number
private minPoolSize: number
// Performance tracking
private hits = 0
private misses = 0
// Debug flag to bypass pooling
public bypassPooling = false
constructor (
public material: THREE.Material,
public worldHeight: number,
viewDistance = 3,
) {
this.updateViewDistance(viewDistance)
console.log(`ChunkMeshManager: Initializing with pool size ${this.poolSize} (min: ${this.minPoolSize}, max: ${this.maxPoolSize})`)
this.initializePool()
}
private initializePool () {
// Create initial pool
for (let i = 0; i < this.poolSize; i++) {
const geometry = new THREE.BufferGeometry()
const mesh = new THREE.Mesh(geometry, this.material)
mesh.visible = false
mesh.matrixAutoUpdate = false
mesh.name = 'pooled-section-mesh'
const poolEntry: ChunkMeshPool = {
mesh,
inUse: false,
lastUsedTime: 0
}
this.meshPool.push(poolEntry)
// Don't add to scene here - meshes will be added to containers
}
}
/**
* Update or create a section with new geometry data
*/
updateSection (sectionKey: string, geometryData: MesherGeometryOutput): THREE.Mesh | null {
// Normal pooling mode
// Check if section already exists
let poolEntry = this.activeSections.get(sectionKey)
if (!poolEntry) {
// Get mesh from pool
poolEntry = this.acquireMesh()
if (!poolEntry) {
console.warn(`ChunkMeshManager: No available mesh in pool for section ${sectionKey}`)
return null
}
this.activeSections.set(sectionKey, poolEntry)
poolEntry.sectionKey = sectionKey
}
const { mesh } = poolEntry
const { geometry } = mesh
// Update geometry attributes efficiently
this.updateGeometryAttribute(geometry, 'position', geometryData.positions, 3)
this.updateGeometryAttribute(geometry, 'normal', geometryData.normals, 3)
this.updateGeometryAttribute(geometry, 'color', geometryData.colors, 3)
this.updateGeometryAttribute(geometry, 'uv', geometryData.uvs, 2)
// Use direct index assignment for better performance (like before)
geometry.index = new THREE.BufferAttribute(geometryData.indices as Uint32Array | Uint16Array, 1)
// Set bounding box and sphere for the 16x16x16 section
geometry.boundingBox = new THREE.Box3(
new THREE.Vector3(-8, -8, -8),
new THREE.Vector3(8, 8, 8)
)
geometry.boundingSphere = new THREE.Sphere(
new THREE.Vector3(0, 0, 0),
Math.sqrt(3 * 8 ** 2)
)
// Position the mesh
mesh.position.set(geometryData.sx, geometryData.sy, geometryData.sz)
mesh.updateMatrix()
mesh.visible = true
// Store metadata
;(mesh as any).tilesCount = geometryData.positions.length / 3 / 4
;(mesh as any).blocksCount = geometryData.blocksCount
poolEntry.lastUsedTime = performance.now()
return mesh
}
/**
* Release a section and return its mesh to the pool
*/
releaseSection (sectionKey: string): boolean {
const poolEntry = this.activeSections.get(sectionKey)
if (!poolEntry) {
return false
}
// Hide mesh and mark as available
poolEntry.mesh.visible = false
poolEntry.inUse = false
poolEntry.sectionKey = undefined
poolEntry.lastUsedTime = 0
// Clear geometry to free memory
this.clearGeometry(poolEntry.mesh.geometry)
this.activeSections.delete(sectionKey)
return true
}
/**
* Get mesh for section if it exists
*/
getSectionMesh (sectionKey: string): THREE.Mesh | undefined {
return this.activeSections.get(sectionKey)?.mesh
}
/**
* Check if section is managed by this pool
*/
hasSection (sectionKey: string): boolean {
return this.activeSections.has(sectionKey)
}
/**
* Update pool size based on new view distance
*/
updateViewDistance (maxViewDistance: number) {
// Calculate dynamic pool size based on view distance
const chunksInView = (maxViewDistance * 2 + 1) ** 2
const maxSectionsPerChunk = this.worldHeight / 16
const avgSectionsPerChunk = 5
this.minPoolSize = Math.floor(chunksInView * avgSectionsPerChunk)
this.maxPoolSize = Math.floor(chunksInView * maxSectionsPerChunk) + 1
this.poolSize ??= this.minPoolSize
// Expand pool if needed to reach optimal size
if (this.minPoolSize > this.poolSize) {
const targetSize = Math.min(this.minPoolSize, this.maxPoolSize)
this.expandPool(targetSize)
}
console.log(`ChunkMeshManager: Updated view max distance to ${maxViewDistance}, pool: ${this.poolSize}/${this.maxPoolSize}, optimal: ${this.minPoolSize}`)
}
/**
* Get pool statistics
*/
getStats () {
const freeCount = this.meshPool.filter(entry => !entry.inUse).length
const hitRate = this.hits + this.misses > 0 ? (this.hits / (this.hits + this.misses) * 100).toFixed(1) : '0'
return {
poolSize: this.poolSize,
activeCount: this.activeSections.size,
freeCount,
hitRate: `${hitRate}%`,
hits: this.hits,
misses: this.misses
}
}
/**
* Cleanup and dispose resources
*/
dispose () {
// Release all active sections
for (const [sectionKey] of this.activeSections) {
this.releaseSection(sectionKey)
}
// Dispose all meshes and geometries
for (const poolEntry of this.meshPool) {
// Meshes will be removed from scene when their parent containers are removed
poolEntry.mesh.geometry.dispose()
}
this.meshPool.length = 0
this.activeSections.clear()
}
// Private helper methods
private acquireMesh (): ChunkMeshPool | undefined {
if (this.bypassPooling) {
return {
mesh: new THREE.Mesh(new THREE.BufferGeometry(), this.material),
inUse: true,
lastUsedTime: performance.now()
}
}
// Find first available mesh
const availableMesh = this.meshPool.find(entry => !entry.inUse)
if (availableMesh) {
availableMesh.inUse = true
this.hits++
return availableMesh
}
// No available mesh, expand pool to accommodate new sections
let newPoolSize = Math.min(this.poolSize + 16, this.maxPoolSize)
if (newPoolSize === this.poolSize) {
newPoolSize = this.poolSize + 8
this.maxPoolSize = newPoolSize
console.warn(`ChunkMeshManager: Pool exhausted (${this.poolSize}/${this.maxPoolSize}). Emergency expansion to ${newPoolSize}`)
}
this.misses++
this.expandPool(newPoolSize)
return this.acquireMesh()
}
private expandPool (newSize: number) {
const oldSize = this.poolSize
this.poolSize = newSize
// console.log(`ChunkMeshManager: Expanding pool from ${oldSize} to ${newSize}`)
// Add new meshes to pool
for (let i = oldSize; i < newSize; i++) {
const geometry = new THREE.BufferGeometry()
const mesh = new THREE.Mesh(geometry, this.material)
mesh.visible = false
mesh.matrixAutoUpdate = false
mesh.name = 'pooled-section-mesh'
const poolEntry: ChunkMeshPool = {
mesh,
inUse: false,
lastUsedTime: 0
}
this.meshPool.push(poolEntry)
// Don't add to scene here - meshes will be added to containers
}
}
private updateGeometryAttribute (
geometry: THREE.BufferGeometry,
name: string,
array: Float32Array,
itemSize: number
) {
const attribute = geometry.getAttribute(name)
if (attribute && attribute.count === array.length / itemSize) {
// Reuse existing attribute
;(attribute.array as Float32Array).set(array)
attribute.needsUpdate = true
} else {
// Create new attribute (this will dispose the old one automatically)
geometry.setAttribute(name, new THREE.BufferAttribute(array, itemSize))
}
}
private clearGeometry (geometry: THREE.BufferGeometry) {
// Clear attributes but keep the attribute objects for reuse
const attributes = ['position', 'normal', 'color', 'uv']
for (const name of attributes) {
const attr = geometry.getAttribute(name)
if (attr) {
// Just mark as needing update but don't dispose to avoid recreation costs
attr.needsUpdate = true
}
}
if (geometry.index) {
geometry.index.needsUpdate = true
}
}
}

View file

@ -7,6 +7,7 @@ import { renderSign } from '../sign-renderer'
import { DisplayWorldOptions, GraphicsInitOptions } from '../../../src/appViewer'
import { chunkPos, sectionPos } from '../lib/simpleUtils'
import { WorldRendererCommon } from '../lib/worldrendererCommon'
import { WorldDataEmitterWorker } from '../lib/worldDataEmitter'
import { addNewStat } from '../lib/ui/newStats'
import { MesherGeometryOutput, InstancingMode } from '../lib/mesher/shared'
import { ItemSpecificContextProperties } from '../lib/basePlayerState'
@ -25,6 +26,7 @@ import { CameraShake } from './cameraShake'
import { ThreeJsMedia } from './threeJsMedia'
import { Fountain } from './threeJsParticles'
import { InstancedRenderer } from './instancedRenderer'
import { ChunkMeshManager } from './chunkMeshManager'
type SectionKey = string
@ -53,6 +55,7 @@ export class WorldRendererThree extends WorldRendererCommon {
cameraContainer: THREE.Object3D
media: ThreeJsMedia
instancedRenderer: InstancedRenderer | undefined
chunkMeshManager: ChunkMeshManager
waitingChunksToDisplay = {} as { [chunkKey: string]: SectionKey[] }
camera: THREE.PerspectiveCamera
renderTimeAvg = 0
@ -106,6 +109,14 @@ export class WorldRendererThree extends WorldRendererCommon {
this.cameraShake = new CameraShake(this, this.onRender)
this.media = new ThreeJsMedia(this)
this.instancedRenderer = new InstancedRenderer(this)
this.chunkMeshManager = new ChunkMeshManager(this.material, this.worldSizeParams.worldHeight, this.viewDistance)
// Enable bypass pooling for debugging if URL param is present
if (new URLSearchParams(location.search).get('bypassMeshPooling') === 'true') {
this.chunkMeshManager.bypassPooling = true
console.log('ChunkMeshManager: Bypassing pooling for debugging')
}
// this.fountain = new Fountain(this.scene, this.scene, {
// position: new THREE.Vector3(0, 10, 0),
// })
@ -137,6 +148,15 @@ export class WorldRendererThree extends WorldRendererCommon {
})
}
override connect (worldView: WorldDataEmitterWorker) {
super.connect(worldView)
// Add additional renderDistance handling for mesh pool updates
worldView.on('renderDistance', (viewDistance) => {
this.chunkMeshManager.updateViewDistance(viewDistance)
})
}
updateEntity (e, isPosUpdate = false) {
const overrides = {
rotation: {
@ -346,8 +366,11 @@ export class WorldRendererThree extends WorldRendererCommon {
text += `B: ${formatBigNumber(this.blocksRendered)} `
if (instancedStats) {
text += `I: ${formatBigNumber(instancedStats.totalInstances)}/${instancedStats.activeBlockTypes}t `
text += `DC: ${formatBigNumber(instancedStats.drawCalls)}`
text += `DC: ${formatBigNumber(instancedStats.drawCalls)} `
}
const poolStats = this.chunkMeshManager.getStats()
const poolMode = this.chunkMeshManager.bypassPooling ? 'BYPASS' : poolStats.hitRate
text += `MP: ${poolStats.activeCount}/${poolStats.poolSize} ${poolMode}`
pane.updateText(text)
this.backendInfoReport = text
}
@ -361,7 +384,7 @@ export class WorldRendererThree extends WorldRendererCommon {
const [x, y, z] = key.split(',').map(x => Math.floor(+x / 16))
// sum of distances: x + y + z
const chunkDistance = Math.abs(x - this.cameraSectionPos.x) + Math.abs(y - this.cameraSectionPos.y) + Math.abs(z - this.cameraSectionPos.z)
const section = this.sectionObjects[key].children.find(child => child.name === 'mesh')!
const section = this.sectionObjects[key].mesh!
section.renderOrder = 500 - chunkDistance
}
@ -402,7 +425,6 @@ export class WorldRendererThree extends WorldRendererCommon {
delete this.waitingChunksToDisplay[chunkKey]
}
// debugRecomputedDeletedObjects = 0
handleWorkerMessage (data: { geometry: MesherGeometryOutput, key, type }): void {
if (data.type !== 'geometry') return
@ -413,55 +435,74 @@ export class WorldRendererThree extends WorldRendererCommon {
this.instancedRenderer?.removeSectionInstances(data.key)
// if (data.key === '48,64,32') {
// console.log('handleWorkerMessage', data.key, this.sectionObjects[data.key], this.sectionInstancingMode[data.key], Object.keys(data.geometry.instancedBlocks).length, data.geometry.positions.length)
// }
// Handle instanced blocks data from worker
if (hasInstancedBlocks) {
this.instancedRenderer?.handleInstancedBlocksFromWorker(data.geometry.instancedBlocks, data.key, this.getInstancingMode(new Vec3(chunkCoords[0], chunkCoords[1], chunkCoords[2])))
}
// Check if chunk should be loaded and has geometry
if (!this.loadedChunks[chunkKey] || !data.geometry.positions.length || !this.active) {
// Release any existing section from the pool
this.chunkMeshManager.releaseSection(data.key)
return
}
// remvoe object from scene
let object = this.sectionObjects[data.key]
if (object) {
this.scene.remove(object)
disposeObject(object)
// disposeObject(object)
delete this.sectionObjects[data.key]
}
object = this.sectionObjects[data.key]
// Use ChunkMeshManager for optimized mesh handling
const mesh = this.chunkMeshManager.updateSection(data.key, data.geometry)
if (!this.loadedChunks[chunkKey] || !data.geometry.positions.length || !this.active) return
if (!mesh) {
console.warn(`Failed to get mesh for section ${data.key}`)
return
}
// if (object) {
// this.debugRecomputedDeletedObjects++
// Create or update the section object container
object = new THREE.Group()
this.sectionObjects[data.key] = object
this.scene.add(object)
// Add the pooled mesh to the container
object.add(mesh)
this.sectionObjects[data.key].mesh = mesh as THREE.Mesh<THREE.BufferGeometry, THREE.MeshLambertMaterial>
// Handle signs and heads (these are added to the container, not the pooled mesh)
this.addSignsAndHeads(object as THREE.Group, data.geometry)
this.updateBoxHelper(data.key)
// Handle chunk-based rendering
if (this.displayOptions.inWorldRenderingConfig._renderByChunks) {
object.visible = false
const chunkKey = `${chunkCoords[0]},${chunkCoords[2]}`
this.waitingChunksToDisplay[chunkKey] ??= []
this.waitingChunksToDisplay[chunkKey].push(data.key)
if (this.finishedChunks[chunkKey]) {
this.finishChunk(chunkKey)
}
}
this.updatePosDataChunk(data.key)
object.matrixAutoUpdate = false
}
private addSignsAndHeads (object: THREE.Group, geometry: MesherGeometryOutput) {
// Clear existing signs and heads
// const childrenToRemove = object.children.filter(child => child.name !== 'mesh' && child.name !== 'helper')
// for (const child of childrenToRemove) {
// object.remove(child)
// disposeObject(child)
// }
const geometry = object?.mesh?.geometry ?? new THREE.BufferGeometry()
geometry.setAttribute('position', new THREE.BufferAttribute(data.geometry.positions, 3))
geometry.setAttribute('normal', new THREE.BufferAttribute(data.geometry.normals, 3))
geometry.setAttribute('color', new THREE.BufferAttribute(data.geometry.colors, 3))
geometry.setAttribute('uv', new THREE.BufferAttribute(data.geometry.uvs, 2))
geometry.index = new THREE.BufferAttribute(data.geometry.indices as Uint32Array | Uint16Array, 1)
const { sx, sy, sz } = data.geometry
// Set bounding box for the 16x16x16 section
geometry.boundingBox = new THREE.Box3(new THREE.Vector3(-8, -8, -8), new THREE.Vector3(8, 8, 8))
geometry.boundingSphere = new THREE.Sphere(new THREE.Vector3(0, 0, 0), Math.sqrt(3 * 8 ** 2))
const mesh = object?.mesh ?? new THREE.Mesh(geometry, this.material)
mesh.position.set(data.geometry.sx, data.geometry.sy, data.geometry.sz)
mesh.name = 'mesh'
if (!object) {
object = new THREE.Group()
object.add(mesh)
}
(object as any).tilesCount = data.geometry.positions.length / 3 / 4;
(object as any).blocksCount = data.geometry.blocksCount
// should not compute it once
if (Object.keys(data.geometry.signs).length) {
for (const [posKey, { isWall, isHanging, rotation }] of Object.entries(data.geometry.signs)) {
// Add signs
if (Object.keys(geometry.signs).length) {
for (const [posKey, { isWall, isHanging, rotation }] of Object.entries(geometry.signs)) {
const signBlockEntity = this.blockEntities[posKey]
if (!signBlockEntity) continue
const [x, y, z] = posKey.split(',')
@ -470,8 +511,10 @@ export class WorldRendererThree extends WorldRendererCommon {
object.add(sign)
}
}
if (Object.keys(data.geometry.heads).length) {
for (const [posKey, { isWall, rotation }] of Object.entries(data.geometry.heads)) {
// Add heads
if (Object.keys(geometry.heads).length) {
for (const [posKey, { isWall, rotation }] of Object.entries(geometry.heads)) {
const headBlockEntity = this.blockEntities[posKey]
if (!headBlockEntity) continue
const [x, y, z] = posKey.split(',')
@ -480,31 +523,6 @@ export class WorldRendererThree extends WorldRendererCommon {
object.add(head)
}
}
if (!this.sectionObjects[data.key]) {
this.sectionObjects[data.key] = object
this.sectionObjects[data.key].mesh = mesh
this.scene.add(object)
}
this.updateBoxHelper(data.key)
if (this.displayOptions.inWorldRenderingConfig._renderByChunks) {
object.visible = false
const chunkKey = `${chunkCoords[0]},${chunkCoords[2]}`
this.waitingChunksToDisplay[chunkKey] ??= []
this.waitingChunksToDisplay[chunkKey].push(data.key)
if (this.finishedChunks[chunkKey]) {
// todo it might happen even when it was not an update
this.finishChunk(chunkKey)
}
}
this.updatePosDataChunk(data.key)
object.matrixAutoUpdate = false
mesh.onAfterRender = (renderer, scene, camera, geometry, material, group) => {
// mesh.matrixAutoUpdate = false
}
}
getSignTexture (position: Vec3, blockEntity, backSide = false) {
@ -1031,10 +1049,13 @@ export class WorldRendererThree extends WorldRendererCommon {
// Remove instanced blocks for this section
this.instancedRenderer?.removeSectionInstances(key)
const mesh = this.sectionObjects[key]
if (mesh) {
this.scene.remove(mesh)
disposeObject(mesh)
// Release section from mesh pool
this.chunkMeshManager.releaseSection(key)
const object = this.sectionObjects[key]
if (object) {
this.scene.remove(object)
disposeObject(object)
}
delete this.sectionObjects[key]
}
@ -1089,6 +1110,7 @@ export class WorldRendererThree extends WorldRendererCommon {
destroy (): void {
this.instancedRenderer?.destroy()
this.chunkMeshManager.dispose()
super.destroy()
}