Optimization/Challenge: Realtime Accurate Blackhole Simulation

Currently

  1. you have to wait a while before it runs
  2. it takes like 30 seconds for even a mini simulation. :sob:
  3. It’s kind of slow even with parallel part done.
    I challenge you to make this run in realtime, maybe 60fps.
    BlackholeRendererV2.rbxl (57.1 KB)
    Good luck, I might chip in if I have any more ideas.
1 Like

I can’t see anything. No parts created.

yes, look in the output, and wait for it to say “Complete!” It’ll take a bit
Once it is done, you’ll see a decently quick blackhole updating with your camera rotation.
However, it only cares about your rotation and not your position.

Oh alright thanks. Let’s see if my 5070ti can handle it :pensive_face:

It’s mostly the CPU working because Roblox simply doesn’t and won’t allow GPU computation
IT would be pretty nice to have especially if players are running on an insanely beefy device.
Then if the developer wants to have it, they could run games in extreme speeds, but yea… Roblox isn’t doing all that.

As if it’s a lot of work for them to implement… :disappointed_face:

Unfortunate. My cpu isn’t that bad though. I have a i7 13700k. I’ll lyk the results in a bit.

can you make a client-side version? I want to test/run it on roblox since it usually works better for me

image


Running it at 200 fps, not bad. 50fps when re-rendering (moving camera)

eh… it wouldn’t be hard, but I’m lowkey busy doing other stuff rn

Im working on a slightly similar problem, instead it is particle physics in roblox. I actually ended up using a module called ParallelScheduler and it is extremely nice for creating VMThreads. Search it up

Can you explain what are you trying to achive with this exactly?

Correction: Roblox does use GPU for client sided things. All Rendering is done client sided.

While yes, roblox DOES use the gpu for rendering, it doesn’t allow the DEVELOPER to utilize the GPU in this manner, unfortunately. :confused:

more optimized blackhole while still looking slightly detailed cuz it’s rly slow.

it’s already running in parallel :sob:
So that is unfortunately, not utilizable here.

I have found a solution to some of this. You need to find a way to parallel your threads with your MAIN thread. So nothing is running after the main thread. Use your micro debugger to ensure nothing like this is occurring

Notice how the heartbeat stage is processing work, then after that the runParallel area starts. What you want is for the runParallel to be paired alongside it. So my recommendation is to have multiple instances of your script in different actors which control only a qaudrant of the work, therefore its split eqaully and all runs at the same time

It’s also important that you limit interaction with the main thread, therefore periodically batch everything and send it via a BindableEvent to the main thread (still in the actor, just not running in desync) and update whatever. This way its truly parallel

You do not want it to look like this.


This is not parallel with the main thread even though it looks like it. The main thread is still having to process work in the heartbeat area (offscreen) then the multi-threading begins, which is NOT GOOD because its still waiting on the main thread to start it.

I found out that those Parallel modules are not very smart to use; use a simple one like this. Currently I’m using this for my mass projectile solver, which is doing around 30k projectiles realtime

-- Inspired by : https://github.com/Bue-von-hon/Sagitta/blob/main/src/Dispatcher/init.lua
local RS = game:GetService("RunService")
local isServer = RS:IsServer()

export type TPool = typeof(setmetatable({}, {})) & {
	_container : Folder?,
	_init : boolean,
	Threads : {Actor}?,
	init : () -> (),
	new : (number, ModuleScript, (...any) -> (...any)) -> (TPool),
	Dispatch : (TPool, {[any] : any}) -> ()
}

--ProjectileThreadPool
local PThreadPool = {} :: TPool
PThreadPool._container = nil
PThreadPool._init = false

function PThreadPool.init() : TPool
	if PThreadPool._init then warn "Already called init..."; return PThreadPool end
	PThreadPool._container = Instance.new "Folder"
	PThreadPool._container.Name = "Threads"
	PThreadPool._container.Parent = isServer and game.ServerScriptService or game.ReplicatedFirst
	
	return PThreadPool
