Memory Leak In a Voxel Game

You can write your topic however you want, but you need to answer these questions:

  1. What do you want to achieve? Keep it simple and clear!
    A way to fix this memory leak problem, so that loading and unloading chunks work as intended without causing memory leaks

  2. What is the issue? Include screenshots / videos if possible!
    Memory leaks happend when a client load a new chunk/unload a chunk, the problem is still unknown which part of the code is causing the memory leak
    Watch memory leak problem | Streamable

  3. What solutions have you tried so far? Did you look for solutions on the Developer Hub?

  • Connecting a property changed signal event of the voxel’s parent, setting the tables of RenderedVoxels[voxel position] to nil if the parent is nil
Voxel:GetPropertyChangedSignal("Parent"):Connect(function()
		if Voxel.Parent == nil then
			VoxelManager.RenderedVoxels[`{voxelPosition.X} {voxelPosition.Y} {voxelPosition.Z}`] = nil
		end
	end)
  • Clearing the ChunkData[unloaded chunk x][unloaded chunk z] when unloading chunk
-- ChunkData is a list of blocks in a chunk relative to that chunk
function ChunkRemoved(chunkPos)
		ChunkManager.ClearChunk(ChunkData, Vector3.new(chunkPos.X, 0, chunkPos.Z))
	end
end

function ChunkManager.ClearChunk(chunkData, chunkPosition: Vector3)
	if chunkData[chunkPosition.X] and chunkData[chunkPosition.X][chunkPosition.Z] then
		chunkData[chunkPosition.X][chunkPosition.Z] = nil
		if next(chunkData[chunkPosition.X]) == nil then
			chunkData[chunkPosition.X] = nil
		end
	end
end
  • Chunk loading is the process of rendering all the voxels/blocks in a chunk trough Instance.new() or voxel caching (:Clone()) and parenting all of the voxels to a part that act as a chunk (lets call this chunk part)
  • Chunk unloading is the process of destroying the chunk part and its automatically deletes all the children (the voxels) and set RenderedVoxels of that voxel position to nil
  • RenderedVoxels is a list of voxel that are rendered and using its position as a key ("{x} {y} {z}")

The Chunk Rendering Code (sorry…)
IM NOT ASKING TO FIX IT!

task.synchronize()
local ChunkFolder = game.Workspace:WaitForChild("Chunks")
local ChunkManager = require(game.ReplicatedStorage.Managers.ChunkManager)
local ChunkRenderer = require(game.ReplicatedStorage.Managers.ChunkManager.ChunkRenderer)
local VoxelManager = require(game.ReplicatedStorage.Managers.VoxelManager)
local BlockDictionary = require(game.ReplicatedStorage.Managers.BlockDictionary)
local ChunkData = require(game.ReplicatedStorage.LocalChunkData)
local ChunkQueue = {}
local isRendering = false

local hb
function doQueue()
	if not isRendering and #ChunkQueue > 0 then
		isRendering = true
		local chunkToRender = ChunkQueue[1]
		if chunkToRender then
			table.remove(ChunkQueue, 1)
			ChunkAdded(chunkToRender)
			isRendering = false
		end
	end
	if #ChunkQueue == 0 and hb then hb:Disconnect(); hb = nil end
end

function AddChunkQueue(chunk)
	table.insert(ChunkQueue, chunk)
	table.sort(ChunkQueue, function(a, b)
		local character = game.Players.LocalPlayer.Character or game.Players.LocalPlayer.CharacterAdded:Wait()
		local humanoidRootPart: Part = character.HumanoidRootPart
		return (humanoidRootPart.Position - a.Position).Magnitude < (humanoidRootPart.Position - b.Position).Magnitude
	end)
	if #ChunkQueue > 0 and not hb then
		hb = game["Run Service"].Heartbeat:Connect(doQueue)
	end
end

--hb = game["Run Service"].Heartbeat:Connect(doQueue)



function RefreshChunk(chunkPosition: Vector3, allBlocks)
	local VoxelToRemove = {}
	task.desynchronize()
	local chunkData = ChunkManager.GetChunk(ChunkData, chunkPosition)
	if chunkData then
		for x, column in chunkData do
			for y, row in column do
				for z, blockId in row do

					if not allBlocks and x % 15 == 0 or z % 15 == 0 then
						local eligible = true
						for _, direction in ChunkRenderer.Directions do
							local realBlockPosition = Vector3.new((chunkPosition.X * 16) + tonumber(x), tonumber(y), (chunkPosition.Z * 16) + tonumber(z))
							if not ChunkManager.IsExist(ChunkData, realBlockPosition + direction) then
								eligible = false
								break
							end
						end
						if eligible then
							local voxel = VoxelManager.GetVoxel2(ChunkManager.ToBlockPosition(chunkPosition, Vector3.new(x,y,z)))
							table.insert(VoxelToRemove, voxel)
						end
					end
				end
			end
		end
	end
	task.synchronize()
	for _,voxel in VoxelToRemove do
		voxel:Destroy()
	end
	VoxelToRemove = nil
