About
A simple library to make Parallel Luau easier using Unity like ComputeBuffers with a Dispatcher.

See Getting Started for a guide.

Why Use ComputeLua?
Fully Typed
ComputeLua has typing for functions, classes, everything. Every type will be also checked and will throw an error if it is the incorrect type, so double check your types.

Big Computing Projects
ComputeLua will allow you to create workers that will repeat the same small task all in parallel. It will then compile all the data the workers made into one table and return it back to you to use in the main thread!

Wave/Ocean simulation
Editable mesh generation
Terrain generation
These all require a lot of small information to be processsed very quickly and that is exactly what doing things in parallel does.

Developer Friendly
ComputeLua may be a bit complex to a newcomer, but the design of the library was made keeping in mind of the ease of use.

Benchmarks
Processing the square root of 2, 8192 times and repeating that 100 times

Using 256 works with a batch size of 32 (8192 / 256)

13th Gen Intel(R) Core(TM) i9-13900F @ 2.00 GHz

1.3.0: 20ms average; 2s total; 50fps average

1.2.1: 40ms average; 4s total; 40fps average

11th Gen Intel(R) Core(TM) i7-1195G7 @ 2.92 GHz

1.3.0: 30ms average; 3s total; 45fps average

1.2.1: 70ms average; 7s total; 17fps average

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 total = 0
local count = 0
local function process()
    local start = os.time()
    
    local _, data = Dispatcher:Dispatch("ProcessSquareRoot", 8192):await()
    total += os.time() - start
    count += 1
    
    if count < 100 then
        process()
    else
        print(`Average time to process: {(total / count) * 1000}ms`)
        print(`Total time to process: {total}s`)
    end
end

process()

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)

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 module code: -- 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

Here is my code i want you to implement this in: 
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:-- MainSimulation Script

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ComputeLua = require(ReplicatedStorage.ComputeLua)
local RunService = game:GetService("RunService")
local Workspace = game:GetService("Workspace")
local CollectionService = game:GetService("CollectionService")

-- Simulation parameters
local texSize = 80 -- Adjust as needed (reasonable sizes: 64, 128)
local cellSize = 4 -- Size of each cell in world units
local simPosition = Vector3.new(-100, 0, -200) -- Adjust to move the fluid grid

-- Controllable simulation parameters
local simulationParameters = {
	fluxStrength = 0.25,        -- Controls the fluid flow strength
	fluxAtten = 0.975,          -- Fluid attenuation
	fluxSpill = 0.0,            -- Fluid spill threshold
	simulationSpeed = 0.03,     -- Controls the simulation step interval
	fluidGenerationRate = 100,  -- Controls the rate at which fluid is added per source per step
	heightmapUpdateInterval = 1 -- Interval in seconds to update the heightmap
}

-- Create an instance of the simulation container
local GMFluxContainer = require(game.ServerScriptService.GMFluxContainer)
local sim = GMFluxContainer.new(texSize, simPosition, cellSize, simulationParameters)

-- Visualization parts parent
local visualizationFolder = Instance.new("Folder")
visualizationFolder.Name = "VisualizationParts"
visualizationFolder.Parent = Workspace

-- Create parts to visualize the simulation
local partsGrid = {}
local basePosition = sim.position

for x = 1, sim.texSize do
	partsGrid[x] = {}
	local worldX = basePosition.X + (x - 1) * sim.cellSize
	for y = 1, sim.texSize do
		local part = Instance.new("Part")
		part.Size = Vector3.new(sim.cellSize, 0.1, sim.cellSize) -- Initial minimal size
		local height = sim.cells[x][y][GMFluxContainer.FluxCell.Height]
		part.Position = Vector3.new(worldX, height + 0.05, basePosition.Z + (y - 1) * sim.cellSize)
		part.Anchored = true
		part.Material = Enum.Material.Slate -- Terrain material
		part.Color = Color3.fromRGB(139, 69, 19) -- Brown color for terrain
		part.Transparency = 0
		part.CastShadow = false
		part.Parent = visualizationFolder -- Parent to visualization folder

		-- Add attribute to track particle emitter
		part:SetAttribute("HasParticleEmitter", false)
		partsGrid[x][y] = part
	end
end

