Problems with Actors and Parallel Luau

I have been encountering some performance problems with the project I am currently working on, thus I am trying to implement multithreading.

The problem I am having is that my threads are not running in parallel. No matter what, the “Render”-Actors keep running in series on one of the Worker threads.

Image of the microprofiler:

My Actors:
image

Mind showing us some of your code in the actors? It looks like the threads are not actually scheduled for parallel execution. Have you used task.desynchronize() or Actor:BindToMessageParallel()?

Actor:BindToMessage() still runs a thread in serial unless you desync it manually, which Actor:BindToMessageParallel() does, but slightly quicker.

The entry for used in every script is:

script:GetActor():BindToMessageParallel("Render", function(boundVector, minX, maxX, doDetailed, doAntiAliasing, doReflectionDepth)
	local endX = boundVector.endX
	local endY = boundVector.endY
	local startX = boundVector.startX
	local startY = boundVector.startY

	_reflectionDepth = doReflectionDepth
	_detailed = doDetailed

	debug.profilebegin("RenderThread1")
	local renderResult = renderThread(minX, maxX, endX, endY, startX, startY)

	if (doAntiAliasing) then
		antiAliasing(renderResult)
	end
	
	debug.profileend()
	script:GetActor():SendMessage("DisplayRender", renderResult)
end)

DisplayRender is used to draw the render result to the EditableImage using another script within the same actor. That script has binds to the actor using :BindToMessage() because editable images are not safe to draw to in parallel.

When I let the ImageHandler (the script which draws to the image and exists in every actor) run using :BindToMessageParallel() it seems to run in parallel, but as I mentioned, it’s unsafe to draw in parallel.

For the sake of convenience I’ll call the scripts ImageRender and ImageHandler. In the screenshot you sent it looks like ImageRenders are still running serially. I suspect your render scripts might contain a yielding function like task.wait().

I’ve played with multithreading a bit. In all the cases below, the receiving script (under the same actor) of the message in “Serial” topic executes in serial the exact same loop as the one after task.synchronize().

Code
script:GetActor():BindToMessageParallel("Parallel", function()
	for i=1, 1000000 do local n = i*i end
	--script.Parent:SendMessage("Serial")
	task.synchronize()
	for i=1, 500000 do local n = i*i end
end)

Works exactly as anticipated. In the second run I executed the part after desync in another script of the same actor, which worked similarly, but what bothered me was how the phase was executed before physics simulation in the next frame, prolonging RenderJob.

Profiler

Desync in the same script:

Resumption in serial from messaging signal:
image
*Couldn’t capture all workers in a single image.

In the release of Parallel Luau V2 it was announced that “task.defer(), task.delay(), and task.wait() now resume in the same context (serial or parallel) that they were called in”.

One of the replies shows how an infinite loop with a task.wait() scheduled in parallel is resumed by the main worker, to which a Roblox dev responded that Parallel Luau is intended to run per-frame tasks in parallel with each other and not to run tasks that last over multiple frames, running in parallel with the engine.

Now I think we should distinguish between two cases of yielding with task.wait():

Code 1
script:GetActor():BindToMessageParallel("Parallel", function()
	for i=1, 1000000 do local n = i*i end
	task.wait()
	script.Parent:SendMessage("Serial")
	--task.synchronize()
	--for i=1, 500000 do local n = i*i end
end)
Profiler 1

image
*Couldn’t capture all workers in a single image.


Code 2
script:GetActor():BindToMessageParallel("Parallel", function()
	for i=1, 1000000 do
		local n = i*i
		if i==500000 then task.wait() end
	end
	script.Parent:SendMessage("Serial")
	--task.synchronize()
	--for i=1, 500000 do local n = i*i end
end)
Profiler


*Couldn’t capture all workers in a single image.

In the first case the threads evidently run in parallel on different workers as opposed to the second case, where the threads are resumed one after another - causing a lot more noticable lag. Not entirely sure why that is so, but it seems the second case mirrors your situation.

@MrChickenRocket apologies for the notification, I pinged you because you probably have more info than me. :slight_smile:

2 Likes

Thank you for your response.

You’re right, I use task.wait() in the ImageRender I removed it and now it seems to run in parallel, thank you very much.

Second problem that arises is that the ImageHandler in running in series to the Main/Render thread and not to the ImageRender thread.

here is the code for the ImageHandler:

local _imageHandler: EditableImage = nil
local _sizeX: number = 0
local _sizeY: number = 0

-- "SetUp" is send from the main-script (in Main/Render thread)
script:GetActor():BindToMessage("SetUp", function(ImageHandler, sizeX, sizeY)
	_imageHandler = ImageHandler
	_sizeX = sizeX
	_sizeY = sizeY
end)

-- "DisplayRender" is send from the ImageRender in the same actor as this script is in
-- "DisplayRender" cannot run in parallel because it's unsafe to call the :WritePixels() method
script:GetActor():BindToMessage("DisplayRender", function(renderResult)
	debug.profilebegin("ImageHandler0")
	
	-- writing the result to the editable image
	_imageHandler:WritePixels(Vector2.new(0,0), Vector2.new(_sizeX,_sizeY), renderResult)
	
	debug.profileend()
end)

This is how it look like on the microprofiler (was not able to capture all in one picture):

And here is how I expect it to look like:

Note that the ImageRender-script and ImageHandler-script are both child of the same actor.
The ImageRender script sends the “DisplayRender” Message to the parent actor and the ImageHandler script connects to the parent actor in series to the given Message.

I’m glad the parallel run works now.

Regarding the second issue, isn’t this how it is supposed to run? I presume the second screenshot was taken when DisplayRender was connected in parallel. Functions in serial should resume together with all other normal scripts. In your end it happens on the main thread, on mine on one of the workers.

I got that problem solved too.

I wanted each thread to render and draw on their own without having anything having to continue on the Main-Thread.

Like this:

Worker0: ImageRender -> ImageHandler

Not like this:

Worker0: ImageRender -------v
Main/Render:           ImageHandler

Note that ImageRender and ImageHandler are on the same thread in the first example.
This made my ray tracer render around 2 seconds faster. However there is a lot more to do for me at this point.

Thank you for your help, I really appreciate it!

This image rendered in 1.34 seconds instead of more than 10 thanks to your help:

1 Like

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