Regardless of yield, BindToRenderStepped creates a new coroutine everytime. Why?

edit: clarified stuffs

Welcome, I’d like to know why this seemingly unorthodox behavior is the case with this function, because of this it seems much less performant of a method to use; compared to connections which have a handy method of checking the yielding and then deciding whether to create a new coroutine or not.

I have tried using yielding, or not yielding, but regardless of whether or not i yield BindToRenderStepped will always print a new wrapped coroutine in the callback.
(This happens irrespective of priority, if you’re curious about that)

Repro: Create a local script that prints coroutine.running() in a bind to render stepped.

game:GetService("RunService"):BindToRenderStep("p", 30, function()
	print(coroutine.running())
end)

Here is an image of this happening: Imgur: The magic of the Internet

Also, I believe i’ve found an oddity in bindtorender’s documentation as well. According to the dev hub page for BindToRenderStepped, I quote:

Note: All rendering updates will wait until the code in the render step finishes. Make sure that any code called by BindToRenderStep runs quickly and efficiently. If code in BindToRenderStep takes too long, then the game visuals will be choppy.

This does not seem to be the case, if all callbacks put into a bindtorendertable were called in a generic for, then this would surely be the case as any function having a yield would slow down the updates from other functions. But creating a new coroutine for the callback to run on each time like it’s doing now will allow updates irrespective of whether you yield or not, even at the cost of more resource usage.

Which is why i’m curious on why BindToRender does not use the same solution as connected events? (Fire a connection once with just a print or something, it will lay only on it’s initially created thread. Call it again, but have the callback yield this time. And a new coroutine will be created)

2 Likes

For some reason I think I never trusted the BindToRender system. It does seem to work differently for some reason. I usually end up doing something similar to this as a RunService solution, maybe this is a better alternative?

--Services
local RunService = game:GetService("RunService")

--Data
local module = {}
local Connections = {}

--API
function module:Unbind(name)
	
	--Loop Clear
	if not name then
		for current, _  in pairs(Connections) do
			module:Unbind(current)
		end
		return
	end

	--Single
	local con = Connections[name]
	if con then
		con:Disconnect()
		con = nil
	end
end

function module:Bind(name,callback)
	module:Unbind(name)
	Connections[name] = RunService.RenderStepped:Connect(function(dt)
		local step = dt * 60 --Just an example, usually this will be scaled properly
		callback(step)
	end)
end

function module:Get(name)
	if name then
		return Connections[name]
	else
		return Connections
	end
end


return module
2 Likes

I believe i have a much simpler alternative, where i have a single render stepped connection that ipairs through a table of callbacks, and said table is kept in order via table.insert + remove

Looping through callbacks sounds horrifying to me not gunna lie, but I’m sure that works perfectly fine regardless lol.

1 Like

Having a new connection for each one is worse, it’s a new coroutine for every connection you make, and another connection that must have it’s :Fire method invoked every frame, compared to just a simple function call. (Also worth nothing is that internally whether the callback yields or not is checked in order to determine whether a new coroutine should be created) What I’m doing seems kinda sparing compared to that.

That makes sense, but is having a bunch of coroutines really that detrimental to performance? Maybe a little more memory is taken up.

Well I don’t know why it would be doing that, but this is one of those magic black box situations. In other words, the implementation of how it all works is kinda not important for us. We just need to trust that Roblox has optimized that to work as intended. Of course, if it’s not working as intended, then that would be a bug.

I don’t know enough about Roblox’s internal rendering pipeline to justify saying that spawning a new thread each frame is bad.

2 Likes

Nah not detrimental in any right, but less is better when it comes to frame by frame

It’s still the same workload per frame but since it’s segmented into different coroutines I wonder if that’s actually handled any differently. Maybe it even contributes to the workload somewhat.

1 Like

Honestly it seems kind of hard to trust roblox in this case, I mean that’s another coroutine that needs to be gced, another place your CPU needs to switch to compared to the alternative. And this seems completely contradictory to what’s mentioned on the documentation for it if we’re using the logic of a standard implementation.

But on that note, could be right about the magic black box.

@Barothoth

It’s still the same workload per frame but since it’s segmented into different coroutines I wonder if that’s actually handled any differently. Maybe it even contributes to the workload somewhat.

Not particularly, connections aren’t only just functions, they’re quite a bit more than that, which is why it is more workload. Also seperate coroutines would for sure improve the execution time (worth noting that’s irrelevant when it comes to how fast ipairs is now), however it would not positively contribute to resource usage in any manner.

1 Like

I wonder how this would compare to while RunService.Heartbeat:Wait() do (not that I would ever really use that)