-- List of collidable objects (Advanced collisions)
local collidableObjectsFolder = Workspace:FindFirstChild("CollidableObjects")
local collidableObjects = {}
if collidableObjectsFolder then
	collidableObjects = collidableObjectsFolder:GetChildren()
end

-- List of water sources
local waterSources = {
	{x = math.floor(sim.texSize / 2), y = math.floor(sim.texSize / 2)},
	-- Add more sources as needed
}

-- Function to generate the terrain heightmap using raycasts
local function generateTerrainHeightmap(sim)
	local heightmap = {}
	local basePosition = sim.position
	local cellSize = sim.cellSize

	local raycastParams = RaycastParams.new()
	raycastParams.FilterType = Enum.RaycastFilterType.Blacklist
	raycastParams.FilterDescendantsInstances = {visualizationFolder}
	raycastParams.IgnoreWater = false

	local maxTerrainHeight = 1000 -- Adjust based on your terrain
	local direction = Vector3.new(0, -maxTerrainHeight - 100, 0)

	for x = 1, sim.texSize do
		heightmap[x] = {}
		local worldX = basePosition.X + (x - 0.5) * cellSize
		for y = 1, sim.texSize do
			local worldZ = basePosition.Z + (y - 0.5) * cellSize
			local origin = Vector3.new(worldX, maxTerrainHeight, worldZ)
			local raycastResult = Workspace:Raycast(origin, direction, raycastParams)
			local height = 0

			if raycastResult then
				height = raycastResult.Position.Y
			else
				-- No hit, set to base level
				height = 0
			end

			heightmap[x][y] = height
		end
	end

	return heightmap
end

-- Generate the initial terrain heightmap
local heightmap = generateTerrainHeightmap(sim)

-- Set the terrain heightmap in the simulation
sim:cellEncodeHeightmap(heightmap, 1) -- Height factor is 1

-- Create Dispatchers and Workers for ComputeLua
local numWorkers = 256 -- Adjust as needed
local fluxWorker = script:WaitForChild("FluxWorker") -- The Worker script for flux computations
local depthWorker = script:WaitForChild("DepthWorker") -- The Worker script for depth computations

local fluxDispatcher = ComputeLua.CreateDispatcher(numWorkers, fluxWorker)
local depthDispatcher = ComputeLua.CreateDispatcher(numWorkers, depthWorker)

-- Create buffer keys for easy access
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")

-- Simulation loop control
local lastHeightmapUpdate = 0
local simulationTimer = 0
local isProcessing = false

