substrate
The substrate namespace — 21 functions.
globals/substrate/createBuffer
substrate.createBuffer(opts: BufferOpts) -> TypedBuffer?
Allocate a typed buffer and return its handle.
A "gpu" buffer is storage a compute shader binds; usage adds
"vertex", "index", "indirect" or "readback" on top of the storage
it always has. A "cpu" buffer lives in the scripting heap and reads back
as a flat array of floats.
The handle's write answers whether the words landed: a payload whose end
falls past the end of the buffer is refused whole on both kinds, so the
buffer keeps what it held and the call answers false. writeU32 and
writeBytes answer the same way, against the same extent.
Parameters
optsBufferOpts—{ type, len, kind?, usage?, name? }—typeis"f32","vec3","vec4","quat"or"mat4";kindis"cpu"(the default) or"gpu".nameis the name a dispatch binds a"gpu"buffer by, and the namesubstrate.getBufferandsubstrate.destroyBufferreach it under.
Returns TypedBuffer? — The buffer handle, or nil when the allocation failed — an unknown type or kind, a zero length, or a name that already holds a GPU buffer of another shape. A name holding a buffer of the SAME type and length hands that buffer back, contents and all; substrate.destroyBuffer frees a name whose buffer is the wrong shape.
local pose = substrate.createBuffer({ type = "mat4", len = boneCount })
local field = substrate.createBuffer({ type = "vec3", len = 4096, kind = "gpu" })
local values = pose:read(0, 16):result()
globals/substrate/destroyBuffer
substrate.destroyBuffer(name: string) -> boolean
Free the GPU buffer name denotes, whatever else still holds a handle
to it.
The allocation goes and the name is free to be created again at any type
and length; every handle that pointed at it answers :alive() false. This
is what releases a name whose creating handle is gone, so a build that
re-runs at a different size gets its name back.
Parameters
namestring— The name the buffer was created under.
Returns boolean — True when a GPU buffer under that name was freed.
substrate.destroyBuffer("env.town.xf")
globals/substrate/getBuffer
substrate.getBuffer(name: string) -> TypedBuffer?
The GPU buffer name denotes, as a handle you now hold.
A name is how a dispatch binds a buffer, so the name is what an owner asks
by once the handle it created with has gone out of scope — a .module
that hot-reloaded, a build that ran in an earlier execute. The handle
carries everything createBuffer's does and releases its reference with
:destroy().
Parameters
namestring— The name the buffer was created under.
Returns TypedBuffer? — The buffer handle, or nil when no GPU buffer holds that name.
local xf = substrate.getBuffer("env.town.xf")
local shape = xf and { xf:type(), xf:length() }
globals/substrate/gpuReadback
substrate.gpuReadback(key: string?) -> Readback?
Wrap the key an FFI read handed back as the Readback that polls it.
Every GPU→CPU read reaches the caller through this, so a texture's read
and a buffer's read answer with the same thing.
Parameters
keystring(optional) — The key the read returned.
Returns Readback? — The Readback, or nil when the read did not start.
local pending = substrate.gpuReadback(compute.readTexture3D(handle))
globals/substrate/listBuffers
substrate.listBuffers() -> { NamedBuffer }
Every named GPU buffer the engine holds, in name order.
Each record states id, name, type ("F32", "Vec3", "Vec4",
"Quat", "Mat4"), len in records, and refs — how many holders it
has. This is what states which names are taken and at what shape.
Returns { NamedBuffer } — Array of { id, name, type, len, refs }.
for _, b in ipairs(substrate.listBuffers()) do print(b.name, b.type, b.len) end
modules/substrate/README
require("@builtin/modules/api/engine/substrate") -- substrate (also available as global 'substrate')
Typed data buffers, on the CPU or the GPU — the engine's one buffer primitive. Public Luau surface over the __substrate Internal FFI namespace.
Usage: local substrate = require("@builtin/modules/api/engine/substrate") Also available as global: substrate
modules/substrate/createBuffer
createBuffer(opts: BufferOpts): TypedBuffer?
Allocate a typed buffer and return its handle.
A "gpu" buffer is storage a compute shader binds; usage adds
"vertex", "index", "indirect" or "readback" on top of the storage
it always has. A "cpu" buffer lives in the scripting heap and reads back
as a flat array of floats.
The handle's write answers whether the words landed: a payload whose end
falls past the end of the buffer is refused whole on both kinds, so the
buffer keeps what it held and the call answers false. writeU32 and
writeBytes answer the same way, against the same extent.
Parameters
optsBufferOpts—{ type, len, kind?, usage?, name? }—typeis"f32","vec3","vec4","quat"or"mat4";kindis"cpu"(the default) or"gpu".nameis the name a dispatch binds a"gpu"buffer by, and the namesubstrate.getBufferandsubstrate.destroyBufferreach it under.
local pose = substrate.createBuffer({ type = "mat4", len = boneCount })
local field = substrate.createBuffer({ type = "vec3", len = 4096, kind = "gpu" })
local values = pose:read(0, 16):result()
modules/substrate/destroyBuffer
destroyBuffer(name: string): boolean
Free the GPU buffer name denotes, whatever else still holds a handle
to it.
The allocation goes and the name is free to be created again at any type
and length; every handle that pointed at it answers :alive() false. This
is what releases a name whose creating handle is gone, so a build that
re-runs at a different size gets its name back.
Parameters
namestring— The name the buffer was created under.
substrate.destroyBuffer("env.town.xf")
modules/substrate/getBuffer
getBuffer(name: string): TypedBuffer?
The GPU buffer name denotes, as a handle you now hold.
A name is how a dispatch binds a buffer, so the name is what an owner asks
by once the handle it created with has gone out of scope — a .module
that hot-reloaded, a build that ran in an earlier execute. The handle
carries everything createBuffer's does and releases its reference with
:destroy().
Parameters
namestring— The name the buffer was created under.
local xf = substrate.getBuffer("env.town.xf")
local shape = xf and { xf:type(), xf:length() }
modules/substrate/gpuReadback
gpuReadback(key: string?): Readback?
Wrap the key an FFI read handed back as the Readback that polls it.
Every GPU→CPU read reaches the caller through this, so a texture's read
and a buffer's read answer with the same thing.
Parameters
keystring?(optional) — The key the read returned.
local pending = substrate.gpuReadback(compute.readTexture3D(handle))
modules/substrate/listBuffers
listBuffers(): { NamedBuffer }
Every named GPU buffer the engine holds, in name order.
Each record states id, name, type ("F32", "Vec3", "Vec4",
"Quat", "Mat4"), len in records, and refs — how many holders it
has. This is what states which names are taken and at what shape.
for _, b in ipairs(substrate.listBuffers()) do print(b.name, b.type, b.len) end
modules/substrate/ready
ready(self): boolean
Whether this read has arrived.
Parameters
selfany(optional)
modules/substrate/result
result(self): { number }?
Drain this read as f32 values, nil while it is still on its way.
Parameters
selfany(optional)
modules/substrate/resultBytes
resultBytes(self): buffer?
Drain this read as a Luau buffer, nil while it is still on its way.
Parameters
selfany(optional)
modules/substrate/resultU32
resultU32(self): { number }?
Drain this read as u32 values, nil while it is still on its way.
Parameters
selfany(optional)
modules/substrate/state
state(self): string
Where this read stands, without draining it and without raising:
"pending", "ready", or "unknown" (already drained).
Parameters
selfany(optional)
typed/builtin//modules/api/engine/substrate/substrate/createBuffer
substrate.createBuffer(opts: BufferOpts) -> TypedBuffer?
Allocate a typed buffer and return its handle.
A "gpu" buffer is storage a compute shader binds; usage adds
"vertex", "index", "indirect" or "readback" on top of the storage
it always has. A "cpu" buffer lives in the scripting heap and reads back
as a flat array of floats.
The handle's write answers whether the words landed: a payload whose end
falls past the end of the buffer is refused whole on both kinds, so the
buffer keeps what it held and the call answers false. writeU32 and
writeBytes answer the same way, against the same extent.
Parameters
optsBufferOpts—{ type, len, kind?, usage?, name? }—typeis"f32","vec3","vec4","quat"or"mat4";kindis"cpu"(the default) or"gpu".nameis the name a dispatch binds a"gpu"buffer by, and the namesubstrate.getBufferandsubstrate.destroyBufferreach it under.
Returns TypedBuffer? — The buffer handle, or nil when the allocation failed — an unknown type or kind, a zero length, or a name that already holds a GPU buffer of another shape. A name holding a buffer of the SAME type and length hands that buffer back, contents and all; substrate.destroyBuffer frees a name whose buffer is the wrong shape.
local pose = substrate.createBuffer({ type = "mat4", len = boneCount })
local field = substrate.createBuffer({ type = "vec3", len = 4096, kind = "gpu" })
local values = pose:read(0, 16):result()
typed/builtin//modules/api/engine/substrate/substrate/destroyBuffer
substrate.destroyBuffer(name: string) -> boolean
Free the GPU buffer name denotes, whatever else still holds a handle
to it.
The allocation goes and the name is free to be created again at any type
and length; every handle that pointed at it answers :alive() false. This
is what releases a name whose creating handle is gone, so a build that
re-runs at a different size gets its name back.
Parameters
namestring— The name the buffer was created under.
Returns boolean — True when a GPU buffer under that name was freed.
substrate.destroyBuffer("env.town.xf")
typed/builtin//modules/api/engine/substrate/substrate/getBuffer
substrate.getBuffer(name: string) -> TypedBuffer?
The GPU buffer name denotes, as a handle you now hold.
A name is how a dispatch binds a buffer, so the name is what an owner asks
by once the handle it created with has gone out of scope — a .module
that hot-reloaded, a build that ran in an earlier execute. The handle
carries everything createBuffer's does and releases its reference with
:destroy().
Parameters
namestring— The name the buffer was created under.
Returns TypedBuffer? — The buffer handle, or nil when no GPU buffer holds that name.
local xf = substrate.getBuffer("env.town.xf")
local shape = xf and { xf:type(), xf:length() }
typed/builtin//modules/api/engine/substrate/substrate/gpuReadback
substrate.gpuReadback(key: string?) -> Readback?
Wrap the key an FFI read handed back as the Readback that polls it.
Every GPU→CPU read reaches the caller through this, so a texture's read
and a buffer's read answer with the same thing.
Parameters
keystring(optional) — The key the read returned.
Returns Readback? — The Readback, or nil when the read did not start.
local pending = substrate.gpuReadback(compute.readTexture3D(handle))
typed/builtin//modules/api/engine/substrate/substrate/listBuffers
substrate.listBuffers() -> { NamedBuffer }
Every named GPU buffer the engine holds, in name order.
Each record states id, name, type ("F32", "Vec3", "Vec4",
"Quat", "Mat4"), len in records, and refs — how many holders it
has. This is what states which names are taken and at what shape.
Returns { NamedBuffer } — Array of { id, name, type, len, refs }.
for _, b in ipairs(substrate.listBuffers()) do print(b.name, b.type, b.len) end