Introducing Luau buffer type [Beta]

Hello Creators!

Today, we are releasing a new built-in Luau data type called ‘buffer’ under a Beta feature in Roblox Studio. This will provide a simple and faster way of working with compact binary data.

To enable this beta, head over to File, select Beta Features, and check Luau Buffer Type item in the list to opt-in.


Updated on 2023-12-13T00:00:00Z:

Updated on 2024-02-05T00:00:00Z:


The ‘buffer’ type represents a fixed-size block of memory and a new global ‘buffer’ library provides functions for the creation and manipulation of the data inside.

Like strings, buffers can contain arbitrary binary data; but unlike strings, data inside buffers can be freely mutated and the buffer library provides a way to interpret bytes inside the buffer as various small numeric types.

This extension was initially proposed in an RFC in our open-source Luau repository; we also received many requests to have a more low-level way to work with memory using fixed-size integer and floating-point types that are not just Luau numbers. Finding that buffers fit the use case very well, we refined the RFC and implemented it.

How do I use buffers?

To create a buffer, call buffer.create(size) to create a zero-initialized buffer with size bytes, or call buffer.fromstring(str) to convert an existing string with binary data into a buffer. From there on, you can use functions like buffer.readi16 or buffer.writef32 to work with data inside the buffer, using various common number representations; refer to buffer documentation for details.

For example, if you happen to have a lot of objects and you’d like to transmit their transforms over the network, you could do something like this:

