[Studio Beta] EditableMesh Batching APIs & Parallel Queries


Hi Creators,

We’re announcing two major Studio Beta upgrades to EditableMesh that make mesh editing significantly faster:

  1. Parallel queries: Every EditableMesh query method can now be safely called from parallel Luau, so you can spread expensive read work (raycasts, closest-point lookups, spatial queries) across multiple threads.
  2. Batching APIs: Nine new methods that let you create, read, update, and remove mesh elements in bulk instead of one call per element. Available now as a Studio Beta.

oldBunny-combined
FPS comparison of using batch API vs. existing API

Enabling the Studio Beta

In Studio, go to File > Beta Features and check the box for EditableMesh Batching. Restart Studio for the new methods to become available.

:high_voltage: Parallel Queries

All of EditableMesh’s query methods — such as RaycastLocal(), FindClosestPointOnSurface(), FindClosestVertex(), and the various Get* accessors — are now safe to call from parallel Luau.

This means you can distribute heavy read workloads across Actor instances and run them concurrently after calling task.desynchronize(). A common use case is casting thousands of rays against a mesh for procedural placement, sampling, or custom collision: split the work across Actors and each one queries the same mesh in parallel.

Click here to view a code sample
-- Script parented under an Actor. Each Actor runs this in parallel.
local actor = script:GetActor()
local direction = Vector3.new(0, -1, 0)

actor:BindToMessage("StartWork", function(origins, samplePoints)
    -- origins is a {Vector3} slice of work for this Actor
    -- samplePoints is a {Part} slice of sampling points
    task.desynchronize() -- enter parallel execution

    local hits = {}
    for i, origin in origins do
        -- RaycastLocal is a query, so it's safe to call in parallel
        local faceId, point = editableMesh:RaycastLocal(origin, direction)
        if faceId then hits[i] = point
        else hits[i] = nil
    end
    
    task.synchronize() -- return to serial before touching shared/mutating state

    for i = 1, #samplePoints do
        samplePoints[i].Position = hits[i]
    end
end)

:hammer_and_wrench: Batching

Previously, building or updating an EditableMesh meant calling a singular method (AddVertex, SetPosition, GetColor, and so on) once per element. For large meshes, the per-call overhead adds up fast. The new batch methods let you do the same work across many elements in a single call, which is typically much more performant than handling them one by one.

Rather than one method per attribute, each batch method shares a small set of general entry points and uses an Enum.MeshAttribute value — or the type already encoded in each mesh ID — to determine which attribute you mean. For example, BatchSetValues() sets positions when called with vertex IDs, but sets normals when called with normal IDs. You cannot mix-and-match ID types within a single call.

New APIs

Creation & Removal:

  • BatchAdd(Enum.MeshAttribute, data...) → {id} — Creates new mesh elements of the given attribute type and returns their IDs. When given Enum.MeshAttribute.Color, this expects both {Color3} and {number} for color and alpha.
  • BatchRemove(faceIds) → () — Removes a batch of faces.
  • Clear() -> () - Removes all mesh data, including vertices, faces, bones, FACS, etc from the mesh.

Modification:

  • BatchSetValues(ids, values) → () — Writes attribute values to a batch of mesh element IDs.
  • BatchSetFaceAttributes(faceIds, attrIdArrays) → () — Sets the per-corner attribute IDs for each face in a batch.
  • BatchSetVertexFaceAttributes(vertexIds, faceIds, attrIds) → () — Sets the attribute at a specific corner for each vertex–face pair in a batch.

Querying:

  • BatchGetValues(ids) → values, values? — Given ColorIDs, this method has 2 returns: {Color3} and {Number} (representing the Color and alpha values). For every other attribute, it just returns a single table with the value {value}
  • BatchGetFaceAttributes(Enum.MeshAttribute, faceIds) → {{id}} — Returns the per-corner attribute IDs for each face in a batch.
  • BatchGetVertexAttributes(Enum.MeshAttribute, vertexIds) → {{id}} — Returns the attribute IDs associated with each vertex in a batch.
  • BatchGetVertexFaceAttributes(Enum.MeshAttribute, vertexIds, faceIds) → {id} — Returns the attribute ID at each specified corner for a batch of vertex–face pairs.

The new Enum.MeshAttribute enum identifies which attribute a batch method operates on. Its values are Vertex, Normal, UV, Color, and Face