RunService.Heartbeat:Connect(function(deltaTime)
	-- Skip if processing is ongoing
	if isProcessing then
		return
	end

	simulationTimer = simulationTimer + deltaTime

	-- Add fluid at specified water sources
	for _, source in ipairs(waterSources) do
		sim:addFluid(source.x, source.y, simulationParameters.fluidGenerationRate)
	end

	-- Perform simulation steps at the specified interval
	if simulationTimer >= simulationParameters.simulationSpeed then
		simulationTimer = 0
		isProcessing = true

		-- Prepare cell data for ComputeLua
		local cellData = {}
		local index = 1
		for x = 1, sim.texSize do
			for y = 1, sim.texSize do
				cellData[index] = sim.cells[x][y]
				index = index + 1
			end
		end

		-- Set ComputeBuffers
		fluxDispatcher:SetComputeBuffer("CellBuffer", cellData)
		fluxDispatcher:SetComputeBuffer("TexSize", {sim.texSize})
		fluxDispatcher:SetComputeBuffer("FluxAtten", {simulationParameters.fluxAtten})
		fluxDispatcher:SetComputeBuffer("FluxStrength", {simulationParameters.fluxStrength})
		fluxDispatcher:SetComputeBuffer("FluxSpill", {simulationParameters.fluxSpill})

		local numThreads = sim.texSize * sim.texSize

		-- Dispatch StepFlux computations
		fluxDispatcher:Dispatch("StepFlux", numThreads):andThen(function(data)
			local updatedCellData = data[CELL_BUFFER_KEY]

			-- Update sim.cells with computed data
			local index = 1
			for x = 1, sim.texSize do
				for y = 1, sim.texSize do
					sim.cells[x][y] = updatedCellData[index]
					index = index + 1
				end
			end

			-- Now proceed to StepDepth
			depthDispatcher:SetComputeBuffer("CellBuffer", updatedCellData)
			depthDispatcher:SetComputeBuffer("TexSize", {sim.texSize})

			-- Dispatch StepDepth computations
			depthDispatcher:Dispatch("StepDepth", numThreads):andThen(function(data)
				local finalCellData = data[CELL_BUFFER_KEY]

				-- Update sim.cells with computed data
				local index = 1
				for x = 1, sim.texSize do
					for y = 1, sim.texSize do
						sim.cells[x][y] = finalCellData[index]
						index = index + 1
					end
				end

				-- Handle collisions (cannot be parallelized due to Roblox API access)
				sim:handleCollisions(collidableObjects)

				-- Update visualization parts based on sim.cells
				for x = 1, sim.texSize do
					local partsColumn = partsGrid[x]
					local simCellsColumn = sim.cells[x]
					local worldX = basePosition.X + (x - 1) * sim.cellSize
					for y = 1, sim.texSize do
						local part = partsColumn[y]
						local cell = simCellsColumn[y]
						local depth = cell[GMFluxContainer.FluxCell.Depth]
						local height = cell[GMFluxContainer.FluxCell.Height]
						local worldZ = basePosition.Z + (y - 1) * sim.cellSize

						if depth > 0 then
							-- Scale the part to represent the fluid depth
							part.Size = Vector3.new(sim.cellSize, depth, sim.cellSize)
							part.Position = Vector3.new(worldX, height + depth / 2, worldZ)
							-- Set material and color for water
							part.Material = Enum.Material.Water
							-- Color intensity based on depth
							local colorIntensity = math.clamp(depth / 10, 0, 1)
							part.Color = Color3.new(0, 0, colorIntensity)
							part.Transparency = 0.3 -- Less transparent with more depth
						else
							-- Minimal size for empty cells
							part.Size = Vector3.new(sim.cellSize, 0.1, sim.cellSize)
							part.Position = Vector3.new(worldX, height + 0.05, worldZ)
							-- Set material and color for terrain
							part.Material = Enum.Material.Slate -- Terrain material
							part.Color = Color3.fromRGB(139, 69, 19) -- Brown color for terrain
							part.Transparency = 0
						end

						-- Handle particle emitters (same as before)
						-- [Your existing code for particle effects]
					end
				end

				-- Reset processing flag
				isProcessing = false
			end)
		end)
	end

	-- Periodically update the heightmap
	lastHeightmapUpdate = lastHeightmapUpdate + deltaTime
	if lastHeightmapUpdate >= simulationParameters.heightmapUpdateInterval then
		lastHeightmapUpdate = 0

		-- Update the heightmap
		heightmap = generateTerrainHeightmap(sim)
		sim:cellEncodeHeightmap(heightmap, 1)
	end
end)

Worker script: -- 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)

worker script: -- DepthWorker Script

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")

ComputeLua.CreateThread(actor, "StepDepth", function(id, bufferData)
	local cells = bufferData[CELL_BUFFER_KEY]
	local texSize = bufferData[TEX_SIZE_BUFFER_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

	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 fluxOut = cell[FluxCell.Flux_LD_Left] + cell[FluxCell.Flux_LD_Down] +
		cell[FluxCell.Flux_RU_Right] + cell[FluxCell.Flux_RU_Up]
	local fluxIn = 0

	-- 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

	-- Flux in from neighbors
	local leftNeighbor = getCellAt(x - 1, y)
	if leftNeighbor then
		fluxIn = fluxIn + leftNeighbor[FluxCell.Flux_RU_Right]
	end
	local rightNeighbor = getCellAt(x + 1, y)
	if rightNeighbor then
		fluxIn = fluxIn + rightNeighbor[FluxCell.Flux_LD_Left]
	end
	local downNeighbor = getCellAt(x, y - 1)
	if downNeighbor then
		fluxIn = fluxIn + downNeighbor[FluxCell.Flux_RU_Up]
	end
	local upNeighbor = getCellAt(x, y + 1)
	if upNeighbor then
		fluxIn = fluxIn + upNeighbor[FluxCell.Flux_LD_Down]
	end

	-- Update depth
	local newDepth = depth - fluxOut + fluxIn
	newDepth = math.max(newDepth, 0)
	cell[FluxCell.Depth] = newDepth

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



This has computelua in it but it has errors. Can you fix all of them and make sure