end

function RefreshBlockNeighbor(blockPosition: Vector3, placing)
	for _, direction in ChunkRenderer.Directions do
		local ChunkPosition, positionInChunk = ChunkManager.ToChunkRelative(blockPosition + direction)
		local neighborId = ChunkManager.IsExist(ChunkData, blockPosition + direction)
		local neighborVoxel = VoxelManager.GetVoxel(ChunkPosition, positionInChunk)
		if neighborId and not neighborVoxel and not placing then
			VoxelManager.CreateVoxel(positionInChunk, {ChunkPosition.X, ChunkPosition.Z}, BlockDictionary.GetId(neighborId))
		end
		if neighborVoxel and placing then
			local eligible = true
			for _, direction2 in ChunkRenderer.Directions do
				if not ChunkManager.IsExist(ChunkData, (blockPosition + direction) + direction2) then
					eligible = false
					break
				end
			end
			if eligible then
				neighborVoxel:Destroy()
			end
		end
	end
end

function ChunkAdded(chunk)
	if chunk:IsA("Part") then
		local ChunkPosition = chunk.Name:split(" ")
		ChunkPosition[1] = tonumber(ChunkPosition[1])
		ChunkPosition[2] = tonumber(ChunkPosition[2])
		if ChunkData[ChunkPosition[1]] and ChunkData[ChunkPosition[1]][ChunkPosition[2]] then
			RenderChunk(ChunkData[ChunkPosition[1]][ChunkPosition[2]], ChunkPosition, chunk)
		end
		chunk.ChunkRemote.OnClientEvent:Connect(function(positionInChunk: Vector3, id: number)
			local blockPosition = ChunkManager.ToBlockPosition(Vector3.new(ChunkPosition[1], 0, ChunkPosition[2]), positionInChunk)
			ChunkManager.SetBlock(ChunkData, blockPosition, id)
			if id then -- place
				VoxelManager.CreateVoxel(positionInChunk, ChunkPosition, BlockDictionary.GetId(tonumber(id)))
				RefreshBlockNeighbor(blockPosition, true)
				--print(VoxelManager.RenderedVoxels)
			else -- destroy
				local voxel = VoxelManager.GetVoxel(Vector3.new(ChunkPosition[1], 0, ChunkPosition[2]), positionInChunk)
				if voxel then voxel:Destroy() end
				RefreshBlockNeighbor(blockPosition)
			end
		end)
		for _, chunkDirection in ChunkRenderer.Direction do
			RefreshChunk(Vector3.new(ChunkPosition[1], 0, ChunkPosition[2]) + chunkDirection)
		end
	end
end

function ToNumberChunkData(chunkData)
	local toNumberChunkData = {}
	for x, column in chunkData do
		for y, row in column do
			for z, blockId in row do
				if not toNumberChunkData[tonumber(x)] then
					toNumberChunkData[tonumber(x)] = {}
				end
				if not toNumberChunkData[tonumber(x)][tonumber(y)] then
					toNumberChunkData[tonumber(x)][tonumber(y)] = {}
				end
				toNumberChunkData[tonumber(x)][tonumber(y)][tonumber(z)] = tonumber(blockId)
			end
		end
	end
	return toNumberChunkData
end

function ChunkRemoved(chunk)
	if chunk:IsA("Part") then
		local ChunkPosition = chunk.Name:split(" ")
		ChunkManager.ClearChunk(ChunkData, Vector3.new(tonumber(ChunkPosition[1]), 0, tonumber(ChunkPosition[2])))
		--print("c removed", ChunkData)
	end
end

ChunkFolder.ChildAdded:Connect(function(chunk)
	local ChunkPosition = chunk.Name:split(" ")
	ChunkPosition[1] = tonumber(ChunkPosition[1])
	ChunkPosition[2] = tonumber(ChunkPosition[2])

	local RequestChunkData = game.ReplicatedStorage.Remotes.RequestChunkData:InvokeServer(ChunkPosition[1], ChunkPosition[2])
	RequestChunkData = ToNumberChunkData(RequestChunkData)
	-- assign chunk data
	if not ChunkData[ChunkPosition[1]] then
		ChunkData[ChunkPosition[1]] = {}
	end
	if not ChunkData[ChunkPosition[1]][ChunkPosition[2]] then
		ChunkData[ChunkPosition[1]][ChunkPosition[2]] = {}
	end
	ChunkData[ChunkPosition[1]][ChunkPosition[2]] = RequestChunkData
	AddChunkQueue(chunk)
end)