Example

Here’s a quad built entirely with batch calls, then animated by reading and writing every vertex position in one shot per frame:

Click here to view
local function createAndAnimate(editableMesh)
    -- Build a quad from four corner positions in one call
    local positions = {
        Vector3.new(0, 0, 0),
        Vector3.new(10, 0, 0),
        Vector3.new(10, 10, 0),
        Vector3.new(0, 10, 0),
    }
    local vIds = editableMesh:BatchAdd(Enum.MeshAttribute.Vertex, positions)

    -- Two triangles. Faces take a 2D array of vertex IDs, not a flat list.
    local fIds = editableMesh:BatchAdd(Enum.MeshAttribute.Face, {
        {vIds[1], vIds[2], vIds[3]},
        {vIds[1], vIds[3], vIds[4]},
    })

    -- Displace every vertex with noise each frame, in one read and one write
    game:GetService("RunService").Heartbeat:Connect(function()
        local verts = editableMesh:GetVertices()
        local pts = editableMesh:BatchGetValues(verts)
        for i, p in pts do
            pts[i] = p + Vector3.new(0, math.noise(p.X, p.Z, os.clock()), 0)
        end
        editableMesh:BatchSetValues(verts, pts)
    end)
end

This is what the above code produces.

:hammer_and_wrench: Example Place File

To help you get started, we’ve prepared an example place where you can observe the performance differences firsthand:

oldBunny-combined

EditableMeshBatchingDemoPlaceV4.rbxl (108.6 KB)

This example demonstrates how batching and parallelism can significantly improve performance.

To test it, open the attached RBXL file in Roblox Studio and press Play to spawn meshes in the designated spawn areas. Use the provided tools to interact with the meshes and monitor the impact on your FPS.

:light_bulb: Tip: For the most accurate performance comparison, test one mesh type at a time rather than spawning both simultaneously.

:bar_chart: Performance

Depending on the API used, batching can be up to ~8x faster than calling the singular methods in a loop, and parallel queries deliver up to ~2x throughput on read-heavy workloads. Actual gains depend on your mesh size and access pattern. The below graphs were tested on a 2019 Huawei Phone 2GB RAM.

Graph comparing render time for BatchSetValues(Enum.MeshAttribute.Vertex, …) and SetPosition(…) under different batch sizes

Graph comparing render time for SetFaceColors(…) in a loop and SetFaceAttributes(Enum.MeshAttribute.Face, …)

Graph comparing render time for parallel and serial version of scanner tool in demo place

:construction: Known Issues & Limitations

  • Skinning is not supported for batching or parallelism, since skinning data is typically not part of the per-frame hot path.
  • Parallel writes are not supported. Only query methods are parallel-safe; any method that mutates the mesh must run in serial.

:rocket: What’s Next?

  • Once we have addressed feedback on this new API, we will make these APIs available in published experiences.
  • We are continuing to work on mitigating the following Editable* API roadbumps. Please stay tuned:
    • Enabling replication
    • Reconciling the extra ID Verification requirement to use Editable* APIs
    • Permissions restrictions
  • In additional to that, we are considering supporting quads on the EditableMesh API as well as editing the results of Solid Modeling operations like Union, Subtract, Intersect, Fragment and Sweep. If you have use cases that would benefit from quads, please share below!

We can’t wait to see what you build with these — drop your feedback, questions, and creations in this thread.

Happy (batch) meshing!
@TheMeshBoy and @monsterjunjun on behalf of the Geometry team

153 Likes

This topic was automatically opened after 10 minutes.

Been waiting for this for a while, this will increase fps a lot! Massive thank you!

8 Likes

I have only one question, when will this be out of beta, because i want to use this sooooo bad THIS IS SUCH A GOOD UPDATE!

10 Likes

Wait… we actually got Batching and Parallel Queries before 2030?! Jokes aside, massive W update! :tada:

Now that the CPU-C++ bridge overhead is handled, the next dream step would definitely be restricted/sandboxed GPU compute APIs for EditableMesh.

Even a specialized GPU pipeline for mesh calculations (without raw HLSL/GLSL) would completely eliminate CPU roundtrips for procedural devs. The demand for low-level performance features is definitely real!

23 Likes

The sooner this releases for published games the better.

3 Likes


10k animated blades of grass at ~110fps, compared to what used to be ~65fps

