Introducing Luau buffer type [Beta]

What use do you have for writing arbitrarily sized integers that aren’t better served with e.g. native i24/u24 support or native bit-level support?

I am genuinely curious because I’ve never ran into a need for an integer whose width wasn’t a multiple of 8.

1 Like

This issue has been fixed. (some characters)

4 Likes

This is GREAT
I saw it on the list today in my studio. This is going to work wonders for sending positional data with mixed precision if that is to become possible.
I do REALLY agree with the others here, we need more control over the buffers. In unity for example we have complete control over what we put in there. This is the best part about it, please don’t leave it out!

This is an amazing feature, especially for emulation or WebAssembly.

Also, it does account for the system’s memory limits.

--!native
local buffers = {}

for i = 1, 100 do
	table.insert(buffers, buffer.create(1073741824))
	task.wait(0.5)
end

Edit: In production, this will crash the server.

2 Likes

Here’s a couple use cases for arbitrary bit length operations:

  • Replicating a list of boolean values. In my position replication system, I replicate a list of booleans showing whether an object moved during a tick. Since a bit is basically a boolean, I compress every boolean into a single bit.
  • Replicating arbitrary precision values. For rotation, 8 bits only gives 255 degrees of rotation, which is noticeably imprecise without even looking very closely. I like to use 10 bits for rotation which gives four times as much precision.

this is really useful but how long until its usable in public games?

1 Like

Hello! Loving buffers so far.
This is somewhat unrelated, but even with buffers and native code me and a friend still can’t get zlib deflate to run fast in Luau, especially for larger data :frowning: Very big issue when sending large chunks of data eg. what you’d be using with editable meshes/images.

There was a post a while back about potentially exposing native compression/decompression functions to Luau. Are there any plans to revisit that, now that Roblox is giving far more flexibility and power to developers?

Being able to not have to reimplement these algorithms ourselves (and avoiding unavoidable performance losses in the process) would be a great boon to developers, especially in the current year where power users are able to do more and more with Luau as time goes on.

2 Likes

I actually really want 24-bit functionality.

The reason why is because I might want to experiment and try writing my own character mover and simple physics engine from scratch and custom replication.
And to replicate data between clients I want to use the smallest amount of data possible.

Vectors use about 12 - 13 bytes of data to replicate through remote events.
By using 24-bit precision I can reduce it to about 9 bytes or 8 bytes if I use 16-bit precision for height.

32-bit numbers use too much memory and 16-bit numbers are too small.
I only need a few decimals of precision and I know how large my maps and environments will be so I want to use 24-bit precision.

This will save lots of bandwidth when there are lots of characters wandering around.

Having bit-level control would benefit me a lot since sometimes I can work with numbers smaller than 8-bit.
I don’t need a full byte if the largest number I need to store or replicate is only going to have a range from 1 to 10.

Having 24-bit read/write operations for buffers would benefit me greatly.
Having bit-level manipulation would be even better.

@WheretIB

2 Likes

We have to wait for all active clients to update to a version where buffer is supported.

3 Likes

Hi! I just benchmarked each version, both in native and non-native.

Write non-native: My implementation is around 1.5x faster.
Write native: My implementations is still around 1.5x faster.

local COUNT = 1000000

