OSGL - EditableImage graphics library

Wait, slight correction. The last code I tested (with --optimize 2) had Vector3s being constructed for every operation, apologies. Here are the tests “fixed”:

Testing for: two numbers
Took a total of 0.8814006745815277ms for 10000000 iterations.
With an average of 8.814006745815277e-08 ms/iteration.

Testing for: Vector3
Took a total of 1.2641102833440527ms for 10000000 iterations.
With an average of 1.2641102833440527e-07 ms/iteration.

And in-game:

Testing for: two numbers
Took a total of 0.8169687063200399ms for 10000000 iterations.
With an average of 8.1696870632004e-08 ms/iteration.

Testing for: Vector3
Took a total of 1.1896188297541812ms for 10000000 iterations.
With an average of 1.1896188297541813e-07 ms/iteration.

Slower, but closer, though.

We quite literally do and that’s how I know how they work, Luau is open source and vector construction can be found in there too.

Can you send the benchmark code by the way? Just wondering since your results aren’t anywhere close to mine.

No? Vector3 is a part of Roblox, not luau. If you try running Vector3.new using the luau exe, it would error.

I would like to see yours, too.

Code (to avoid bloating the message)
--!optimize 2
local ITERATIONS = 10_000_000

type Fn = () -> ()

local function timeIteration(fn: Fn)
    local start = os.clock()
    fn()
    local finish = os.clock()
    
    return finish - start    
end

local function timeFunction(name: string, fn: Fn)
    local totalTime = 0
    
    for i = 1, ITERATIONS do
        totalTime += timeIteration(fn)
    end
    
    print(`Testing for: {name}`)
    print(`Took a total of {totalTime}ms for {ITERATIONS} iterations.`)
    print(`With an average of {totalTime / ITERATIONS} ms/iteration.\n`)
end

timeFunction("two numbers", function()
    local x = 1
    local y = 1

    x += 10
    y += 10

    x -= 10
    y -= 10

    x *= 10
    y *= 10

    x /= 10
    y /= 10
end)

local position = Vector3.new(1, 1)
local offset = Vector3.new(10, 10)

timeFunction("Vector3", function()
    position += offset
    position -= offset
    position *= offset
    position /= offset
end)

I’m not sure why you’re assuming that but I can help you understand how it actually works.

As you might know a few years ago there was an announcement about how Vector3 was added as a built-in type. This specifically mentioned that it was already added to Luau for a while, but Roblox just hasn’t been using it with Vector3 yet:

Internally, our VM has supported a built-in ‘vector’ value type for some time now with built-in vector operations, but the Roblox Vector3 userdata class didn’t use that machinery.

Luau currently does not allow you to create this internal data type as it does not offer a standard library yet for constructing this data type. Though internal functions for doing so do exist, as you can find in the language’s source code

This constructs an object without any metatables directly attached. Roblox attaches a metatable to all vectors initially by doing a single internal setmetatable call on a vector all the way at the start, which causes the global metatable for all vector types to be set to whatever metatable Roblox passes. Because this metatable is global, no other setmetatable calls are required to add the behavior required to make vectors work like they do in Roblox. That’s just because only tables and userdata can have their own metatable, all other data types share a single global metatable.

Now to explain why your benchmark code is giving different results, I’ll just give you my benchmark so you can test it for yourself. I rewrote it quickly with some comments to explain certain decisions since I’m not home right now.

my benchmark code
--!optimize 2
local ITERATIONS = 1e8

local function testNumber()

	-- define the numbers in such a way that luau considers them "variable" instead of constant
	-- this would normally be optimized which isn't realistic for a real world scenario
	local x, y; x, y = 0, 0
	local ox, oy; ox, oy = 1, 1
	for i = 1, ITERATIONS do
		x = x + ox
		y = y + oy
		x = x - ox
		y = y - oy
		x = x * ox
		y = y * oy
		x = x / ox
		y = y / oy
	end
	return x, y
end

local function testVector()
	-- because 
	local vec; vec = Vector3.new(0, 0)
	local vecO; vecO = Vector3.new(1, 1)
	for i = 1, ITERATIONS do
		vec = vec + vecO
		vec = vec - vecO
		vec = vec * vecO
		vec = vec / vecO
	end
	return vec
end

local function bench(name, func)
	local start = os.clock()
	func()
	local time = os.clock() - start
	print("benchmarking " .. name)
	print(string.format("\tresult: %-.2f nanoseconds", time / ITERATIONS * 1_000_000_000))
end

bench("number", testNumber)
bench("vector", testVector)

These are my results:
image

I wouldn’t argue about the VM and actual Luau code, since I personally am not a C++ person. But you mentioned that Luau doesn’t allow me to create that type:

So basically it’s not there?


Looking at your testing code, the implementation is different, but the core code seems to be exactly the same? :thinking:
How are the results different? I tried editing my code to match yours yet the results stayed the same; Vector3s were slower.

There is no library for it that you can directly access, but there are internal functions that can be used to create this built-in vector types. That’s just how it is but this might change soon.

The difference is that the benchmark uses a single big for loop instead of functions, which have a function call overhead each time you iterate. This overhead makes it harder to see the difference between the two and might cause the benchmark ran after to run slower because of reasons I wouldn’t be able to explain properly. Also doing it this way allows us to find out how long the benchmark took in nanoseconds a lot more accurately, since os.clock is otherwise not precise enough to measure the time difference. Your benchmark made numbers and vector operations take somewhere around the 50 to 100 nanoseconds, which isn’t actually the time it takes to execute these operations in practice, unless you had a function which only did this operation and nothing else.