25 Likes

skinned meshes might be a better option for this specific case but nice :cat_face:

2 Likes

quick technical question, but are you guys are using glBufferSubData or equivalent to efficiently upload the vbo changes?

and for EditableImages, are you guys using glTexSubImage2D or equivalent to upload a region of changed pixels to the gpu instead of doing an entire reupload ?

these were lingering in my mind a-bit

1 Like

I use thousands of EditableMeshes to build collisions on the server side of the sandbox game I’m working on. Using BatchAdd I was able to cut the time taken to create my meshes in half. However, the time taken to create these meshes was already relatively low in my situation. The real bottleneck for me is in the time it takes to apply the EditableMesh to a MeshPart via CreateMeshPartAsync. It currently takes about 15 milliseconds to create each MeshPart (is it only creating one per simulation frame?), causing my game to take up to a minute to load. Creating MeshParts accounts for almost the entirety of my load time.

Now I could technically reduce that time by creating larger EditableMeshes that utilize the entire triangle limit, but then I run into another issue. Since you have to create a new MeshPart to apply collision changes, every time a mesh is edited, I have to pay the cost of CreateMeshPartAsync and the cost of replicating the entire mesh over to the client again. If I use the maximum triangle count per mesh, there is very noticeable lag between when the client requests an edit on the mesh and when the collisions actually change. To solve this, I have to make my meshes about 1/6 of the maximum triangle count, meaning 6 times the load time on my game. I could just have the server only create collision meshes around the player to avoid the load times, but I like the simplicity of my current approach, and there aren’t any runtime performance issues with it after the game loads (my collision meshes are very simple). If anyone has any ideas of things that could be done to improve this let me know.

You mentioned there is work being done on replication. I hope it can solve some of my problems. If collision changes could be replicated with creating a new MeshPart, and done so without resending the entire mesh on every edit, that would be amazing. If I could somehow batch create MeshParts (or at least create more than one per frame), that would also be amazing.

One more thing, it would be super useful if the client side limits on EditableMeshes could be eased a bit. I understand Roblox has to run on a wide variety of devices, but 8 EditableMeshes on the client is very restrictive. I would be using far, far more memory if I built my collisions with BaseParts, so having such a tight “memory limit” on EditableMeshes seems odd. I guess I’m not exactly a graphics programmer and don’t know all of the implications.

Overall, good update. I like the direction this is going and the things that EditableMeshes enable me to do. Thanks for reading this, I’ve being doing a lot with EditableMeshes lately and thought I’d share my thoughts.

6 Likes

w update, been waiting for this

1 Like

They should just get rid of this limit entirely. It’s ridiculous that they think we as developers are incapable of optimizing our code for our own target hardware, especially when these limits don’t even reflect the actual capabilities of the hardware they are currently running on. The potential of editable instances get held back so badly because of this arbitrary limitation.

12 Likes

Realistically anything better than current limits would be so much better. A hard cap on 8 editable meshes makes them impossible to use in a lot of scenarios

2 Likes

THANK YOU!
my feature request has finally been answered. :face_holding_back_tears::pray:
im genuinely so amazed at the team lately, please keep the updates coming!!

one question: will we ever see increased limits for tri/vertex count per EditableMesh?

4 Likes

But how many EditableMesh can there be in place at once? Wasn’t there a limit? I remember a limit, and the limit was scary. :frowning:

In that one cloth hack week demo.

3 Likes

Are we able to have more than 8-9 EditableMeshes on the client? that’s the entire bottleneck of EditableMesh in its current state. Sadly not usable until that is resolved.

7 Likes

YES!! Finally some usefulness to parallel luau!!

1 Like

ive tried skinned grass and other methods and theyre all slower when it comes to doing it at large scale. The best part about using editable meshes is that multiple meshparts can use the same editable mesh which means i only need to do the calculations one 1 patch of grass and i can copy that over and over:

640,000 total moving blades of grass covering 400 studs (~105 FPS)


if you attempted this with skinned meshes or any other method at all, your pc would explode

11 Likes

Now if only we had an api to push this to the gpu…

7 Likes

Yes, with a large number of bones, performance collapses—causing cluster spikes—and the Animator doesn’t always work; adding a Humanoid rig creates excessive overhead, making Editable Mesh an increasingly viable option.

2 Likes