end

function PThreadPool.new(threads: number, module: ModuleScript, callback: (...any) -> (...any)) : TPool
	local Actors = {}
	
	for i=1, threads do
		local Template = script.Actor:Clone()
		local Context = (isServer and Template.Script or Template.LocalScript)
		local _ = (isServer and Template.LocalScript or Template.Script):Destroy()
		Template.Name = "Thread" .. i
		Template.Output.Event:Connect(callback)
		Template:SetAttribute("task", 0)
		
		Context.Parent = Template
		Template.Parent = PThreadPool._container
		
		Context.Enabled = true
		
		Actors[i] = Template
		task.delay(0, function()
			Template:SendMessage("start", module)
		end)
	end

	RS.PostSimulation:Wait()

	return setmetatable({Threads = Actors}, {__index = PThreadPool})
end


function PThreadPool:Dispatch(info: {[any] : any})
	table.sort(self.Threads, function(a, b)
		return a:GetAttribute("task") < b:GetAttribute("task")
	end)
	local Thread = self.Threads[1] :: Actor
	Thread:SendMessage("dispatch", info)
end

return PThreadPool

Your sorting logic is also very important, you’ll want to spread the load evenly. This one uses a task based one,

hm.
Is there any difference between task.delay(0,function() and task.defer/task.spawn?
Also, does this work because of RS.PostSimulation:Wait()?
How does it run separately from the mainthread? Like what’s the magic?

RS.PostSimulation and task.delay basically allows the actors to be initiated properly. Also theres no magic behind this, it simply spawns 8 actor instances or whatever is desired, however 8 is generally the max threads you’ll get(tested it myself). Well this small module allows you to parallel a module using actors. Theres a bit more that goes to it, you have to make a module with a format like so

First a script or localscript is spawned with the following:

local Actor = script.Parent :: Actor

Actor:BindToMessage("start", function(module: ModuleScript)
	require(module).init(script.Parent.Output, script.Parent)
end)

next the module (example),

local module = {}
module.event = nil :: BindableEvent?
module.actor = nil :: Actor?

module.projectiles = {}
module.removed_ids = {}

function module._getAvailableIndex()
	if module.removed_ids[1] then
		local index = module.removed_ids[1]
		table.remove(module.removed_ids, 1)
		return index
	else
		return #module.projectiles + 1
	end
end

function module.beginDispatch(info)
	module.actor:SetAttribute("task", module.actor:GetAttribute("task") + 1)
	print("dispatch: " .. info["id"] .. " to " .. module.actor.Name)
end

function module.completeDispatch(info)
	module.actor:SetAttribute("task", module.actor:GetAttribute("task") - 1)
end

function module.workDispatch(info)
	local index = module._getAvailableIndex()
	module.projectiles[index] = info
end

function module.onDispatch(info)
	module.beginDispatch(info)
	module.workDispatch(info)
	module.completeDispatch(info)
end

function module.init(output_event: BindableEvent, actor: Actor)
	module.event = output_event
	module.actor = actor
	
	actor:BindToMessage("dispatch", module.onDispatch)
	
	warn(actor.Name .. " has started...")
	output_event:Fire("Help me")
end

return module

Essentially this allows you to compute everything in parallel. No magic, just roblox!

Heres a small projectile simulation I made of 1,000,000 projectiles. (No instance manipulation though, I’ll add that after. After instance manipulation I was able to simulate around 180,000 projectiles with 180,000 instances too.

External Media

Just a interesting video to see the performance.

In the live client, it is actually limited to 3 threads on PC by default (DFIntRuntimeConcurrency), and on the server it is even more limited.
However that doesn’t mean you should have exactly the same amount of actors as available CPU cores, as having more allows for better load balancing between the cores.

https://create.roblox.com/docs/scripting/multithreading#best-practices