local function writei24KII( b: buffer, offset: number, value: number ): ()
	buffer.writei16( b, offset, value//2^8 )
	buffer.writeu8( b, offset+2, value )
end

local start = os.clock()
for i = 1, COUNT do
	local new = buffer.create( 3 )
	local number = math.random( -2^31, -2^31-1 )
	
	writei24KII( new, 0, number )
end
warn( os.clock() - start ) -- ~0.06s nn, ~0.04s n
local COUNT = 1000000

local TempBuffer = buffer.create( 4 )
local function writei24TNA( b: buffer, o: number, value: number)
	buffer.writei32(TempBuffer, 0, value)
	buffer.copy(b, o, TempBuffer, 0, 3)
end

local start = os.clock()
for i = 1, COUNT do
	local new = buffer.create( 3 )
	local number = math.random( -2^31, -2^31-1 )

	writei24TNA( new, 0, number )
end
warn( os.clock() - start ) -- ~0.09s nn, ~0.06s n

Read non-native: Mine is around 3x faster.
Read: Yours is around 1.5x faster. I didn’t expect this difference!

local COUNT = 1000000

local function writei24KII( b: buffer, offset: number, value: number ): ()
	buffer.writei16( b, offset, value//2^8 )
	buffer.writeu8( b, offset+2, value )
end
local buffers: { buffer } = table.create( COUNT )
for i = 1, COUNT do
	local new = buffer.create( 3 )
	writei24KII( new, 0, math.random( -2^31, -2^31-1 ) )
	buffers[ i ] = new
end

--

local function readi24KII( b: buffer, offset: number ): number
	return 2^8*buffer.readi16( b, offset ) + buffer.readu8( b, offset + 2 )
end

local start = os.clock()
for _, b in buffers do
	readi24KII( b, 0 )
end
warn( os.clock() - start ) -- ~0.02s nn, ~0.004s n
local COUNT = 1000000

local function writei24KII( b: buffer, offset: number, value: number ): ()
	buffer.writei16( b, offset, value//2^8 )
	buffer.writeu8( b, offset+2, value )
end
local buffers: { buffer } = table.create( COUNT )
for i = 1, COUNT do
	local new = buffer.create( 3 )
	writei24KII( new, 0, math.random( -2^31, -2^31-1 ) )
	buffers[ i ] = new
end

--

local TempBuffer = buffer.create(4)
local function readi24TNA(b: buffer, o: number): number
	buffer.copy(TempBuffer, 0, b, o, 3)
	-- Sign extend
	buffer.writeu8(TempBuffer, 3, if buffer.readu8(TempBuffer, 2) >= 128 then 0xFF else 0)
	return buffer.readi32(TempBuffer, 0)
end

local start = os.clock()
for _, b in buffers do
	readi24TNA( b, 0 )
end
warn( os.clock() - start ) -- ~0.06s nn, ~0.0025s n

But… I don’t think this conversation would’ve needed to happen if readi24/writei24 (along with the unsigned variants) were native to Luau, and would honestly probably be faster than both of our implementations. Hint hint. Wink wink. Please.

6 Likes

I don’t see the beta option in studio, did this go live?

1 Like


Still there for me.
Also checked in game–not yet I don’t think.

1 Like

I’m noticing that the buffer struct clearly stores a length with the unsigned int data type. Right off the bat that tells me a minimum of 32 bits are being allocated in order to store the size of a buffer. I was wondering if there are any optimizations done in the Roblox network handler that reduces the need to pass a 32 bit length variable through a remote event header in order to optimize a games kb/s incoming/outgoing. If I wanted to only send lets say 8 bits (1 byte) of data to the client from the server through a remote event, from what this struct implies (https://github.com/luau-lang/luau/blob/c26d820902ac66740bf2054e0822b7024a67c4cf/VM/src/lobject.h#L267C4-L267C4), there must be 32 bits allocated for the length of a buffer plus 8 bits for the actual buffer. It seems to me that buffers should definitely be used for a large amount of data being passed. However, this still leads me to wonder if or when we will be given specific sized data types such as int_8, int_16, and int_32 variables without having to use hacky methods like Vector2Int16s to send small packets of information efficiently & expediently to the client.

1 Like

Sorry for replying to this post so much, but I noticed that buffers did go live, but are unable to be sent over the network, despite them working when being manipulated.
Is this intended? Thanks.
(I am currently just checking if buffer then)

1 Like

Buffer replication today adds a minimum of 2 bytes for buffers smaller than 128 bytes.
I think it’s fair to recommend buffers for cases where you have more data to send than a few bytes and to try merging multiple small buffers together if that’s profitable.

It is very unlikely that we will introduce such types.

There will be an update added in the announcement post when the feature will become globally available.

1 Like

How would that even work?
Buffers are essentially just long binary strings your code has to interpret using the various read methods to make sense off, the engine has no means of knowing what the data inside the buffer actually means without you telling it.

Since you said this, could you guys please make EditableImage:ReadPixels(), :WritePixels() use buffers, or have an overload for buffers? I need it to be lightning fast to iterate their data!

1 Like

I tested out adding a buffer version of ReadPixels and WritePixels, and unfortunately, it doesn’t provide much speedup for most use cases. ReadPixels and WritePixels are much faster, but the reading and writing into the buffer are slower than into the table. Use cases where you would already want to use a buffer are much faster since you don’t need to go from a buffer back into a table so we still might want to add this, however, you would need to profile your individual use case to see if it is faster or not if we do.

2 Likes

Mainly I would use it to be able to iterate all the pixels. This takes quite a while, but it may not even be Read/Write pixel’s fault. It could just be the large amount of iterations!

Perhaps we could eventually get an UpdatePixel method which takes in a function and calls it for every pixel? It would probably be faster than making this ourselves, because I know DrawImage is very fast.