extends Node ## ## MeshPool - recycles ArrayMesh instances to avoid allocation churn. ## ## On low-RAM Android, creating and freeing ArrayMesh objects every frame causes ## GC spikes and fragmentation. This pool keeps a bounded number of meshes alive ## and reuses them. Meshes handed back to the pool have their surfaces cleared, ## freeing their GPU buffers while keeping the object (and its native handle). ## signal pool_grew(size: int) signal pool_trimmed(freed: int) ## Hard cap so the pool itself cannot become the memory leak. @export var max_pooled: int = 32 var _free: Array[ArrayMesh] = [] var _live_count: int = 0 var _peak_live: int = 0 ## Get a cleared mesh. Reuses a pooled one if available. func acquire_mesh() -> ArrayMesh: var mesh: ArrayMesh if _free.size() > 0: mesh = _free.pop_back() else: mesh = ArrayMesh.new() _live_count += 1 _peak_live = maxi(_peak_live, _live_count) return mesh ## Return a mesh for reuse. Clears surfaces so its VRAM is released immediately. func release_mesh(mesh: ArrayMesh) -> void: if mesh == null: return _live_count = maxi(0, _live_count - 1) # Drop all surfaces: this frees the vertex buffers on the GPU side. for i in range(mesh.get_surface_count()): pass mesh.clear_surfaces() if _free.size() < max_pooled: _free.append(mesh) else: # Pool is full: let Godot free this one (mesh has no surfaces = cheap). pass ## Remove pooled-but-unused meshes down to `keep`. Call on scene transitions. func trim(keep: int = 8) -> void: var freed := 0 while _free.size() > keep: _free.pop_back() freed += 1 if freed > 0: pool_trimmed.emit(freed) func stats() -> Dictionary: return { "pooled": _free.size(), "live": _live_count, "peak_live": _peak_live, }