Render waits for Actors to complete their work

Hello, it’s my first time using actors, and I’m creating a multithreaded client-sided terrain generation system.

How would I make it such that the work I’m trying to compute doesn’t interfere with the render step? Currently it produces lag spikes as shown:

I have important parts of my scripts attached below, I’m not including the entire scripts for simplicity’s sake, however I can create a pastebin if needed.

Actor Local Script:

actor:BindToMessageParallel("GenerateChunk", function (id: Vector3int16)
	-- TODO: skip if you can guarantee it will be completely empty/not visible
	
	local bounds = TGService:GetChunkBounds(id)
	local region = TGService:BoundsToRegion(bounds)

	local materials = CreateVoxelArray(region, Enum.Material.Air) :: {{{ Enum.Material }}}
	local occupancy = CreateVoxelArray(region, 0) :: {{{ number }}}

	local chunkVoxelWidth = #occupancy
	-- xv is the voxel index of the chunk in the x direction
	for xv = 1, chunkVoxelWidth do
		for zv = 1, chunkVoxelWidth do
			local x = bounds.Min.X + xv * 4 -- chunk voxel to world position
			local z = bounds.Min.Z + zv * 4
			local terrainHeight = math.noise(
				x * HorizontalScale, 
				z * HorizontalScale, 
				0
			) 
				* VerticalScale
			
			if terrainHeight < bounds.Min.Y then continue end

			for yv = 1, chunkVoxelWidth do
				local y = bounds.Min.Y + yv * 4
				if y < terrainHeight then
					occupancy[xv][yv][zv] = math.min((terrainHeight - y) / 4, 1)
					materials[xv][yv][zv] = Enum.Material.Sand
				end
			end
		end
	end

	task.synchronize()
	Terrain:WriteVoxels(region, 4, materials, occupancy)
end)

Terrain Generation Service Script:

function TGService.Start(self: TerrainGenerationService)
	for i = 1, 64 do
		local worker = script.TerrainWorker:Clone()
		worker.Parent = ReplicatedFirst
		table.insert(self.Workers, worker)
		worker.Name ..= #self.Workers
	end
	
	RS.Heartbeat:Connect(function()
		local renderedChunks = {}
		
		local visibleChunks = self:GetVisibleChunks()
		if not visibleChunks then return end
		
		local chunksToGenerate = {}
		for _, chunk in visibleChunks do
			if table.find(self.RenderedChunks, chunk) then
				table.insert(renderedChunks, chunk)
				table.remove(self.RenderedChunks, table.find(self.RenderedChunks, chunk))
				continue
			end
			
			table.insert(chunksToGenerate, chunk)
			table.insert(renderedChunks, chunk)
		end
		
		self:GenerateChunks(chunksToGenerate)
		self:ClearChunks(self.RenderedChunks)
		
		self.RenderedChunks = renderedChunks
	end)
end


function TGService.GenerateChunks(self: TerrainGenerationService, ids: { Vector3int16 })
	for _, id in ids do
		local worker = self:GetNextWorker()
		worker:SendMessage("GenerateChunk", id)
	end
end


function TGService.GetNextWorker(self: TerrainGenerationService)
	local worker = self.Workers[self.NextWorkerIdx]
	self.NextWorkerIdx = (self.NextWorkerIdx) % #self.Workers + 1
	return worker
end

I’ve asked myself the same question before and i don’t think it’s possible to do in roblox. spreading the workload over more frames to flatten the lag spike is the best i can come up with.

What do you think would be the best way to do this? Because theres always the issue of assigning too much work and coming back to the same issue, or assigning too little work and chunks are loaded too slowly

I’ve just created this small script. It does the loop indefinitly until the loop took longer than 1/60 or 0.016 ms to calculate. Then it breaks out of the loop to render the frame and starts calculating again. I have not used this in an actual game though so i’m not sure if this has any flaws or not. I hope this helps

local targetFPS = 1/60

while true do
	
	local start = os.clock()
	i = 0
	
	debug.profilebegin("Calculate")
	while true do
		
		i += 1 -- do whatever calculation you want here
		
		local currentTime = os.clock() - start
		
		if currentTime >= targetFPS then
			break
		end
	end
	debug.profileend()

	print(`{i} calculations done`)
	task.wait()
end

I’m not familiar with this kind of code, but what if you load smaller chunks of terrain more often and a farther distance from the camera?
Are you removing chunks of terrain at the same time or just removing them from a table? How about staggering that so you aren’t generating and clearing all the chunks at the same time.