ChunkFolder.ChildRemoved:Connect(ChunkRemoved)

function RenderChunk(chunkData, chunkPosition, chunkPart: Part)
	local count = 0
	local ChaceDictionaryBlockId = {}
	local ChaceVoxel = {}
	task.desynchronize()
	local chunkDataToRender = ChunkRenderer.RemoveHiddenBlocks(ChunkData, chunkPosition)
	task.synchronize()
	for x, column in chunkDataToRender do
		for y, row in column do
			for z, blockId in row do
				local blockIdDict = ChaceDictionaryBlockId[blockId]
				if not blockIdDict then
					blockIdDict = BlockDictionary.GetId(tonumber(blockId))
					ChaceDictionaryBlockId[blockId] = blockIdDict
				end
				local voxel: Part
				if ChaceVoxel[blockIdDict] then
					voxel = ChaceVoxel[blockIdDict]:Clone()
					local blockPositionReal = Vector3.new((chunkPosition[1] * 16) + x, y, (chunkPosition[2] * 16) + z)
					voxel.Name = `{x} {y} {z}`
					
					VoxelManager.RenderedVoxels[`{blockPositionReal.X} {blockPositionReal.Y} {blockPositionReal.Z}`] = voxel
					voxel:GetPropertyChangedSignal("Parent"):Connect(function()
						if voxel.Parent == nil then
							VoxelManager.RenderedVoxels[`{blockPositionReal.X} {blockPositionReal.Y} {blockPositionReal.Z}`] = nil
						end 
					end)
					voxel.Position = blockPositionReal * 3
				else
					voxel = VoxelManager.CreateVoxel(Vector3.new(x,y,z), chunkPosition, blockIdDict, true)
					ChaceVoxel[blockIdDict] = voxel:Clone()
				end
				
				if voxel and chunkPart then voxel.Parent = chunkPart end
				count += 1
				if count == 50 then task.wait() count = 0 end
			end
		end
	end
end

game.ReplicatedStorage.Bindables.GetBlock.OnInvoke = function(blockPositon)
	return ChunkManager.IsExist(ChunkData, blockPositon)
end

Im sorry for providing a whole messy code :pray:
But im only asking in guidance to spot where is the memory leak actually came from?

I have tried to load and unload chunks until the client memory usage is 4gb
Help, answer and guidance to this problem is appreciated :pray:

1 Like

Have you tried using the memory profiler in Studio? It might help you point out what exactly is causing the rise in memory.

I have not, How do you open memory profiler?

If you press f9 while playing you’ll see the developer console pop up. From there you can press the dropdown in the top left and click “Memory”

I’ve not used it much myself so I can’t really say what you should look for, but hopefully it helps identify the issue.


How would i know which part of the script that cause the memory leak from this?

1 Like

seems like render/lightgrid uses lots of memory.

nevermind its always like that

check out the new LuauHeap tool, it’s at the bottom of the dev console dropdown and it’s far more useful than the memory tab for debugging memory issues

1 Like

Thanks for the reply guys, i have tried to open the LuauHeap and this is what it shows


the “Part” class (the voxel) seems to be stuck in the memory, and this is likely the reason of the memory leak which is the voxels, and the script i provided is the root of all this memory leak problem, but i dont know which part and line causing it, also how could i see the refrence of those part? so that its eligible to be garbage collected

under unique references you can see where instances that aren’t parented are being referenced:

from what i know about luau’s gc, a variable is only garbage collected if there’s no active references to it, unless in roblox’s case it’s an instance in which it also has to be destroyed otherwise it doesn’t get gc’d
i’m not really sure about how metatables work with gc but they haven’t really been an issue for me, i really need to look into it more


I can’t seem to find the unparented part that are being refrence

I know this isn’t about fixing the memory leak, but I don’t recommend using a signal on voxels, since theres so many of them. Assuming your trying to detect when they are destroyed, why not trigger the function in the event manually in whatever code that destroys a block. Doing this would probably fix your memory leak.

I have tried to do that, but the results is the same, the memory leaks still happend before the “signal on voxel” even after the “signal on the voxel”

i’ve just noticed that connections to parts that don’t get stored in a variable aren’t shown in unique references, so if connections not stored in a variable aren’t shown, maybe references to parts are being set to nil but not destroyed

ChaceVoxel doesn’t seem to be getting cleared perhaps . ?

Destroying the part does fix this issue!
Thank you so much for helping guys! (@Uhsleep91, @NotCasry, @nothing_1649, @savio14562, @PuffoThePufferfish)

1 Like

ChaceVoxel should be automatically garbage collected when the RenderChunk function is completed.

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.