Log inGet started

substrate

Updated 5 September 2026

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

  • opts BufferOpts{ type, len, kind?, usage?, name? }type is "f32", "vec3", "vec4", "quat" or "mat4"; kind is "cpu" (the default) or "gpu". name is the name a dispatch binds a "gpu" buffer by, and the name substrate.getBuffer and substrate.destroyBuffer reach 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

  • name string — 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

  • name string — 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

  • key string (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

  • opts BufferOpts{ type, len, kind?, usage?, name? }type is "f32", "vec3", "vec4", "quat" or "mat4"; kind is "cpu" (the default) or "gpu". name is the name a dispatch binds a "gpu" buffer by, and the name substrate.getBuffer and substrate.destroyBuffer reach 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

  • name string — 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

  • name string — 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

  • key string? (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

  • self any (optional)

modules/substrate/result

result(self): { number }?

Drain this read as f32 values, nil while it is still on its way.

Parameters

  • self any (optional)

modules/substrate/resultBytes

resultBytes(self): buffer?

Drain this read as a Luau buffer, nil while it is still on its way.

Parameters

  • self any (optional)

modules/substrate/resultU32

resultU32(self): { number }?

Drain this read as u32 values, nil while it is still on its way.

Parameters

  • self any (optional)

modules/substrate/state

state(self): string

Where this read stands, without draining it and without raising: "pending", "ready", or "unknown" (already drained).

Parameters

  • self any (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

  • opts BufferOpts{ type, len, kind?, usage?, name? }type is "f32", "vec3", "vec4", "quat" or "mat4"; kind is "cpu" (the default) or "gpu". name is the name a dispatch binds a "gpu" buffer by, and the name substrate.getBuffer and substrate.destroyBuffer reach 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

  • name string — 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

  • name string — 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

  • key string (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
  • api
  • reference