I’m aware of function overheads, but the overhead is found for both of them, which equals out, but either way, I also checked the function overhead when trying to match your code, results:

Testing for: two numbers
Took a total of 0.29839400004129857ms for 10000000 iterations.
With an average of 29.839400004129857 ns/iteration.

Testing for: Vector3
Took a total of 0.7638640000950545ms for 10000000 iterations.
With an average of 76.38640000950545 ns/iteration.
Code
--!optimize 2
local ITERATIONS = 10_000_000

type Fn = () -> ()

local function timeIteration(fn: Fn)
    local start = os.clock()
    fn()
    local finish = os.clock()
    
    return finish - start    
end

local function timeFunction(name: string, fn: Fn)
    local totalTime = 0
    
    --for i = 1, ITERATIONS do
        totalTime += timeIteration(fn)
    --end
    
    print(`Testing for: {name}`)
    print(`Took a total of {totalTime}ms for {ITERATIONS} iterations.`)
    print(`With an average of {totalTime / ITERATIONS * 1_000_000_000} ns/iteration.\n`)
end

timeFunction("two numbers", function()
    local x = 1
    local y = 1
    
    for i = 1, ITERATIONS do
        x += 10
        y += 10

        x -= 10
        y -= 10

        x *= 10
        y *= 10

        x /= 10
        y /= 10
    end
end)

local position = Vector3.new(1, 1)
local offset = Vector3.new(10, 10)

timeFunction("Vector3", function()
    for i = 1, ITERATIONS do
        position += offset
        position -= offset
        position *= offset
        position /= offset
    end
end)

I heavily recommend both parties to use Boatbomber’s Benchmarking plugin when measuring performance on such an infinitesimal scale. Here are the results from the benchmarker:

Code used to generate this
--!optimize 2
return {
	ParameterGenerator = function()
		return function() end
	end,

	Functions = {
		["Two numbers"] = function(Profiler)
			local x = 1
			local y = 1

			for i = 1, 100_000 do
				x += 10
				y += 10

				x -= 10
				y -= 10

				x *= 10
				y *= 10

				x /= 10
				y /= 10
			end
		end,

		["Vector3"] = function(Profiler)
			local position = Vector3.new(1, 1)
			local offset = Vector3.new(10, 10)

			for i = 1, 100_000 do
				position += offset
				position -= offset
				position *= offset
				position /= offset
			end
		end,
	},
}
1 Like

Turn this into:

timeFunction("Vector3", function()
	local position = Vector3.new(1, 1)
	local offset = Vector3.new(10, 10)
	
	for i = 1, ITERATIONS do
		position += offset
		position -= offset
		position *= offset
		position /= offset
	end
end)

It’s slower because you defined the vectors as upvalues which make them a lot slower to access.

1 Like

Ah. This final edit indeed makes it faster. Pretty interesting. Tbf I’m not sure which of those to count as a more real-world situation, as the performance, as we saw, heavily depends on how you define and use the vectors. Now for the final and arguably a very important issue: API. Using Vector3 will be a bit weird, don’t you think? If there’s a good way around it, we’ll finally do the switch to it. I tried with Vector2s and they seem waaaaaaaaaay slower, so they’re out of the question,

You can always consider internally using Vector3, and still allowing the user to pass regular numbers instead. You should just test things out and see what ends up being most performant. It might not work well everywhere but the ones that can make use of it could probably get a pretty good performance boost out of it.

I’ll discuss it with Saw. Thanks for the helpful information!


Thanks, @PysephDEV. The idea was originally yours!

2 Likes

Version 1.3b is out!

This update contains an internal rewrite of the library, allowing more modularity. Also switch to using the brand new buffer as a pixel storage, yielding MUCH better performance (200%)!

Check it all out at the github releases page.

Changelog:

  • Rewrite library internals for more modularity
  • Add config file
  • Add buffer as the new default pixel storage
  • Massive performance boost! (200% on normal rendering!)
  • More extended API
    • More functions for handling colors in the color module!
      • Add newRGB - to be used when a component isn’t needed, provides a small performance boost.
      • Add setR, setG, setB, and setA
      • Add tint
    • Window
      • Add Tint to DrawableObject
      • Add TintRegion to DrawableObject
      • Add FloodFill to DrawableObject
  • Updated documentation
  • Updated image-converter file to allow drag and drop into it - used for OSGL images.
  • Support MacOs for the image-converter (both Intel-based and Apple Silicon based)!
2 Likes

Minor Update, V1.31b

This minor update includes 2 bug fixes that didn’t let rectangles render properly. The issue can be found on the github here.

More updates & enhancements coming soon :eyes:

Is anyone getting an issue for when you use the module and getting the error:

"Unable to create an Instance of type “EditableImage”

Just a few things to note:

  • I’m testing this in-studio.
  • I’m using an example script which was shown to work.
  • EditableImage and EditableMesh are both enabled in beta features.

You have to create EditableImages a different way now. (The library hasn’t been updated)


local AssetService = game:GetService("AssetService")
local myEditableImage = AssetService:CreateEditableImage({ Size = Vector2.new(32, 32) })

Thanks! I appreciate the help a lot! :grinning:

more specially this snippet from osgl-graphics/src/DrawableObject/window.luau needs to be updatd

i’ll make a pull request

1 Like

Hi, I’ve reviewed and merged your changes! Thanks for making the pull request! :tada: