Getting Started
Install
Install ComputeLua directly from Roblox. Then, place it into ReplicatedStorage so both the Server and Client can access it.

Basic Setup
The first thing you always do with any library, is require the module. So, let's require it.

local ReplicatedStorage = game:GetService("ReplicatedStorage")

local ComputeLua = require(ReplicatedStorage.ComputeLua)

Creating a Dispatcher
Without a Dispatcher nothing will run, so this is pretty important.

A Dispatcher's whole job is to handle a lot of workers and execute their threads. It will manage all of this so you don't have to.

You can create a Dispatcher by called the ComputeLua.CreateDispatcher() method. This method will take in two parameters.

numWorkers -- How many workers do you want this Dispatcher to handle? This will vary depending on your needs, but it is recommended to pick a number that is a multiple of 4.
worker -- The template of the worker script, this is what will be running the threads you create.
You are going to need a worker template, so, for now, let's create a blank script and place it as a child of the current script.

local ReplicatedStorage = game:GetService("ReplicatedStorage")

local ComputeLua = require(ReplicatedStorage.ComputeLua)

local worker = script.Worker
local numWorkers = 256

local Dispatcher = ComputeLua.CreateDispatcher(numWorkers, worker)

caution
Workers will be parented under their template when they are created by the Dispatcher. Make sure they are able to run in their current location.

ComputeBuffers
ComputeBuffers are the main way to send bulks of data for the workers to process and spit out a result. They are really just very large tables of elements.

To create a ComputeBuffer, all you need to call is the Dispatcher.SetComputeBuffer() method, this method takes in two parameters

bufferName -- What is the name of this buffer? If you name two buffers the same name, they will override their data
bufferData -- What is the data of this buffer? This is the large table of elements which was said before
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local ComputeLua = require(ReplicatedStorage.ComputeLua)

local worker = script.Worker
local numWorkers = 256

local Dispatcher = ComputeLua.CreateDispatcher(numWorkers, worker)

Dispatcher:SetComputeBuffer("buffer", table.create(8192, 2))

danger
ComputeBuffers can only have certain data types

First, the keys of the data must only be numbers, this will allow fast and effective sending of the data.
Second, the only data types allowed for the data are:
Vector2
Vector3
CFrame
Color3
UDim
UDim2
number
boolean
string
Table containing any of these (nested tables)
-- Allowed
{
    15,
    Vector3.zero,
    false,
    "string",
    CFrame.new(),
    {
        Vector3.new(0, 2, 1)
    },
    [7] = "sfas"
}

-- Not allowed
{
    stringKey = 1512,
    function() end
}


If you ever wish to change the data in a ComputeBuffer that was already made. Then, just call the Dispatcher.SetComputeBuffer() method again and it will override the data.

Workers
Workers are the brain of the operation. They are what is actually running the code and processing the data.

First, you will need to check if the current script is running in an actor, this is to make sure that this script can run in parallel.

local actor = script:GetActor()
if actor == nil then
    return
end

After that, you want to require ComputeLua so you can access its functions.

danger
NEVER call the "Parallel Unsafe" functions within ComputeLua. This will either cause an error or break something

local ReplicatedStorage = game:GetService("ReplicatedStorage")

local actor = script:GetActor()
if actor == nil then
    return
end

local ComputeLua = require(ReplicatedStorage.ComputeLua)

Now, you want to get the data key's of every ComputeBuffer to reference them later. In this example the only ComputeBuffer we have is called "buffer", so you get the key by calling ComputeLua.GetBufferDataKey()

local ReplicatedStorage = game:GetService("ReplicatedStorage")

local actor = script:GetActor()
if actor == nil then
    return
end

local ComputeLua = require(ReplicatedStorage.ComputeLua)

local BUFFER_KEY = ComputeLua.GetBufferDataKey("buffer")

Finally, the last thing you need to do is create a thread. You can easily create a thread by running ComputeLua.CreateThread(). This takes in three parameters

actor -- ComputeLua needs this to keep track of the workers
threadName -- This should be unique to prevent overlap.
callback -- This is a function that is called when the thread is executed. It takes in two parameters.
id -- This is the dispatch ID. You can easily use the dispatch ID to focus on one value within Compute Buffers.
bufferData -- The massive table containing all your buffer data
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local actor = script:GetActor()
if actor == nil then
    return
end

local ComputeLua = require(ReplicatedStorage.ComputeLua)

local BUFFER_KEY = ComputeLua.GetBufferDataKey("buffer")

ComputeLua.CreateThread(actor, "ProcessSquareRoot", function(id: number, bufferData: SharedTable)
    
end)

Now to get the data of the buffer you want, you want to first find the data for that buffer by indexing bufferData with the buffer's key

Then, if your buffers are configured correctly then the index in that buffer's data of the data you wish to edit will be the dispatch's ID, in our case is the variable id. So, index the buffer's data with the dispatch ID

local ReplicatedStorage = game:GetService("ReplicatedStorage")

local actor = script:GetActor()
if actor == nil then
    return
end

local ComputeLua = require(ReplicatedStorage.ComputeLua)

local BUFFER_KEY = ComputeLua.GetBufferDataKey("buffer")

ComputeLua.CreateThread(actor, "ProcessSquareRoot", function(id: number, bufferData: SharedTable)
    local value = bufferData[BUFFER_KEY][id]
end)

Finally, once you are done processing your data you must return a value (if no value is returned, an error is raised). In this case, we will simply run math.sqrt() on it

But, ComputeLua needs to know what ComputeBuffer this value was assigned to so it can reassign it back, so you are required to return a table.

How it works is by one element equals two indices in the table. The first index is the buffer's key and the second one is the data. For example, in our case we only have one ComputeBuffer so the returning table will be {BUFFER_1_KEY, data_1}. But, if you had three ComputeBuffers and edited all of them, then your returning table would be {BUFFER_1_KEY, data_1, BUFFER_2_KEY, data_2, BUFFER_3_KEY, data_3}

So filling in the values, our returning table will be {BUFFER_KEY, math.sqrt(value)}

local ReplicatedStorage = game:GetService("ReplicatedStorage")

local actor = script:GetActor()
if actor == nil then
    return
end

local ComputeLua = require(ReplicatedStorage.ComputeLua)

local BUFFER_KEY = ComputeLua.GetBufferDataKey("buffer")

ComputeLua.CreateThread(actor, "ProcessSquareRoot", function(id: number, bufferData: SharedTable)
    local value = bufferData[BUFFER_KEY][id]
    return {BUFFER_KEY, math.sqrt(value)}
end)

Dispatching
Now it's time to execute your workers. You can do this by dispatching your Dispatcher by running Dispatcher:Dispatch(). This takes in two required arguments and one optional.

thread -- The name of the thread to dispatch, this is the same name as the one in the worker
numThreads -- How many times of this thread should be calling
overridebatchSize -- (optional) (NOT RECOMMENDED) override the default batch size
The Dispatch method will return a Promise. You can either await this promise, which will yield the current thread, or you could use :andThen() which will run the function passed after the Promise is resolved, this is what is recommended.

The resulting data works like this, it is the same massive table that the workers were given but now with the new data!

You will need to use ComputeLua.GetBufferDataKey() to get the index of the buffer you want to access, from there it is just the dispatch's ID starting at 1

local ReplicatedStorage = game:GetService("ReplicatedStorage")

local ComputeLua = require(ReplicatedStorage.ComputeLua)

local worker = script.Worker
local numWorkers = 256

local Dispatcher = ComputeLua.CreateDispatcher(numWorkers, worker)

Dispatcher:SetComputeBuffer("buffer", table.create(8192, 2))

local BUFFER_KEY = ComputeLua.GetBufferDataKey("buffer")
Dispatcher:Dispatch("ProcessSquareRoot", 8192):andThen(function(data: {[number]: {ComputeBufferDataType}})
    local bufferData = data[BUFFER_KEY]
end)


From here you can do whatever you wish with the data

Cleaning Up
Finally, Dispatchers take up memory and if you don't get rid of them after you are done using them then problems will occur.

Simply call the Dispatcher:Destroy() method to get rid of your Dispatcher

Dispacther:Destroy()
Dispatcher
A Dispatcher is the one in charge of everything. It is the one holding the data from your ComputeBuffers and sending those off to the many many workers it has to process them.

Multiple Dispatchers can be made, as many as you want.

Below is a more detailed explaination about Dispatchers compared to Getting Started

Creating a Dispatcher
To create a Dispatcher you will just need to call ComputeLua.CreateDispatcher() with the correct arguments. This will return a Dispatcher which can be used to dispatch a thread or create/set ComputeBuffers

Dispatcher:SetComputeBuffer() This will set a ComputeBuffer's data. If that certain ComputeBuffer does not exist, then it will create it.

Dispatcher:DestroyComputeBuffer() This will delete a ComputeBuffer's data. Just a simple cleanup function.

Dispatching Threads
Dispatching a thread is very simple. All you need to do is call Dispatcher:Dispatch() with the correct arguments. This will return a Promise so you can process that Promise as you please.

Once the Promise is resolved, it will return the resulting data that the workers worked on

Example
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local ComputeLua = require(ReplicatedStorage.ComputeLua)

local worker = script.Worker
local numWorkers = 256

local Dispatcher = ComputeLua.CreateDispatcher(numWorkers, worker)

Dispatcher:SetComputeBuffer("buffer", table.create(8192, 2))

local BUFFER_KEY = ComputeLua.GetBufferDataKey("buffer")
Dispatcher:Dispatch("ProcessSquareRoot", 8192):andThen(function(data: {[number]: {ComputeBufferDataType}})
    local bufferData = data[BUFFER_KEY]
end)

ComputeBuffer
The ComputeBuffer is what sends data between your Dispatcher and workers. It is very important since it's the only way to send and receive data.

Creating a ComputeBuffer
To create a ComputeBuffer you will just need to call Dispatcher:SetComputeBuffer() with the correct arguments. This will either set or create a ComputeBuffer with the data you give it.

caution
ComputeBuffers have only certain data types allowed. Refer to Getting Started to find out those types.

Common Practices
The workers use their dispatch ID to index the ComputeBuffers' data to figure out the exact data they are working with. Therefore if you have a lot of data to edit, such as positions for some terrain generation, then if you store them in one list and send them in a ComputeBuffer you will be able to use the dispatch ID to find the exact data to edit. Dispatch IDs go from 1 to how many threads you are dispatching.

Another important issue is ComputeBuffers will be usually used for lots of data. So if you give the workers a dynamic table (one that has nothing in it and will resize itself when new values are put into it) then when you fill it out to complete the data it will be a bit slow, especially if you need to run this a lot per game tick.

For this reason, you should precreate all the data you are going to put into a ComputeBuffer.

table.create(5000, Vector3.zero) -- This will precreate a table of 5000 entries with a zero-ed out Vector3 at each one.


Functions cannot be sent to the workers
You cannot send functions to workers through a ComputeBuffer. Therefore you will have to have the function on the worker script and then send the parameters required to run the function through a ComputeBuffer.

Examples
Load in a list of vertices
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local worker = script.Worker
local numWorkers = 256 -- 4 * 64

local ComputeLua = require(ReplicatedStorage.ComputeLua)
local Dispatcher = ComputeLua.CreateDispatcher(numWorkers, worker)

local numVertices = 10000
local vertices = table.create(numVertices, Vector3.zero) -- Create a table of 10000 entries all with Vector3.zero

Dispatcher.SetComputeBuffer("PositionBuffer", vertices)

local POSITION_BUFFER_KEY = ComputeLua.GetBufferDataKey("PositionBuffer")
Dispatcher:Dispatch("CalculatePositions", numVertices):andThen(function(data: {[number]: {ComputeBufferDataType}})
    local vertices = data[POSITION_BUFFER_KEY]
    for _, pos in pairs(vertices) do
        editableMesh:AddVertex(pos) -- Add the vertex position to the mesh
    end
end)

Worker
A worker is a script that is cloned by the Dispatcher which will perform small tasks. It may not seem important at first but the Dispatcher will clone a lot of things and they will all run in parallel, allowing these small tasks to add up into one big tasks that would take several seconds or even minutes to calculated if it was running serially.

Creating a Worker
To create a worker script, you just create a new script and pass in that script into the ComputeLua.CreateDispatcher() method as the worker template in whatever script is running the Dispatchers.

Make sure to disable it so it doesn't run without the Dispatcher cloning it.

This will automatically clone the workers the Dispatcher needs and enable them.

Worker parent is the template
The Dispatcher will automatically parent the workers to the worker template. So wherever the worker template is should be a spot where that script can run.

The basic layout of a worker script is:

Check if this script has an actor
Require ComputeLua
Create the threads
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local actor = script:GetActor()
if actor == nil then
    return
end

local ComputeLua = require(ReplicatedStorage.ComputeLua)

Creating a Thread
To create a thread you simply call ComputeLua.CreateThread() with the correct arguments. This will connect a new thread so the Dispatcher knows what function to call when invoking this worker. The thread name should be unique to prevent overlapping functions.

The CreateThread method's last argument is the callback. This is the function that will run for every worker that gets executed. It takes in two parameters, the dispatch ID and the buffer data.

ComputeLua.CreateThread(actor, "ProcessSquareRoot", function(id: number, bufferData: SharedTable)
    
end)

The dispatch ID (id) is the current ID of the worker, this starts at 1 and ends at how many threads that the Dispatcher is executing.

The buffer data (bufferData) is the massive table of data that the ComputeBuffers are put into. This is where you get everything you need to process.

To get a certain ComputeBuffer within the buffer data, you will first need the key of it. You can get this key by running ComputeLua.GetBufferDataKey() with the name of the ComputeBuffer. Now simply index the buffer data table with that key to get the data for that buffer. It is recommended that you get the buffer's key outside of the thread function since that will be called many times so no need to ask for the key every time.

local BUFFER_KEY = ComputeLua.GetBufferDataKey("buffer")
ComputeLua.CreateThread(actor, "ProcessSquareRoot", function(id: number, bufferData: SharedTable)
    local data = bufferData[BUFFER_KEY]
end)

Example : Apply noise to positions
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local actor = script:GetActor()
if actor == nil then
    return
end

local ComputeLua = require(ReplicatedStorage.ComputeLua)

local POSITION_BUFFER_KEY = ComputeLua.GetBufferDataKey("PositionBuffer")
ComputeLua.CreateThread(actor, "CalculateNoise", function(id: number, bufferData: SharedTable)
    local PositionBuffer = bufferData[POSITION_BUFFER_KEY]

    local position = PositionBuffer[id]
    return math.nosie(position.x, position.y, position.z)
end)

Here is the code of the module:-- ComputeLua
-- blorbee
-- Stable Release (1.3.0) - 9/24/2024

local RunService = game:GetService("RunService")
local Promise = require(script.promise)

type Promise<T...> = {
	andThen: (self: Promise<T...>, successHandler: (...any) -> ...any, failureHandler: (...any) -> ...any) -> Promise<...any>,
	catch: (self: Promise<T...>, failureHandler: (...any) -> ...any) -> Promise<...any>,
	cancel: (self: Promise<T...>) -> (),
	finally: (self: Promise<T...>, finallyHandler: (...any) -> ...any) -> Promise<...any>,
	await: (self: Promise<T...>) -> (boolean, T...),
}

--[=[
	@class Dispatcher

	Responsible for handling and dispatching workers. 
	This should never be created by a worker or in parallel.

	```lua
	local ReplicatedStorage = game:GetService("ReplicatedStorage")

	local ComputeLua = require(ReplicatedStorage.ComputeLua)

	local worker = script.Worker
	local numWorkers = 256

	local Dispatcher = ComputeLua.CreateDispatcher(numWorkers, worker)
	```
]=]
local Dispatcher = {}
Dispatcher.__index = Dispatcher

--[=[
	@type BufferDataType Vector2 | Vector3 | CFrame | Color3 | UDim | UDim2 | number | boolean | string
	@within Dispatcher
	The only data types allowed in buffers
]=]
--[=[
	@type BufferDataType Vector2 | Vector3 | CFrame | Color3 | UDim | UDim2 | number | boolean | string
	@within ComputeLua
	The only data types allowed in buffers
]=]

--[=[
	@type ComputeBufferDataType { [number]: BufferDataType | ComputeBufferDataType }
	@within Dispatcher
	Type for the data of a ComputeBuffer
]=]
--[=[
	@type ComputeBufferDataType { [number]: BufferDataType | ComputeBufferDataType }
	@within ComputeLua
	Type for the data of a ComputeBuffer
]=]

export type BufferDataType = Vector2 | Vector3 | CFrame | Color3 | UDim | UDim2 | number | boolean | string
export type ComputeBufferDataType = {[number]: BufferDataType | ComputeBufferDataType}

export type Dispatcher = typeof(setmetatable({} :: DispatcherSelf, Dispatcher))

type DispatcherSelf = {
	numWorkers: number,
	worker: Script | LocalScript,
	workerFolder: Folder,
	workers: {Actor},
	workerRemote: BindableEvent,
	computeBuffers: {[number]: ComputeBufferDataType},

	Dispatch: (self: Dispatcher, thread: string, numThreads: number, overrideBatchSize: number?) -> Promise<{[number]: {ComputeBufferDataType}}>,
	SetComputeBuffer: (self: Dispatcher, bufferName: string, bufferData: ComputeBufferDataType) -> (),
	DestroyComputeBuffer: (self: Dispatcher, bufferName: string) -> (),
	Destroy: (self: Dispatcher) -> ()
}
--[=[
	@prop numWorkers number
	@within Dispatcher
	@readonly
	@private
]=]
--[=[
	@prop computeBuffers { [number]: ComputeBufferDataType }
	@within Dispatcher
	@readonly
	@private
]=]
--[=[
	@prop worker Script | LocalScript
	@within Dispatcher
	@readonly
	@private
]=]
--[=[
	@prop workerFolder Folder
	@within Dispatcher
	@readonly
	@private
]=]
--[=[
	@prop workers {Actor}
	@within Dispatcher
	@readonly
	@private
]=]
--[=[
	@prop workerRemote BindableEvent
	@within Dispatcher
	@readonly
	@private
]=]

export type ComputeLua = {
	CreateDispatcher: (numWorkers: number, worker: Script | LocalScript) -> Dispatcher,
	CreateThread: (actor: Actor, threadName: string, callback: (id: number, bufferData: SharedTable) -> {[number]: any}) -> (),
	GetBufferDataKey: (bufferName: string) -> number
}

local function doesBufferElementHaveCorrectDataType(element: BufferDataType): boolean
	if typeof(element) ~= "table" 
		and typeof(element) ~= "Vector2"
		and typeof(element) ~= "Vector3"
		and typeof(element) ~= "CFrame"
		and typeof(element) ~= "Color3"
		and typeof(element) ~= "UDim"
		and typeof(element) ~= "UDim2"
		and typeof(element) ~= "number"
		and typeof(element) ~= "boolean"
		and typeof(element) ~= "string"
	then
		return false
	end
	return true
end

local function doesComputeBufferDataHaveCorrectTyping(bufferData: ComputeBufferDataType): boolean
	local function checkTable(t: {BufferDataType})
		for key, value in pairs(t) do
			if typeof(key) ~= "number" then
				return false
			end
			if typeof(value) == "table" and not checkTable(value) then
				return false
			end
			if not doesBufferElementHaveCorrectDataType(value) then
				return false
			end
		end
		return true
	end
	return checkTable(bufferData)
end

local function convertStringToNumber(str: string): number
	local hash = 0
	local prime = 31
	local mod = math.pow(2, 32) - 1
	for i = 1, #str do
		local char = string.byte(str, i)
		hash = (hash * prime + char) % mod
	end
	return math.abs(hash)
end

--[=[
	Create a Dispatcher
	@since v1.0.0
	@tag Parallel Unsafe
	@private

	@param numWorkers number -- How many workers to create
	@param worker Script | LocalScript -- The worker template to use
	@return Dispatcher
]=]
function Dispatcher._new(numWorkers: number, worker: Script | LocalScript)
	assert(type(numWorkers) == "number", "numWorkers must be a number")
	assert(typeof(worker) == "Instance", "worker must be an Instance")
	assert(worker:IsA("Script") or worker:IsA("LocalScript"), "worker must be a Script or LocalScript")
	assert(numWorkers > 0, "numWorkers must be greater than 0")

	local self = setmetatable({} :: Dispatcher, Dispatcher)

	self.numWorkers = math.round(numWorkers)
	self.worker = worker
	self.workers = table.create(numWorkers, 0)
	self.constantBuffer = {}
	self.computeBuffers = {}

	local folder = Instance.new("Folder")
	folder.Name = "Workers"
	folder.Parent = self.worker
	self.workerFolder = folder

	self.workerRemote = Instance.new("BindableEvent")

	for i = 1, numWorkers do
		local actor = Instance.new("Actor")
		local w = self.worker:Clone()
		w.Parent = actor
		w.Enabled = true
		self.workers[i] = actor
	end

	for _, actor in self.workers do
		actor.Parent = self.workerFolder
	end

	return self
end

--[=[
	Dispatch a number of threads to the workers
	@since v1.3.0
	@tag Parallel Unsafe

	@param thread string -- The name of the thread to dispatch, this is the same name as the one in the workers
	@param numThreads number -- How many times of this thread should be calling
	@param overrideBatchSize number? -- (NOT RECOMMENDED) override the default batch size
	@return Promise
]=]
function Dispatcher.Dispatch(self: Dispatcher, thread: string, numThreads: number, overrideBatchSize: number?)
	assert(type(numThreads) == "number", "numThreads must be a number")
	if numThreads <= 0 then
		warn("numThreads must be greater than 0")
		return Promise.reject()
	end
	assert(type(thread) == "string", "thread must be a string")
	if overrideBatchSize ~= nil then
		assert(type(overrideBatchSize) == "number", "overrideBatchSize must be a number")
		assert(overrideBatchSize > 0, "overrideBatchSize must be greater than 0")
		overrideBatchSize = math.ceil(overrideBatchSize)	
	end

	local batchSize = overrideBatchSize or math.floor(numThreads / #self.workers)
	local data = {}
	for key, bufferData in pairs(self.computeBuffers) do
		data[key] = bufferData
	end
	data = SharedTable.new(data)

	return Promise.defer(function(res)
		local result = data
		local workersDone = 0
		local connection = self.workerRemote.Event:Connect(function(id: number, bufferKey: number, data: any, calls: number)
			result[bufferKey][id] = data
			workersDone += calls
		end)

		local workerCount = #self.workers
		local index = 1
		for i = 1, numThreads, batchSize do
			self.workers[index]:SendMessage(thread, i, batchSize, numThreads, data, self.workerRemote)
			index = (index % workerCount) + 1
		end

		repeat
			RunService.Stepped:Wait()
		until workersDone >= numThreads

		if connection ~= nil then
			connection:Disconnect()
		end
		connection = nil

		res(result)
	end)
end

--[=[
	Set/Create ComputeBuffer data
	@since v1.3.0
	@tag Parallel Unsafe

	@param bufferName string -- The name of the buffer
	@param bufferData ComputeBufferDataType -- The data to set
]=]
function Dispatcher.SetComputeBuffer(self: Dispatcher, bufferName: string, bufferData: ComputeBufferDataType)
	assert(doesComputeBufferDataHaveCorrectTyping(bufferData), "bufferData must have only elements that are acceptable ComputeBuffer types")
	assert(typeof(bufferName) == "string", "bufferName must be a string")
	local key = convertStringToNumber(bufferName)
	self.computeBuffers[key] = bufferData
end

--[=[
	Delete a ComputeBuffer's data
	@since v1.3.0
	@tag Parallel Unsafe
	
	@param bufferName string -- The name of the buffer
]=]
function Dispatcher.DestroyComputeBuffer(self: Dispatcher, bufferName: string)
	assert(typeof(bufferName) == "string", "bufferName must be a string")
	local key = convertStringToNumber(bufferName)
	self.computeBuffers[key] = nil
end

--[=[
	The cleanup function for a Dispatcher. This is important to call to free up memory
	@since v1.0.0
	@tag Parallel Unsafe
]=]
function Dispatcher.Destroy(self: Dispatcher)
	for i = 1, self.numWorkers do
		self.workers[i]:Destroy()
	end
	self.workerRemote:Destroy()
	self.workerFolder:Destroy()
	table.clear(self.computeBuffers)
	table.clear(self)
	table.freeze(self)
end


--[=[
	@class ComputeLua

	The handler for the Dispatcher, threads, and data keys

	```lua
	local ReplicatedStorage = game:GetService("ReplicatedStorage")
	local ComputeLua = require(ReplicatedStorage.ComputeLua)
	```
]=]
local ComputeLua: ComputeLua = {}

--[=[
	Create a Dispatcher to run a bunch of workers in parallel
	@since v1.0.0
	@tag Parallel Unsafe

	@param numWorkers number -- How many workers to use, balanced this with performance and speed
	@param worker Script | LocalScript -- The template script to clone as the worker
	@return Dispatcher
]=]
function ComputeLua.CreateDispatcher(numWorkers: number, worker: Script | LocalScript): Dispatcher
	return Dispatcher._new(numWorkers, worker)
end

--[=[
	Create a thread for the Dispatcher to execute
	@since v1.3.0
	@tag Serial Unsafe

	@param actor Actor -- The Actor to bind the thread to, this should be the same actor as the parent of the worker.
	@param threadName string -- The unique name of the thread.
	@param callback (id: number, bufferData: SharedTable) -> {any} -- The function that will be executed when the thread is called and what returns the data
]=]
function ComputeLua.CreateThread(actor: Actor, threadName: string, callback: (id: number, bufferData: SharedTable) -> {any})
	assert(typeof(actor) == "Instance", "actor must be an Actor")
	assert(type(threadName) == "string", "threadName must be a string")
	assert(type(callback) == "function", "callback must be a function")
	assert(actor:IsA("Actor"), "actor must be an Actor instance")

	actor:BindToMessageParallel(threadName, function(
		startDispatchId: number, 
		batchSize: number,
		numThreads: number,
		bufferData: SharedTable,
		resultRemote: BindableEvent
	)
		for i = startDispatchId, math.min(startDispatchId + (batchSize - 1), numThreads) do
			local result = callback(i, bufferData)
			if result ~= nil then
				local amount = #result
				for i = 1, amount, 2 do
					resultRemote:Fire(i, result[i], result[i + 1], 2 / amount)
				end
			else
				error(`Thread {threadName}'s workers is not returning a value!`)
			end
		end
	end)
end

--[=[
	Get a ComputeBuffer's data key through its name
	@since v1.3.0

	@param bufferName string -- The name of the buffer
	@return number
]=]
function ComputeLua.GetBufferDataKey(bufferName: string): number
	return convertStringToNumber(bufferName)
end

return ComputeLua


Can you fix my code completely to work with the module. My code:module: -- GMFluxContainer ModuleScript

local GMFluxContainer = {}
GMFluxContainer.__index = GMFluxContainer

-- FluxCell constants
GMFluxContainer.FluxCell = {
	Height = 0,          -- cell[0]
	Depth = 1,           -- cell[1]
	Flux_LD_Left = 2,    -- cell[2]
	Flux_LD_Down = 3,    -- cell[3]
	Flux_RU_Right = 4,   -- cell[4]
	Flux_RU_Up = 5,      -- cell[5]
}

-- Constructor
function GMFluxContainer.new(texSize, position, cellSize, parameters)
	local self = setmetatable({}, GMFluxContainer)

	-- Simulation parameters
	self.fluxStrength = parameters.fluxStrength or 0.250
	self.fluxAtten = parameters.fluxAtten or 0.975
	self.fluxSpill = parameters.fluxSpill or 0.0

	self.texSize = texSize or 64
	self.cellSize = cellSize or 2

	-- Position of the fluid grid in the world
	self.position = position or Vector3.new(0, 0, 0)

	-- Initialize data structures
	self.cells = {}
	for x = 1, self.texSize do
		self.cells[x] = {}
		for y = 1, self.texSize do
			self.cells[x][y] = {
				[GMFluxContainer.FluxCell.Height] = 0,
				[GMFluxContainer.FluxCell.Depth] = 0,
				[GMFluxContainer.FluxCell.Flux_LD_Left] = 0,
				[GMFluxContainer.FluxCell.Flux_LD_Down] = 0,
				[GMFluxContainer.FluxCell.Flux_RU_Right] = 0,
				[GMFluxContainer.FluxCell.Flux_RU_Up] = 0,
			}
		end
	end

	return self
end

-- Function to perform a step of the flux simulation (will be parallelized)
function GMFluxContainer:stepFlux()
	-- Function now remains empty since it's handled by ComputeLua workers
end

-- Function to perform a depth update step (will be parallelized)
function GMFluxContainer:stepDepth()
	-- Function now remains empty since it's handled by ComputeLua workers
end

-- The rest of the module remains the same
-- Function to handle collisions (Advanced collisions)
function GMFluxContainer:handleCollisions(collidableObjects)
	-- [Your existing collision handling code]
end

-- Encode terrain heightmap
function GMFluxContainer:cellEncodeHeightmap(heightmap, factor)
	for x = 1, self.texSize do
		for y = 1, self.texSize do
			local height = heightmap[x][y] * factor
			self.cells[x][y][GMFluxContainer.FluxCell.Height] = height
		end
	end
end

-- Add fluid at a specific position
function GMFluxContainer:addFluid(x, y, amount)
	if x >= 1 and x <= self.texSize and y >= 1 and y <= self.texSize then
		self.cells[x][y][GMFluxContainer.FluxCell.Depth] = self.cells[x][y][GMFluxContainer.FluxCell.Depth] + amount
	end
end

-- Get fluid depth at a position
function GMFluxContainer:getFluidDepthAt(x, y)
	if x >= 1 and x <= self.texSize and y >= 1 and y <= self.texSize then
		return self.cells[x][y][GMFluxContainer.FluxCell.Depth]
	else
		return 0
	end
end

return GMFluxContainer

Scirpt: -- FluxWorker Script (Revised)

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local actor = script:GetActor()
if actor == nil then
	return
end

local ComputeLua = require(ReplicatedStorage.ComputeLua)

-- Get buffer keys
local CELL_BUFFER_KEY = ComputeLua.GetBufferDataKey("CellBuffer")
local TEX_SIZE_BUFFER_KEY = ComputeLua.GetBufferDataKey("TexSize")
local FLUX_ATTEN_KEY = ComputeLua.GetBufferDataKey("FluxAtten")
local FLUX_STRENGTH_KEY = ComputeLua.GetBufferDataKey("FluxStrength")
local FLUX_SPILL_KEY = ComputeLua.GetBufferDataKey("FluxSpill")

ComputeLua.CreateThread(actor, "StepFlux", function(id, bufferData)
	local cells = bufferData[CELL_BUFFER_KEY]
	local texSize = bufferData[TEX_SIZE_BUFFER_KEY][1]
	local fluxAtten = bufferData[FLUX_ATTEN_KEY][1]
	local fluxStrength = bufferData[FLUX_STRENGTH_KEY][1]
	local fluxSpill = bufferData[FLUX_SPILL_KEY][1]

	local cell = cells[id]

	-- Compute x and y from id
	local x = math.floor((id - 1) / texSize) + 1
	local y = ((id - 1) % texSize) + 1

	-- Helper function to get neighboring cell
	local function getCellAt(x, y)
		if x >= 1 and x <= texSize and y >= 1 and y <= texSize then
			local idx = (x - 1) * texSize + y
			return cells[idx]
		else
			return nil
		end
	end

	local FluxCell = {
		Height = 0,
		Depth = 1,
		Flux_LD_Left = 2,
		Flux_LD_Down = 3,
		Flux_RU_Right = 4,
		Flux_RU_Up = 5,
	}

	local depth = cell[FluxCell.Depth]
	local height = cell[FluxCell.Height]
	local totalHeight = height + depth

	-- Neighbor positions
	local neighbors = {
		left = getCellAt(x - 1, y),
		right = getCellAt(x + 1, y),
		down = getCellAt(x, y - 1),
		up = getCellAt(x, y + 1),
	}

	-- Initialize fluxes
	local fluxes = {left = 0, right = 0, down = 0, up = 0}
	local totalFluxOut = 0

	-- Compute fluxes to neighbors
	for dir, neighbor in pairs(neighbors) do
		if neighbor then
			local neighborTotalHeight = neighbor[FluxCell.Height] + neighbor[FluxCell.Depth]
			local fluxComponent
			if dir == 'left' then
				fluxComponent = cell[FluxCell.Flux_LD_Left]
			elseif dir == 'down' then
				fluxComponent = cell[FluxCell.Flux_LD_Down]
			elseif dir == 'right' then
				fluxComponent = cell[FluxCell.Flux_RU_Right]
			elseif dir == 'up' then
				fluxComponent = cell[FluxCell.Flux_RU_Up]
			end
			local deltaHeight = totalHeight - neighborTotalHeight
			local flux = fluxAtten * fluxComponent + fluxStrength * math.max(0, deltaHeight)
			fluxes[dir] = flux
			totalFluxOut = totalFluxOut + flux
		end
	end

	-- Limit total flux to available depth
	local maxFlux = math.max(0, depth - fluxSpill)
	if totalFluxOut > maxFlux and totalFluxOut > 0 then
		local scale = maxFlux / totalFluxOut
		for dir in pairs(fluxes) do
			fluxes[dir] = fluxes[dir] * scale
		end
	end

	-- Update fluxes in cell
	cell[FluxCell.Flux_LD_Left] = fluxes.left or 0
	cell[FluxCell.Flux_LD_Down] = fluxes.down or 0
	cell[FluxCell.Flux_RU_Right] = fluxes.right or 0
	cell[FluxCell.Flux_RU_Up] = fluxes.up or 0

	-- Return updated cell
	return {CELL_BUFFER_KEY, cell}
end)