-- each object takes 3*4 bytes for position and 3*2 bytes for orientation
local buf = buffer.create(#objects * (12 + 6))

for i, obj: Part in objects do
    local offset = (i - 1) * (12 + 6) -- note: buffer offsets are 0-based
    local pos, ori = obj.Position, obj.Orientation

    -- 12 bytes for position, 4 bytes per component
    buffer.writef32(buf, offset + 0, pos.X)
    buffer.writef32(buf, offset + 4, pos.Y)
    buffer.writef32(buf, offset + 8, pos.Z)

    -- 6 bytes for orientation, 2 bytes per component
    -- note: orientation components use degrees with -180..180 range
    -- we're a little sloppy here and use a subset of the full i16 range
    -- when reading this data, make sure to divide i16 by 100
    buffer.writei16(buf, offset + 12, math.round(ori.X * 100))
    buffer.writei16(buf, offset + 14, math.round(ori.Y * 100))
    buffer.writei16(buf, offset + 16, math.round(ori.Z * 100))
end

Note: This is just one of many ways to represent translations and orientations.

What to watch out for

Buffers cannot currently be stored via attributes, but they can be passed using bindable/remote events, so you can simply pass the resulting buf object to BindableEvent.FireClient to replicate the data.

Keep in mind that just like a table, passing a buffer through Roblox APIs copies the data and doesn’t preserve the identity of that buffer reference.

Why are buffers useful?

Buffers are generally useful when working with binary data. Algorithms such as terrain serialization, audio and image processing, custom replication and compression, hashing as well as implementations of other virtual machines can benefit from a buffer type.

Compared to strings, buffers are easier and faster to extract data from (strings support string.unpack but this interface isn’t easy to use and isn’t very performant), and also support generating data whereas with strings it is difficult to produce packed data directly, requiring the use of temporary strings and table.concat.

Compared to number arrays, buffers are more compact in memory, take less space during storage or transmission, and can be faster to access in some cases. Large number arrays also can have a significant cost for garbage collection whereas buffers are fairly cheap in that regard.

Unlike number arrays, buffers are not automatically resizeable. When the buffer size is not known ahead of time, we recommend either estimating a reasonable upper bound and creating a slightly larger buffer, computing the precise size using another pass over the input data, or resizing the buffer adaptively using buffer.create/buffer.copy (this should be done with care to amortize the resize cost).

How do buffers interact with other engine features?

Buffers are supported by bindable and remote events and functions; importantly, buffers implement transparent on-the-wire compression. A sufficiently large buffer will be compressed before sending the data to the client/server and decompressed on the other side automatically, thereby reducing bandwidth; when buffers are used for replication, we recommend grouping smaller buffers into larger buffers, similar to the example above, to take maximum advantage of the compression.

Please keep in mind that network replication limits are smaller than the largest supported buffer size, and you should keep buffers that are sent over remote events under 50 MB.

We hope that buffers will be a good alternative for existing bit buffer libraries because of this: even though buffers require byte-based access instead of bit-based access, the compression can identify patterns where individual bytes can be compressed to occupy less than 8 bits each, as well as identify and collapse repetitive structures. Buffers should be significantly faster compared to bit buffer libraries as well.

Buffers also greatly benefit from native code generation; when –!native is used and the NCG preview is enabled, buffer accesses are fully optimized and inlined into the calling code, providing significant speedup compared to string access.

Please note that buffers currently are not supported by DataStore APIs; using buffer.tostring(buf) may not produce a string that can be stored in DataStores because it will often contain non-UTF8 bytes.

Looking for feedback

We plan to incorporate your feedback and make the feature officially available early next year. Please let us know if this new data type is useful for your experiences, if you’re missing any specific functionality, or if you find any bugs.

In the first release, we tried to focus on providing core functions, with the ability to build additional helpers on top of them. By gathering feedback we will get a better understanding of what to change/add in the future. We are also interested in code that is heavily reliant on buffers that we can use for our open-source benchmarks as this would help us ensure maximal performance for this feature.

Resources

Documentation for buffers can be found on the Roblox Creator Hub and also on Luau website.

Thanks to our open-source contributors to Luau on GitHub for the original proposal, @Dekkonot for early feedback, and to @WheretIB and @zeuxcg for refining the proposal and implementing the feature!

297 Likes

This topic was automatically opened after 10 minutes.

Very excited to use this and UnreliableRemoteEvents for massive performance gains!

38 Likes

This is fantastic to see, although the lack of bit-level manipulation does make it a bit trickier to migrate to. For my data formats, I calculate the minimum bit width to store numbers (ex: no reason to store everything in 32 bits if 6 bits is enough). It isn’t critical for my use case, but it was a consideration I had before.

Edit: The compression mentioned below may overcome this. I’ll do some tests later with an article I previously did.

Every time I have used a bit buffer implementation, it was for data storage. Are there plans to provide a native way to create DataStore-safe to store the values? Without a native Base64 or equivalent, making it safe is a lot more work than I’d like.

45 Likes

Would love to thank the Luau team for being receptive to my RFC and making the glorious WebAssembly takeover quicker.

34 Likes

It’s worth noting for those who are curious:

Buffers do not have a cursor. This is a deliberate decision because it complicates some decisions (what would happen to the cursor’s position when sent over the network?) and makes the underlying implementation slightly more annoying.

You’ll have to track where you’re reading and writing from manually.

19 Likes

Could someone give me an scenario where I would want to use this pratically, please

8 Likes

the current implementations of bit-buffers in Luau were absurdly slow so really happy to see this getting added natively to Luau!

8 Likes

This is very exciting! I’ve been wanting to have more low-level control over memory.

(we must further carcinize roblox now :crab:)

6 Likes

What compression algorithm is used on buffers when sent over the network?

5 Likes

Current compression method that’s being used is Zstd.

17 Likes

What compression level is being used here or is this still subject to tinkering? Also, thanks!

4 Likes

Interesting. Cant wait to see what the community creates

4 Likes

Awesome! I have a use-case right now where I need to replicate tons of simulated physics data from server to client in my billiards game. I’m using a 3rd party BitBuffer module for it right now. Excited to see what the speed-up is using the new buffer (and curious to see if it reduces network significantly more or not).

17 Likes

I noticed while reading the announcement, and I think that was a good decision. Want a cursor? You can create a simple wrapper. Don’t want the cursor? Use it as-is. Going the opposite of creating a non-cursor buffer from a cursor buffer would be tricky.

12 Likes

Does this mean that in the future, we can read/write image and audio data to create images and sounds with a script? :fearful: That’d be pretty sick if we could do that.

10 Likes

For representing large pieces of data whose structure and types you know. For example, saving chunk data in voxel games. The only way to achieve this before was to represent it as an array and concat it into a string, which is incredibly wasteful and slow. JSON is good enough for most use cases, but this makes advanced data manipulation way easier, and possible at all in some cases.

6 Likes

Man it feels like my mind just exploded.
This is AWESOME!

I’m very obsessed with structuring data and keeping things as tiny and compact as possible when sending stuff over a network.

This is a programmer/nerd dream coming true on Roblox.
Can’t wait to learn how these things work and feel like a pro for knowing how to mash a few bytes together.

7 Likes

This looks very useful!
Would it be possible to implement a readBits/writeBits function based off of readi8?*

3 Likes

Great question!

Okay so let’s say you have a inventory system for your players in a game, and their settings and everything.

But normally, if you stored everything using numbers, strings and whatnot, it would use quite a bit of data.

Let’s say your player has a "Sword" and a "Shield" in their inventory, Sword is 5 characters and Shield is 6 characters, that is 11 bytes.

Your player also has a Potion of Healing in their inventory, which is 17 bytes just for the name of the item alone.

This would be a inefficient way to store items, so let’s switch over to a ID-based system instead.

We know our game only has 150 items, so we can use 1 byte (which has a range from 0 to 256) to store the item IDs.

So Sword will be 1.
Shield will be 2.
Potion of Healing will be 3.
And so on…

Instead of using names, we store all the items the player has as numbers ranging from 0 to 256 and thus only need 1 byte per item.

Now our inventory system only needs 3 bytes to store the 3 items the player has instead of 28 bytes if we stored their names instead.

And we can store these things in a buffer which (according to the OP) can compress the data even further to a smaller size.

Super neat stuff!

And this can also be applied to sending data over the network (Remote Events).
Reducing memory and bandwidth use should make your experiences smoother and less laggy.

(Not the best example but I tried explaining it here.)

24 Likes