globals
The globals namespace — 1296 functions.
globals/ColorSequence/deserialize
ColorSequence.deserialize(data: any?) -> ColorSequenceObj
Rebuild a ColorSequence from a {kind = "ColorSequence", keypoints = {...}} payload produced by :serialize(). Used by scene save/load.
Parameters
dataany(optional) — The serialized payload.
Returns ColorSequenceObj — A fresh ColorSequenceObj with the deserialized keypoints.
local c = ColorSequence.deserialize(savedData)
globals/ColorSequence/new
ColorSequence.new(...: any?) -> ColorSequenceObj
Construct a ColorSequence from a constant color (3-array {r,g,b} or {r=,g=,b=} record), a two-point lerp from c0 to c1, or a keypoints array. An entry of that array is a named { time =, value =, envelope? = } record, a { time, {r,g,b}, envelope? } pair, or a bare {r,g,b} colour whose time is its place in the list — so a list of colours is a ramp through them. envelope is optional and may be a single number (broadcast across channels) or a 3-array. Up to 64 keypoints; the first must anchor at time = 0, the last at time = 1. NaN / Inf rejected. @builtin::systems.particles.curves reads the same three keypoint shapes.
Parameters
...any(optional) —(color),(c0, c1), or({ keypoint, ... })where a keypoint is{time =, value =, envelope? =},{time, {r,g,b}, envelope?}, or{r,g,b}.
Returns ColorSequenceObj — A ColorSequenceObj with :evaluate, :sample, :keypoints, :duration, :serialize, :destroy.
local solid = ColorSequence.new({ 1, 0.5, 0.25 })
local fade = ColorSequence.new({ 1, 1, 1 }, { 0, 0, 0 })
local bow = ColorSequence.new({ { time = 0, value = {1,0,0} }, { time = 0.5, value = {0,1,0} }, { time = 1, value = {0,0,1} } })
local stops = ColorSequence.new({ { 0, {1,0,0} }, { 1, {0,0,1} } })
local ramp = ColorSequence.new({ { 1, 0.85, 0.35 }, { 1, 0.35, 0.05 } })
globals/Entity
Entity: any
Entity wrapper namespace — auto-injected by the prelude from @builtin::modules.entity_reflect. Also reachable as the lowercase entity(...) proxy constructor.
globals/Entity/allTypes
Entity.allTypes() -> { string }
List all registered reflectable component types in this engine instance.
Returns { string } — Array of component name strings.
local types = Entity.allTypes()
globals/Entity/distance
Entity.distance(entityIdA: string, entityIdB: string) -> number?
Compute the straight-line distance between two entities' Transform.position fields.
Parameters
entityIdAstring— First entity id.entityIdBstring— Second entity id.
Returns number? — The distance, or nil when either entity is missing a position.
local d = Entity.distance("player", "enemy")
globals/Entity/getComponents
Entity.getComponents(entityId: string) -> { string }?
List all reflected component types on an entity.
Parameters
entityIdstring— The entity id.
Returns { string }? — Array of component name strings, or nil when the entity has no reflected components.
local comps = Entity.getComponents("player")
globals/Entity/getField
Entity.getField(entityId: string, component: string, field: string) -> any
Get a specific component field value. An engine-struct component reads through reflection; a component declared in Luau reads through its component ref, so one call serves both.
Parameters
entityIdstring— The entity id.componentstring— The component name (e.g."Transform","Model").fieldstring— The field name (e.g."position","tintBlend").
Returns any — The field value, or nil when the entity/component/field is missing. A component name this engine does not declare raises.
local pos = Entity.getField("player", "Transform", "position")
globals/Entity/getName
Entity.getName(entityId: string) -> string?
Get the name of an entity from its Name component.
Parameters
entityIdstring— The entity id.
Returns string? — The name string, or nil when the entity has no Name component.
local name = Entity.getName("player")
globals/Entity/getPosition
Entity.getPosition(entityId: string) -> Vec3?
Get the position of an entity — shortcut for getField(id, "Transform", "position").
Parameters
entityIdstring— The entity id.
Returns Vec3? — The position { x, y, z }, or nil when the entity has no Transform.
local pos = Entity.getPosition("player")
globals/Entity/getRotation
Entity.getRotation(entityId: string) -> Quat?
Get the rotation of an entity — shortcut for getField(id, "Transform", "rotation").
Parameters
entityIdstring— The entity id.
Returns Quat? — The rotation quaternion { x, y, z, w }, or nil when the entity has no Transform.
local rot = Entity.getRotation("player")
globals/Entity/getScale
Entity.getScale(entityId: string) -> Vec3?
Get the scale of an entity — shortcut for getField(id, "Transform", "scale").
Parameters
entityIdstring— The entity id.
Returns Vec3? — The scale { x, y, z }, or nil when the entity has no Transform.
local scale = Entity.getScale("player")
globals/Entity/getSchema
Entity.getSchema(componentName: string) -> { ComponentFieldSchema }?
Get the full schema of a component type — field names + types.
Parameters
componentNamestring— The component name.
Returns { ComponentFieldSchema }? — Array of { name, type } schema entries, or nil when the component is not registered.
local schema = Entity.getSchema("Transform")
globals/Entity/isVisible
Entity.isVisible(entityId: string) -> boolean
Check if an entity is visible — reads the Visible component. Missing component is treated as visible.
Parameters
entityIdstring— The entity id.
Returns boolean — true when visible (or no Visible component is present), false when explicitly hidden.
local visible = Entity.isVisible("player")
globals/Entity/patch
Entity.patch(entityId: string, component: string, fields: { [string]: any }) -> boolean
Patch multiple fields on a single component at once. Serves an engine-struct component and a component declared in Luau alike.
Parameters
entityIdstring— The entity id.componentstring— The component name.fields{ [string]: any }—{ fieldName = value, ... }map of fields to write.
Returns boolean — true when every named field was written. A component name this engine does not declare raises.
Entity.patch("player", "Transform", { position = pos, scale = scl })
globals/Entity/setField
Entity.setField(entityId: string, component: string, field: string, value: any?) -> boolean
Set a specific component field value. An engine-struct component writes through reflection; a component declared in Luau writes through its component ref, so one call serves both.
Parameters
entityIdstring— The entity id.componentstring— The component name.fieldstring— The field name.valueany(optional) — The new value.
Returns boolean — true when the write succeeded, false otherwise. A component name this engine does not declare raises.
Entity.setField("player", "Transform", "position", { x = 0, y = 1, z = 0 })
globals/Entity/setPosition
Entity.setPosition(entityId: string, pos: Vec3)
Set the position of an entity — shortcut for setField(id, "Transform", "position", pos).
Parameters
entityIdstring— The entity id.posVec3— The new position{ x, y, z }.
Entity.setPosition("player", { x = 0, y = 1, z = 0 })
globals/Entity/setScale
Entity.setScale(entityId: string, scale: Vec3)
Set the scale of an entity — shortcut for setField(id, "Transform", "scale", scale).
Parameters
entityIdstring— The entity id.scaleVec3— The new scale{ x, y, z }.
Entity.setScale("player", { x = 1, y = 1, z = 1 })
globals/Entity/snapshot
Entity.snapshot(entityId: string) -> any
Snapshot all reflected components on an entity into a { ComponentName = { field = value, ... }, ... } map.
Parameters
entityIdstring— The entity id.
Returns any — The snapshot table, or nil when the entity does not exist.
local snap = Entity.snapshot("player")
globals/Field/alias
Field.alias(target: string | { string }, description: string?) -> FieldDesc<any>
Alias for one or more existing fields. An alias is an ACCEPTED key that is not stored itself — it routes the written value to the real field(s) it points at, so a component answers to a caller's natural key without hand-rolling translation code, and both the runtime and the LSP recognise the key.
The key works everywhere the fields it targets do: as a
component.add init key, and as a read and a write on the live
component. Reading it returns what the target(s) hold right now.
Two forms:
Field.alias("radius")— rename. The value is written verbatim to the single target field (running that field's normal coercion, so an alias onto an assetRef field resolves the ref), and reads back as that field's value.Field.alias({ "colorR", "colorG", "colorB" })— fan-out. The value is destructured across the targets: an array{a, b, c}positionally, or a named{r=, g=, b=}/{x=, y=, z=}table by the target's position (r/x, g/y, b/z, a/w). It reads back as an array in target order, soc.color = c.colorround-trips.
An alias never replicates and is never persisted — the fields it targets own their own Sync/NoSync, so no mode argument is taken.
Parameters
targetstring | { string }— A single target field name, or an array of target field names.descriptionstring(optional) — Documents the alias for the LSP; nil to leave it undocumented.
Returns FieldDesc<any> — FieldDesc descriptor with kind = "alias".
type = Field.alias("kind")
color = Field.alias({ "colorR", "colorG", "colorB" })
globals/Field/assetRef
Field.assetRef(category: C & string, default: AssetRef<C> | I | nil?, mode: SyncMode, marker: (SerializedMode | string | FieldOptions)?) -> FieldDesc<AssetRef<C, I>>
Typed asset reference field. C is the asset category — a
singleton string type inferred from the category argument
("material", "mesh", "@user/customCategory", etc.). One
constructor handles every category, including user-registered ones.
Default accepts a resolved AssetRef<C> handle, an identity string
(full @library::path form OR a bare leaf name resolved
category-locally via asset.resolve(identity, category)), or nil.
At registration the engine resolves any string default through the
same category-aware resolver public_newindex uses for runtime
writes, so the first read of public.<field> already returns a
resolved envelope — not a raw string.
Parameters
categoryC & string— Asset category as a string literal ("material","mesh", etc.). Inferred intoC.defaultAssetRef<C> | I | nil(optional) — AssetRef envelope, identity string, or nil.modeSyncMode—SyncorNoSync— required.marker(SerializedMode | string | FieldOptions)(optional) —Serialized, a description string, or a{ serialized, description }options table; omit for neither.
Returns FieldDesc<AssetRef<C, I>> descriptor.
material = Field.assetRef("material", "@my-library::materials.gold", Sync)
source = Field.assetRef("bundle", nil, Sync)
globals/Field/bitmask
Field.bitmask(bits: number, default: number?, mode: SyncMode, marker: (SerializedMode | string | FieldOptions)?) -> FieldDesc<number>
Bit-mask number field. bits declares the width the consumer can
address; a default or a write that is not a whole number in
0 .. 2^bits - 1 is rejected, naming the width. Use it wherever a numeric
field is read as a set of bits rather than as a quantity — what the field
reads back is then a mask, so a read-back is evidence the value took.
Parameters
bitsnumber— How many bits wide the mask is, 1..53.defaultnumber(optional) — Numeric default forpublic.<field>, or nil to leave it unset.modeSyncMode—SyncorNoSync— required.marker(SerializedMode | string | FieldOptions)(optional) —Serialized, a description string, or a{ serialized, description }options table; omit for neither.
Returns FieldDescbitmask constraint.
lightChannels = Field.bitmask(32, 0, Sync)
globals/Field/bool
Field.bool(default: boolean?, mode: SyncMode, marker: (SerializedMode | string | FieldOptions)?) -> FieldDesc<boolean>
Boolean field. nil leaves the field unset.
Parameters
defaultboolean(optional) — Boolean default forpublic.<field>, or nil to leave it unset.modeSyncMode—SyncorNoSync— required.marker(SerializedMode | string | FieldOptions)(optional) —Serialized, a description string, or a{ serialized, description }options table; omit for neither.
Returns FieldDesc
enabled = Field.bool(true, Sync)
globals/Field/color
Field.color(default: color?, mode: SyncMode, marker: (SerializedMode | string | FieldOptions)?) -> FieldDesc<color>
Color field. Default is a color — either
{r = .., g = .., b = .., a = ..?} or {r, g, b, a?}.
Parameters
defaultcolor(optional) — color default forpublic.<field>.modeSyncMode—SyncorNoSync— required.marker(SerializedMode | string | FieldOptions)(optional) —Serialized, a description string, or a{ serialized, description }options table; omit for neither.
Returns FieldDesc
tint = Field.color({ 1, 1, 1, 1 }, Sync)
globals/Field/componentRef
Field.componentRef(componentType: T & string, default: ComponentRef<T> | nil?, mode: SyncMode, marker: (SerializedMode | string | FieldOptions)?) -> FieldDesc<ComponentRef<T>>
Typed component reference field. T is the component-type
name — a singleton string type inferred from the componentType
argument ("Camera", "Transform", "@user/Inventory"). The
engine validates the referent exists and is of the declared type
at every write.
Parameters
componentTypeT & string— Component type name as a string literal. Inferred intoT.defaultComponentRef<T> | nil(optional) — ComponentRef envelope or nil.modeSyncMode—SyncorNoSync— required.marker(SerializedMode | string | FieldOptions)(optional) —Serialized, a description string, or a{ serialized, description }options table; omit for neither.
Returns FieldDesc<ComponentRef
aimCam = Field.componentRef("Camera", nil, NoSync)
globals/Field/dataRef
Field.dataRef(contract: C & string, default: AssetRef<"data"> | string | nil?, mode: SyncMode, marker: (SerializedMode | string | FieldOptions)?) -> FieldDesc<AssetRef<"data">>
Contract-constrained typed-data reference field. Accepts only
.data assets whose dataType contract chain includes contract.
Rides the assetRef machinery (category "data") — dependency graph,
sync, and rehydration behave exactly like Field.assetRef — with the
contract gate enforced through the generic field-constraint hook on
every write and on the registration-time default.
Parameters
contractC & string— The required dataType contract identity. Inferred intoC.defaultAssetRef<"data"> | string | nil(optional) — AssetRef envelope, identity string, or nil.modeSyncMode—SyncorNoSync— required.marker(SerializedMode | string | FieldOptions)(optional) —Serialized, a description string, or a{ serialized, description }options table; omit for neither.
Returns FieldDesc<AssetRef<"data">> descriptor.
weapon = Field.dataRef("weapon", nil, Sync)
globals/Field/entityRef
Field.entityRef(default: EntityRef | string | nil?, mode: SyncMode, marker: (SerializedMode | string | FieldOptions)?) -> FieldDesc<EntityRef>
Entity reference field. Accepts a live entity proxy (EntityRef),
a raw entity-id string, or nil (no target). Writes are normalised to
the plain id string for storage/replication; reads return a live
EntityRef proxy (or nil), so public.<field>:method() and
public.<field>.id work directly without re-resolving.
Parameters
defaultEntityRef | string | nil(optional) — Live entity proxy, entity-id string, or nil.modeSyncMode—SyncorNoSync— required.marker(SerializedMode | string | FieldOptions)(optional) —Serialized, a description string, or a{ serialized, description }options table; omit for neither.
Returns FieldDesc
target = Field.entityRef(nil, Sync)
globals/Field/enum
Field.enum(values: { string }, default: string?, mode: SyncMode, marker: (SerializedMode | string | FieldOptions)?) -> FieldDesc<string>
Closed-set string field. values declares every member; a default or a
write outside the set is rejected with the whole set named. The editor
renders the members as a choice and the LSP completes them.
Parameters
values{ string }— The members, as an array of non-empty, distinct strings.defaultstring(optional) — The default member, or nil to leave the field unset.modeSyncMode—SyncorNoSync— required.marker(SerializedMode | string | FieldOptions)(optional) —Serialized, a description string, or a{ serialized, description }options table; omit for neither.
Returns FieldDescenum constraint.
fit = Field.enum({ "exact", "hull" }, "hull", Sync)
globals/Field/instantiableRef
Field.instantiableRef(default: AssetRef<any> | string | nil?, mode: SyncMode, marker: (SerializedMode | string | FieldOptions)?) -> FieldDesc<AssetRef<any>>
Scene-instantiable asset reference field — accepts ANY asset whose
type can be instantiated into a scene, gated by CAPABILITY rather than a
hardcoded type list. Rides the assetRef machinery with no category filter
(any asset type resolves), and the generic field-constraint hook rejects,
on every write and on the registration-time default, any asset whose type
defines no instantiate method (ref:canInstantiate() is false). A
new scene-instantiable asset type is accepted here the moment it defines
the hook — no edit to this field or its consumers. The uniform
ref:instantiate(target?, opts?) is how a consumer then instantiates the
assigned asset (Asset.component, a viewport drop, a tool argument).
Parameters
defaultAssetRef<any> | string | nil(optional) — AssetRef envelope, identity string, or nil.modeSyncMode—SyncorNoSync— required.marker(SerializedMode | string | FieldOptions)(optional) —Serialized, a description string, or a{ serialized, description }options table; omit for neither.
Returns FieldDesc<AssetRef
source = Field.instantiableRef(nil, Sync)
globals/Field/list
Field.list(element: FieldDesc<any>, mode: SyncMode, marker: (SerializedMode | string | FieldOptions)?) -> FieldDesc<ListValue>
List field — an array of one repeated element type. The element is the Field constructor descriptor every item conforms to, often a Field.struct for a list of records. The list value is an array of the element's value type. Like Field.struct, the engine descends the element schema to resolve nested asset refs into envelopes, so a stack of structs each holding an asset ref has every ref appear in the asset dependency graph, validates each item, and the LSP type-checks the array. The default value is an empty list. The element declares its own Sync or NoSync for typing; the list's own mode governs replication of the whole array as a unit.
Parameters
elementFieldDesc<any>— The Field constructor descriptor each item conforms to.modeSyncMode— Sync or NoSync — required.marker(SerializedMode | string | FieldOptions)(optional) —Serialized, a description string, or a{ serialized, description }options table; omit for neither.
Returns FieldDesc<ListValue> — FieldDesc whose value is an array of the element's values.
globals/Field/number
Field.number(default: number?, mode: SyncMode, marker: (SerializedMode | string | FieldOptions)?) -> FieldDesc<number>
Number field. nil leaves the field unset, so a component can treat an
absent value as "derive this from somewhere else" without a second field
recording whether the first one was authored.
Parameters
defaultnumber(optional) — Numeric default forpublic.<field>, or nil to leave it unset.modeSyncMode—SyncorNoSync— required.marker(SerializedMode | string | FieldOptions)(optional) —Serialized, a description string, or a{ serialized, description }options table; omit for neither.
Returns FieldDesc
positionX = Field.number(0, Sync)
globals/Field/quat
Field.quat(default: quat?, mode: SyncMode, marker: (SerializedMode | string | FieldOptions)?) -> FieldDesc<quat>
Quaternion field. Default is a quat — either
{x = .., y = .., z = .., w = ..} or {x, y, z, w}.
Parameters
defaultquat(optional) — quat default forpublic.<field>.modeSyncMode—SyncorNoSync— required.marker(SerializedMode | string | FieldOptions)(optional) —Serialized, a description string, or a{ serialized, description }options table; omit for neither.
Returns FieldDesc
rotation = Field.quat({ 0, 0, 0, 1 }, Sync)
globals/Field/range
Field.range(min: number?, max: number?, default: number?, mode: SyncMode, marker: (SerializedMode | string | FieldOptions)?) -> FieldDesc<number>
Bounded number field. min and max declare the interval the value
means something in; a default or a write outside it is rejected with the
interval named. Either bound may be nil, leaving that side open. The value
the field reads back is one the system consuming it can use, and a number
that lands outside is reported where it was written.
Parameters
minnumber(optional) — Lowest accepted value, or nil to leave the low side open.maxnumber(optional) — Highest accepted value, or nil to leave the high side open.defaultnumber(optional) — Numeric default forpublic.<field>, or nil to leave it unset.modeSyncMode—SyncorNoSync— required.marker(SerializedMode | string | FieldOptions)(optional) —Serialized, a description string, or a{ serialized, description }options table; omit for neither.
Returns FieldDescrange constraint.
volume = Field.range(0, 1, 1, Sync)
globals/Field/resource
Field.resource(category: C & string, default: AssetRef<C> | Handle<C> | string | nil?, mode: SyncMode, marker: (SerializedMode | string | FieldOptions)?) -> FieldDesc<AssetRef<C> | Handle<C>>
Category-gated RESOURCE field — accepts EITHER a persistent
AssetRef<C> OR a live GPU Handle<C>, gated by category. This is the
renderer-facing field type (e.g. Model.model, Model.material, material
texture slots): content can author a persistent asset OR pass a runtime
handle (renderer.<resource>.create(...)); the component bridges either to
the GPU resource. The category gate still holds — an AssetRef<audio> or a
wrong-category handle (a TextureHandle on a "mesh" slot) is a type error
AND a runtime rejection. Use Field.assetRef instead when the field MUST be
a persistent asset (handles rejected).
With Sync, persistent-asset values replicate to peers; a live GPU
handle value is local by construction and stays local — peers keep the
last replicated asset value.
Parameters
categoryC & string— Resource category string literal ("mesh","texture","material", ...). Inferred intoC.defaultAssetRef<C> | Handle<C> | string | nil(optional) —AssetRef<C>/Handle<C>/ identity string / nil.modeSyncMode—SyncorNoSync— required.marker(SerializedMode | string | FieldOptions)(optional) —Serialized, a description string, or a{ serialized, description }options table; omit for neither.
Returns FieldDesc<AssetRef
model = Field.resource("mesh", nil, Sync)
globals/Field/string
Field.string(default: string?, mode: SyncMode, marker: (SerializedMode | string | FieldOptions)?) -> FieldDesc<string>
String field. nil leaves the field unset.
Parameters
defaultstring(optional) — String default forpublic.<field>, or nil to leave it unset.modeSyncMode—SyncorNoSync— required.marker(SerializedMode | string | FieldOptions)(optional) —Serialized, a description string, or a{ serialized, description }options table; omit for neither.
Returns FieldDesc
label = Field.string("hello", Sync)
globals/Field/struct
Field.struct(schema: FieldSchema, mode: SyncMode, marker: (SerializedMode | string | FieldOptions)?) -> FieldDesc<StructValue>
Struct field — a table whose keys are themselves typed fields. The schema maps each subfield name to its Field constructor descriptor; the struct value is a table holding one value per subfield. Use this instead of Field.table when the table carries asset references or other typed data: the engine descends the schema to resolve nested asset refs into envelopes at registration and at write time, so they appear in the asset dependency graph, validates writes per subfield, and the LSP type-checks the shape. Each subfield declares its own Sync or NoSync for typing; the struct's own mode governs replication of the whole value as a unit.
Parameters
schemaFieldSchema— Map of subfield name to a Field constructor descriptor.modeSyncMode— Sync or NoSync — required.marker(SerializedMode | string | FieldOptions)(optional) —Serialized, a description string, or a{ serialized, description }options table; omit for neither.
Returns FieldDesc<StructValue> — FieldDesc whose value is a table of the subfields' values.
globals/Field/table
Field.table(default: T, mode: SyncMode, marker: (SerializedMode | string | FieldOptions)?) -> FieldDesc<T>
Generic table field. T is the table's shape — usually
inferred from the default value, or supplied explicitly via
an explicit ascription Field.table({} :: MyShape, mode) when the default doesn't cover every
key the runtime will write. The engine accepts any Luau table as
a value at write time; per-shape enforcement is opt-in static
typing only.
Parameters
defaultT— Table value to use as the default forpublic.<field>.modeSyncMode—SyncorNoSync— required.marker(SerializedMode | string | FieldOptions)(optional) —Serialized, a description string, or a{ serialized, description }options table; omit for neither.
Returns FieldDesc
idMap = Field.table({} :: { [string]: string }, NoSync)
globals/Field/taggedRef
Field.taggedRef(tag: string, default: AssetRef<any> | string | nil?, mode: SyncMode, marker: (SerializedMode | string | FieldOptions)?) -> FieldDesc<AssetRef<any>>
Tag-constrained asset reference field — accepts any asset carrying
tag in its .metadata.tags, whatever its type. This is how a slot
states the KIND of asset it takes (a camera behavior, a player visual)
without naming the assets themselves: a new asset becomes assignable the
moment it is tagged, with no edit here or in the consumer. Rides the
assetRef machinery with no category filter — dependency graph, sync and
rehydration behave exactly like Field.assetRef — and the generic
field-constraint hook rejects an untagged asset on every write and on the
registration-time default. asset.list({ fields = { tags = tag } })
enumerates what fits the slot.
Parameters
tagstring— The tag an assigned asset must carry.defaultAssetRef<any> | string | nil(optional) — AssetRef envelope, identity string, or nil.modeSyncMode—SyncorNoSync— required.marker(SerializedMode | string | FieldOptions)(optional) —Serialized, a description string, or a{ serialized, description }options table; omit for neither.
Returns FieldDesc<AssetRef
behavior = Field.taggedRef("cameraBehavior", nil, Sync)
globals/Field/vec2
Field.vec2(default: vec2?, mode: SyncMode, marker: (SerializedMode | string | FieldOptions)?) -> FieldDesc<vec2>
Parameters
defaultvec2(optional)modeSyncModemarker(SerializedMode | string | FieldOptions)(optional)
Returns FieldDesc<vec2>
globals/Field/vec3
Field.vec3(default: vec3?, mode: SyncMode, marker: (SerializedMode | string | FieldOptions)?) -> FieldDesc<vec3>
Vec3 field. Default is a vec3 — either {x = .., y = .., z = ..}
or the 3-element array form {x, y, z}.
Parameters
defaultvec3(optional) — vec3 default forpublic.<field>.modeSyncMode—SyncorNoSync— required.marker(SerializedMode | string | FieldOptions)(optional) —Serialized, a description string, or a{ serialized, description }options table; omit for neither.
Returns FieldDesc
offset = Field.vec3({ 0, 0, 0 }, Sync)
globals/Material
Material: any
Material helpers namespace — auto-injected by the prelude from @builtin::modules.material_utils.
globals/Material/Apply
Material.Apply(entityId: string, materialRef: MaterialRefOrName) -> boolean
PascalCase back-compat alias for apply.
Parameters
entityIdstring— Target entity id.materialRefMaterialRefOrName— Either an AssetRef envelope or a material-name string.
Returns boolean — True on success.
globals/Material/Create
Material.Create(name: string, opts_or_shader: MaterialOpts | string | nil?, props: MaterialOpts?) -> MaterialRef?
PascalCase back-compat alias for create. Accepts the legacy 3-arg form (name, shader_string, opts_table) by folding shader into opts, and the canonical 2-arg form (name, opts).
Parameters
namestring— Material name.opts_or_shaderMaterialOpts | string | nil(optional) — Either an options table OR a legacy shader string.propsMaterialOpts(optional) — Optional extra options table — only used whenopts_or_shaderis a shader string.
Returns MaterialRef? — The AssetRef envelope, or nil on failure.
Material.Create("gold", "pbr", { color = { 1, 0.85, 0.2 } }) -- legacy 3-arg
Material.Create("gold", { shader = "pbr", color = { 1, 0.85, 0.2 } }) -- canonical
Material.Create("neon", { shader = "pbr", emissive_color = { 0.1, 0.9, 1 }, emissive = 5 }) -- cyan glow at 5× intensity
globals/Material/Exists
Material.Exists(name: MaterialRefOrName) -> boolean
PascalCase back-compat alias for exists.
Parameters
nameMaterialRefOrName— AssetRef envelope or material-name string.
Returns boolean — True when the material is registered.
globals/Material/GetProperty
Material.GetProperty(materialName: MaterialRefOrName, propertyName: string) -> any
PascalCase back-compat alias for getProperty.
Parameters
materialNameMaterialRefOrName— AssetRef envelope or material-name string.propertyNamestring— Property name.
Returns any — The property value, or nil when not found.
globals/Material/GetPropertyNames
Material.GetPropertyNames(materialName: MaterialRefOrName) -> { string }?
PascalCase back-compat alias for getPropertyNames.
Parameters
materialNameMaterialRefOrName— AssetRef envelope or material-name string.
Returns { string }? — Array of property names, or nil when the material is not found.
globals/Material/SetProperty
Material.SetProperty(materialName: MaterialRefOrName, property: string, value: any?) -> any
PascalCase alias. Writes the property on the material ASSET by name/ref (a runtime change every entity using it takes; matRef:saveDefinition() writes it into mat.yaml) — distinct from M.setProperty, which targets the material on one entity's model.
Parameters
materialNameMaterialRefOrName— AssetRef envelope or material-name string.propertystring— Property name.valueany(optional) — New value.
Returns any — True on success.
Material.SetProperty("gold", "roughness", 0.1)
globals/Material/SetTexture
Material.SetTexture(materialName: MaterialRefOrName, slot: string, textureRef: string) -> boolean
PascalCase back-compat alias for setTexture.
Parameters
materialNameMaterialRefOrName— AssetRef envelope or material-name string.slotstring— Texture slot name ("albedo","normal", etc.).textureRefstring— Texture reference string.
Returns boolean — True on success.
globals/Material/Update
Material.Update(target: any?, props: { [string]: any }) -> number
PascalCase alias for update.
Parameters
targetany(optional) — Material name / AssetRef, OR an entity — a proxy, an entity-id, or the display name the entity carries.props{ [string]: any }— Table of{ [propertyName] = value }pairs.
Returns number — Number of properties applied.
Material.Update("gold", { base_color = { 1, 0, 0 }, roughness = 0.15 })
globals/Material/apply
Material.apply(entity: any?, materialRef: MaterialRefOrName) -> boolean
Apply a material to an entity's Model / SkinnedModel component by setting its material field. Accepts either an AssetRef envelope (from Material.create) or a bare material-name string. Errors when the entity has no Model or SkinnedModel — a material only renders where there is a mesh.
Parameters
entityany(optional) — The entity to apply to — an entity proxy (recommended: a validated handle to a real entity), an entity-id string, or the display name the entity carries.materialRefMaterialRefOrName— Either an AssetRef envelope or a material-name string.
Returns boolean — True on success.
Material.apply(entityId, "gold")
local mat = Material.create("gold", { ... }); Material.apply(entityId, mat)
globals/Material/create
Material.create(name: string, opts: MaterialOpts?) -> MaterialRef?
Create a named material in the MaterialRegistry. Returns the canonical AssetRef envelope ({ __ref, type="material", name, guid }) — pass directly to Material.apply, the Model / SkinnedModel material field, or any AssetRef<material> consumer.
Parameters
namestring— Material name. Must be a non-empty string.optsMaterialOpts(optional) — Optional material options.shader?,color?,roughness?,metallic?,emissive?(the HDR glow intensity as a number, or a glow colour),emissive_color?(the glow colour),textures?.
Returns MaterialRef? — The AssetRef envelope on success, or nil on failure.
local gold = Material.create("gold", { color = { 1, 0.85, 0.2 }, metallic = 1 })
globals/Material/exists
Material.exists(name: MaterialRefOrName) -> boolean
Check whether a material exists in the registry. Accepts an AssetRef envelope or a bare material name.
Parameters
nameMaterialRefOrName— AssetRef envelope or material-name string.
Returns boolean — True when the material is registered.
if Material.exists("gold") then ... end
globals/Material/getProperty
Material.getProperty(materialName: MaterialRefOrName, propertyName: string) -> any
Read the current value of a material property.
Parameters
materialNameMaterialRefOrName— AssetRef envelope or material-name string.propertyNamestring— Property name ("roughness","metallic","base_color", etc.).
Returns any — The property value, or nil when not found.
local r = Material.getProperty("gold", "roughness")
globals/Material/getPropertyNames
Material.getPropertyNames(materialName: MaterialRefOrName) -> { string }?
List the property names exposed by a registered material.
Parameters
materialNameMaterialRefOrName— AssetRef envelope or material-name string.
Returns { string }? — Array of property names, or nil when the material is not found.
local props = Material.getPropertyNames("gold")
globals/Material/setProperties
Material.setProperties(target: any?, props: { [string]: any }) -> number
Set many material properties in one call. Addresses the target the same
way setProperty does: a material name / AssetRef writes the material ASSET
(affecting every entity using it), an entity proxy / entity-id / entity name writes the
material bound to that entity's Model / SkinnedModel. Each key resolves
against the shader's declared vocabulary, so the spellings create accepts
reach the same uniforms; a key the shader does not expose is skipped, which
lets one patch table serve materials built on different shaders.
Parameters
targetany(optional) — Material name / AssetRef, OR an entity — a proxy, an entity-id, or the display name the entity carries.props{ [string]: any }— Table of{ [propertyName] = value }pairs.
Returns number — Number of properties applied.
Material.setProperties("gold", { roughness = 0.2, metallic = 0.9 })
Material.setProperties(entityId, { base_color = { 1, 0, 0 } })
globals/Material/setProperty
Material.setProperty(target: any?, property: string, value: any?)
Set a material property. Addresses the target the same way getProperty does: pass a material name / AssetRef to write the material ASSET (affecting every entity using it), or an entity — a proxy, an id, or a display name — to write the material bound to that entity's Model / SkinnedModel. Errors when an entity target has no Model or SkinnedModel.
Parameters
targetany(optional) — Material name / AssetRef, OR an entity — a proxy, an entity-id, or the display name the entity carries.propertystring— Property name.valueany(optional) — New value (type depends on the property).
Material.setProperty("gold", "roughness", 0.4) -- by material name
Material.setProperty(entityId, "roughness", 0.4) -- by entity
globals/Material/setTexture
Material.setTexture(materialName: MaterialRefOrName, slot: string, textureRef: any?) -> boolean
Set a texture slot on a named material.
Parameters
materialNameMaterialRefOrName— AssetRef envelope or material-name string.slotstring— Texture slot name ("albedo","normal", etc.).textureRefany(optional) — Texture reference. Formats:"color:r,g,b,a","@builtin::textures.foo", or a render-output guid (camera target, video handle).
Returns boolean — True on success.
Material.setTexture("gold", "albedo", "@builtin::textures.gold")
globals/Material/update
Material.update(target: any?, props: { [string]: any }) -> number
Change an existing material from a property table — the counterpart to
create, taking the same table shape. Addresses its target and counts its
writes the way setProperties does.
Parameters
targetany(optional) — Material name / AssetRef, OR an entity — a proxy, an entity-id, or the display name the entity carries.props{ [string]: any }— Table of{ [propertyName] = value }pairs.
Returns number — Number of properties applied.
Material.update("gold", { color = { 1, 0.85, 0.2 }, roughness = 0.15 })
globals/NumberRange/new
NumberRange.new(min: number, max: number?) -> any
Construct a NumberRange. Pass one number for a constant range
(min == max); pass two for a uniform random range. Reversed
arguments are normalized to ascending order.
Parameters
minnumber— Lower bound.maxnumber(optional) — Upper bound; defaults tomin.
Returns any
local lifetime = NumberRange.new(1.0) -- always 1.0
local speed = NumberRange.new(0.5, 2.0) -- random
globals/NumberSequence/deserialize
NumberSequence.deserialize(data: any?) -> NumberSequenceObj
Rebuild a NumberSequence from a {kind = "NumberSequence", keypoints = {...}} payload produced by :serialize(). Used by scene save/load.
Parameters
dataany(optional) — The serialized payload.
Returns NumberSequenceObj — A fresh NumberSequenceObj with the deserialized keypoints.
local s = NumberSequence.deserialize(savedData)
globals/NumberSequence/new
NumberSequence.new(...: any?) -> NumberSequenceObj
Construct a NumberSequence from one of three signatures: a constant value, a two-point lerp from v0 to v1, or a keypoints array of { time, value, envelope? } records. Envelope defaults to 0 when omitted. Up to 64 keypoints; the first must anchor at time = 0, the last at time = 1. NaN / Inf in time / value / envelope is rejected.
Parameters
...any(optional) —(v),(v0, v1), or({ {time, value, envelope?}, ... }).
Returns NumberSequenceObj — A NumberSequenceObj with :evaluate, :sample, :keypoints, :duration, :serialize, :destroy.
local fade = NumberSequence.new(1.0)
local fadeOut = NumberSequence.new(1.0, 0.0)
local size = NumberSequence.new({{time=0,value=0.5,envelope=0.1},{time=0.5,value=1.5},{time=1,value=0}})
globals/Physics
Physics: any
Physics namespace — collisions, joints, raycasts, body manipulation. Auto-injected by the prelude from @builtin::modules.api.engine.physics. Same table as the lowercase physics alias.
globals/Physics/COLLIDER_COMPONENTS
Physics.COLLIDER_COMPONENTS()
Every collider component, in the order a lookup walks them. Shape is the component's identity, so code that works on "whatever collider this entity has" walks this list rather than guessing a shape.
for _, name in ipairs(Physics.COLLIDER_COMPONENTS) do ... end
globals/Physics/addCollider
Physics.addCollider(entityId: string | entityRef, component: string, config: table?)
Add a collider component to an entity, naming the shape you want.
Parameters
entityIdstring | entityRef— Target entity id.componentstring— One ofPhysics.COLLIDER_COMPONENTS.configtable(optional) — The component's own fields, e.g.{ radius = 0.5 }for a sphere.
Physics.addCollider(id, "SphereCollider", { radius = 0.5 })
globals/Physics/addConstraint
Physics.addConstraint(entityId: string | entityRef, opts: table?)
Add a transform constraint to an entity.
Parameters
entityIdstring | entityRef— Target entity id.optstable(optional) — Optional constraint description (targetEntityId, position, rotation, scale, lookAt, targetPosition, axes, weight).
Physics.addConstraint(id, { targetEntityId = parent, position = true })
globals/Physics/addJoint
Physics.addJoint(entityIdA: string | entityRef, entityIdB: string | entityRef, opts: table?)
Add a Joint component connecting two entities. Accepts either
vec3-style anchor inputs (localAnchor = {x,y,z}) or pre-split
scalar keys (localAnchorX/Y/Z).
Parameters
entityIdAstring | entityRef— Entity that hosts the Joint component.entityIdBstring | entityRef— Connected entity.optstable(optional) — Optional joint description (kind, anchors, axis, stiffness, damping, restLength, maxDistance, breakForce, breakTorque).
Physics.addJoint(a, b, { kind = "fixed" })
Physics.addJoint(a, b, { kind = "hinge", axis = {x=0,y=1,z=0} })
Physics.addJoint(a, b, { kind = "rope", maxDistance = 8 })
Physics.addJoint(a, b, { kind = "fixed", breakForce = 1200, breakTorque = 800 })
globals/Physics/addVelocity
Physics.addVelocity(a: string | entityRef | number | vec3, b: (number | vec3)?, c: number?, d: number?)
Add to the linear velocity of an entity. Same call shapes as
setVelocity.
Parameters
astring | entityRef | number | vec3— dx, a{x, y, z}delta vector, or an entity id (explicit target).b(number | vec3)(optional) — dy, dx, or the delta vector depending on call form.cnumber(optional) — dz or dy depending on call form.dnumber(optional) — Optional dz when targeting an explicit entity.
Physics.addVelocity(0, 5, 0)
Physics.addVelocity(entityId, 0, 5, 0)
Physics.addVelocity(entityId, {x=0, y=5, z=0})
globals/Physics/addWheelCollider
Physics.addWheelCollider(entityId: string | entityRef, config: table?)
Add a WheelCollider to an entity. The entity must be a child (or descendant) of a rigid body — the system walks up the hierarchy to find the Physics component.
Parameters
entityIdstring | entityRef— Target entity id.configtable(optional) — Optional wheel configuration (radius?,suspensionDistance?,springRate?,damperRate?,motorTorque?,brakeTorque?,steerAngle?,forwardFriction?,sidewaysFriction?,is2D?).
Physics.addWheelCollider(id, { radius = 0.35, motorTorque = 500 })
globals/Physics/applyForce
Physics.applyForce(entityIdOrForce: string | entityRef | vec3, force: vec3?)
Apply a force to an entity's rigid body for the next physics step — call every frame for continuous thrust. With one argument the script-context entity is targeted; with two args the explicit entity id wins.
Parameters
entityIdOrForcestring | entityRef | vec3— Entity id (when paired withforce) OR a force vector for the script-context entity.forcevec3(optional) — Optional force vector when targeting an explicit entity.
Physics.applyForce({x=0, y=10, z=0})
Physics.applyForce(entityId, {x=0, y=10, z=0})
globals/Physics/applyForceAtPoint
Physics.applyForceAtPoint(entityId: string | entityRef, force: vec3, point: vec3)
Apply a force at a specific world-space point — generates the matching torque from the lever arm.
Parameters
entityIdstring | entityRef— Target entity id.forcevec3— Force vector.pointvec3— World-space application point.
Physics.applyForceAtPoint(id, {x=0,y=10,z=0}, {x=1,y=0,z=0})
globals/Physics/applyImpulse
Physics.applyImpulse(entityIdOrImpulse: string | entityRef | vec3, impulse: vec3?)
Apply an instantaneous impulse (one-shot velocity change). With one argument the script-context entity is targeted; with two args the explicit entity id wins.
Parameters
entityIdOrImpulsestring | entityRef | vec3— Entity id (withimpulse) OR an impulse vector for the script-context entity.impulsevec3(optional) — Optional impulse vector when targeting an explicit entity.
Physics.applyImpulse({x=0, y=5, z=0})
Physics.applyImpulse(entityId, {x=0, y=5, z=0})
globals/Physics/applyTorque
Physics.applyTorque(entityIdOrTorque: string | entityRef | vec3, torque: vec3?)
Apply a torque to an entity's rigid body for the next physics step — call every frame for continuous spin-up. With one argument the script-context entity is targeted; with two args the explicit entity id wins.
Parameters
entityIdOrTorquestring | entityRef | vec3— Entity id (withtorque) OR a torque vector for the script-context entity.torquevec3(optional) — Optional torque vector when targeting an explicit entity.
Physics.applyTorque({x=0, y=1, z=0})
Physics.applyTorque(entityId, {x=0, y=1, z=0})
globals/Physics/bodyState
Physics.bodyState(entityId: string | entityRef) -> PhysicsBodyState?
Everything the solver holds for one body — its type, mass, centre of mass, inertia, gravity scale, damping, lock flags, CCD, collision groups, sleep state, velocities, the force and torque queued for the next step, its colliders, contacts, joints and transform constraints, and why it is not moving.
Parameters
entityIdstring | entityRef— Entity id or proxy.
Returns PhysicsBodyState? — A PhysicsBodyState — with exists = false for an entity that carries no rigid body — or nil when nothing in the scene answers to that id.
local b = Physics.bodyState(id); print(b.bodyType, b.mass, b.stillness)
if not Physics.bodyState(id).exists then print("no body was built") end
globals/Physics/boxCast
Physics.boxCast(origin: vec3, halfExtents: vec3, direction: vec3, maxDistance: number?, exclude: (string | {string})?) -> table?
Cast a box along a direction and return the first hit.
Parameters
originvec3— Box center at the start of the cast.halfExtentsvec3— Half the size of the box on each axis.directionvec3— Cast direction.maxDistancenumber(optional) — Optional distance limit.exclude(string | {string})(optional) — Optional entity id, or array of entity ids, to exclude from hits. Applied while sweeping rather than to the answer, so a cast that starts inside an excluded collider reports what is behind it.
Returns table? — Hit table { entityId, point, normal, distance, startedInside }, or nil if nothing hit. startedInside is false for a surface the query crossed on its way there, and true when the query's own start already lay inside that collider — where distance is 0, point is the start itself, and normal is the shortest way out of the collider. normal is a unit vector either way.
local hit = Physics.boxCast(o, {x=0.5,y=0.5,z=0.5}, dir, 10)
local hit = Physics.boxCast(o, {x=0.5,y=0.5,z=0.5}, dir, 10, { selfId, carriedId })
globals/Physics/capsuleCast
Physics.capsuleCast(origin: vec3, radius: number, halfHeight: number, direction: vec3, maxDistance: number?, exclude: (string | {string})?) -> table?
Cast an upright capsule along a direction and return the first hit.
This is the sweep that answers whether a body of that shape fits through
a passage: a capsule of radius r reports a hit on anything that leaves
it less than 2 * r of clearance.
Parameters
originvec3— Capsule centre at the start of the cast.radiusnumber— Capsule radius.halfHeightnumber— Distance from the centre to either cap centre. The capsule standshalfHeight + radiustall in each direction.directionvec3— Cast direction.maxDistancenumber(optional) — Optional distance limit.exclude(string | {string})(optional) — Optional entity id, or array of entity ids, to exclude from hits. Applied while sweeping rather than to the answer, so a cast that starts inside an excluded collider reports what is behind it.
Returns table? — Hit table { entityId, point, normal, distance, startedInside }, or nil if nothing hit. startedInside is false for a surface the query crossed on its way there, and true when the query's own start already lay inside that collider — where distance is 0, point is the start itself, and normal is the shortest way out of the collider. normal is a unit vector either way.
local hit = Physics.capsuleCast(o, 0.3, 0.6, dir, 0.5)
local hit = Physics.capsuleCast(o, 0.3, 0.6, dir, 0.5, selfId)
globals/Physics/colliderCount
Physics.colliderCount() -> number
How many colliders the physics world holds. Zero means no ray, cast or overlap fired into this world can hit anything, so it is what separates a query that MISSED from a query fired into a world that holds nothing to hit. Read off the collider set itself, so it costs the same whatever the world holds.
Returns number — colliders across the whole physics world.
if Physics.colliderCount() == 0 then print("nothing here is solid") end
globals/Physics/colliderGeometry
Physics.colliderGeometry(options: table?) -> table?
Read the physics world as drawable triangles: every collider triangulated in world space into one indexed mesh, in GPU buffers ready to draw.
Box, sphere, capsule, cylinder, cone, convex, triangle-mesh and heightfield
colliders return their real surface, and a compound returns its children
folded together; a shape with no triangulation returns its bounding box and
reports exact = false.
options.colors is POSITIONAL over colliderManifest() — entry i colours
collider i — so you can colour by role, shape, entity or anything else you
read there. A position you leave out takes options.defaultColor.
The returned buffers are yours: destroy them when you replace them.
Parameters
optionstable(optional) —{ tessellation = "low"|"medium"|"high", colors = { {r,g,b,a}, ... }, defaultColor = {r,g,b,a} }.
Returns table? — { vertices, indices, vertexCount, indexCount, colliders } where each entry of colliders is { entity, colliderName?, shapeType, role, exact, firstIndex, indexCount }.
local geo = Physics.colliderGeometry({ tessellation = "high" })
globals/Physics/colliderManifest
Physics.colliderManifest() -> table
List every physics collider in the world with what it is and what it takes part in — no geometry, so it is the cheap read to make before deciding what to do with each one.
role is one of static, dynamic, kinematic, sensor. A sensor
is a collider the simulation holds as one, reported ahead of the body type
behind it, and a collider with no rigid body is static. exact says whether colliderGeometry would return this
collider's true surface or its bounding box.
Every collider of one entity shares its entity, so this is what to key
per-object decisions on. The order is stable across calls over an unchanged
world, which is what makes colliderGeometry's positional colours usable.
Returns table — Array of { entity, colliderName?, shapeType, role, exact }.
for _, c in ipairs(Physics.colliderManifest()) do print(c.entity, c.role) end
globals/Physics/colliderOn
Physics.colliderOn(entityId: string | entityRef) -> string?
Which collider component an entity carries, or nil when it carries none.
Parameters
entityIdstring | entityRef— Target entity id.
Returns string? The component name, e.g. "SphereCollider".
local which = Physics.colliderOn(id)
globals/Physics/colliderShapes
Physics.colliderShapes(entityId: string | entityRef) -> table
Read an entity's resolved physics collider shape(s) as the physics engine sees them, including auto-sized colliders.
shapeType is one of box, sphere, capsule, convex, mesh,
heightfield, compound, other — the shape the simulation is
running, so a mesh collider reads mesh.
params carries half-extents for a box, radius for a sphere, radius
and half-height for a capsule, and the collider's bounding half-extents
for the shapes that have no parametric description. A convex collider
reports its outline in linePoints instead.
Parameters
entityIdstring | entityRef— Target entity id.
Returns table — Array of resolved collider shapes (empty if none): { shapeType, position, rotation, params, linePoints, name? }.
local shapes = Physics.colliderShapes(id)
globals/Physics/contacts
Physics.contacts(entityId: string | entityRef) -> { PhysicsContact }
Every contact one body's colliders are in right now, with the other entity, the normal, how deeply the two interpenetrate, the impulse the last step applied, and each contact point.
Parameters
entityIdstring | entityRef— Entity id or proxy.
Returns { PhysicsContact } — An array of PhysicsContact — empty when the body touches nothing, or when the entity carries no rigid body.
for _, c in Physics.contacts(id) do print(c.other, c.deepestPenetration) end
globals/Physics/getAngularVelocity
Physics.getAngularVelocity(entityId: (string | entityRef)?) -> vec3?
Read the angular velocity of an entity's rigid body.
Parameters
entityId(string | entityRef)(optional) — Target entity id or proxy; resolves from script context when omitted.
Returns vec3? — Angular velocity in rad/s, or nil if the entity has no rigid body.
local w = Physics.getAngularVelocity(id)
globals/Physics/getGravity
Physics.getGravity() -> vec3
Read the current world gravity vector.
Returns vec3 — Gravity vector in m/s² (negative y is "down" in the default world).
local g = Physics.getGravity()
globals/Physics/getVelocity
Physics.getVelocity(entityId: (string | entityRef)?) -> vec3?
Read the linear velocity of an entity's rigid body.
Parameters
entityId(string | entityRef)(optional) — Target entity id or proxy; resolves from script context when omitted.
Returns vec3? — Velocity in m/s, or nil if the entity has no rigid body.
local v = Physics.getVelocity(id)
globals/Physics/getWheelState
Physics.getWheelState(entityId: string | entityRef) -> table?
Read a wheel collider's runtime state. Reads the native component the wheel system writes after each physics step.
Parameters
entityIdstring | entityRef— Target entity id (must carry a WheelCollider component).
Returns table? — { isGrounded, compression, angularVelocity }, or nil if the component is absent.
local state = Physics.getWheelState(id)
globals/Physics/hasLineOfSight
Physics.hasLineOfSight(fromId: string, toId: string) -> boolean
Check whether two entities have line-of-sight between their origins.
Parameters
fromIdstring— Viewer entity id.toIdstring— Target entity id.
Returns boolean — true when no collider sits between them (including coincident origins), false otherwise.
if Physics.hasLineOfSight(a, b) then ... end
globals/Physics/ignoreCollision
Physics.ignoreCollision(entityIdA: string | entityRef, entityIdB: string | entityRef, ignore: boolean?)
Toggle ignored-collision state between two specific entities.
Parameters
entityIdAstring | entityRef— First entity id.entityIdBstring | entityRef— Second entity id.ignoreboolean(optional) — Whentrue(default) collisions between the pair are skipped.
Physics.ignoreCollision(a, b, true)
globals/Physics/isSleeping
Physics.isSleeping(entityId: (string | entityRef)?) -> boolean?
Whether an entity's rigid body is currently asleep (at rest and not simulating). A body sleeps once it stops moving, to save simulation cost.
Parameters
entityId(string | entityRef)(optional) — Target entity id or proxy; resolves from script context when omitted.
Returns boolean? — true if asleep, false if awake, or nil if the entity has no rigid body.
if Physics.isSleeping(id) then Physics.wakeUp(id) end
globals/Physics/jointBreaks
Physics.jointBreaks() -> table
Every joint that has broken since the last call to this function.
A joint breaks when the reaction it carries exceeds the breakForce
(newtons of linear reaction) or breakTorque (the angular row of the same
reaction) its joint was given; each joint
reports once and its constraint is already released when the record
arrives. The 256 most recent are kept: a structure that comes apart while
nothing reads them drops the oldest beyond that, as the engine's own queue
does beyond 1024.
Returns table — Array of { entityId, connectedEntityId, kind, impulse, angularImpulse, force, torque, position }, oldest first.
for _, e in ipairs(Physics.jointBreaks()) do print(e.entityId, e.force) end
globals/Physics/jointReaction
Physics.jointReaction(entityId: string | entityRef) -> table?
The load an entity's joint is carrying right now, as the constraint
solver resolved it on the last physics step. This is the same quantity a
break threshold is measured against, so it is what to size breakForce
and breakTorque from.
Parameters
entityIdstring | entityRef— Entity carrying the Joint component.
Returns table? — { impulse, angularImpulse, force, torque, position }, or nil when the entity owns no joint.
local r = Physics.jointReaction(id); print(r and r.force)
globals/Physics/observe
Physics.observe(entityId: (string | entityRef)?, opts: table?) -> PhysicsObservation?
Read the solver's own state — the world's accounting, and what it
holds for each body plus why it is not moving one. Every value comes off
the simulation rather than the Physics component, so a write the solver
refused or clamped reads back as what it kept. Answers in edit mode as
well as play mode.
Parameters
entityId(string | entityRef)(optional) — Report on this one entity. Omit for every body in the world.optstable(optional) —{ bodies: boolean?, contactPoints: boolean? }—bodies = falsebuilds the world accounting alone, andcontactPoints = falsekeeps each contact pair's normal, depth, impulse and point count while leaving out the individual points. Both default to true.
Returns PhysicsObservation? — A PhysicsObservation, or nil when entityId names nothing in the scene. bodies is an array, not a table keyed by entity id — each entry names its own entity in entity.
local o = Physics.observe(); for _, b in o.bodies do print(b.entity, b.stillness) end
local o = Physics.observe(id); print(o.bodies[1].stillness, o.bodies[1].stillnessDetail)
globals/Physics/onJointBreak
Physics.onJointBreak(fn: (table) -> ()) -> () -> ()
Call fn for every joint that breaks from now on, with the same record
jointBreaks returns.
Parameters
fn(table) -> ()— Receives one break record per broken joint.
Returns () -> () — A function that removes this listener.
local off = Physics.onJointBreak(function(e) print(e.kind, e.force, e.position) end)
globals/Physics/overlapSphere
Physics.overlapSphere(center: vec3, radius: number) -> table
Find every entity id whose colliders overlap a sphere.
Parameters
centervec3— Sphere center in world space.radiusnumber— Sphere radius.
Returns table — Array of overlapping entity ids.
local ids = Physics.overlapSphere({x=0,y=0,z=0}, 5)
globals/Physics/pumpJointBreaks
Physics.pumpJointBreaks()
Deliver every joint break the simulation has recorded to the registered
listeners. An enabled Joint component calls this each tick, so listeners
fire on their own wherever joints come from that component. A joint made by
writing ecs.PhysicsJoint directly has no such tick behind it — call this
each frame, or poll jointBreaks, to deliver its breaks.
Physics.pumpJointBreaks()
globals/Physics/raycast
Physics.raycast(origin: vec3, direction: vec3, maxDistance: number?, exclude: (string | {string})?) -> table?
Cast a ray and return the first hit. Answers from COLLIDERS ALONE: a
mesh that renders but carries no collider is not in the physics world, so a
ray fired through it reports the same nil a ray through open air does.
renderer.raycast answers the same ray against the geometry the renderer
DRAWS, which is what reads the surface of a terrain, a procedurally
generated mesh, or any plain Model.
Parameters
originvec3— Ray origin in world space.directionvec3— Ray direction (does not need to be unit-length; the engine normalises).maxDistancenumber(optional) — Maximum distance along the ray (defaults to 1000).exclude(string | {string})(optional) — Optional entity id, or array of entity ids, to exclude from hits.
Returns table? — Hit table { entityId, point, normal, distance, startedInside }, or nil if nothing hit. startedInside is false for a surface the query crossed on its way there, and true when the query's own start already lay inside that collider — where distance is 0, point is the start itself, and normal is the shortest way out of the collider. normal is a unit vector either way. A nil says the ray met no COLLIDER, which Physics.colliderCount() separates from a world that holds none for it to meet.
local hit = Physics.raycast({x=0,y=2,z=0}, {x=0,y=-1,z=0})
local hit = Physics.raycast(origin, dir, 50, { selfId, carriedId })
if Physics.colliderCount() == 0 then hit = renderer.raycast(eye, down, 200) end
globals/Physics/raycastAll
Physics.raycastAll(origin: vec3, direction: vec3, maxDistance: number?, maxHits: number?, exclude: (string | {string})?) -> table
Cast a ray and return every hit up to maxHits. Answers from COLLIDERS
ALONE, so a rendered mesh with no collider is absent from the result;
renderer.raycastAll answers the same ray against the geometry the
renderer draws.
Parameters
originvec3— Ray origin in world space.directionvec3— Ray direction.maxDistancenumber(optional) — Optional distance limit along the ray.maxHitsnumber(optional) — Optional cap on the number of hits returned.exclude(string | {string})(optional) — Optional entity id, or array of entity ids, to exclude from hits.
Returns table — Array of hit tables { entityId, point, normal, distance, startedInside } — empty when nothing was hit. startedInside is false for a surface the query crossed on its way there, and true when the query's own start already lay inside that collider — where distance is 0, point is the start itself, and normal is the shortest way out of the collider. normal is a unit vector either way.
local hits = Physics.raycastAll(origin, dir, 50, 4)
globals/Physics/raycastBetween
Physics.raycastBetween(fromId: string, toId: string, maxDistance: number?) -> table?
Cast a ray from one entity toward another and return the first hit.
Parameters
fromIdstring— Origin entity id.toIdstring— Target entity id.maxDistancenumber(optional) — Optional distance cap (default 1000).
Returns table? — Hit table, or nil if the entities are coincident or nothing was hit.
local hit = Physics.raycastBetween(a, b)
globals/Physics/raycastScreen
Physics.raycastScreen(sx: number, sy: number, maxDistance: number?, exclude: (string | {string})?) -> table?
Cast a ray from a screen pixel into the scene and return the first hit. Unprojects the pixel with screenToRay, then casts with raycast.
Parameters
sxnumber— Screen X in viewport-local pixels (the space ofinput.mouse_positionandscreenToRay).synumber— Screen Y in viewport-local pixels.maxDistancenumber(optional) — Maximum distance along the ray (defaults to 1000, matchingraycast).exclude(string | {string})(optional) — Optional entity id, or array of entity ids, to exclude from hits.
Returns table? — Hit table { entityId, point, normal, distance, startedInside }, or nil on a miss or when no camera has rendered yet. startedInside is false for a surface the query crossed on its way there, and true when the query's own start already lay inside that collider — where distance is 0, point is the start itself, and normal is the shortest way out of the collider. normal is a unit vector either way.
local m = input.mouse_position; local hit = Physics.raycastScreen(m[1], m[2])
globals/Physics/removeCollider
Physics.removeCollider(entityId: string | entityRef) -> string?
Remove whichever collider component an entity carries.
Parameters
entityIdstring | entityRef— Target entity id.
Returns string? The component that was removed, or nil when there was none.
Physics.removeCollider(id)
globals/Physics/removeConstraint
Physics.removeConstraint(entityId: string | entityRef, index: number?)
Remove transform constraints from an entity (if any are present).
Parameters
entityIdstring | entityRef— Target entity id.indexnumber(optional) — Optional constraint index (currently ignored — the whole component is removed).
Physics.removeConstraint(id)
globals/Physics/removeJoint
Physics.removeJoint(entityId: string | entityRef)
Remove the Joint component from an entity (if present).
Parameters
entityIdstring | entityRef— Target entity id.
Physics.removeJoint(id)
globals/Physics/removeWheelCollider
Physics.removeWheelCollider(entityId: string | entityRef)
Remove the WheelCollider component from an entity (if present).
Parameters
entityIdstring | entityRef— Target entity id.
Physics.removeWheelCollider(id)
globals/Physics/setAngularDamping
Physics.setAngularDamping(entityIdOrDamping: string | entityRef | number, damping: number?)
Set angular damping on an entity's rigid body. One-arg form targets the script-context entity.
Parameters
entityIdOrDampingstring | entityRef | number— Entity id (withdamping) OR damping value (script-context entity).dampingnumber(optional) — Optional explicit damping when targeting another entity.
Physics.setAngularDamping(0.1)
Physics.setAngularDamping(entityId, 0.1)
globals/Physics/setAngularVelocity
Physics.setAngularVelocity(a: string | entityRef | number | vec3, b: (number | vec3)?, c: number?, d: number?)
Set the angular velocity of an entity (radians/sec). Same call
shapes as setVelocity.
Parameters
astring | entityRef | number | vec3— x-component, a{x, y, z}vector, or an entity id (explicit target).b(number | vec3)(optional) — y-component, x-component, or the vector depending on call form.cnumber(optional) — z-component or y-component depending on call form.dnumber(optional) — Optional z-component when targeting an explicit entity.
Physics.setAngularVelocity(0, 0, 1)
Physics.setAngularVelocity(entityId, 0, 0, 1)
Physics.setAngularVelocity(entityId, {x=0, y=0, z=1})
globals/Physics/setBodyType
Physics.setBodyType(entityId: string | entityRef, bodyType: string)
Change a rigid body's type at runtime. Mass, colliders, and joints are preserved — only the body's response to forces and position writes changes.
Parameters
entityIdstring | entityRef— Target entity id.bodyTypestring— One of"dynamic","kinematic","static".
Physics.setBodyType(entityId, "kinematic")
globals/Physics/setCcdEnabled
Physics.setCcdEnabled(entityIdOrEnabled: string | entityRef | boolean, enabled: boolean?)
Enable or disable continuous collision detection on an entity's rigid body. One-arg form targets the script-context entity.
Parameters
entityIdOrEnabledstring | entityRef | boolean— Entity id (withenabled) OR boolean (script-context entity).enabledboolean(optional) — Optional explicit boolean when targeting another entity.
Physics.setCcdEnabled(true)
Physics.setCcdEnabled(entityId, true)
globals/Physics/setCollisionGroups
Physics.setCollisionGroups(entityId: string | entityRef, membership: number, filter: number)
Set the collision-group membership and filter bitmasks on an
entity's colliders. Adds a CollisionGroup component if missing.
Parameters
entityIdstring | entityRef— Target entity id.membershipnumber— Bitmask: which groups this collider belongs to.filternumber— Bitmask: which groups this collider can collide with.
Physics.setCollisionGroups(id, 0x0001, 0xFFFF)
globals/Physics/setGravity
Physics.setGravity(gravity: vec3)
Replace the world gravity vector.
Parameters
gravityvec3— New gravity vector in m/s².
Physics.setGravity({x=0, y=-9.81, z=0})
globals/Physics/setGravityScale
Physics.setGravityScale(entityIdOrScale: string | entityRef | number, scale: number?)
Set the per-entity gravity scale (1.0 = normal, 0.0 = no gravity). One-arg form targets the script-context entity.
Parameters
entityIdOrScalestring | entityRef | number— Entity id (withscale) OR scale value (script-context entity).scalenumber(optional) — Optional explicit scale when targeting another entity.
Physics.setGravityScale(0.5)
Physics.setGravityScale(entityId, 0.5)
globals/Physics/setJointMotor
Physics.setJointMotor(entityId: string | entityRef, targetVelocity: number, maxForce: number)
Set a motor on an entity's joint.
Parameters
entityIdstring | entityRef— Target entity id (must carry a Joint component).targetVelocitynumber— Desired joint velocity.maxForcenumber— Maximum force the motor can apply.
Physics.setJointMotor(id, 5.0, 1000)
globals/Physics/setLinearDamping
Physics.setLinearDamping(entityIdOrDamping: string | entityRef | number, damping: number?)
Set linear damping on an entity's rigid body (0 = no damping). One-arg form targets the script-context entity.
Parameters
entityIdOrDampingstring | entityRef | number— Entity id (withdamping) OR damping value (script-context entity).dampingnumber(optional) — Optional explicit damping when targeting another entity.
Physics.setLinearDamping(0.05)
Physics.setLinearDamping(entityId, 0.05)
globals/Physics/setMass
Physics.setMass(entityIdOrMass: string | entityRef | number, mass: number?)
Set the mass of an entity's rigid body (kg). One-arg form targets the script-context entity.
Parameters
entityIdOrMassstring | entityRef | number— Entity id (withmass) OR mass value (script-context entity).massnumber(optional) — Optional explicit mass when targeting another entity.
Physics.setMass(10)
Physics.setMass(entityId, 10)
globals/Physics/setRotationLocks
Physics.setRotationLocks(entityId: string | entityRef, x: boolean, y: boolean, z: boolean)
Lock or unlock rotation on specific axes.
Parameters
entityIdstring | entityRef— Target entity id.xboolean— Lock rotation about the world X axis.yboolean— Lock rotation about the world Y axis.zboolean— Lock rotation about the world Z axis.
Physics.setRotationLocks(id, false, true, false)
globals/Physics/setTranslationLocks
Physics.setTranslationLocks(entityId: string | entityRef, x: boolean, y: boolean, z: boolean)
Lock or unlock translation on specific axes.
Parameters
entityIdstring | entityRef— Target entity id.xboolean— Lock translation along the world X axis.yboolean— Lock translation along the world Y axis.zboolean— Lock translation along the world Z axis.
Physics.setTranslationLocks(id, false, false, true)
globals/Physics/setVelocity
Physics.setVelocity(a: string | entityRef | number | vec3, b: (number | vec3)?, c: number?, d: number?)
Set the linear velocity of an entity. Accepts (x, y, z) or a
{x, y, z} vector for the script-context entity, or the same
prefixed with an explicit entityId.
Parameters
astring | entityRef | number | vec3— x-component, a{x, y, z}vector, or an entity id (explicit target).b(number | vec3)(optional) — y-component, x-component, or the vector depending on call form.cnumber(optional) — z-component or y-component depending on call form.dnumber(optional) — Optional z-component when targeting an explicit entity.
Physics.setVelocity(0, 10, 0)
Physics.setVelocity(entityId, 0, 10, 0)
Physics.setVelocity(entityId, {x=0, y=10, z=0})
globals/Physics/sphereCast
Physics.sphereCast(origin: vec3, radius: number, direction: vec3, maxDistance: number?, exclude: (string | {string})?) -> table?
Cast a sphere along a direction and return the first hit.
Parameters
originvec3— Sphere center at the start of the cast.radiusnumber— Sphere radius.directionvec3— Cast direction.maxDistancenumber(optional) — Optional distance limit.exclude(string | {string})(optional) — Optional entity id, or array of entity ids, to exclude from hits. Applied while sweeping rather than to the answer, so a cast that starts inside an excluded collider reports what is behind it.
Returns table? — Hit table { entityId, point, normal, distance, startedInside }, or nil if nothing hit. startedInside is false for a surface the query crossed on its way there, and true when the query's own start already lay inside that collider — where distance is 0, point is the start itself, and normal is the shortest way out of the collider. normal is a unit vector either way.
local hit = Physics.sphereCast(o, 0.5, dir, 10)
local hit = Physics.sphereCast(o, 0.5, dir, 10, selfId)
globals/Physics/stepCost
Physics.stepCost() -> PhysicsStepCost?
What the last physics step cost, stage by stage — the same figures
worldState().step carries, for a caller that wants only these. Each
covers that one step rather than a window of them, and consecutive steps
over the same resting scene vary by tens of percent, so several samples
averaged is the honest read of what a step costs.
Returns PhysicsStepCost? — A PhysicsStepCost, or nil on a frame where the pipeline did not step — a paused simulation, or a world still bootstrapping.
local c = Physics.stepCost(); if c then print(c.stepMs, c.narrowPhaseMs) end
globals/Physics/stillnessReasons
Physics.stillnessReasons() -> { string }
Every reason whyStill can answer with, in the order the engine
considers them. Read from the engine, so the list is the one the answers
come from.
Returns { string } — An array of reason names.
for _, reason in Physics.stillnessReasons() do print(reason) end
globals/Physics/touching
Physics.touching(entityId: string | entityRef, otherId: string | entityRef) -> (boolean, number, { PhysicsContactPoint })
Whether two entities are touching, and how deeply.
Parameters
entityIdstring | entityRef— Entity id or proxy.otherIdstring | entityRef— The other entity id or proxy.
Returns (boolean, number, { PhysicsContactPoint }) — (touching, deepestPenetration, points) — deepestPenetration is in metres and 0 for surfaces that meet without overlapping.
local hit, depth = Physics.touching(a, b); print(hit, depth)
globals/Physics/wakeUp
Physics.wakeUp(entityId: (string | entityRef)?)
Wake an entity's sleeping rigid body so it resumes simulating. The
motion setters (applyImpulse, setVelocity, setAngularVelocity) wake
the body for you; call this to wake one explicitly.
Parameters
entityId(string | entityRef)(optional) — Target entity id or proxy; resolves from script context when omitted.
Physics.wakeUp(id)
globals/Physics/whyStill
Physics.whyStill(entityId: string | entityRef) -> (string?, string?)
Why the solver is not moving a body. Returns nil when it IS moving
it, and otherwise one of noBody, simulationNotStepping, disabled,
static, kinematic, infiniteMass, translationLocked,
gravityDisabled, asleep, outsideIsland, resting, aboutToMove —
the nearest cause, so the answer names the thing to change. A second return
carries the detail: which collider it rests on and how deeply, what its
effective gravity works out to, and so on.
Parameters
entityIdstring | entityRef— Entity id or proxy.
Returns (string?, string?) — (reason, detail).
local why, detail = Physics.whyStill(id); if why then print(why, detail) end
globals/Physics/worldState
Physics.worldState() -> PhysicsWorldState
How many bodies, colliders, joints and contacts the simulation holds
right now, with world gravity, the timestep, whether the pipeline is
stepping at all, and what the last step cost. Counted off the solver, so
a body that failed to build is absent here while its Physics component
still exists.
Returns PhysicsWorldState — A PhysicsWorldState.
local w = Physics.worldState(); print(w.bodies.awake .. "/" .. w.bodies.total .. " awake")
print(Physics.worldState().contacts.touchingPairs .. " pairs touching")
globals/Transform
Transform: any
Transform helpers namespace — auto-injected by the prelude from @builtin::modules.transform. Positions, distances, directions, quaternion constructors, and the look-at builders lookRotation (pure) and lookAt (aims an entity). Same table as the lowercase transform alias.
globals/Transform/direction
Transform.direction(fromX: number, fromY: number, fromZ: number, toX: number, toY: number, toZ: number) -> (number, number, number)
Normalized direction vector from point A to point B. Returns zeros when the two points coincide (within ~0.001 units).
Parameters
fromXnumber— From x.fromYnumber— From y.fromZnumber— From z.toXnumber— To x.toYnumber— To y.toZnumber— To z.
Returns (number, number, number) — Three numbers dx, dy, dz — the unit direction.
local dx, dy, dz = Transform.direction(0, 0, 0, 1, 0, 0)
globals/Transform/directionBetween
Transform.directionBetween(entityA: string | EntityRef, entityB: string | EntityRef) -> (number, number, number)
Normalized world-space direction from one entity to another, read from their world positions. Returns zeros if either entity can't be resolved.
Parameters
entityAstring | EntityRef— Source entity (id string or proxy).entityBstring | EntityRef— Target entity (id string or proxy).
Returns (number, number, number) — Three numbers dx, dy, dz — the unit direction.
local dx, dy, dz = Transform.directionBetween("cam", "target")
globals/Transform/distance
Transform.distance(x1: number, y1: number, z1: number, x2: number, y2: number, z2: number) -> number
Euclidean distance between two world-space positions.
Parameters
x1number— First point x.y1number— First point y.z1number— First point z.x2number— Second point x.y2number— Second point y.z2number— Second point z.
Returns number — The Euclidean distance.
local d = Transform.distance(0, 0, 0, 1, 1, 1)
globals/Transform/distanceBetween
Transform.distanceBetween(entityA: string | EntityRef, entityB: string | EntityRef) -> number?
Distance between two entities in world space. Each entity's world position is what is measured, so a parent's offset counts toward the distance the way the scene shows it.
Parameters
entityAstring | EntityRef— First entity (id string or proxy).entityBstring | EntityRef— Second entity (id string or proxy).
Returns number? — The Euclidean distance, or nil when either entity can't be resolved.
local d = Transform.distanceBetween("cam", "box")
globals/Transform/euler
Transform.euler(qx: number, qy: number, qz: number, qw: number) -> (number, number, number)
Convert quaternion to euler angles (yaw, pitch, roll) in radians.
Parameters
qxnumber— Quaternion x.qynumber— Quaternion y.qznumber— Quaternion z.qwnumber— Quaternion w.
Returns (number, number, number) — Three numbers yaw, pitch, roll (Y, X, Z rotations).
local yaw, pitch, roll = Transform.euler(0, 0, 0, 1)
globals/Transform/eulerToQuat
Transform.eulerToQuat(yaw: number, pitch: number?, roll: number?) -> (number, number, number, number)
Identity-aware overload of euler-to-quaternion. Uses the negative-yaw
convention shared with quatFromYaw, quatFromYawPitch, lookAtQuat, and
T.euler extraction — so T.euler(T.eulerToQuat(y, p, r)) returns
(y, p, r). Order is yaw (Y) then pitch (X) then roll (Z).
Parameters
yawnumber— Y-axis rotation in radians.pitchnumber(optional) — X-axis rotation in radians. Defaults to 0.rollnumber(optional) — Z-axis rotation in radians. Defaults to 0.
Returns (number, number, number, number) — Four numbers qx, qy, qz, qw.
local qx, qy, qz, qw = Transform.eulerToQuat(math.pi / 2)
globals/Transform/lerp
Transform.lerp(ax: number, ay: number, az: number, bx: number, by: number, bz: number, t: number) -> (number, number, number)
Linearly interpolate between two positions.
Parameters
axnumber— Start x.aynumber— Start y.aznumber— Start z.bxnumber— End x.bynumber— End y.bznumber— End z.tnumber— Interpolation factor[0, 1].
Returns (number, number, number) — Three numbers — the interpolated position.
local x, y, z = Transform.lerp(0, 0, 0, 1, 1, 1, 0.5)
globals/Transform/lerp1
Transform.lerp1(a: number, b: number, t: number) -> number
Linearly interpolate two scalars.
Parameters
anumber— Start value.bnumber— End value.tnumber— Interpolation factor[0, 1].
Returns number — The interpolated scalar.
local v = Transform.lerp1(0, 10, 0.5)
globals/Transform/lerpAngle
Transform.lerpAngle(a: number, b: number, t: number) -> number
Lerp between two angles via the shortest arc; returns a value in [-pi, pi].
Parameters
anumber— Start angle in radians.bnumber— End angle in radians.tnumber— Interpolation factor[0, 1].
Returns number — The interpolated angle, normalized to [-pi, pi].
local a = Transform.lerpAngle(0, math.pi, 0.5)
globals/Transform/localToWorld
Transform.localToWorld(px: number, py: number, pz: number, pqx: number, pqy: number, pqz: number, pqw: number, lx: number, ly: number, lz: number) -> (number, number, number)
Transform a local-space position into world space using a parent pose.
Parameters
pxnumber— Parent position x.pynumber— Parent position y.pznumber— Parent position z.pqxnumber— Parent rotation x.pqynumber— Parent rotation y.pqznumber— Parent rotation z.pqwnumber— Parent rotation w.lxnumber— Local x.lynumber— Local y.lznumber— Local z.
Returns (number, number, number) — Three numbers wx, wy, wz — the world position.
local wx, wy, wz = Transform.localToWorld(px, py, pz, pqx, pqy, pqz, pqw, lx, ly, lz)
globals/Transform/lookAt
Transform.lookAt(entityOrId: string | EntityRef, txOrTarget: any?, ty: any?, tz: number?, up: any?) -> (boolean, string?)
Make an entity face a world position. The target slot accepts three
explicit coordinates, one point as { x, y, z } / { x =, y =, z = } / a
vector, or an entity — an id string, an entity NAME, or a proxy — whose
WORLD position is resolved. A table carrying an entity id reads as that
entity; any other table reads as the point it spells. The subject slot
takes the three entity spellings.
Everything here is world space: the subject and the target are
read as entity(id).position and the aim is written as
entity(id).rotation, so a parent under either one moves the entity and
the aim still lands on the point named.
Returns whether the rotation was written, so a caller that named an entity
the scene does not carry learns the aim did not happen instead of reading
a stale orientation back as the answer.
Parameters
entityOrIdstring | EntityRef— Entity id, name, or proxy for the entity to rotate.txOrTargetany(optional) — A number (world x), a point table, or an entity id / name / proxy whose world position is resolved as the look-at target.tyany(optional) — World y of the target. Omitted whentxOrTargetis a point or an entity.tznumber(optional) — World z of the target. Omitted whentxOrTargetis a point or an entity.upany(optional) — Optional world up hint deciding the roll —{ x, y, z },{ x =, y =, z = }or a vector. World +Y when omitted. It never bends the aim; it only says which way is up around it. When the target slot is an entity or a point this is the third argument, and when it is coordinates the fifth.
Returns (boolean, string?) — True when the entity's world rotation was written, and nil for the second value. The target and up slots take any value, because naming which of the shapes arrived is this call's own job: a value that is none of them comes back as a reason rather than as an error raised out of the argument check. False plus a reason otherwise: "unresolved" when a reference names no entity, "no-transform" when one carries no transform, "incomplete-target" when the target spells no point — coordinates with a y or z missing, or a table carrying neither three numbers nor x/y/z, "incomplete-up" when the up hint spells none either, "degenerate" when the two points coincide so no facing direction exists.
Transform.lookAt("cam", 0, 1, 0)
Transform.lookAt("cam", "box") -- resolve target entity position
Transform.lookAt(cam, box) -- entity proxies for both
Transform.lookAt("cam", { 0, 1, 0 }) -- one point table
Transform.lookAt("cam", "box", { 0, 0, 1 }) -- rolled to a +Z up
globals/Transform/lookAtQuat
Transform.lookAtQuat(fx: number, fy: number, fz: number, tx: number, ty: number, tz: number) -> (number?, number?, number?, number?)
Compute quaternion to look from origin position toward a target.
Returns four components (qx, qy, qz, qw), or nil when the from
and to points are too close to derive a meaningful direction.
Parameters
fxnumber— Origin x.fynumber— Origin y.fznumber— Origin z.txnumber— Target x.tynumber— Target y.tznumber— Target z.
Returns (number?, number?, number?, number?) — Four numbers qx, qy, qz, qw — the look-at quaternion. Nil when degenerate.
local qx, qy, qz, qw = Transform.lookAtQuat(0, 0, 0, 1, 0, 1)
globals/Transform/lookRotation
Transform.lookRotation(fx: number, fy: number, fz: number, tx: number, ty: number, tz: number, ux: number?, uy: number?, uz: number?) -> (number?, number?, number?, number?)
The rotation that aims an entity standing at one world point at another,
with a world up hint deciding the roll. Where lookAtQuat derives the aim
from yaw and pitch alone — clamping the pitch just short of vertical, so a
point directly overhead comes back a twentieth of a degree off — this builds
all three axes, so the aim lands on the point at any elevation and straight
up and straight down are ordinary cases.
The aimed axis is the entity's local -Z, the same forward quatFromBasis,
Transform.lookAt and entity(id):lookAt state and the direction
entity(id).transform.forward reads back.
The up hint is a world direction the entity's own +Y is turned toward as
far as the aim allows; it never bends the forward axis. A hint parallel to
the aim leaves the roll undetermined, and a hint of no length names no
direction — both fall back to a stable roll rather than a NaN.
Parameters
fxnumber— Eye x — where the entity stands.fynumber— Eye y.fznumber— Eye z.txnumber— Target x — the world point it faces.tynumber— Target y.tznumber— Target z.uxnumber(optional) — Up hint x. World +Y when the hint is omitted.uynumber(optional) — Up hint y.uznumber(optional) — Up hint z.
Returns (number?, number?, number?, number?) — Four numbers qx, qy, qz, qw. Nil when the eye and the target coincide, so no facing direction exists.
local qx, qy, qz, qw = Transform.lookRotation(0, 2, 10, 0, 1, 0)
entity("cam").rotation = { Transform.lookRotation(0, 2, 10, 0, 1, 0) }
-- a dutch tilt: the same aim, rolled by leaning the up hint
local q = { Transform.lookRotation(0, 2, 10, 0, 1, 0, 0.2, 1, 0) }
globals/Transform/normalizeAngle
Transform.normalizeAngle(a: number) -> number
Normalize an angle into [-pi, pi].
Parameters
anumber— The angle in radians.
Returns number — The same angle wrapped into [-pi, pi].
local a = Transform.normalizeAngle(3 * math.pi)
globals/Transform/orbit
Transform.orbit(centerX: number, centerY: number, centerZ: number, radius: number, height: number, angle: number) -> (number, number, number, number, number, number, number)
Position + rotation for orbiting around a center point. Returns the world position followed by the orientation that faces the center.
Parameters
centerXnumber— Center x.centerYnumber— Center y.centerZnumber— Center z.radiusnumber— Horizontal distance from the center.heightnumber— Vertical offset fromcenterY.anglenumber— Orbital angle in radians.
Returns (number, number, number, number, number, number, number) — Seven numbers x, y, z, qx, qy, qz, qw.
local x, y, z, qx, qy, qz, qw = Transform.orbit(0, 1, 0, 5, 2, t)
globals/Transform/quatFromAxisAngle
Transform.quatFromAxisAngle(ax: number, ay: number, az: number, angle: number) -> (number, number, number, number)
Create quaternion from axis and angle (radians). Returns the identity quaternion when the axis is degenerate (length < 0.001).
Parameters
axnumber— Axis x.aynumber— Axis y.aznumber— Axis z.anglenumber— Rotation angle in radians.
Returns (number, number, number, number) — Four numbers qx, qy, qz, qw.
local qx, qy, qz, qw = Transform.quatFromAxisAngle(0, 1, 0, math.pi)
globals/Transform/quatFromBasis
Transform.quatFromBasis(rx: number, ry: number, rz: number, ux: number, uy: number, uz: number, fx: number, fy: number, fz: number) -> (number, number, number, number)
Build the rotation whose right, up and forward ARE the given axes. Where
lookAtQuat derives a rotation from a direction alone — yaw and pitch, with
pitch clamped just short of straight up or down and no say in the roll — this
states all three axes, so a view straight down has a defined image-up instead
of whatever the yaw implied. The axes are expected orthonormal and are used as
given: right and up are the entity's local +X and +Y, forward its local
-Z (the direction it faces).
Parameters
rxnumber— Right axis x.rynumber— Right axis y.rznumber— Right axis z.uxnumber— Up axis x.uynumber— Up axis y.uznumber— Up axis z.fxnumber— Forward axis x.fynumber— Forward axis y.fznumber— Forward axis z.
Returns (number, number, number, number) — x, y, z, w of the rotation quaternion.
local qx, qy, qz, qw = Transform.quatFromBasis(1,0,0, 0,1,0, 0,0,-1) -- identity
-- looking straight down with the subject's front toward the top of frame
local qx, qy, qz, qw = Transform.quatFromBasis(1,0,0, 0,0,-1, 0,-1,0)
globals/Transform/quatFromYaw
Transform.quatFromYaw(yaw: number) -> (number, number, number, number)
Create quaternion from yaw (Y-axis rotation) in radians. Uses the
negative-yaw convention shared with quatFromYawPitch, lookAtQuat,
and T.euler extraction — so T.euler(T.quatFromYaw(y)) round-trips
to y.
Parameters
yawnumber— Rotation in radians around the Y axis.
Returns (number, number, number, number) — Four numbers qx, qy, qz, qw.
local qx, qy, qz, qw = Transform.quatFromYaw(math.pi / 2)
globals/Transform/quatFromYawPitch
Transform.quatFromYawPitch(yaw: number, pitch: number) -> (number, number, number, number)
Create quaternion from yaw and pitch in radians.
Parameters
yawnumber— Y-axis rotation in radians.pitchnumber— X-axis rotation in radians.
Returns (number, number, number, number) — Four numbers qx, qy, qz, qw.
local qx, qy, qz, qw = Transform.quatFromYawPitch(0, math.pi / 4)
globals/Transform/quatIdentity
Transform.quatIdentity() -> (number, number, number, number)
Identity quaternion (0, 0, 0, 1).
Returns (number, number, number, number) — Four numbers qx, qy, qz, qw — the identity.
local qx, qy, qz, qw = Transform.quatIdentity()
globals/Transform/quatInverse
Transform.quatInverse(qx: number, qy: number, qz: number, qw: number) -> (number, number, number, number)
Quaternion inverse. Equal to the conjugate for unit quaternions.
Parameters
qxnumber— Quaternion x.qynumber— Quaternion y.qznumber— Quaternion z.qwnumber— Quaternion w.
Returns (number, number, number, number) — Four numbers qx, qy, qz, qw — the inverse.
local ix, iy, iz, iw = Transform.quatInverse(qx, qy, qz, qw)
globals/Transform/quatMul
Transform.quatMul(ax: number, ay: number, az: number, aw: number, bx: number, by: number, bz: number, bw: number) -> (number, number, number, number)
Quaternion multiplication: returns qa * qb (composition: rotate
by qb then qa).
Parameters
axnumber— Left quat x.aynumber— Left quat y.aznumber— Left quat z.awnumber— Left quat w.bxnumber— Right quat x.bynumber— Right quat y.bznumber— Right quat z.bwnumber— Right quat w.
Returns (number, number, number, number) — Four numbers qx, qy, qz, qw — the composed quaternion.
local qx, qy, qz, qw = Transform.quatMul(ax, ay, az, aw, bx, by, bz, bw)
globals/Transform/quatRotateVec
Transform.quatRotateVec(qx: number, qy: number, qz: number, qw: number, vx: number, vy: number, vz: number) -> (number, number, number)
Rotate a 3-vector by a quaternion.
Parameters
qxnumber— Quaternion x.qynumber— Quaternion y.qznumber— Quaternion z.qwnumber— Quaternion w.vxnumber— Vector x.vynumber— Vector y.vznumber— Vector z.
Returns (number, number, number) — Three numbers — the rotated vector.
local rx, ry, rz = Transform.quatRotateVec(qx, qy, qz, qw, 1, 0, 0)
globals/Transform/quatToEuler
Transform.quatToEuler(qx: number, qy: number, qz: number, qw: number) -> (number, number, number)
Convert quaternion to (yaw, pitch, roll). Alias of euler with
the explicit name so callers don't have to remember the order.
Parameters
qxnumber— Quaternion x.qynumber— Quaternion y.qznumber— Quaternion z.qwnumber— Quaternion w.
Returns (number, number, number) — Three numbers yaw, pitch, roll (Y, X, Z rotations).
local yaw, pitch, roll = Transform.quatToEuler(qx, qy, qz, qw)
globals/Transform/readVec3
Transform.readVec3(value: Vec3Input, label: string?) -> { number }
Normalize a vector a caller wrote to a plain { x, y, z } array.
Accepts a positional array {1, 2, 3}, a keyed table
{x =, y =, z =}, or a live vec handle. Missing components read as 0.
Raises when the value is not a vector; label names the caller in
that error.
Parameters
valueVec3Input— The vector to normalize.labelstring(optional) — Name reported in the error when the value is not a vector. Defaults to "Transform".
Returns { number } — A three-element array { x, y, z }.
local v = Transform.readVec3({ x = 1, y = 2, z = 3 })
globals/Transform/slerp
Transform.slerp(ax: number, ay: number, az: number, aw: number, bx: number, by: number, bz: number, bw: number, t: number) -> (number, number, number, number)
Spherical linear interpolation between two quaternions. Picks the shortest path (flips sign if dot < 0). Falls back to lerp+normalize when the two quats are very close (avoids div-by-zero on near-parallel inputs).
Parameters
axnumber— Start quaternion x.aynumber— Start quaternion y.aznumber— Start quaternion z.awnumber— Start quaternion w.bxnumber— End quaternion x.bynumber— End quaternion y.bznumber— End quaternion z.bwnumber— End quaternion w.tnumber— Interpolation factor[0, 1].
Returns (number, number, number, number) — Four numbers qx, qy, qz, qw — the interpolated unit quaternion.
local qx, qy, qz, qw = Transform.slerp(0, 0, 0, 1, 1, 0, 0, 0, 0.5)
globals/Transform/snapVec3
Transform.snapVec3(v: { number }, step: number | Vec3Input) -> { number }
Quantize each component of a vector to the nearest multiple of
step — a number for uniform steps, or a vector for per-axis steps.
A step of 0 on an axis leaves that axis at its exact value.
Parameters
v{ number }— The vector to quantize, as{ x, y, z }.stepnumber | Vec3Input— Uniform step size, or a per-axis vector of step sizes.
Returns { number } — A three-element array { x, y, z } snapped to the step grid.
local v = Transform.snapVec3({ 1.4, 2.6, -0.4 }, 1)
globals/Transform/toQuaternion
Transform.toQuaternion(rotation: any?, label: string?) -> { number }
Normalize a rotation a caller wrote to a { qx, qy, qz, qw }
quaternion. Accepts a quaternion ({x,y,z,w} or {x=,y=,z=,w=}) or
euler DEGREES ({pitch,yaw,roll} or {pitch=,yaw=,roll=}), so one
call site takes whichever form the caller finds natural. This is the
reading every rotation-taking surface in the engine shares, so a
quaternion and euler degrees mean the same thing at all of them.
Raises when the value matches no form; label names the caller in that
error, and a value that is one of the shapes a quaternion helper returns
is named as such along with the packing it goes in as.
Parameters
rotationany(optional) — The rotation to normalize, in any form of theRotationInputunion.labelstring(optional) — Name reported in the error when the value is not a rotation. Defaults to "Transform".
Returns { number } — A four-element array { qx, qy, qz, qw }.
local q = Transform.toQuaternion({ pitch = 0, yaw = 90, roll = 0 })
globals/Transform/tryQuaternion
Transform.tryQuaternion(rotation: any?, label: string?) -> ({ number }?, string?)
Read a rotation a caller wrote WITHOUT raising: returns the
canonical { qx, qy, qz, qw }, or nil and the message describing what
arrived. The forms are the RotationInput union — a quaternion
({x,y,z,w} or {x=,y=,z=,w=}) or euler DEGREES ({pitch,yaw,roll} or
{pitch=,yaw=,roll=}). Takes any value because reporting on a value that
is none of those forms is the whole job; a setter built on this raises the
returned message itself, so the error points at the line that wrote the
value rather than at the reading.
Parameters
rotationany(optional) — The value to read as a rotation.labelstring(optional) — Name reported in the message. Defaults to "Transform".
Returns ({ number }?, string?) — The quaternion { qx, qy, qz, qw }, or nil and the message.
local q, why = Transform.tryQuaternion(value, "myTool")
globals/Transform/vec/add
Transform.vec.add(ax: number, ay: number, az: number, bx: number, by: number, bz: number) -> (number, number, number)
Component-wise vec3 addition.
Parameters
axnumber— First vector x.aynumber— First vector y.aznumber— First vector z.bxnumber— Second vector x.bynumber— Second vector y.bznumber— Second vector z.
Returns (number, number, number) — Three numbers — the sum.
local x, y, z = Transform.vec.add(1, 2, 3, 4, 5, 6)
globals/Transform/vec/cross
Transform.vec.cross(ax: number, ay: number, az: number, bx: number, by: number, bz: number) -> (number, number, number)
Cross product a x b.
Parameters
axnumber— First vector x.aynumber— First vector y.aznumber— First vector z.bxnumber— Second vector x.bynumber— Second vector y.bznumber— Second vector z.
Returns (number, number, number) — Three numbers cx, cy, cz — the cross product.
local cx, cy, cz = Transform.vec.cross(1, 0, 0, 0, 1, 0)
globals/Transform/vec/dot
Transform.vec.dot(ax: number, ay: number, az: number, bx: number, by: number, bz: number) -> number
Dot product of two vec3s.
Parameters
axnumber— First vector x.aynumber— First vector y.aznumber— First vector z.bxnumber— Second vector x.bynumber— Second vector y.bznumber— Second vector z.
Returns number — The scalar dot product.
local d = Transform.vec.dot(1, 0, 0, 0, 1, 0)
globals/Transform/vec/length
Transform.vec.length(x: number, y: number, z: number) -> number
Euclidean length of a vec3.
Parameters
xnumber— Vector x.ynumber— Vector y.znumber— Vector z.
Returns number — The length.
local len = Transform.vec.length(1, 2, 3)
globals/Transform/vec/normalize
Transform.vec.normalize(x: number, y: number, z: number) -> (number, number, number)
Normalize a vec3. Returns zeros when the input is degenerate (length < 1e-8).
Parameters
xnumber— Vector x.ynumber— Vector y.znumber— Vector z.
Returns (number, number, number) — Three numbers — the unit-length vec3.
local nx, ny, nz = Transform.vec.normalize(0, 5, 0)
globals/Transform/vec/scale
Transform.vec.scale(x: number, y: number, z: number, s: number) -> (number, number, number)
Component-wise scalar multiplication of a vec3.
Parameters
xnumber— Vector x.ynumber— Vector y.znumber— Vector z.snumber— Scalar factor.
Returns (number, number, number) — Three numbers — the scaled vec3.
local x, y, z = Transform.vec.scale(1, 2, 3, 2)
globals/Transform/vec/sub
Transform.vec.sub(ax: number, ay: number, az: number, bx: number, by: number, bz: number) -> (number, number, number)
Component-wise vec3 subtraction (a - b).
Parameters
axnumber— First vector x.aynumber— First vector y.aznumber— First vector z.bxnumber— Second vector x.bynumber— Second vector y.bznumber— Second vector z.
Returns (number, number, number) — Three numbers — the difference.
local x, y, z = Transform.vec.sub(4, 5, 6, 1, 2, 3)
globals/Transform/worldToLocal
Transform.worldToLocal(px: number, py: number, pz: number, pqx: number, pqy: number, pqz: number, pqw: number, wx: number, wy: number, wz: number) -> (number, number, number)
Transform a world-space position into a parent's local space.
Parameters
pxnumber— Parent position x.pynumber— Parent position y.pznumber— Parent position z.pqxnumber— Parent rotation x.pqynumber— Parent rotation y.pqznumber— Parent rotation z.pqwnumber— Parent rotation w.wxnumber— World x.wynumber— World y.wznumber— World z.
Returns (number, number, number) — Three numbers lx, ly, lz — the local position.
local lx, ly, lz = Transform.worldToLocal(px, py, pz, pqx, pqy, pqz, pqw, wx, wy, wz)
globals/animation/animating
animation.animating() -> { AnimationBody }
The bodies the engine measured a changing pose on — what is animating right now.
Returns { AnimationBody } — An array of AnimationBody.
for _, b in animation.animating() do print(b.entity, b.clips[1] and b.clips[1].name) end
globals/animation/bodies
animation.bodies() -> { AnimationBody }
Every body the engine holds animation state for.
Returns { AnimationBody } — An array of AnimationBody.
for _, b in animation.bodies() do print(b.entity, b.matched .. "/" .. b.total) end
globals/animation/body
animation.body(entityId: string | EntityRef) -> AnimationBody?
The report for one body, or nil when the engine holds no animation state for it. Accepts the body itself or any ancestor of it, so a character root answers for the skinned body underneath it.
Parameters
entityIdstring | EntityRef— The entity's stable id, or an EntityRef.
Returns AnimationBody? — An AnimationBody, or nil.
local b = animation.body(hero.id); print(b and b.reason)
globals/animation/clips
animation.clips(entityId: string | EntityRef) -> { AnimationClip }
The clips contributing to a body's pose right now, with their playheads and their retarget coverage.
Parameters
entityIdstring | EntityRef— The entity's stable id, or an EntityRef.
Returns { AnimationClip } — An array of AnimationClip.
for _, c in animation.clips(hero.id) do print(c.name, c.time, c.matched) end
globals/animation/coverage
animation.coverage(entityId: string | EntityRef) -> (number, number)
How many of a body's bones the clips driving it actually reach.
Returns (matched, total). A clip that retargets onto nothing reads
(0, 50) while its playhead advances; a partial retarget reads its own
count, so 3 of 50 is as visible as none.
Parameters
entityIdstring | EntityRef— The entity's stable id, or an EntityRef.
Returns (number, number) — (matched, total).
local m, t = animation.coverage(hero.id); print(m .. "/" .. t)
globals/animation/declare
animation.declare(entityId: string, facts: { [string]: any })
Publish what an animator is running on a body, so the observation names
its clips, playheads and retarget coverage beside the pose the engine
measures. The shipped animators declare through AnimGraph:publish; a
custom animator calls this itself, once per frame it runs.
Parameters
entityIdstring— The body the animator drives.facts{ [string]: any }—{ driver, bound, playing, outputKind, failure, clips }, where each clip is{ name, nodeKind, time, duration, playing, finished, looping, weight, matched, total, unmatched }.
animation.declare(body.id, { driver = "MyAnimator", playing = true, clips = {} })
globals/animation/forget
animation.forget(entityId: string)
Drop the declaration and the pose evidence the engine holds for one body. An animator calls this when it releases a body, so the observation reports the body as undriven from the next frame.
Parameters
entityIdstring— The body to drop.
animation.forget(body.id)
globals/animation/observe
animation.observe() -> AnimationObservation
Report what the engine is posing right now and why a body is not moving. One read covering every body the engine holds animation state for, each with the pose evidence the engine measured on its armature beside the clips the animator driving it declared. Answers in edit mode as well as play mode.
Returns AnimationObservation — An AnimationObservation.
local a = animation.observe(); print(a.animatingCount, a.riggedBodyCount)
for _, b in animation.observe().bodies do print(b.entity, b.animating, b.reason) end
globals/animation/whyStill
animation.whyStill(entityId: string | EntityRef) -> (string?, string?)
Why the body on an entity is not animating. Returns nil when it IS
animating, and otherwise one of deactivated, noRiggedSkeleton,
noGraph, clipUnreadable, noOutputNode, retargetMatchedNoRoles,
stopped, finished, paused, poseNotApplied, poseUnchanged — the
nearest cause, so the answer names the thing to change. A second return
carries the animator's own words when it could not build a graph.
An entity the engine holds no animation state for is answered from the
entity itself, in the same order the engine resolves a body it does hold:
one carrying no rigged Skeleton is noRiggedSkeleton, and a rigged one
nothing drives is noGraph. An id no entity carries is neither — the
reason is nil and the detail says so.
Parameters
entityIdstring | EntityRef— The entity's stable id, or an EntityRef.
Returns (string?, string?) — (reason, detail).
local why, detail = animation.whyStill(hero.id); if why then print(why, detail) end
globals/assert
assert(cond, msg?) -> value
Assert condition is truthy. Returns value if true.
globals/asset/add_tag
asset.add_tag(ref: RefArg, tag: string)
Add a tag to the asset's .metadata.tags. Idempotent.
Creates the sidecar and the tags array if missing.
Parameters
refRefArg— Any name the asset has.tagstring— Tag to add.
asset.add_tag("brick", "wip")
globals/asset/alias
asset.alias(ref: RefArg, alias: string) -> boolean
Add a name the asset answers to. asset.resolve, leaf
shorthand, and the typed-argument coercion that content refs
travel through all reach the asset by the alias from here on,
exactly as they do by its identity — so a material naming a
shader by its alias resolves, and content written against an
older name keeps working after a rename. The name survives
writes to the asset's own files.
Raises when the name already resolves to a DIFFERENT asset — an
alias extends the identity namespace and never takes a name out
of another asset's hands — and when it is shaped like a guid or
a VFS path, forms that resolve before identity lookup, so an
alias in that shape could never answer.
Parameters
refRefArg— The asset gaining the name.aliasstring— The additional name. Any identity form: a bare leaf (standard) or a scope-qualified path (@builtin::shaders.legacy).
Returns boolean — True when newly added, false when the asset already answered to it.
asset.alias("@builtin::shaders.pbr", "standard")
globals/asset/aliases
asset.aliases(ref: RefArg) -> { string }
The additional names this asset answers to, beyond its own
identity — what asset.alias registered, plus the
package-relative ~pkg.tail form when the asset lives inside a
package.
Parameters
refRefArg— Any name the asset has.
Returns { string } — Array of alias names in canonical identity form.
for _, n in asset.aliases("pbr") do print(n) end
globals/asset/canCreate
asset.canCreate(typeName: string) -> boolean
Whether asset.create can instance typeName: the type declares
creation logic (a behavior.luau onCreate hook) or ships a
template/ skeleton the hookless fallback clones. A type with neither
— one whose instances only arrive by import — answers false. The query
a creation UI derives its offering from, so what it offers is what
asset.create accepts.
Parameters
typeNamestring— Registered asset type (e.g. "material", "scene").
Returns boolean — true when asset.create(typeName, …) can produce one.
if asset.canCreate(kind) then asset.create(kind, name) end
globals/asset/categories
asset.categories() -> { string }
List every asset category the engine currently recognises.
Use to discover valid type argument values for the rest of
asset.*.
Returns { string } — Array of category names.
for _, c in asset.categories() do print(c) end
globals/asset/containing
asset.containing(path: string) -> AssetRef?
Walk path's ancestors and return an AssetRef handle for
the OUTERMOST category-folder containing it (e.g. main.scene
for "/source/scenes/main.scene/scene.json"). Returns nil for
paths outside any registered asset type.
Parameters
pathstring— VFS path to inspect.
Returns AssetRef? — AssetRef handle, or nil.
local a = asset.containing("/source/scenes/main.scene/scene.json")
globals/asset/cpuResident
asset.cpuResident(ref: RefArg, typeName: string?) -> boolean
True when the asset is CPU-resident — a live script-component context
holds it (a component's assetRef field, or an imperative
asset.resolve/ref made while a component is the caller), which is what
warms its bytes into memory. The CPU pool is a different pool from the
device's: asset.observe().cpu lists it, asset.observe().textures /
.meshes list what the device holds, and an asset can be in one and not
the other.
Parameters
refRefArg— Any name the asset has — handle, identity, guid, name or path.typeNamestring(optional) — Category to restrict the match to. Omit to search every category.
Returns boolean — true when a live context holds it.
if asset.cpuResident(ref) then print("bytes are warm") end
globals/asset/create
asset.create(typeName: string, name: string, opts: { [string]: any }?) -> AssetRef
Instance a new asset of an existing type. Runs the type's
behavior.luau onCreate(name, opts) hook to produce the asset's
files, then writes them under /source/<name>.<type>/. This is the
single generic asset-creation API. Refuses to clobber an existing
edit-mode asset unless opts.overwrite = true, which re-authors it in
place and keeps the existing guid (only the checksum changes). Pairs with
asset.exists for content generators that re-run over the same names.
A create made from a script component's callback or a scene entrypoint is
output the world reproduces on every load, so it is filed in the ephemeral
/runtime/assets/ store instead, where the saved manifest never carries a
second copy of it. name and folder spell the same IDENTITY in either
store, so a reference written against that identity resolves the asset
wherever the call filed it, and one generator run from an execute and
from a component names one asset.
Parameters
typeNamestring— Registered asset type to instance (e.g. "material", "texture").namestring— Destination asset name (becomes/source/<name>.<typeName>). A bare identity — passopts.folderto place it in a subfolder rather than spelling a path here. The accepted shape is the type's to declare:^[A-Za-z][A-Za-z0-9_]*$unless itsbehavior.luauexports anamePattern, asguidedoes to takegetting-startedand01-overview. This call names the category FIRST and the asset second. Every otherasset.*call taking both names them the other way round —asset.exists(name, category),asset.tryResolve(ref, category)— so a create-then-check pair readsif not asset.exists(n, t) then asset.create(t, n, opts) end. A call whose two arguments are read into each other, at either end of that pair, is refused and told which way round the call reads.opts{ [string]: any }(optional) — Optional table forwarded to the type'sonCreatehook, minus four framework keys consumed here and never seen by the hook:folder(a relative subfolder under/sourceto author the asset in, so generated content groups instead of accumulating at the source root, and the asset's identity carries that folder as its dotted prefix),into(author INSIDE a resolved container ref),dest(an absolute destination path), andoverwrite(re-author in place, keeping the guid).
Returns AssetRef — The created asset's AssetRef — the SAME interned instance asset.resolve returns (guid/__ref/path + the type's ref methods: :getBytes, :ensureHandle, :serialize, …). Disk-only: nothing is uploaded to CPU/GPU.
local m = asset.create("material", "brick", { shader = "pbr" })
local mesh = asset.create("mesh", "tree", { positions = {...}, indices = {...} })
local mesh = asset.create("mesh", "rock", { positions = {...}, indices = {...}, folder = "terrain/props" })
local mesh = asset.create("mesh", name, { positions = p, indices = i, overwrite = true }) -- idempotent rebuild
globals/asset/declareReferenceArg
asset.declareReferenceArg(call: string, position: number, assetType: string)
Declare that call's argument at 1-based position names an
asset of type, so a string literal written there is recorded as a
reference. The positional counterpart of
asset.declareReferenceField, for a call that takes its asset as a
plain argument — including a world's own spawn helper, which is
where a name most often stops being visible to the reference graph.
A lookup whose asset is its FIRST argument (asset.resolve and its
siblings) is already read and needs no declaration. Only a literal —
or a name the file holds in a top-level string constant — is
recorded; anything computed is reported as a dynamic resolve.
Parameters
callstring— The callee as it is written at a call site.positionnumber— Which argument holds the name, counting from 1.assetTypestring
asset.declareReferenceArg("spawnModel", 3, "mesh")
globals/asset/declareReferenceField
asset.declareReferenceField(call: string, field: string, assetType: string)
Declare that call's options table names an asset of type in
its field, so a string literal written there is recorded as a
reference by whatever writes the file. This is what puts an API
that takes an asset BY NAME into the reference graph: the named
asset becomes a dependency, travels with the content that names it
into a pack or a pull, and a name nothing answers to becomes an
unresolved dependency worldValidation reports and the push gate
refuses. A field holding a TABLE of names — a material's textures
— records every name in it. Declare once, beside the API; a call
taking its asset as the FIRST positional argument is already read
and needs no declaration. Only a literal is recorded; a computed
name resolves at runtime and is reported as a dynamic resolve.
Parameters
callstring— The callee as it is written at a call site.fieldstring— The options-table field holding the name, read at the table's own level.assetTypestring
asset.declareReferenceField("fx.beam", "material", "material")
globals/asset/declareReferenceKey
asset.declareReferenceKey(assetType: string, key: string, refType: string)
Declare that, in a data file belonging to an assetType asset,
the top-level key names an asset of type — a .material's
mat.yaml naming the shader it draws with and the textures it
binds. The names a format holds are references as surely as ones
written in code: recording them carries a material's shader along
with the material into a pack or a pull, and turns a name nothing
answers to into an unresolved dependency instead of a surface that
renders as the magenta error material. A key holding a table of
names records one per entry.
Parameters
assetTypestring— The category owning the file, e.g. "material".keystring— The top-level key holding the name(s).refTypestring
asset.declareReferenceKey("material", "shader", "shader")
globals/asset/deps
asset.deps(ref: RefArg, type: string?) -> DepsResult
Return the asset's outbound dependency graph — every other asset recorded as a content dependency of it.
Parameters
refRefArg— Any name the asset has.typestring(optional) — Category hint (optional).
Returns DepsResult — { deps = { { asset_guid, origin, literal, via, ... } } }.
for _, d in asset.deps("main.scene").deps do print(d.asset_guid) end
globals/asset/describe
asset.describe(typeName: string) -> DescribeResult
The creation contract for an asset type: the parameters its
onCreate(name, opts) hook accepts, as data. kind is "schema"
(typed contract), "legacy" (untyped opts — anything passes), "none"
(template scaffold — takes no opts), or "error" (the type's schema
failed to parse; error says why). contract is the human-readable
rendering validation errors print.
Parameters
typeNamestring— Registered asset type to describe (e.g. "texture").
Returns DescribeResult — the creation contract.
local contract = asset.describe("texture").contract
globals/asset/diagnose
asset.diagnose(ref: RefArg) -> any
Why one asset can or cannot be used, read from the engine rather than
from what the caller asked for. Always carries usable; when false,
reason is one of asset.unusableReasons() and detail is the engine's
own message. primary names the file the type's declared primary list
resolved to, so an asset that loaded a preview image instead of its
payload shows the wrong filename rather than a successful load. The
payload's bytes are read by the engine's own decoder wherever it has one
for that container, so usable is the verdict a load would reach and the
call costs that decode.
Parameters
refRefArg— Any name the asset has — handle, identity, guid, name or path.
Returns any — DiagnoseRecord
local d = asset.diagnose("myTex") if not d.usable then print(d.reason, d.detail) end
globals/asset/exists
asset.exists(name: string, typeName: string) -> boolean
Parameters
namestringtypeNamestring
Returns boolean
globals/asset/get_field
asset.get_field(ref: RefArg, key: string) -> any
Read one top-level field from the asset's .metadata.
Parameters
refRefArg— Any name the asset has.keystring— Field name.
Returns any — Field value or nil.
local author = asset.get_field("brick", "author")
local settings = asset.get_field("tree", "settings") -- → a Luau table
globals/asset/gpuResident
asset.gpuResident(ref: RefArg) -> boolean
True when the device holds a texture or mesh under this asset's guid, read off the inventory the renderer publishes.
Parameters
refRefArg— Any name the asset has — handle, identity, guid, name or path.
Returns boolean — true when the device holds it.
print(asset.gpuResident("@builtin::models.Sample.DamagedHelmet"))
globals/asset/guid
asset.guid(ref: RefArg, type: string?) -> string
Return the guid for an asset.
Parameters
refRefArg— Any name the asset has.typestring(optional) — Category hint (optional).
Returns string — Guid.
local g = asset.guid("@builtin::components.Camera")
globals/asset/has_field
asset.has_field(ref: RefArg, key: string) -> boolean
True when the asset's .metadata carries the named field.
Parameters
refRefArg— Any name the asset has.keystring— Field name.
Returns boolean — True when present.
if asset.has_field("brick", "author") then end
globals/asset/has_tag
asset.has_tag(ref: RefArg, tag: string) -> boolean
True when the asset's .metadata.tags contains tag.
Parameters
refRefArg— Any name the asset has.tagstring— Tag to check for.
Returns boolean — True when present.
if asset.has_tag("brick", "wip") then end
globals/asset/identity
asset.identity(ref: RefArg, type: string?) -> string
Return the canonical identity for an asset.
Parameters
refRefArg— Any name the asset has.typestring(optional) — Category hint (optional).
Returns string — Canonical identity.
local id = asset.identity("brick")
globals/asset/import
asset.import(path: string) -> string?
Import a raw source file NOW and return the produced asset path
(a .bundle for a model, .texture for an image, .audio for a
sound, …), or nil if no importer claims it. This is the deterministic,
on-demand counterpart to the engine's automatic import-on-write: it runs
in the calling task and returns only when the import is complete. Pair it
with a quiet write — vfs.write(path, bytes, { quiet = true }) lands the
raw bytes without firing the automatic importer, then asset.import(path)
imports them under your control, so you can act on the result instead of
polling for the import to appear.
Parameters
pathstring— The raw source VFS path to import (e.g. a just-written.glb).
Returns string? — The produced asset path, or nil when nothing claimed it.
local bundle = asset.import("/zero/source/generated/chest.glb")
globals/asset/inspect
asset.inspect(ref: RefArg, type: string?) -> InspectRecord
Everything known about one asset in a single record: identity, guid,
source, type, scope and origin, its description and tags, the ref methods
its type exposes, and the type's own inspect detail when it declares one.
The read-everything counterpart to asset.resolve, which hands back a ref.
Parameters
refRefArg— AnAssetRef, an identity string, or a path.typestring(optional) — Narrow the resolve to one asset type when assets of several categories answer to the same bare name.
Returns InspectRecord — The inspect record.
local rec = asset.inspect("@builtin::materials.default")
local rec = asset.inspect(name, "mesh"); print(rec.guid, #rec.tags)
globals/asset/list
asset.list(type_or_opts: (AssetCategory | ListOpts)?, scope: string?, opts: ListOpts?) -> ListResult
Query registered assets, returning each match as a resolved
AssetRef handle. Every filter narrows the same enumeration and
they compose: path selects a VFS subtree (the folder and
everything under it), type keeps only those asset types within
it, scope keeps only that scope, and fields keeps only assets
whose .metadata matches. type and path each take one value or
a list matching any of its entries, and all / any / none
group whole filters — none excludes what it matches. order,
limit, and offset shape the result: matches come back ordered
by identity unless order names another field
(name / path / type / guid).
Each entry is the same envelope asset.resolve returns (__ref /
type / name / guid / identity / path), so it can be passed
anywhere an AssetRef is accepted, and the result carries
:first() / :random() / :filter() / :sort() and friends.
type takes the same values asset.categories() lists. The first
positional argument is a path when it is absolute, a type
otherwise. An unknown key raises, as does a table setting both
type and its older spelling category.
A static (literal) type or path makes the enumeration part of the
calling file's content dependencies when it is saved — the set
travels with published content, so consumers get at-least the
authoring world's assets.
Parameters
type_or_opts(AssetCategory | ListOpts)(optional) — Type or VFS path filter (a static literal so the enumeration can be captured for publish), or the full query table.scopestring(optional) — Scope filter (when first arg is a type).optsListOpts(optional) — The query table — seeListOpts.
Returns ListResult — The matched AssetRef handles, as a result carrying query methods.
local hero = asset.list({ path = "/zero/source/props", type = "mesh", fields = { tags = "hero" } }):first()
globals/asset/list_field_values
asset.list_field_values(key: string) -> { any }
Distinct values seen for the named field across every
asset's .metadata.
Parameters
keystring— Field name.
Returns { any } — Array of distinct values.
local authors = asset.list_field_values("author")
globals/asset/list_fields
asset.list_fields() -> { string }
Distinct top-level field keys observed across every asset's
.metadata. Useful for tooling discovering custom keys in use.
Returns { string } — Array of field names.
for _, k in asset.list_fields() do print(k) end
globals/asset/meta
asset.meta(ref: RefArg, type: string?) -> AssetMeta
Read the asset's engine-owned identity record (guid /
checksum). Distinct from .metadata (agent-editable);
for that use asset.metadata.
Parameters
refRefArg— Any name the asset has.typestring(optional) — Category hint (optional).
Returns AssetMeta — Metadata table.
local m = asset.meta("brick") -- { guid = ..., checksum = ... }
globals/asset/metadata
asset.metadata(ref: RefArg, type: string?) -> AssetMeta
Read the asset's agent-editable .metadata sidecar as a
Lua table. Missing sidecar returns {}. Distinct from
asset.meta (engine-owned).
Parameters
refRefArg— Any name the asset has.typestring(optional) — Category hint (optional).
Returns AssetMeta — JSON-shaped table; empty when no sidecar exists.
local md = asset.metadata("brick")
globals/asset/observe
asset.observe() -> any
What the engine is holding for content right now, in one reading:
textures and meshes (one row per resource the device holds, each with
the bytes it costs, its dimensions or buffer split, and where it came
from), cpu (one row per asset a live script-component context holds),
and totals — the aggregates those rows sum to, so the listing reconciles
against renderer.textureMemory() and renderer.gpuMemory().
Each pool is named because they are different pools: an asset can be on
the device and not CPU-resident, or the reverse. devicePublished is
false when no renderer has published an inventory and cpuPublished when
the scripting VM has not published its pool — an engine that cannot answer
reads differently from one answering with nothing resident.
Returns any — ResidencyReading
local r = asset.observe() print(#r.textures, r.totals.textureBytes)
globals/asset/preview
asset.preview(ref: RefArg, opts: { [string]: any }?, type: string?) -> { [string]: any }
Render a preview of an asset. Resolves the ref and dispatches to its
type's preview ref-method when present; otherwise returns the
{ available = false } sentinel ("no preview available for this type").
Parameters
refRefArg— Any name the asset has.opts{ [string]: any }(optional) — Optional{ size = { width, height }, angle = { yaw, pitch } }.typestring(optional) — Category hint (optional).
Returns { [string]: any } — { available, imageBase64?, width?, height?, bounds?, stats?, reason? }.
local p = asset.preview("@builtin::materials.gold", { size = { width = 512, height = 512 } })
globals/asset/primaryFile
asset.primaryFile(ref: RefArg) -> any
The file the asset type's declared primary list resolves to inside
this asset, as the loader itself resolves it. resolved is false when no
declaration matched and path is then absent; declared is the type's
own primary list in match order.
Parameters
refRefArg— Any name the asset has — handle, identity, guid, name or path.
Returns any — { path: string?, resolved: boolean, isFolder: boolean, declared: { string } }
print(asset.primaryFile("myTex").path)
globals/asset/ref
asset.ref(ref: RefArg, type: string?) -> AssetRef
Build a reference handle for an asset — the canonical ref
envelope constructor. Identical shape to asset.resolve;
preferred name for the author-side use case (embedding refs in
YAML / JSON / Luau output).
Naming the asset here reads exactly as naming it in asset.resolve,
down to raising on a miss: a literal is recorded as this source's
dependency, a computed name is a dynamic resolve — free in a tool,
refused on the gameplay path. asset.exists(name, type) is the probe a
computed name can ask.
Parameters
refRefArg— The asset to reference — an identity, a guid, a VFS path, or a handle.typestring(optional) — Category hint (optional).
Returns AssetRef — Ref handle. Raises on a miss or ambiguity, as asset.resolve does.
local r = asset.ref("animations.idle", "animation")
globals/asset/reloadPending
asset.reloadPending(ref: RefArg, typeName: string?) -> boolean
True while a write to this asset still owes it a reload — the write is inside the settle window that collects one authoring step's writes, or its reload is queued and the engine has not run it yet. False means every content change written so far has reached its subscribers, so a consumer bound to the asset now cannot be interrupted by a reload the earlier writes already earned. The recording is synchronous with the write, so a call made right after one already reads true.
Parameters
refRefArg— Any name the asset has — handle, identity, guid, name or path.typeNamestring(optional) — Category to restrict the match to. Omit to search every category.
Returns boolean — true while a content-change reload is still owed.
repeat task.wait() until not asset.reloadPending(ref)
globals/asset/reloadSeq
asset.reloadSeq(ref: RefArg, typeName: string?) -> number
How many content-change reloads this asset has been through — the
count of onAssetReload dispatches the engine has RUN for it. A write to
a file inside an asset does not reload it on the spot: the writes of one
authoring step are collected for a settle window and the reload runs on a
later frame. Read this, write, then poll for a larger number to learn the
write's reload has actually reached subscribers. Monotonic per asset and
session-scoped; 0 for an asset whose content has not changed since boot.
Parameters
refRefArg— Any name the asset has — handle, identity, guid, name or path.typeNamestring(optional) — Category to restrict the match to. Omit to search every category.
Returns number — content-change reloads dispatched for this asset.
local at = asset.reloadSeq(ref)
vfs.write(asset.source(ref) .. "/graph.json", encoded)
repeat task.wait() until asset.reloadSeq(ref) > at
globals/asset/remove_field
asset.remove_field(ref: RefArg, key: string)
Remove one top-level field from the asset's .metadata.
No-op when the field isn't present.
Parameters
refRefArg— Any name the asset has.keystring— Field name.
asset.remove_field("brick", "author")
globals/asset/remove_tag
asset.remove_tag(ref: RefArg, tag: string)
Remove a tag from the asset's .metadata.tags. No-op when
the tag isn't present.
Parameters
refRefArg— Any name the asset has.tagstring— Tag to remove.
asset.remove_tag("brick", "wip")
globals/asset/resolve
asset.resolve(ref: RefArg, type: (C & string)?) -> AssetRef<C>
Find an asset. The returned handle carries every name form
the asset has (guid, identity, path, type) so
downstream code can read any one of them without calling
resolve again. Raises when ref resolves to no asset — or, with a
type, to no asset of that type — and when ref reaches more than one
asset, where it names the candidates for you to pick from instead of
picking one of them. A <scope>::-qualified identity reaches exactly one:
@root::name for the asset this world holds at its source root, the
library identity (@builtin::…) for a library's. For the same lookup
answering a miss with nil, use asset.tryResolve(ref, type).
A name written as a string LITERAL is recorded as this source's
dependency on that asset, so the asset travels with the content and
still resolves once someone installs it in another world. A COMPUTED
name cannot be written down, so nothing pins what it reaches: that is a
dynamic resolve — free in a tool, refused on the gameplay path (a
component or scene entrypoint). asset.tryResolve, asset.ref and
asset.source read the name they are given exactly this way too, so
which of the four you reach for changes neither answer. To ask whether a
computed name has files without reaching a handle, use
asset.exists(name, type).
Parameters
refRefArg— The asset to find — an identity, a guid, a VFS path, or a handle.type(C & string)(optional) — Category to restrict the match to (optional). Separates a bare name that assets of different categories share (asset.resolve("cube", "mesh")); where several assets of the SAME category answer to it, the scope-qualified identity is what separates them. A reference naming a file an importer has since promoted (wall.pngafter the texture importer turned it intowall.texture) resolves to the promoted asset, and says so in the log once per reference.
Returns AssetRef<C> — Asset handle, carrying the category when one was named — so the methods that category defines are checked on the result. Raises (rather than returning nil) on a miss, and on a name that reaches more than one asset.
local a = asset.resolve("@builtin::components.Camera")
globals/asset/set_field
asset.set_field(ref: RefArg, key: string, value: any?)
Set one field in the asset's .metadata, creating the sidecar if missing.
Sibling fields are preserved. When the new value AND the existing value are
both maps (objects), the new value DEEP-MERGES into the existing one, so
writing one sub-key never drops the others — set_field(ref, "settings", { keepCpu = true }) keeps every other setting. Arrays and scalars replace.
Clear a whole field with asset.remove_field; replace the entire sidecar with
asset.set_metadata.
Parameters
refRefArg— Any name the asset has.keystring— Field name.valueany(optional) — Field value (any JSON-serialisable Lua value).
asset.set_field("brick", "author", "me")
asset.set_field("tree", "settings", { keepCpu = true }) -- merges; sibling settings kept
globals/asset/set_metadata
asset.set_metadata(ref: RefArg, data: AssetMeta)
Replace the asset's .metadata sidecar with the given
table. Pass an empty table to clear all fields.
Parameters
refRefArg— Any name the asset has.dataAssetMeta— Full JSON-shaped contents for the sidecar.
asset.set_metadata("brick", { author = "me", tags = { "wip" } })
globals/asset/source
asset.source(ref: RefArg, type: string?) -> string
Return the VFS source path for an asset.
It reaches the asset, so the name carries the same reference contract
asset.resolve's does: a literal is recorded as this source's
dependency, a computed name is a dynamic resolve — free in a tool,
refused on the gameplay path. asset.exists(name, type) is the probe a
computed name can ask.
Parameters
refRefArg— Any name the asset has.typestring(optional) — Category hint (optional).
Returns string — VFS source path.
local p = asset.source("brick") -- "/source/brick.material"
globals/asset/tags
asset.tags(ref: RefArg) -> { string }
Convenience read of the .metadata.tags array.
Parameters
refRefArg— Any name the asset has.
Returns { string } — Array of tag strings.
for _, t in asset.tags("brick") do print(t) end
globals/asset/tryResolve
asset.tryResolve(ref: RefArg, type: string?, base: string?) -> AssetRef?
The same lookup asset.resolve performs, answering a miss with nil
instead of raising. Every name form, the same type narrowing, and the
same handle on success — so "use it if it is there" needs no pcall
around a call whose failure would otherwise be indistinguishable from a
real error.
This consults the asset REGISTRY, so it sees registered assets wherever
their files live, @builtin:: ones included. asset.exists(name, type)
answers the narrower question of whether an asset's files are present in
the current mode's store.
A name that reaches more than one asset still raises — that is a question about the reference, not about presence, and a nil there would report absence for content that is present twice.
It reaches the asset, so the name carries the same reference contract
asset.resolve's does: a literal is recorded as this source's
dependency, a computed name is a dynamic resolve — free in a tool,
refused on the gameplay path. asset.exists(name, type) is the probe a
computed name can ask.
Parameters
refRefArg— The asset to look up — an identity, a guid, a VFS path, or a handle.typestring(optional) — Category to restrict the match to (optional). A reference naming a file an importer has since promoted resolves to the promoted asset, the same asasset.resolve.basestring(optional) — Referring VFS path a~/~.tailref expands against, the same asasset.resolve's — so the two answer the same question and differ only in what a miss is.
Returns AssetRef? — Asset handle, or nil when the reference resolves to no asset.
local mat = asset.tryResolve(name, "material")
globals/asset/typeRef
asset.typeRef(target: RefArg) -> string?
Return the pinned asset_type reference (the type's guid)
that the asset is an instance of. Resolve the full type with
asset.resolve(asset.typeRef(target)). Returns nil for loose
files / assets with no pinned type.
Parameters
targetRefArg— Asset handle / identity / guid / VFS path.
Returns string? — Pinned type's guid, or nil.
local t = asset.resolve(asset.typeRef("brick"))
globals/asset/unusableReasons
asset.unusableReasons() -> { string }
Every reason asset.diagnose can report an asset unusable for, sorted.
Returns { string } — Array of reason names.
for _, r in ipairs(asset.unusableReasons()) do print(r) end
globals/asset/validate
asset.validate(ref: RefArg, type: string?) -> ValidateResult
Validate an asset folder against its type's type.yaml, plus
the type's own semantic validation. Structural problems come from
type.yaml — missing required files, unsatisfied one_of_group
alternatives, and (when allow_unlisted: false) unexpected
children. validated = false when no type.yaml is registered
— nothing structural to check. On top of that, when the asset's
type ships a behavior.luau exporting a top-level
validate(assetRef) -> { { code, message, severity? } }, its
reported problems (severity defaults to "error") are appended to
problems; error-severity problems flip ok to false, warnings
leave it untouched. A hook that raises or returns a non-table is
itself reported as a validate.hook_failed error problem — a
broken hook blocks. A type with no validate export behaves
exactly as the structural check alone.
world.push calls this per user asset, so a type's semantic
validation is enforced at publish time with no further wiring.
Parameters
refRefArg— Any name the asset has.typestring(optional) — Category hint (optional).
Returns ValidateResult — { ok, typeName, validated, problems }.
local v = asset.validate("@builtin::components.Camera")
globals/asset/warmup
asset.warmup(ref: RefArg, opts: WarmupOpts?) -> WarmupResult
Warm an asset's bytes into CPU memory and follow its declared
content dependencies to each referenced asset, deduped by
guid. Type-agnostic (reads the generic ref graph) and CPU-only —
never touches the GPU. Side-effect-free name resolution (uses
asset.guid/asset.deps, not asset.resolve).
Parameters
refRefArg— Any name the root asset has — handle, identity, guid, or path.optsWarmupOpts(optional) — Optional{ vias, max }— restrict ref-edge kinds / cap closure size.
Returns WarmupResult — { closure, count } — the deduped guid closure warmed and its size.
local w = asset.warmup("@builtin::scenes.test_arena")
globals/audio/decode
audio.decode(zaud: buffer | string) -> (string?, number?, number?)
Decode a ZAUD payload into interleaved f32 PCM. A PCM payload comes
back as the frames its header accounts for, held to the whole frames the
bytes behind it fill, so the sample count is always a whole number of
channels and a consumer walking it channels at a time ends on a frame.
Parameters
zaudbuffer | string— A ZAUD payload — a buffer or a binary string.
Returns (string?, number?, number?) — (pcm, sampleRate, channels), or (nil, err).
globals/audio/device
audio.device() -> AudioDeviceStatus
What the engine's audio output is doing: state is "open" while a
stream is running on an output device and "silent" while none is, and
device names the device an open stream runs on. The counters record
what the engine has been through keeping one open — faults a live
stream reported, changes of the host's default output, reopens the
engine made, failedOpens the platform refused, and the glitches a
listener heard as dropouts, with lastError carrying what the platform
said. A device that goes away leaves the mixer running and the engine
opening a stream again as soon as one is there.
Returns AudioDeviceStatus — An AudioDeviceStatus.
local d = audio.device(); print(d.state, d.device, d.reopens)
globals/audio/encode
audio.encode(sourceBytes: buffer | string, opts: { [string]: any }?) -> (string?, string?)
Encode container audio bytes (ogg / mp3 / wav / flac) into a ZAUD
payload. Every decoded sample must be finite; a source whose samples carry
a NaN or an infinity comes back as (nil, err) naming how many fail and
where the first one sits.
Parameters
sourceBytesbuffer | string— Encoded source audio bytes — a buffer or a binary string.opts{ [string]: any }(optional) —{ codec: "opus"|"pcm"?, bitrateKbps: number?, vbr: boolean?, sampleRate: number?, forceMono: boolean?, loopStart: number?, loopEnd: number? }
Returns (string?, string?) — The ZAUD bytes, or (nil, err).
local zaud = audio.encode(oggBytes, { bitrateKbps = 96 })
globals/audio/encodePcm
audio.encodePcm(pcm: any?, sampleRate: number, channels: number, opts: { [string]: any }?) -> (string?, string?)
Encode raw interleaved f32 PCM into a ZAUD payload. Every sample must
be finite; a buffer carrying a NaN or an infinity comes back as
(nil, err) naming how many fail and where the first one sits, so a
filter that diverged over part of a bake is caught before it is written.
The sample count is a whole number of channels: a buffer with a tail
over comes back as (nil, err) naming the whole frames it holds and the
samples past them.
Parameters
pcmany(optional) — Interleaved f32 samples — a buffer or a binary string of little-endian f32, the shapemicrophone.samplesandaudio.decodehand back, or a flat number array. A byte payload's samples are its 4-byte lanes, and a length that stops partway through one comes back as(nil, err)naming the whole samples it holds and the bytes past them.sampleRatenumber— Source sample rate in Hz.channelsnumber— 1 or 2, and a divisor of the sample count.opts{ [string]: any }(optional) — Same shape asaudio.encode.
Returns (string?, string?) — The ZAUD bytes, or (nil, err).
local s = microphone.status(); local zaud = audio.encodePcm(microphone.samples(), s.sampleRate, 1)
globals/audio/info
audio.info(zaud: buffer | string) -> (AudioInfo?, string?)
Read a ZAUD payload's header. A PCM payload's samples are its bytes, and
the header is read against them: a sample count differing from
frames * channels comes back as (nil, err) naming both counts, and a
sample carrying a NaN or an infinity comes back as (nil, err) naming how
many fail and where the first one sits, so the header handed back describes
a clip that is as long as it says and can sound. The header describes the
clip's shape — rate, channels, frames, duration, codec, loop points. What
the samples do where a whole-clip loop wraps is a reading of its own,
audio.loopSeam, which is the call that answers whether a bed cycles
without a click.
Parameters
zaudbuffer | string— A ZAUD payload — a buffer or a binary string.
Returns (AudioInfo?, string?) — An AudioInfo table, or (nil, err).
local info = audio.info(zaud); print(info.durationMs)
globals/audio/levels
audio.levels() -> AudioLevels
The master mix's peak and RMS over the meter's most recent closed window, measured without recording anything.
Returns AudioLevels — An AudioLevels.
local l = audio.levels(); print(l.peak, l.rms, l.windowMs)
globals/audio/listener
audio.listener() -> AudioListenerState
Where the scene is heard from, how many active listeners exist, and which entity's listener drives the ears.
Returns AudioListenerState — An AudioListenerState.
local l = audio.listener(); print(l.present, l.count, l.entity)
globals/audio/loopSeam
audio.loopSeam(zaud: buffer | string) -> (AudioLoopSeam?, string?)
Measure what a clip's samples do where a whole-clip loop wraps, so a bed
can be judged before anyone hears it tick. The wrap's own step
(|x[1] - x[frames]|) is reported against the step the signal ordinarily
makes between neighbouring samples, as ratio = step / meanStep — a figure
in the units the signal itself moves in, so a quiet ambience and a loud
drone are read the same way. A bed whose partials wrap reads near 1; one
carrying a strike at its head and silence at its tail reads in the tens.
ratio and the step / meanStep / maxStep beside it belong to the
worst channel, channel names it, and channels carries every channel's
own reading. seamless is ratio <= threshold, the same threshold
asset.create("soundClip", ...) warns past. The reading is taken on the
DECODED samples, so it answers for what the codec left behind and for a
clip that arrived already encoded and whose source buffer nobody holds.
Costs a decode of the whole payload; audio.info reads a header without
one.
Parameters
zaudbuffer | string— A ZAUD payload — a buffer or a binary string.
Returns (AudioLoopSeam?, string?) — An AudioLoopSeam table, or (nil, err).
local seam = audio.loopSeam(clipRef:getBytes()); print(seam.ratio, seam.seamless)
globals/audio/mixer
audio.mixer() -> AudioMixerLevels
The levels the mixer is applying to the mix right now: the master
level, whether the mix is muted, and the level of every channel one has
been set on. A channel absent from channels plays at unity, so a
source naming it is heard at the volume it asks for.
Returns AudioMixerLevels — An AudioMixerLevels.
local m = audio.mixer(); print(m.master, m.muted, m.channels.music)
globals/audio/observe
audio.observe() -> AudioObservation
Report what the mixer is making audible right now, and why a source is not. One read covering every live voice with the mixer's own playback state and effective gain, the master mix's level, the mixer's voice accounting, the listener, the output device the mix is reaching, and what the subsystem costs. Answers in edit mode as well as play mode.
Returns AudioObservation — An AudioObservation.
local a = audio.observe(); print(a.audibleCount, a.levels.rms)
for _, v in audio.observe().voices do print(v.entity, v.mixerState, v.silence) end
globals/audio/peakSince
audio.peakSince(window: number) -> number?
The loudest peak the master mix reached across the meter's windows
that closed after its windows count stood at window. audio.levels()
carries the window that closed last, so a reader sees the windows its own
frames happen to land on; this spans all of them, which is what measuring
a sound shorter than the gap between two reads takes. Take the mark from
audio.levels().windows before the sound starts, wait until windows has
advanced past the sound's length, then read the span.
Parameters
windownumber— Awindowscount taken fromaudio.levels()earlier.
Returns number? — The loudest window peak in the span, or nil when the meter holds no peak for it — nothing has closed since window, or the span reaches further back than the meter's history of recent windows, so a reader that came back too late learns that instead of reading the maximum of the part that survived.
local mark = audio.levels().windows
local peak = audio.peakSince(mark)
globals/audio/profile
audio.profile() -> AudioProfile
What the audio subsystem has cost since the profiling window opened —
the streaming pump, clip decode, clip encode, voice starts, and building
the observation itself. Every total is a SUM across that window rather
than a per-frame figure, and the window runs from the last
audio.resetProfile() or from engine start. For what a frame costs now,
reset, let frames pass, then divide by the frames the window reports.
Returns AudioProfile — An AudioProfile.
audio.resetProfile(); task.wait(1); local p = audio.profile()
print("per frame:", (p.pump.totalMs + p.observe.totalMs) / p.frames)
globals/audio/resetProfile
audio.resetProfile()
Open a new audio profiling window, discarding what the previous one
measured. Call this before timing a stretch of frames: without it
audio.profile() reports totals reaching back to engine start.
audio.resetProfile()
globals/audio/setChannelVolume
audio.setChannelVolume(channel: string, volume: number)
Set the level of one mixer channel — the channel an Audio
component names, such as "sfx", "music" or "ambient", or any name the
scene invents. It scales every voice on that channel and nothing else,
reaches voices that are already playing, and comes back per voice as
gain.channel. A channel no level has been set on plays at unity.
Parameters
channelstring— The channel name, matchingAudio.channel.volumenumber— Channel level, 0..1.
audio.setChannelVolume("music", 0.3)
for _, v in audio.voices() do print(v.channel, v.gain.channel) end
globals/audio/setMasterVolume
audio.setMasterVolume(volume: number)
Set the master level of the mix, on the engine's 0..1 amplitude
scale. It scales every voice whatever channel it plays on, reaches
voices that are already playing, and comes back per voice as
gain.master.
Parameters
volumenumber— Master level, 0..1.
audio.setMasterVolume(0.5)
globals/audio/setMuted
audio.setMuted(muted: boolean)
Silence or unsilence the whole mix. A muted mix sounds nothing
whatever its master and channel levels read, every voice reports
masterSilent, and unmuting hands the levels back untouched.
Parameters
mutedboolean— Whether the mix is silenced.
audio.setMuted(true)
globals/audio/voice
audio.voice(entityId: string) -> AudioVoice?
The voice on one entity, or nil when that entity carries no audio source.
Parameters
entityIdstring— The entity's stable id.
Returns AudioVoice? — An AudioVoice, or nil.
local v = audio.voice(e.id); print(v and v.mixerState)
globals/audio/voiceAccounting
audio.voiceAccounting() -> AudioVoiceAccounting
How many voices the mixer can hold, how many are in use, how many
are free — read off the mixer's own tracks, so the free count is the one
a play call is granted or refused against. The two pools are reported
apart: capacity / inUse / free are the main track, which carries
the NON-spatial voices, while a spatial voice plays through its own
sub-track and is counted by spatialInUse instead. sourcesHolding
counts both pools from the sources that own them, so it equals
inUse + spatialInUse while every voice answers to a source.
Returns AudioVoiceAccounting — An AudioVoiceAccounting.
local v = audio.voiceAccounting(); print(v.inUse .. "/" .. v.capacity)
local v = audio.voiceAccounting(); print(v.sourcesHolding - (v.inUse + v.spatialInUse))
globals/audio/voices
audio.voices() -> { AudioVoice }
Every live audio source with the mixer's opinion of it.
Returns { AudioVoice } — An array of AudioVoice.
for _, v in audio.voices() do print(v.clip, v.gain.effective) end
globals/audio/whySilent
audio.whySilent(entityId: string) -> (string?, string?)
Why the source on an entity is making no sound. Returns nil when it
IS sounding, and one of noBackend, noDevice, notResident, neverStarted,
refused, paused, ended, gainZero, channelSilent,
masterSilent, outOfRange when it is not — the nearest cause, so the
answer names the thing to change. A second
return carries the mixer's own words when it refused the source, and
"no audio source on this entity" when nothing there plays at all.
Parameters
entityIdstring— The entity's stable id.
Returns (string?, string?) — (reason, detail).
local why = audio.whySilent(e.id); if why then print(why) end
globals/av/is_live
av.is_live() -> boolean
True if a live-stream session is currently active.
Returns boolean — Whether the live encoder is running.
if av.is_live() then av.stop_live() end
globals/av/is_recording
av.is_recording() -> boolean
True if a recording session is currently active.
Returns boolean — Whether a recording is in progress.
print("recording:", av.is_recording())
globals/av/live
av.live(opts: LiveOpts?) -> string?
Start the live-stream encoder. The stream is served at
/engine/live.stream and reverse-proxied at
/stream/<instance>/live.stream as a binary length-prefixed
protocol consumed by the multiviewer UI's WebCodecs decoder.
When texture_handle is set, the encoder reads from that GPU
texture's guid (a Camera pointed at it via setTargetTexture)
instead of the scene's viewport — that's how spectator cameras
work. Returns a stream URL, or nil when unsupported or a
session is already active.
Parameters
optsLiveOpts(optional) — Encoder options.
Returns string? — Stream URL or nil.
local url = av.live({ width = 1280, height = 720, fps = 60 })
globals/av/record
av.record(path: string, opts: RecordOpts?) -> (string?, string?)
Start recording the engine output to a VFS path. Default dir
is /zero/runtime/recordings/ when path is not absolute. The
take runs until av.stop_recording() unless opts bounds it
with max_duration_sec (seconds of the take's own clock) or
frames (captured frames); max_duration_sec wins when both are
given, and the bound in force reads back as
av.status().recordingBound. With
no chroma/range opts the format defaults to full-range 4:4:4
HEVC where the GPU supports it, else 4:2:0. On the "software"
backend the take is H.264 encoded on the CPU, which costs the run
it records: read av.status().recordingAchievedFps against
recordingRequestedFps to see the rate it reached. What the take
did with the master mix reads back as
av.status().recordingAudio. cadence picks the clock the take
stamps its frames from. "realtime" (the default) stamps each
frame with the wall-clock slot it was captured in, so a recorded
session is watched back at the speed it happened and an engine
ticking under fps leaves slots empty. "frame" stamps every
rendered frame one fixed slot after the last, so a timeline whose
own clock advances a step per rendered frame — a cutscene, a
scripted demo, anything on a fixed timestep — is delivered at the
length that timeline runs to, however slowly the engine drew it: a
take of frames = n at fps is n / fps seconds of film, and a
max_duration_sec bound counts that film's seconds. A "frame"
take records silent, because the master mix plays in wall-clock
seconds and cannot share a file with a fixed-step picture;
recordingAudio says so, and an explicit audio = true beside it
is refused. Record the sound as a second "realtime" take.
camera names the camera the take draws its film from — an entity
proxy, an entity id, or an entity name. That camera holds the viewport
for as long as the take runs, above the priority contest and above
camera.setEditorOverride, so the film is its view and the frames
carry everything the presented frame carries. Only frames that camera
drew go into the film, and a camera that never draws the viewport ends
the take with the reason on av.status().recordingError — so a take is
the view it named or it is no take. The camera belongs to the take:
nothing is written to it, and the viewport is back under its own
contest the moment the take ends. It reads back as
av.status().recordingCamera while the take runs. Omitted, the take
records whichever camera holds the viewport, which in an engine on the
editor profile is the editor's own fly camera rather than the scene's.
renderLayers is the render-layer include spec the viewport is
drawn under for as long as the take runs — the same token string a
capture takes: all seeds every layer, name adds one and !name
drops one, so "all !EditorUI !debug" films the scene without the
editor's chrome or the authoring overlays (gizmos, light and probe
icons, frustums, collider wireframes) over it, and
"all !ui !EditorUI !debug" drops the authored HUD as well. The
viewport admits geometry and screens by that one spec, so it states
the whole picture. It belongs to the take: nothing is written to the
camera it is stated against, and the moment the take ends — its
bound reached, stopped, or refused — the viewport is back under the
camera's own spec. While a take states layers, the window shows what
the film holds, and av.status().recordingLayers reads the spec
back. Omitted, the take records the engine output as presented.
Returns the destination path of a session that is open and
recording, or nil
and the reason it is not — an adapter that cannot encode, a take
already running, an option the encoder rejects, a resolution the
device refuses. The engine opens the session, so the call waits
for it: run it where it can yield, wrapping it in task.spawn
from a callback that cannot. How a take finished reads back as
av.status().recordingEnd; a refused request puts its reason
there and on recordingError and leaves no take report behind,
while a request refused because a take is already running leaves
that take's report as it is.
Parameters
pathstring— VFS destination path.optsRecordOpts(optional) — Encoder options (optional).
Returns (string?, string?) — Destination VFS path of the open recording, or nil. Why the recording was refused, when it was.
local clip = av.record("intro.mp4", { fps = 60 })
local film = av.record("cut.mp4", { fps = 24, frames = 24 * 181, cadence = "frame" })
local clean = av.record("take.mp4", { fps = 24, renderLayers = "all !EditorUI !debug" })
local shot = av.record("film.mp4", { fps = 24, frames = 240, camera = "FilmCamera" })
globals/av/status
av.status() -> AvStatus
Report the encoder subsystem's state. Always available
regardless of GPU support. backend is the encode backend in use
— "vulkan" or "vaapi" on an adapter with a media engine,
"software" where encode runs on the CPU — and hardware is true
for the first two, so a caller that pays for the take in engine
time knows which it is getting. codecs lists what the backend
encodes with the recording default first. live is true while the
av.live stream is running. A take of its own reads back on the
recording fields: recording is the destination of the take in
flight, recordingBound what will end it, recordingEnd how the
most recent one ended, and recordingError why one produced no
file. recordingAudio is the codec the take is writing the master
mix with ("opus"), or the reason the file carries no audio track
— read it to tell a film with a soundtrack from a silent one.
recordingLayers is the render-layer include spec the armed take is
drawing the viewport under, in the words its caller wrote, and nil for
a take that stated none — the reading that answers what is in the
picture rather than how much of it there is. recordingCamera is the
entity id of the camera the take's most recent captured frame was drawn
from, and stands as the source of the most recent take once that take
has ended — it is read off the frame the engine drew, so it answers
which view a film holds whether or not the take named a camera.
recordingCadence is the clock the take stamps its frames from,
"realtime" or "frame", and so what the tally below is a reading
against. What the take produced reads off recordingFrames,
recordingBytes (every byte the take has produced so far, climbing
while it runs and ending equal to the size of the file),
recordingSeconds (the timeline those frames cover),
recordingAchievedFps (the rate they arrived at) and
recordingRequestedFps (the rate asked for) — live while a take
runs, and its final tally once it ends. On "realtime" the
requested rate is a ceiling and an engine ticking under it reaches
less; on "frame" every rendered frame is a slot of the recorded
timeline, so recordingSeconds is that timeline's length and
recordingAchievedFps is the rate it plays back at.
Returns AvStatus — Encoder status table.
local s = av.status(); print(s.backend, s.hardware, s.recordingAudio)
globals/av/stop_live
av.stop_live() -> boolean
Stop any active live-stream session.
Returns boolean — True if a session was stopped, false if none was active.
av.stop_live()
globals/av/stop_recording
av.stop_recording(handle: string?) -> (boolean, string?)
Stop the active recording (or the one for the given promise
handle). Returns true when a recording was armed at call time. A
false return carries a second value naming how the most recent
recording already ended — the bound it reached, or the failure
that cut it short — and nil when no recording has run at all. The
engine finalizes the take on its next tick: wait for
av.is_recording() to go false, then read what it produced off
av.status().
Parameters
handlestring(optional) — Promise handle of a specific recording (optional).
Returns (boolean, string?) — True if a recording was stopped. How the most recent recording ended, when nothing was armed.
local stopped, ended = av.stop_recording()
globals/await
await(promiseHandle: string): any
Yield the current coroutine until the given promise resolves and return its value. Pair with any FFI function that returns a promise handle (asset.load, http.get_json, scene.save, delay, ...).
Parameters
promiseHandlestring— promise handle returned by an async FFI call
Returns any — Whatever value the resolved promise carries (nil if the promise resolved without a value).
globals/base64/decode
base64.decode(text: string) -> (string?, string?)
Decode standard-alphabet base64 text back to the original binary string.
Parameters
textstring— Base64 text to decode.
Returns (string?, string?) decoded bytes on success, or (nil, errmsg).
local bytes = base64.decode(text)
globals/base64/encode
base64.encode(bytes: buffer | string) -> string
Encode a binary string to standard-alphabet (padded) base64 text.
Parameters
bytesbuffer | string— Binary bytes to encode.
Returns string — Base64 text.
local text = base64.encode(jpegBytes)
globals/batch
batch(fn)
Execute a function with batched mutations. Operations inside the function are collected and executed in order at the end, instead of immediately. Use for bulk operations where you want to control when mutations flush. Within the batch, operations still execute in order — spawn before component.add before impulse.
Parameters
fnfunction— Function containing batched operations
globals/blend/destroyLayout
blend.destroyLayout(handle: number) -> boolean
Drop the layout from the registry.
Parameters
handlenumber— Layout handle.
Returns boolean — True if the layout existed and was removed.
globals/blend/layout
blend.layout(slots: { BlendSlot }, totalStride: number?) -> number?
Register a record-stride layout. Each slot is
{ offset, stride, op } where op is "lerp" / "slerp" /
"sum" / "step". Slerp slots must have stride 4.
totalStride defaults to max(offset + stride) across slots;
pass an explicit value when records contain padding past the
last slot.
Parameters
slots{ BlendSlot }— Array of slot tables.totalStridenumber(optional) — Optional explicit record stride.
Returns number? — Layout handle, or nil.
local l = blend.layout({ { offset = 0, stride = 3, op = "lerp" } })
globals/blend/lerpInto
blend.lerpInto(outBuffer: Substrate.TypedBuffer, layout: number, aBuffer: Substrate.TypedBuffer, bBuffer: Substrate.TypedBuffer, t: number) -> boolean
Two-input crossfade shortcut. Equivalent to
blend.weightedInto(out, layout, { {a, 1-t}, {b, t} }).
Faster for the common A/B fade case because it skips the
inputs-table walk.
Parameters
outBufferSubstrate.TypedBuffer— The buffer written into.layoutnumber— Layout handle.aBufferSubstrate.TypedBuffer— The A side of the fade.bBufferSubstrate.TypedBuffer— The B side of the fade.tnumber— Crossfade weight on B (0..1).
Returns boolean — True on success.
globals/blend/weightedInto
blend.weightedInto(outBuffer: Substrate.TypedBuffer, layout: number, inputs: { BlendInput }) -> boolean
Combine N weighted input buffers into the output buffer
using the layout's slot ops. The output buffer's length must
be a whole multiple of layout.totalStride; every input
buffer must be at least as long as the output. Returns false
on any handle / size mismatch.
Parameters
outBufferSubstrate.TypedBuffer— The buffer written into.layoutnumber— Layout handle.inputs{ BlendInput }— Array of{ buffer, weight }.
Returns boolean — True on success.
globals/bundle/update
bundle.update(entityId: string, bundleRef: BundleRef?) -> any
Re-compose a bundle from an entity's current hierarchy and write
it back to the bundle's on-disk path. The VFS write triggers the
engine's generic asset hot-reload pipeline, which fires
onAssetReload(field) on every component subscribed to this
bundle's guid via a declared asset field — those components
reconcile per their own policy.
Parameters
entityIdstring— The entity whose hierarchy is captured into the bundle.bundleRefBundleRef(optional) — Optional. When omitted, the ref is inferred from the entity'sAsset.sourcefield. When given, the explicit ref wins.
Returns any — True on success (forwarded from the bundle assetType's :update).
bundle.update(entityId) -- infer from Asset
bundle.update(entityId, { guid = "..." }) -- explicit ref
globals/camera/active
camera.active() -> string?
Entity id of the on-screen render camera this frame — whichever camera wins the viewport by priority (the editor fly-camera in edit mode, the gameplay camera in play). Render features, billboards, and input bases that must follow the human's on-screen view read this.
Returns string? — Entity id of the on-screen camera, or nil if none is active — including the frame after that camera's entity is despawned.
local camId = camera.active()
globals/camera/cut
camera.cut()
Declare that the camera on screen cuts: the next frame it draws stands somewhere it did not travel to. Motion vectors are the difference between where a surface projects now and where it projected on the camera's previous frame, and everything temporal reads that difference — the shutter reconstructs the frame by walking it, a temporal resolve reprojects its history along it. Across a cut that difference describes a displacement no surface made, so the frame is reconstructed from taps a whole screen away and belongs to neither shot. A declared cut leaves the camera with no previous frame for exactly one frame, which is the state its very first frame is already in, so every consumer reads zero motion across the cut. Declare it in the same step that places the camera at the new station; declaring it again before that frame draws still costs the one frame. Handing the viewport from one camera to another is already a cut without being declared one: the incoming camera stands where it always stood, and the engine performs the handover, so it is what states it.
camera.cut(); entity(camId).position = { 40, 6, -12 }
globals/camera/editor
camera.editor() -> string?
Entity id of the editor fly-camera (the EditorOnly authoring camera), or
nil if the scene has none. This is the camera the editor viewport renders
through, so it is the one a capture of the screen sees. Its pose is its
entity transform: assign entity(id).position to move it and aim it with
the camera toolbox's lookAt, which makes a screen capture repeatable
instead of whatever pose the instance booted with.
Returns string? — Entity id of the editor camera, or nil.
local camId = camera.editor()
local id = camera.editor(); entity(id).position = { 12, 8, 12 }; tools.use("camera", "lookAt", id, { 0, 0, 0 })
globals/camera/editorOverride
camera.editorOverride() -> string?
Entity id currently overriding viewport selection, or nil when the viewport is decided by highest-priority-wins.
Returns string? — Entity id of the overriding camera, or nil.
local owner = camera.editorOverride()
globals/camera/get
camera.get(target: (string | EntityRef)) -> CameraReport?
One camera's report from the observation — the same record
camera.list yields, for the camera the caller names. Takes an entity id,
an entity name, or an entity proxy, the same way the camera tools do.
Parameters
target(string | EntityRef)— Entity id, entity name, or entity proxy of the camera to report on.
Returns CameraReport? — The report, or nil when nothing resolves or that camera has none.
local c = camera.get(camera.active()); print(c.frame.far, c.authored.far)
local c = camera.get("minimapCam"); print(c.rendering, c.reason)
globals/camera/list
camera.list() -> { CameraReport }
Every camera in the world as a compact row each, ordered the way the
renderer resolves the on-screen camera: highest priority first. Reads the
same observation camera.observe does, so a row can never disagree with
the full report about whether a camera is enabled or which one drew.
Returns { CameraReport } — One row per camera entity, or an empty list before the first frame.
for _, c in ipairs(camera.list()) do print(c.name, c.enabled, c.rendering) end
globals/camera/main
camera.main() -> string?
Entity id of the main scene camera — the scene camera the viewport is
drawn from, and while the editor fly-camera holds the screen, the scene
camera that would take it. The gameplay/PlayerPrototype camera, an
agent-placed scene camera, or a cutscene camera. Never the editor camera;
nil if the scene has only the editor camera. It comes off the same
selection the frame does, so writing a pose to it moves what is drawn
whenever a scene camera is on screen. The scene camera with the highest
authored priority takes it; cameras tied on priority settle on the order
the frame visits them, so a scene that needs a specific camera — a
prototype and the clone play makes of it both stand at 0 — states a
distinct priority rather than resting on that order. For the camera drawn
on screen whichever partition owns it, use camera.active().
Returns string? — Entity id of the main scene camera, or nil — including the frame after that camera's entity is despawned, before the scene elects another.
local camId = camera.main(); local cam = camId and entity(camId)
globals/camera/motionTally
camera.motionTally() -> { frames: number, withoutHistory: number }
Frames the camera on screen has drawn, and how many of them had no previous frame to difference their motion vectors against — its first frame, every declared cut, and every frame the viewport changes hands on. Both counts are monotonic across the session, so two readings either side of a run say what happened in between.
Returns { frames: number, withoutHistory: number } — { frames, withoutHistory }.
local before = camera.motionTally().withoutHistory
globals/camera/observe
camera.observe() -> CameraObservation?
Every camera in the world, for the frame that has just been drawn.
Answers "why is this camera not showing what I expect" in one call:
rendering says whether each camera drew and reason names the single
cause when it did not — "disabled", "entityInactive", "targetMissing",
"noLayers", "outranked", "notDrawn". Each camera carries both
projections: authored is what the Camera component holds and frame is
what the renderer actually built, with mismatch naming every field the
two disagree on — so a clip range or a lens the frame did not use is one
field read. frame, viewProj, frustum and the cost numbers describe
a camera that drew; every cost is for that one frame.
One snapshot is published per drawn frame, from after the frame is drawn,
so a read describes the last frame rather than the world at the instant of
the call — a write and a read in one script step return the frame that ran
before the write. Put a task.wait() between them to compare a camera
either side of a change; frame counts the frames observed, so a poll can
wait for it to advance.
Returns CameraObservation? — The observation, or nil before the first frame has been drawn.
local obs = camera.observe(); for _, c in ipairs(obs.cameras) do print(c.name, c.rendering, c.reason) end
local dark = {}; for _, c in ipairs(camera.observe().cameras) do if not c.rendering then table.insert(dark, c.name .. ": " .. tostring(c.reason)) end end
globals/camera/setEditorOverride
camera.setEditorOverride(entityId: string?)
Give one camera the viewport outright, or pass nil to clear it. While set, that camera IS the on-screen camera and priority is never consulted, so no authored priority can take the viewport from it — which is what makes an authoring camera safe to fly over a scene holding a camera at any priority. An override naming a camera that is despawned or disabled falls back to highest-priority-wins rather than blanking the screen.
Parameters
entityIdstring(optional) — Entity id of the camera to route the viewport to, or nil to clear.
camera.setEditorOverride(camera.editor())
camera.setEditorOverride(nil)
globals/camera/viewData
camera.viewData(target: ((string | EntityRef)?)?) -> CameraView?
Camera render data. Called with no argument it is the active viewport
camera's data for this frame: world position, which projection it drew and
the field describing that frame, viewport pixel size, the 6 world-space
frustum planes (the same inward-pointing, normalized planes the renderer
culls with), and the view-projection matrix. The camera state a render
feature needs for camera-relative work — LOD selection, frustum culling,
billboards. Render features also get it as ctx.camera.
Called with an entity id it is that camera's data, read from the frame's
camera observation, and carries the identity the bare form has no room for:
which camera it describes, which frame it was built for, what it rendered
into, and the render layers it resolved to.
Parameters
target((string | EntityRef)?)(optional) — Entity id, name, or proxy of the camera to read, or nil for the viewport camera.
Returns CameraView? — The camera view data, or nil when that camera drew no frame.
local c = camera.viewData(); if c then print(c.position.x, c.fovY) end
local minimap = camera.viewData("minimapCam"); print(minimap.output.target, minimap.viewport.w)
globals/channel/create
channel.create(opts: ChannelOpts) -> number?
Register a keyframe channel. times is the sorted keyframe
time array; values is the packed value array (layout depends
on interp); stride is the floats-per-sample width; interp
is "step" | "linear" | "slerp" | "cubicHermite". Returns
the channel handle, or nil on malformed input.
Parameters
optsChannelOpts—{ times, values, stride, interp }.
Returns number? — Channel handle, or nil.
local h = channel.create({ times = ts, values = vs, stride = 3, interp = "linear" })
globals/channel/destroy
channel.destroy(handle: number) -> boolean
Drop the channel from the registry.
Parameters
handlenumber— Channel handle.
Returns boolean — True if the channel existed and was removed.
globals/channel/sampleInto
channel.sampleInto(ch: number, time: number, buf: Substrate.TypedBuffer, offset: number) -> boolean
Sample the channel at time and write stride floats into
the buffer starting at f32 index offset. Returns false
on unknown handle, layout mismatch, or out-of-bounds; the
buffer is unchanged on failure.
Parameters
chnumber— Channel handle.timenumber— Sample time in seconds.bufSubstrate.TypedBuffer— The buffer written into.offsetnumber— Starting f32 index in the buffer.
Returns boolean — True on success.
globals/channel/sampleManyInto
channel.sampleManyInto(ch: number, time: number, buf: Substrate.TypedBuffer, offsets: { number }) -> boolean
Sample once, blit the result into every position in
offsets. Saves the per-offset binary search when one channel
feeds many bones / particles / parameters.
Parameters
chnumber— Channel handle.timenumber— Sample time in seconds.bufSubstrate.TypedBuffer— The buffer written into.offsets{ number }— Array of f32 indices.
Returns boolean — True on success.
globals/channel/sampleQuat
channel.sampleQuat(ch: number, time: number) -> (number?, number?, number?, number?)
Convenience accessor for stride-4 quaternion channels.
Parameters
chnumber— Channel handle.timenumber— Sample time.
Returns (number?, number?, number?, number?) — (x, y, z, w) or nil.
globals/channel/sampleVec3
channel.sampleVec3(ch: number, time: number) -> (number?, number?, number?)
Convenience accessor for stride-3 channels. Returns the three components as multiret, or nil if the channel is unknown / has a different stride.
Parameters
chnumber— Channel handle.timenumber— Sample time.
Returns (number?, number?, number?) — (x, y, z) or nil.
local x, y, z = channel.sampleVec3(h, t)
globals/color/coerce
color.coerce(value: any?) -> Color?
Read a value written in any of the shapes a colour is authored
in — a hex string, an {r=,g=,b=,a=} map, or an {r,g,b,a} array
— as an sRGB color table. Returns nil when the value does not
describe a colour, so a caller can name the value it was handed
instead of substituting one. Channels absent from a map or array
read as 0; alpha absent reads as 1.
Parameters
valueany(optional) — Value to read as a colour.
Returns Color? — sRGB color table, or nil when value is not a colour.
local c = color.coerce("#5a5a62") or color.coerce({ 0.2, 0.7, 0.2 })
globals/color/complementary
color.complementary(c: Color) -> Color
Complementary color — rotate hue 180° in Oklch space.
Parameters
cColor— Input color.
Returns Color — Complementary sRGB color.
local accent = color.complementary(primary)
globals/color/darken
color.darken(c: Color, amount: number) -> Color
Decrease the lightness of a color in Oklch perceptual space.
Parameters
cColor— Input color.amountnumber— Lightness decrease 0-1.
Returns Color — Darkened sRGB color.
local pressed = color.darken(base, 0.1)
globals/color/desaturate
color.desaturate(c: Color, amount: number) -> Color
Decrease the chroma (saturation) of a color in Oklch space.
Parameters
cColor— Input color.amountnumber— Chroma decrease (typically 0-0.2).
Returns Color — Less saturated sRGB color.
local muted = color.desaturate(base, 0.05)
globals/color/hex
color.hex(hexString: string) -> Color?
Parse a hex color string into an sRGB color table. Accepts 3,
4, 6, or 8 hex digits with or without a leading # (e.g. "#f00",
"f00f", "#ff0000", "ff000080"). Returns nil on parse failure.
Parameters
hexStringstring— Hex color string.
Returns Color? — sRGB color table or nil.
local fromCss = color.hex("#ff8800")
globals/color/hsl
color.hsl(h: number, s: number, l: number) -> Color
Build a color from HSL (h: 0-360, s: 0-1, l: 0-1).
Returned as sRGB.
Parameters
hnumber— Hue (degrees, 0-360).snumber— Saturation (0-1).lnumber— Lightness (0-1).
Returns Color — sRGB color table { r, g, b, a = 1 }.
local teal = color.hsl(180, 0.5, 0.5)
globals/color/hsla
color.hsla(h: number, s: number, l: number, a: number) -> Color
Build a color from HSLA, returned as sRGB.
Parameters
hnumber— Hue (0-360).snumber— Saturation (0-1).lnumber— Lightness (0-1).anumber— Alpha (0-1).
Returns Color — sRGB color table { r, g, b, a }.
local fadedTeal = color.hsla(180, 0.5, 0.5, 0.3)
globals/color/hsv
color.hsv(h: number, s: number, v: number) -> Color
Build a color from HSV (h: 0-360, s: 0-1, v: 0-1).
Parameters
hnumber— Hue (0-360).snumber— Saturation (0-1).vnumber— Value / brightness (0-1).
Returns Color — sRGB color table { r, g, b, a = 1 }.
local primary = color.hsv(220, 0.7, 0.9)
globals/color/lighten
color.lighten(c: Color, amount: number) -> Color
Increase the lightness of a color in Oklch perceptual space.
Parameters
cColor— Input color.amountnumber— Lightness increase 0-1.
Returns Color — Lightened sRGB color.
local hover = color.lighten(base, 0.1)
globals/color/linear
color.linear(r: number, g: number, b: number, a: number?) -> Color
Build a color from linear RGB values (not gamma-corrected), output converted to sRGB. Useful for GPU-correct blending. Alpha defaults to 1.
Parameters
rnumber— Linear red (0-1).gnumber— Linear green (0-1).bnumber— Linear blue (0-1).anumber(optional) — Alpha (0-1, default 1).
Returns Color — sRGB color table { r, g, b, a }.
local gpuBlue = color.linear(0.0, 0.0, 1.0)
globals/color/mix
color.mix(c1: Color, c2: Color, t: number) -> Color
Perceptually blend two colors in Oklch space — better than RGB mixing for gradients.
Parameters
c1Color— First color.c2Color— Second color.tnumber— Blend factor 0-1 (0 = c1, 1 = c2).
Returns Color — Blended sRGB color.
local mid = color.mix(color.rgb(255, 0, 0), color.rgb(0, 0, 255), 0.5)
globals/color/mixRgb
color.mixRgb(c1: Color, c2: Color, t: number) -> Color
Linearly blend two colors in sRGB space — simple, but not
perceptually uniform. Prefer color.mix for natural gradients.
Parameters
c1Color— First color.c2Color— Second color.tnumber— Blend factor 0-1.
Returns Color — Blended sRGB color.
local plain = color.mixRgb(a, b, 0.5)
globals/color/oklch
color.oklch(l: number, c: number, h: number) -> Color
Build a color from Oklch perceptual color space (l: 0-1,
c: 0-0.4, h: 0-360). Ideal for perceptually uniform gradients
and color manipulation.
Parameters
lnumber— Lightness (0-1).cnumber— Chroma / saturation (0-0.4).hnumber— Hue (0-360).
Returns Color — sRGB color table { r, g, b, a = 1 }.
local accent = color.oklch(0.7, 0.15, 30)
globals/color/rgb
color.rgb(r: number, g: number, b: number) -> Color
Build an sRGB color from CSS-style 0-255 RGB channels. Alpha defaults to 1. Channels are normalised to 0-1 on the way out so the result composes with every other color helper.
Parameters
rnumber— Red channel (0-255).gnumber— Green channel (0-255).bnumber— Blue channel (0-255).
Returns Color — sRGB color table { r, g, b, a = 1 }, normalised to 0-1.
local red = color.rgb(255, 0, 0)
globals/color/rgba
color.rgba(r: number, g: number, b: number, a: number) -> Color
Build an sRGB color from CSS-style 0-255 RGB channels with explicit alpha. RGB are normalised to 0-1; alpha is taken as-is in the 0-1 range.
Parameters
rnumber— Red channel (0-255).gnumber— Green channel (0-255).bnumber— Blue channel (0-255).anumber— Alpha (0-1).
Returns Color — sRGB color table { r, g, b, a }.
local halfRed = color.rgba(255, 0, 0, 0.5)
globals/color/rotateHue
color.rotateHue(c: Color, degrees: number) -> Color
Rotate the hue of a color by a given number of degrees in Oklch space.
Parameters
cColor— Input color.degreesnumber— Hue rotation (positive or negative).
Returns Color — Hue-rotated sRGB color.
local triadic = color.rotateHue(base, 120)
globals/color/saturate
color.saturate(c: Color, amount: number) -> Color
Increase the chroma (saturation) of a color in Oklch space.
Parameters
cColor— Input color.amountnumber— Chroma increase (typically 0-0.2).
Returns Color — More saturated sRGB color.
local pop = color.saturate(base, 0.05)
globals/color/toHex
color.toHex(c: Color) -> string
Convert a color to a hex string. Returns "#rrggbb" or
"#rrggbbaa" if alpha is not 1.
Parameters
cColor— Input color.
Returns string — Hex color string.
print(color.toHex(color.rgb(255, 136, 0))) -- "#ff8800"
globals/color/toHsl
color.toHsl(c: Color) -> HslColor
Convert a color to HSL.
Parameters
cColor— Input color.
Returns HslColor — HSL color table { h, s, l, a } (h: 0-360, s/l: 0-1).
local hsl = color.toHsl(base)
globals/color/toLinear
color.toLinear(c: Color) -> Color
Convert a color from sRGB to linear RGB space — useful for GPU calculations that need linear-space values.
Parameters
cColor— Input sRGB color.
Returns Color — Linear RGB color table.
local gpu = color.toLinear(base)
globals/color/toOklch
color.toOklch(c: Color) -> OklchColor
Convert a color to Oklch perceptual color space.
Parameters
cColor— Input color.
Returns OklchColor — Oklch color table { l, c, h, a } (l: 0-1, c: 0-0.4, h: 0-360).
local okl = color.toOklch(base)
globals/color/withAlpha
color.withAlpha(c: Color, a: number) -> Color
Return a copy of a color with a different alpha value.
Parameters
cColor— Input color.anumber— New alpha (0-1).
Returns Color with modified alpha.
local ghost = color.withAlpha(base, 0.3)
globals/compute/absentReasons
compute.absentReasons() -> { string }
Every reason compute.diagnose reports, sorted. resident is the one
that means the resource is there.
Returns { string } — The closed set, as strings.
for _, r in ipairs(compute.absentReasons()) do print(r) end
globals/compute/beginBvh
compute.beginBvh(instances: { any }, opts: { [string]: any }?) -> (number?, string?)
Start the build compute.buildBvh runs, without running any of it.
Takes the same instances and options and reports the same non-resident
guids, and returns an id compute.stepBvh advances a bounded slice at a
time and compute.finishBvh collects. Each mesh the instances name is
copied as this is called — once per guid however many instances share it
— so the CPU mesh may be unloaded on the next line and the build still
finishes on the copy it holds. compute.buildBvhSliced is the whole loop
as one call.
Parameters
instances{ any }— Array of{ guid, transform, attributes? }mesh instances.opts{ [string]: any }(optional) — Optional{ maxLeaf? }— max triangles per leaf.
Returns (number?, string?) — The build id, or (nil, err) naming any non-resident guid.
local id = compute.beginBvh(gather.instances)
globals/compute/buildBvh
compute.buildBvh(instances: { any }, opts: { [string]: any }?) -> (any, string?)
Build a bounding-volume hierarchy over the world-space triangles of a
set of mesh instances and upload it as two named compute buffers —
geometry never passes through the scripting heap. Each instance is
{ guid, transform, attributes? }: guid names a mesh resident in the
meshcpu store (materialise with ref:load() / meshcpu.load),
transform is 16 numbers, row-major, translation in slots 4/8/12, and
attributes is up to 40 floats stamped onto every triangle of that
instance (surface colors, material ids, physics tags — whatever the
consuming shader wants per-surface). Triangles pack 18 vec4 each
(v0/v1/v2, n0/n1/n2, uvs, then 10 attribute vec4s — float slots 32..
carry the instance attributes, zero when absent) in BVH leaf order;
nodes 2 vec4 each (min + first-or-left, max + leaf-tagged
count-or-right). Consumers: GI baking, ray-traced passes, GPU picking,
navmesh and SDF generation.
Parameters
instances{ any }— Array of{ guid, transform, attributes? }mesh instances.opts{ [string]: any }(optional) — Optional{ maxLeaf? }— max triangles per leaf.
Returns (any, string?) — { nodes, tris, nodeCount, triCount } — nodes and tris are buffer handles the caller owns, passed to a dispatch like any other and destroyed when the hierarchy is done with. Or (nil, err) naming any non-resident guid.
local bvh = compute.buildBvh(instances)
shader:dispatch({ buffers = { bvh.nodes, bvh.tris }, workgroups = { 64, 1, 1 } })
globals/compute/buildBvhSliced
compute.buildBvhSliced(instances: { any }, opts: { [string]: any }?) -> (any, string?)
The hierarchy compute.buildBvh builds, spread over as many frames as
it takes: a slice of the build per frame, so a scene's triangle count
costs the frame loop budgetMs at a time instead of the whole build at
once. Yields, so it is called from a task. The result is the same pair of
buffers and the same counts compute.buildBvh returns.
Parameters
instances{ any }— Array of{ guid, transform, attributes? }mesh instances.opts{ [string]: any }(optional) — Optional{ maxLeaf?, budgetMs? }— max triangles per leaf, and the wall time one frame may spend on the build (default 4 ms).
Returns (any, string?) — { nodes, tris, nodeCount, triCount }, or (nil, err).
local built, err = compute.buildBvhSliced(gather.instances, { budgetMs = 4 })
globals/compute/bvhBuilds
compute.bvhBuilds() -> { any }
What the builds started by compute.beginBvh and not yet finished are
costing, oldest id first. Each row is { id, phase, triangles, units, slices, cpuMs, uploadedBytes }: phase is "gather", "build",
"serialize", "upload" or "ready", triangles how many have been
gathered, units the work units run, slices the compute.stepBvh
calls they ran in, cpuMs the wall time spent inside those calls, and
uploadedBytes how much of the hierarchy has reached the GPU.
Returns { any } — Array of build rows.
print(#compute.bvhBuilds(), "hierarchies in flight")
globals/compute/cancelBvh
compute.cancelBvh(id: number) -> boolean
Drop a build along with the triangles it has gathered.
Parameters
idnumber— Build id fromcompute.beginBvh.
Returns boolean — True when the id named a build.
compute.cancelBvh(id)
globals/compute/compile
compute.compile(nameOrHandle: string | { [string]: any } | AssetRef, opts: { [string]: any }?) -> boolean
Compile a compute shader from inline WGSL + a declarative binding
schema — the same codegen a .computeShader asset uses. The engine
generates the @group/@binding declarations from bindings/params,
so the source writes only @compute fn main. Symmetric with
registerShader, but with zero-scaffolding bindings (incl. textures,
samplers, storage textures and a params uniform). For asset-backed
shaders prefer authoring a .computeShader (compiled automatically);
use this for dynamic/generated compute shaders.
Parameters
nameOrHandlestring | { [string]: any } | AssetRef— Shader identity to register under (string or asset handle).opts{ [string]: any }(optional) —{ source, entryPoint?, bindings, params? }—bindingsis an ordered list of{ name, kind, access?, element?, format?, array? }.
Returns boolean — True on success.
compute.compile("my_sim", { source = WGSL, bindings = { { name = "data", kind = "buffer", access = "read_write", element = "f32" } } })
globals/compute/compileByName
compute.compileByName(ref: string | { [string]: any } | AssetRef)
Optional explicit pre-warm for a .computeShader asset (idempotent —
fingerprint-guarded). NORMALLY UNNECESSARY: compute.dispatch / dispatchEx
auto-compile a .computeShader on first use. Reach for this only to avoid
the one-frame first-dispatch warm-up in a latency-critical spot. Accepts an
identity/guid string or a resolved asset handle (its .identity is used).
Parameters
refstring | { [string]: any } | AssetRef— A.computeShaderidentity/guid string, or a resolved asset handle.
compute.compileByName("@builtin::shaders.compute_double")
globals/compute/copyBufferToTexture
compute.copyBufferToTexture(bufferName: string, textureKey: string, width: number, height: number, format: string?) -> boolean
Copy a compute buffer into a cached GPU texture under
textureKey, staying on the GPU. The path for an image a compute
pass produced: the buffer holds tightly-packed rows in the format's
texel layout, and the result is an ordinary cached texture — sample
it from a material, or pack it into the shared feature-texture array.
Rows must be a multiple of 256 bytes (at rgba16f, any width from 32
up in powers of two).
Parameters
bufferNamestring— Source compute buffer.textureKeystring— Cache key to register the texture under.widthnumber— Texture width in texels.heightnumber— Texture height in texels.formatstring(optional) — Texel format:"rgba16f"(default),"rgba32f","rgba8".
Returns boolean — True when the copy was queued.
compute.copyBufferToTexture("gi_resolved", "lm_wall", 128, 128)
globals/compute/createBuffer
compute.createBuffer(name: string, opts: { [string]: any }) -> boolean
Allocate a buffer under name, sized in bytes.
Parameters
namestring— The name a dispatch binds it by.opts{ [string]: any }—{ size, readback? }—sizein bytes.
Returns boolean — True once allocated.
globals/compute/createSampler
compute.createSampler(name: string, opts: { [string]: any }?) -> boolean
Create a named GPU sampler. opts: filter/wrap settings.
Parameters
namestringopts{ [string]: any }(optional)
Returns boolean
globals/compute/createStorageTexture2D
compute.createStorageTexture2D(name: string, opts: { [string]: any }) -> boolean
Create a 2D storage texture (compute-writable render target). opts: { width, height, format? }.
Parameters
namestringopts{ [string]: any }
Returns boolean
globals/compute/createTexture3D
compute.createTexture3D(name: string, opts: { [string]: any }) -> boolean
Create a 3D texture volume. opts: { width, height, depth, format?, storage? }.
Parameters
namestring— Unique volume name.opts{ [string]: any }— Dimensions + format (r8/r16f/r32f/rgba8/rgba16f/rgba32f).
Returns boolean — True on success (mutation queued).
globals/compute/createTextureHistory
compute.createTextureHistory(name: string, opts: { [string]: any }) -> boolean
Create a temporal history buffer (ping-pong textures) for a target. opts: { width, height, format? }.
Parameters
namestringopts{ [string]: any }
Returns boolean
globals/compute/destroyBuffer
compute.destroyBuffer(name: string) -> boolean
Release the buffer allocated under name.
Parameters
namestring— The name it was created under.
Returns boolean — True if a buffer under that name was released.
globals/compute/destroySampler
compute.destroySampler(name: string) -> boolean
Release a named sampler created by compute.createSampler and free
it. The counterpart to that call, alongside destroyBuffer,
destroyTexture, destroyTexture3D, destroyStorageTexture2D and
destroyTextureHistory. The manager's own defaults (linear_clamp,
linear_repeat, nearest_clamp) are kept for the session, since a
compute pass binds them by name.
Parameters
namestring— Sampler name.
Returns boolean — True when the release was queued.
compute.createSampler("soft", { filter = true }); compute.destroySampler("soft")
globals/compute/destroyShader
compute.destroyShader(name: string) -> boolean
Destroy a named compute shader pipeline.
Parameters
namestring— Shader name.
Returns boolean — True on success.
globals/compute/destroyShaderEx
compute.destroyShaderEx(name: string) -> boolean
Destroy a shader registered via registerShaderEx.
Parameters
namestring
Returns boolean
globals/compute/destroyStorageTexture2D
compute.destroyStorageTexture2D(name: string) -> boolean
Destroy a named 2D storage texture.
Parameters
namestring
Returns boolean
globals/compute/destroyTexture
compute.destroyTexture(textureKey: string) -> boolean
Release the cached GPU texture copyBufferToTexture registered
under textureKey, freeing its memory. Call it once the image is no
longer sampled. Writing the same key again replaces the texture, so a
key you keep re-using holds one allocation.
Parameters
textureKeystring— Cache key the texture was registered under.
Returns boolean — True when the release was queued.
compute.destroyTexture("lm_wall")
globals/compute/destroyTexture3D
compute.destroyTexture3D(name: string) -> boolean
Destroy a named 3D volume and free its GPU memory.
Parameters
namestring
Returns boolean
globals/compute/destroyTextureHistory
compute.destroyTextureHistory(name: string) -> boolean
Destroy a named texture-history buffer.
Parameters
namestring
Returns boolean
globals/compute/diagnose
compute.diagnose(key: string) -> { [string]: any }
Whether a resource is filed under key right now, and when none is,
which state the inventory says the key is in. A key out of a dispatch
failure resolves here; a mistyped one reports why it does not.
Parameters
keystring— The resource key, verbatim.
Returns { [string]: any } — { key, exists, reason, resource?, current? }. resource is the row when one is filed under the key. reason is one of compute.absentReasons(). current names the live key when the owner holds a resource under the same name at a different serial.
local d = compute.diagnose(key)
if not d.exists then print(d.reason, d.current) end
globals/compute/dispatch
compute.dispatch(shaderNameOrHandle: string | { [string]: any } | AssetRef, opts: DispatchOpts) -> boolean
Dispatch a compute shader with bound buffers. Accepts a
shader name string or an asset handle from asset.load().
Parameters
shaderNameOrHandlestring | { [string]: any } | AssetRef— Shader name or asset handle.optsDispatchOpts—{ buffers, workgroups }. A zero in any workgroup dimension is refused and recorded as a dispatch failure — clamp a computed count withmath.max(1, math.ceil(n / 64)).
Returns boolean — True once the dispatch is queued. What the engine then did with it is in compute.failing().
compute.dispatch("blur", { buffers = { "src", "dst" }, workgroups = { 8, 8 } })
globals/compute/dispatchEx
compute.dispatchEx(shaderNameOrHandle: string | { [string]: any } | AssetRef, opts: { [string]: any }) -> boolean
Dispatch a compute shader with extended texture/storage/sampler bindings.
Asset-backed .computeShaders resolve to their stable guid (collision-safe,
lazily compiled on first dispatch); raw registerShaderEx names pass through.
resources covers the bindings the shader DECLARES. A params: block's
uniform is engine-owned — the compile creates and packs it, setParam
writes it, and the dispatch binds it — so it takes no entry here.
Parameters
shaderNameOrHandlestring | { [string]: any } | AssetRef— Shader name or asset handle.opts{ [string]: any }—{ resources, workgroups }— each resource is{ kind, name }. A zero in any workgroup dimension is refused and recorded as a dispatch failure — clamp a computed count withmath.max(1, math.ceil(n / 64)).
Returns boolean — True once the dispatch is queued. What the engine then did with it is in compute.failing().
globals/compute/dispatchOnVertices
compute.dispatchOnVertices(shaderNameOrHandle: string | { [string]: any } | AssetRef, opts: DispatchOnVerticesOpts) -> boolean
Dispatch a compute shader with a model's vertex buffer bound
at binding 0 (read_write). Use to mutate vertex positions
directly. Asset-backed .computeShaders resolve to their stable guid
(collision-safe, lazily compiled on first dispatch); raw
registerShader names pass through.
Parameters
shaderNameOrHandlestring | { [string]: any } | AssetRef— Shader name or asset handle.optsDispatchOnVerticesOpts—{ model, buffers?, workgroups }. A zero in any workgroup dimension is refused and recorded as a dispatch failure — clamp a computed count withmath.max(1, math.ceil(n / 64)).
Returns boolean — True once the dispatch is queued. What the engine then did with it is in compute.failing().
compute.dispatchOnVertices("flatten", { model = "@user/mesh", workgroups = { 64 } })
globals/compute/failing
compute.failing() -> { { [string]: any } }
Every compute dispatch whose most recent run FAILED, one record per
(shader, target) pair. A dispatch is recorded into a command encoder
frames after the call that asked for it returned, so a pass that stops
running reports here rather than through that call's return value: each
record carries the shader key, the target it writes (a mesh guid for a
dispatch over vertices, the buffers it bound for one that writes only
those), how
many dispatches and failures it has had, and lastError. An empty result
means every dispatch the engine has been given is running.
Returns { { [string]: any } } — Array of { shader, target, dispatches, failures, ok, lastFrame, lastFailedFrame?, lastError? }.
for _, d in ipairs(compute.failing()) do print(d.shader, d.lastError) end
globals/compute/finishBvh
compute.finishBvh(id: number) -> (any, string?)
Hand over a finished build's hierarchy as the same two buffers
compute.buildBvh returns, and release the build. The slices put every
byte of it on the GPU as they ran, so this costs the frame it is called
in the handover and nothing of the scene.
Parameters
idnumber— Build id fromcompute.beginBvh, stepped until"ready".
Returns (any, string?) — { nodes, tris, nodeCount, triCount }, or (nil, err) when the id names no build or the build still has work left.
local built = compute.finishBvh(id)
globals/compute/getReadbackResult
compute.getReadbackResult(resultKey: string) -> { number }?
Poll for a completed read-back and return its bytes as a
1-indexed array of f32 values, nil if pending. The f32
reinterpretation applies to whatever the buffer holds: bytes
written as u32 1, 2, 3, 4 read back here as 1.4e-45, 2.8e-45, 4.2e-45, 5.6e-45 — use getReadbackResultU32() for those, or
getReadbackResultBytes() for a buffer the rest of the buffer
surface accepts. Result is consumed on retrieval, and polling a key
that was never issued raises rather than reading as forever-pending.
Parameters
resultKeystring— Key returned byreadBuffer().
Returns { number }? — 1-indexed array of f32 values, or nil if not ready.
local floats = compute.getReadbackResult(key)
globals/compute/getReadbackResultBytes
compute.getReadbackResultBytes(resultKey: string) -> buffer?
Poll for a completed read-back and get its raw bytes as a
buffer, copied once. The read counterpart of writeBufferBytes:
read values out with buffer.readf32 / buffer.readu32, or hand the
buffer straight to writeBuffer — a payload that stays packed never
becomes a table. Result is consumed on retrieval.
Parameters
resultKeystring— Key returned byreadBuffer().
Returns buffer? — The read-back's bytes, or nil if not ready.
local bytes = compute.getReadbackResultBytes(key)
local firstTexel = buffer.readf32(bytes, 0)
globals/compute/getReadbackResultU32
compute.getReadbackResultU32(resultKey: string) -> { number }?
Poll for a completed read-back interpreting bytes as u32. Returns array of integer values if ready, nil if pending.
Parameters
resultKeystring— Key returned byreadBuffer().
Returns { number }? — Array of u32 values, or nil if not ready.
globals/compute/isReadbackReady
compute.isReadbackReady(resultKey: string) -> boolean
Check if a readback result is available without consuming it.
Raises for a key this engine never issued, or whose result was already
drained — nil/false already means "still in flight", so a mistyped
key reports itself instead of polling forever. Use readbackState()
to test that case without raising.
Parameters
resultKeystring— Key returned byreadBuffer().
Returns boolean — True if the result is ready.
globals/compute/observe
compute.observe() -> { [string]: any }
Every GPU resource the compute subsystem is holding right now — its storage and uniform buffers, its 3D textures, its 2D storage targets, its history pairs and its samplers — with what each one costs and which shader asked for it. This is the call to reach for when compute is holding memory and you do not know what, or when a key out of a dispatch failure needs matching against what exists.
Returns { [string]: any } — { published, generation, resources, totals }. Each row of resources carries key, kind (buffer / uniformBuffer / texture3d / storageTexture2d / textureHistory / sampler), owner ({ shader, name, serial }, read off the key), bytes, format, width, height, depth, usage (the bits it was created with — storage, copySrc, copyDst, vertex, index, indirect, uniform, sampled, sampler) and createdFrame. totals is { count, bytes, byKind }, what the rows sum to — and totals.bytes is the compute figure of renderer.gpuMemory(), read off the same registries. published is false when no renderer has published a reading yet, which is the engine saying it cannot answer rather than answering with nothing. The reading is the one the renderer published, republished on a frame where a registry gained or lost an entry: a resource created earlier in this same script is in the next reading, so wait a frame before asking about it, and generation moves when it arrives.
local r = compute.observe()
print(r.totals.count, r.totals.bytes)
for _, res in ipairs(r.resources) do print(res.key, res.kind, res.bytes) end
globals/compute/program/compile
compute.program.compile(key: string, spec: { [string]: any }) -> boolean
Register a compiled program under key from WGSL plus a declared
binding schema. The engine generates the @group/@binding declarations
from the schema, expands #includes, naga-validates, and registers the
result.
Parameters
keystring— The key to register under.spec{ [string]: any }—{ source, entryPoint?, bindings, params }— the parsed schema.
Returns boolean — True on success.
globals/compute/program/destroy
compute.program.destroy(key: string) -> boolean
Release the program registered under key.
Parameters
keystring— The program's key.
Returns boolean — True on success.
globals/compute/program/dispatch
compute.program.dispatch(key: string, opts: { [string]: any }) -> boolean
Dispatch the program under key with one buffer per declared storage
binding, in declaration order.
Parameters
keystring— The program's key.opts{ [string]: any }—{ buffers, workgroups }. A zero in any workgroup dimension is refused and recorded as a dispatch failure — clamp a computed count withmath.max(1, math.ceil(n / 64)).
Returns boolean — True once the dispatch is queued. What the engine then did with it is in compute.program.status(key).
globals/compute/program/dispatchEx
compute.program.dispatchEx(key: string, opts: { [string]: any }) -> boolean
Dispatch the program under key with explicit resources — one
{ kind, name } per declared binding, in declaration order.
Parameters
keystring— The program's key.opts{ [string]: any }—{ resources, workgroups }. A zero in any workgroup dimension is refused and recorded as a dispatch failure — clamp a computed count withmath.max(1, math.ceil(n / 64)).
Returns boolean — True once the dispatch is queued. What the engine then did with it is in compute.program.status(key).
globals/compute/program/dispatchOnVertices
compute.program.dispatchOnVertices(key: string, opts: { [string]: any }) -> boolean
Dispatch the program under key over a mesh's vertices. The mesh
opts.model names fills the shader's vertices binding, and opts.buffers
fills the remaining storage bindings.
Parameters
keystring— The program's key.opts{ [string]: any }—{ model, buffers?, workgroups }. A zero in any workgroup dimension is refused and recorded as a dispatch failure — clamp a computed count withmath.max(1, math.ceil(n / 64)).
Returns boolean — True once the dispatch is queued. What the engine then did with it is in compute.program.status(key).
globals/compute/program/setParam
compute.program.setParam(key: string, prop: string, value: number) -> boolean
Write one scalar of the program's params: uniform. A value set before
the program's first compile is the value it starts with.
Parameters
keystring— The program's key.propstring— Parameter name as declared.valuenumber— New scalar value.
Returns boolean — True on success.
globals/compute/program/status
compute.program.status(key: string) -> { { [string]: any } }
What the engine did with the dispatches of the program under key,
one record per target.
Parameters
keystring— The program's key.
Returns { { [string]: any } } — Array of dispatch records, most recently dispatched first.
globals/compute/programState
compute.programState(ref: string | { [string]: any } | AssetRef) -> (string, string?)
Where a shader's compiled program stands. A registration is queued
from script and the pipeline is built on the render side frames later,
so the call that asked for the compile cannot say whether it produced a
program: "absent" (the engine holds nothing under this key and nothing
is in flight — never asked for, or released), "pending" (asked for, not
on the device yet — a recompile of a resident program reads pending too,
because what it produces is a different program from the one bound now),
"ready" (compiled and resident, so a dispatch binds it), or "failed"
(the most recent registration produced no program), returned with the
reason as a second value. Wait for "ready" before a dispatch whose
result is read back, rather than for a count of frames.
Parameters
refstring | { [string]: any } | AssetRef— A.computeShaderidentity/guid string, a resolved asset handle, or the name a raw registration chose.
Returns (string, string?) — "absent", "pending", "ready" or "failed", and the reason when "failed".
if compute.programState(carve) == "ready" then carve:dispatch(opts) end
globals/compute/readBuffer
compute.readBuffer(name: string) -> string
Start a GPU→CPU read of the buffer under name.
Parameters
namestring— The name it was created under.
Returns string — The result key to poll with getReadbackResult*. A read that could not start answers with the empty key, which every drain reports as unknown — the same shape a caller already handles.
globals/compute/readTexture3D
compute.readTexture3D(name: string) -> string
Start a GPU→CPU readback of a named 3D volume. Returns a result key to poll.
Parameters
namestring
Returns string
globals/compute/readbackState
compute.readbackState(resultKey: string) -> string
Where a readback key stands, without consuming it and without
raising: "pending" (issued, GPU has not delivered), "ready"
(delivered, waiting to be drained), or "unknown" (never issued by
readBuffer(), or already drained — a result is delivered once).
Parameters
resultKeystring— Key returned byreadBuffer().
Returns string — "pending", "ready", or "unknown".
if compute.readbackState(key) == "ready" then ... end
globals/compute/registerShader
compute.registerShader(nameOrHandle: string | { [string]: any } | AssetRef, opts: ShaderOpts?) -> boolean
Register a compute shader. Accepts an asset handle from
asset.load(), or (name, opts) with inline WGSL source.
Parameters
nameOrHandlestring | { [string]: any } | AssetRef— Shader name or asset handle.optsShaderOpts(optional) — Shader options.bindingscomes from the source's own@group(0) @binding(n)declarations when omitted; supplying a count that disagrees with them raises. EveryreadOnlyBindingsentry names one of those declared bindings, as a whole number from 0 tobindings - 1; an entry outside that run raises.
Returns boolean — True on success.
compute.registerShader("blur", { source = WGSL, entryPoint = "main" })
globals/compute/registerShaderEx
compute.registerShaderEx(nameOrHandle: string | { [string]: any } | AssetRef, opts: { [string]: any }?) -> boolean
Register a compute shader with extended texture/sampler bindings. Accepts a name + opts or an asset handle.
Parameters
nameOrHandlestring | { [string]: any } | AssetRefopts{ [string]: any }(optional)
Returns boolean
globals/compute/resources
compute.resources(owner: any?) -> { any }
The resource rows on their own, optionally narrowed to what one shader owns.
Parameters
ownerany(optional) — A.computeShaderref, its guid, or its asset identity. Omit for every resource compute holds. A value carrying no shader raises, so a narrowing that cannot be done reads as an error rather than as the whole inventory. A guid stands for itself, so resources outlive the asset that made them and stay reachable by their owner.
Returns { any } — An array of rows in the shape compute.observe().resources carries. Empty when the owner holds nothing.
for _, r in ipairs(compute.resources(shaderRef)) do print(r.key, r.bytes) end
globals/compute/setParam
compute.setParam(nameOrHandle: string | { [string]: any } | AssetRef, prop: string, value: number) -> boolean
Set a named scalar parameter on a .computeShader (a params:
entry in its bindings.yaml). Updates the shader's params uniform
in place; the next dispatch sees the new value. No effect on raw
registerShader shaders, which have no params block.
Parameters
nameOrHandlestring | { [string]: any } | AssetRef— Shader identity (the.computeShaderasset name), or the handleasset.load/asset.resolvereturns — the same formsdispatchtakes.propstring— Parameter name as declared inbindings.yaml.valuenumber— New scalar value (numbers only).
Returns boolean — True on success.
compute.setParam("my_sim", "scale", 4.0)
globals/compute/stepBvh
compute.stepBvh(id: number, budgetMs: number?) -> (string?, string?)
Advance a build by as many work units as budgetMs buys, and report
whether it has finished: "pending" means there is work left,
"ready" means compute.finishBvh will hand over the buffers. The
slices carry the hierarchy onto the GPU as well as building it, so a
build that reads "ready" has already uploaded every byte of itself. A
slice always runs at least one unit, so a budget of 0 advances the build
by exactly one and the largest single unit sets the floor under a slice.
Parameters
idnumber— Build id fromcompute.beginBvh.budgetMsnumber(optional) — Wall time this slice may spend, in milliseconds (default 4).
Returns (string?, string?) — "pending" or "ready", or (nil, err) when the id names no build.
while compute.stepBvh(id, 4) == "pending" do task.wait() end
globals/compute/textureFormatBytes
compute.textureFormatBytes(format: string) -> number
Bytes-per-voxel for a texture format string (rgba16f, r8, ...).
Parameters
formatstring
Returns number
globals/compute/writeBuffer
compute.writeBuffer(name: string, values: { number } | buffer | string, offset: number?) -> boolean
Write words into the buffer under name.
Parameters
namestring— The name it was created under.values{ number } | buffer | string— The floats to write, or abuffer/ binary string already holding them.offsetnumber(optional) — 32-bit word offset to write at.
Returns boolean — True on success.
globals/compute/writeBufferBytes
compute.writeBufferBytes(name: string, bytes: buffer | string, offsetBytes: number?) -> boolean
Write packed bytes into the buffer under name.
Parameters
namestring— The name it was created under.bytesbuffer | string— The payload.offsetBytesnumber(optional) — Byte offset to write at.
Returns boolean — True on success.
globals/compute/writeBufferU32
compute.writeBufferU32(name: string, values: { number } | buffer | string, offsetBytes: number?) -> boolean
Write 32-bit words into the buffer under name.
Parameters
namestring— The name it was created under.values{ number } | buffer | string— The words to write.offsetBytesnumber(optional) — Byte offset to write at.
Returns boolean — True on success.
globals/compute/writeFloatsTexture3D
compute.writeFloatsTexture3D(name: string, floats: { number }, formatOrOpts: (string | { [string]: any })?) -> boolean
Upload float values into a named 3D volume, packed via the given format (default rgba16f).
Parameters
namestringfloats{ number }formatOrOpts(string | { [string]: any })(optional)
Returns boolean
globals/compute/writeTexture3D
compute.writeTexture3D(name: string, data: buffer | string | { number }) -> boolean
Upload raw bytes (u8) into a named 3D volume. A buffer or a binary
string holds the volume's byte layout verbatim and crosses in one copy —
the shape a file's voxel payload arrives in; an array carries one byte
value (0..255) per entry.
Parameters
namestring— Volume name.databuffer | string | { number }— Voxel bytes as abuffer, a binary string, or an array of bytes.
Returns boolean — True on success (mutation queued).
globals/computed
computed(fn: () -> any): any
Mark a public.X = computed(fn) field as a derived read-only property — auto-injected by the prelude's component_proxy.
globals/debugger/__diagnostics
debugger.__diagnostics() -> DebuggerDiagnostics
Internal diagnostic counters for debugging the debugger
itself: { installs, debugbreakHits }.
Returns DebuggerDiagnostics — Diagnostic counters.
globals/debugger/addWatch
debugger.addWatch(expr: string) -> number
Register an expression to re-evaluate on every pause.
Parameters
exprstring— Luau expression.
Returns number — Watch id.
globals/debugger/continue_
debugger.continue_() -> boolean
Resume the paused thread.
Returns boolean — True if a thread was paused, false if nothing was paused.
globals/debugger/disableAll
debugger.disableAll()
Disable every registered breakpoint. Records persist; bytecode BREAK ops are cleared.
globals/debugger/disconnect
debugger.disconnect(handle: number) -> boolean
Disconnect an onBreak or onResume callback.
Parameters
handlenumber— Handle returned by onBreak/onResume.
Returns boolean — True if the handle existed.
globals/debugger/enableAll
debugger.enableAll()
Enable every registered breakpoint and re-install them in the VM bytecode.
globals/debugger/evaluate
debugger.evaluate(expr: string, frame: number?) -> (string?, string?)
Evaluate an expression against the paused frame's
environment. Returns (value, error).
Parameters
exprstring— Luau expression.framenumber(optional) — 1-based frame index (default 1).
Returns (string?, string?) — (value, error).
globals/debugger/getLocals
debugger.getLocals(frame: number?) -> { [string]: string }
Locals captured at the active pause for the given frame index (1 = top). Values are stringified for safe display.
Parameters
framenumber(optional) — 1-based frame index (default 1).
Returns { [string]: string } — { [name] = string }.
globals/debugger/getPauseInfo
debugger.getPauseInfo() -> PauseInfo?
Info about the active pause, or nil if nothing is paused.
Returns PauseInfo? — { path, line, reason } or nil.
globals/debugger/getStack
debugger.getStack() -> { Frame }
Captured stack from the active pause, top frame first. Empty when nothing is paused.
Returns { Frame } — Array of Frame tables.
globals/debugger/getUpvalues
debugger.getUpvalues(frame: number?) -> { [string]: string }
Upvalues captured at the active pause for the given frame.
Parameters
framenumber(optional) — 1-based frame index.
Returns { [string]: string } — { [name] = string }.
globals/debugger/getWatchValue
debugger.getWatchValue(id: number) -> (string?, string?)
Re-evaluate the watch expression against the paused frame's
environment and return (value, error).
Parameters
idnumber— Watch id.
Returns (string?, string?) — (value, error).
globals/debugger/getWatches
debugger.getWatches() -> { Watch }
Snapshot of all watches with their last evaluated value and error, sorted by id.
Returns { Watch } — Array of Watch tables.
globals/debugger/isPauseOnError
debugger.isPauseOnError() -> boolean
Current pause-on-error toggle state for this VM.
Returns boolean — True if enabled.
globals/debugger/isPaused
debugger.isPaused() -> boolean
Whether the debugger currently has a paused thread.
Returns boolean — True if paused.
globals/debugger/listBreakpoints
debugger.listBreakpoints() -> { Breakpoint }
Snapshot of every registered breakpoint, sorted by id
ascending. Each entry reports whether it is installed:
chunkNames lists the loaded chunks carrying it, and
pendingReason says why an empty list is empty.
Returns { Breakpoint } — Array of breakpoint tables.
globals/debugger/onBreak
debugger.onBreak(fn: (PauseInfo) -> ()) -> number
Register a callback invoked on every pause with
{ path, line, reason }. Returns a handle usable with
debugger.disconnect.
Parameters
fn(PauseInfo) -> ()— Callback.
Returns number — Handle.
globals/debugger/onResume
debugger.onResume(fn: () -> ()) -> number
Register a callback invoked when the paused thread is resumed.
Parameters
fn() -> ()— Callback.
Returns number — Handle.
globals/debugger/removeBreakpoint
debugger.removeBreakpoint(id: number) -> boolean
Remove the breakpoint with the given id.
Parameters
idnumber— Breakpoint id returned by setBreakpoint.
Returns boolean — True if removed, false if the id was unknown.
globals/debugger/removeWatch
debugger.removeWatch(id: number) -> boolean
Remove the watch with the given id.
Parameters
idnumber— Watch id.
Returns boolean — True if removed.
globals/debugger/setBreakpoint
debugger.setBreakpoint(path: string, line: number, opts: BreakpointOpts?) -> Breakpoint
Set a breakpoint at line in the script path names — its
VFS path, its require identity, or the chunk name it loaded
under. An installed breakpoint carries resolvedLine and lists
the loaded chunks holding it in chunkNames; one whose script is
not loaded carries pendingReason, an empty chunkNames, and
installs itself when that script loads.
Parameters
pathstring— VFS path, require identity, or chunk name.linenumber— 1-based source line.optsBreakpointOpts(optional) —{ condition?, logMessage?, hitCount?, enabled? }.
Returns Breakpoint — The breakpoint table.
local bp = debugger.setBreakpoint("/zero/source/main.luau", 42)
print(bp.pendingReason or ("installed in " .. bp.chunkNames[1]))
globals/debugger/setPauseOnError
debugger.setPauseOnError(enabled: boolean)
When true, uncaught Luau errors fire the onBreak callback (observation only — the error still propagates).
Parameters
enabledboolean— Toggle state.
globals/debugger/stepInto
debugger.stepInto() -> boolean
Run until the next line, descending into any function call.
Returns boolean — True if a step was scheduled.
globals/debugger/stepOut
debugger.stepOut() -> boolean
Run until the current frame returns; pauses in the caller.
Returns boolean — True if a step was scheduled.
globals/debugger/stepOver
debugger.stepOver() -> boolean
Run until the next line in the current frame. Calls inside the current line are skipped.
Returns boolean — True if a step was scheduled.
globals/debugger/toggleBreakpoint
debugger.toggleBreakpoint(path: string, line: number) -> Breakpoint?
Toggle a breakpoint at the given line: removes if present, adds otherwise.
Parameters
pathstring— VFS path, require identity, or chunk name.linenumber— 1-based line.
Returns Breakpoint? — Breakpoint table if added, nil if removed.
globals/declare
declare(spec)
Declare top-level component metadata: executionOrder (i32), role (string — the job this component fills when several types fill the same one, e.g. "collider"), bindings ({string}), syncedFunctions ({string}), initKeys ({string} — init-table keys the component normalises itself, beyond its public fields), nativeFields ({[string]: string | {string}} — fields of the SAME-NAMED native ECS component this component computes, each mapped to the field(s) of its own it computes them from, e.g. { lightType = "kind" }; a refusal naming one of those fields answers with the name that holds instead of a direct row write the next refresh replaces). Per-field schema (type, default, Sync/NoSync) lives in public = {...} via Field.<kind>(default, mode) constructors — see man field. Must be called at top level, not inside any function.
Parameters
spectable— { executionOrder = N, role = "collider", bindings = {...}, syncedFunctions = {...}, initKeys = {...}, nativeFields = { nativeField = "ownField" } }
globals/delay
delay(seconds) -> promise
Returns a promise that resolves after N seconds. Use with await(delay(2)) to pause execution.
Parameters
secondsnumber— Delay duration in seconds
Returns string — Promise handle for use with await()
globals/editorTools
editorTools(scope: string) -> table
Bind a toolbox handle whose methods call each tool and return its value directly, raising on failure. Superseded by tools.use("
globals/effects/backends
effects.backends() -> { string }
The backend kinds an effect can be built out of, in name order. The
runtime ships emitter, geometry, material, decal and feature.
Returns { string } — Array of kind names.
print(table.concat(effects.backends(), ", "))
globals/effects/describe
effects.describe(identity: string) -> { [string]: any }
What an effect declares about itself: its family, a one-line summary, every parameter with its type, default and documented range, and the cost one unpooled play of it was measured to draw. The one call to make against an unfamiliar effect before playing it.
Parameters
identitystring— The effect's canonical identity, or a short name.
Returns { [string]: any } — { identity, family, summary, cost, params }.
local d = effects.describe("explosion"); print(d.family, d.cost.gpuMs)
globals/effects/drain
effects.drain() -> { [string]: number }
Free every backend the pool is holding idle. The pool keeps what it has leased for as long as the engine runs — that is what makes repeated firing cost nothing after the first — and this is the one call that gives it back. A backend a live play still holds is left to that play's own end.
Returns { [string]: number } — { freed, kept }.
print(effects.drain().freed)
globals/effects/families
effects.families() -> { string }
Every family the effects in this world declare, sorted — the values
list { family = … } filters on. An effect declaring no family is not one
of them.
Returns { string } — Array of family names.
for _, f in ipairs(effects.families()) do print(f, #effects.list({ family = f })) end
globals/effects/list
effects.list(opts: table?) -> { string }
The canonical identity of every effect this world can play, sorted.
These are the exact strings play takes. Pass { family = "combat" } to
get only the effects of one family — the catalogue filtered the way an
effect declares itself.
Parameters
optstable(optional) —{ family? = string }. A family is matched without regard to case.
Returns { string } — Array of identities.
for _, id in ipairs(effects.list()) do print(id) end
for _, id in ipairs(effects.list({ family = "combat" })) do print(id) end
globals/effects/observe
effects.observe() -> { [string]: any }
What the runtime is holding and driving right now — every live play with
the reason it is silent when it is, plus what the pool has leased out and
what it is keeping idle, in instances and in GPU bytes. This is how a caller
and a test tell a working effect from a silent one, and how they tell a pool
warming to a wider burst from something leaking: the pool is sized by the
most effects it has had to cover at once, which peakLive and peakLeased
report beside the current totals.
Returns { [string]: any } — The observation.
local o = effects.observe(); print(o.live, o.leased, o.pooled, o.bytes)
print(o.peakLive, o.peakLeased) -- the widest burst the pool covers
globals/effects/play
effects.play(identity: string, opts: table?) -> any
Play an effect once at a world position. The effect allocates what it needs from the shared pool, draws itself, and gives everything back when it ends — with no update loop on the caller's side.
Parameters
identitystring— The effect's canonical identity, or a short name that reaches exactly one effect.optstable(optional) —{ position? = { x, y, z }, rotation? = quat, direction? = { x, y, z }, params? = { … }, duration? = number, held? = boolean }. Anythingparamsomits takes the effect's declared default, and an effect that declares adurationparameter reads its length from there rather than fromdurationhere.
Returns any — The play handle — stop, cancel, retarget, setParam, isPlaying, isFinished, seek, stats, whySilent.
local h = effects.play("@builtin::systems.effects.combat.explosion", {
position = { 0, 2, 0 }, params = { scale = 4, coreColor = { 1, 0.4, 0.1 } },
})
globals/effects/playOn
effects.playOn(identity: string, target: any?, opts: table?) -> any
Play an effect on an entity: it starts where the entity stands and ends
if the entity leaves the world. Move it with the entity by calling
handle:retarget(theEntity) as it goes.
Parameters
identitystring— The effect's canonical identity, or a short name.targetany(optional) — An entity proxy or entity id.optstable(optional) — The same optionsplaytakes;positionis read from the entity.
Returns any — The play handle.
local h = effects.playOn("explosion", drum, { params = { scale = 3 } })
globals/effects/registerBackend
effects.registerBackend(kind: string, backend: table)
Register a new way of drawing under a kind name, so an effect family
that needs one the runtime does not ship adds it rather than widening the
runtime. Every effect reaches it through ctx.lease(kind, spec).
Parameters
kindstring— The kind name a spec asks for.backendtable— The backend —key,acquire,seat,start,stop,quiet,place,bytes,active,silenceandfree.
effects.registerBackend("ribbonTrail", myBackend)
globals/effects/silenceReasons
effects.silenceReasons() -> { { reason: string, means: string } }
The closed set of reasons a play can be producing nothing, in the order
a reading resolves them — nearest cause first — each with what it means.
Every reason an observation reports is one of these.
Returns { { reason: string, means: string } } — Array of { reason, means }.
for _, r in ipairs(effects.silenceReasons()) do print(r.reason, r.means) end
globals/egress/credentialNames
egress.credentialNames() -> { string }
List the names of configured credentials. Names only — secret values are never exposed to Luau.
Returns { string } — Array of configured credential names.
for _, n in ipairs(egress.credentialNames()) do print(n) end
globals/egress/fetch
egress.fetch(name: string, method: string, url: string, headers: Headers?, body: JsonBody?, response: EgressResponseType?) -> string?
Perform an HTTP request with a named credential injected
server-side (in Rust). Returns a promise handle for
task.await(), or nil when the credential is unknown or url
is outside the credential's allowed base_url. The secret is
never exposed to Luau. This is the seam that production points
at the ZeroMind egress endpoint.
Parameters
namestring— Credential name registered by the trusted VM.methodstring— HTTP method, e.g. "GET" or "POST".urlstring— Request URL (must start with the credential'sbase_url).headersHeaders(optional) — Extra header key-value pairs.bodyJsonBody(optional) — JSON body (encoded automatically).responseEgressResponseType(optional) —"json"(default) or"bytes".
Returns string? — Promise handle for task.await(), or nil if refused.
local h = egress.fetch("meshy", "POST", url, nil, { prompt = p })
globals/egress/hasCredential
egress.hasCredential(name: string) -> boolean
Whether a named credential is configured. Returns only a boolean — never the value. Service handlers use this to fail with a clear "not configured" message.
Parameters
namestring— Credential name.
Returns boolean — True if configured.
if not egress.hasCredential("meshy") then error("set MESHY_API_KEY") end
globals/engine/discardPlayChanges
engine.discardPlayChanges() -> ()
Arm the leave-play safeguard's deliberate discard for the play session this is called from, so that session's play to edit flip proceeds and discards its unaccepted changes.
Returns ()
globals/engine/gameplayReady
engine.gameplayReady -> boolean
Whether gameplay simulation is running: not paused, and the play scene materialized. Read-only.
Returns boolean
globals/engine/gpuCompute
engine.gpuCompute -> boolean
Whether this process holds a live GPU device, so compute dispatch is available. Read-only.
Returns boolean
globals/engine/headless
engine.headless -> boolean
Whether this boot renders offscreen with no window a person can see. Content that only serves someone at a display stands down when it reads true. Read-only.
Returns boolean
globals/engine/markScriptingBaseline
engine.markScriptingBaseline() -> number
Record the scripting registries — world-event subscriptions, the
four lifecycle-watcher lists, and the require cache — as they stand
right now, and make that the point engine.resetScriptingState()
restores to. Replaces any previous mark. Returns the new mark's
generation, counting from 1.
Mark once the engine is serving rather than while it boots: the
registries keep growing as the prelude subscribes, the world
entrypoint runs and the startup scene loads, so a mark taken partway
through sits below the rest of that work and the first reset would
remove it.
Returns number
engine.markScriptingBaseline()
world.on("player_join", function() end)
engine.resetScriptingState() -- the subscription above is gone
globals/engine/mode
engine.mode -> "edit" | "play"
The engine mode this process is in, edit or play. Assigning it takes the flip, side effects and all.
Returns "edit" | "play"
globals/engine/offDeviceRebuilt
engine.offDeviceRebuilt(id: number) -> boolean
Remove an engine.onDeviceRebuilt subscriber by its watcher id.
Returns true when a live watcher carried that id, false when it named
none — already removed, or never registered.
Parameters
idnumber— Watcher id returned byengine.onDeviceRebuilt.
Returns boolean
local id = engine.onDeviceRebuilt(function() end)
engine.offDeviceRebuilt(id)
globals/engine/offModeChange
engine.offModeChange(id: number) -> boolean
Remove an engine.onModeChange subscriber by its watcher id.
Returns true when a live watcher carried that id, false when it
named none — already removed, or never registered.
Parameters
idnumber— Watcher id returned byengine.onModeChange.
Returns boolean
local id = engine.onModeChange(function() end)
engine.offModeChange(id)
globals/engine/offPauseChange
engine.offPauseChange(id: number) -> boolean
Remove an engine.onPauseChange subscriber by its watcher id.
Returns true when a live watcher carried that id, false when it
named none.
Parameters
idnumber— Watcher id returned byengine.onPauseChange.
Returns boolean
globals/engine/offWorldLoaded
engine.offWorldLoaded(id: number) -> boolean
Remove an onWorldLoaded subscriber by its watcher id.
Parameters
idnumber
Returns boolean
globals/engine/offWorldReady
engine.offWorldReady(id: number) -> boolean
Remove an engine.onWorldReady subscriber by its watcher id.
Returns true when a live watcher carried that id, false when it
named none.
Parameters
idnumber— Watcher id returned byengine.onWorldReady.
Returns boolean
globals/engine/offWorldUnloading
engine.offWorldUnloading(id: number) -> boolean
Remove an engine.onWorldUnloading subscriber by its watcher
id. Returns true when a live watcher carried that id, false when it
named none.
Parameters
idnumber— Watcher id returned byengine.onWorldUnloading.
Returns boolean
globals/engine/onDeviceRebuilt
engine.onDeviceRebuilt(callback: (number) -> ()) -> number
Register a callback that fires after the engine has answered a lost render device by building another one. The callback receives the new device generation — a number that counts the devices this session has run on, and moves exactly once per rebuild. Returns a watcher id.
A device is lost when the driver resets, when the GPU is taken away, or when a browser reclaims a WebGPU context. Everything the engine can re-derive by itself it does: meshes, materials, shaders, render passes and the UI are all back on the new device before this fires. What it cannot re-derive is what YOUR content made and only the GPU held — a texture uploaded from pixels a script computed, a compute buffer it filled, a render target it created. Make those again here.
Content that owns no GPU resource of its own needs no subscriber: asset handles re-materialise on their next use.
Parameters
callback(number) -> ()— Function invoked as(generation: number).
Returns number
engine.onDeviceRebuilt(function(generation)
-- the noise field lived only on the GPU, so it is computed again
regenerateNoiseTexture()
end)
globals/engine/onModeChange
engine.onModeChange(callback: (string, string) -> ()) -> number
Register a callback that fires synchronously whenever
engine.mode changes. Callback receives (newMode, oldMode) as
strings. Returns a watcher id for future removal. Consumers
(player_spawner, camera_spawner, editor-UI bootstrap, world
entrypoint top-level onModeChange, etc.) all subscribe through
this single API — there is no other fire path. Mode is engine
state, so the watcher hangs off the engine module.
Parameters
callback(string, string) -> ()— Function invoked as(newMode: string, oldMode: string).
Returns number
local id = engine.onModeChange(function(new, old)
print("flipped " .. old .. " -> " .. new)
end)
globals/engine/onPauseChange
engine.onPauseChange(callback: (boolean, boolean) -> ()) -> number
Register a callback that fires synchronously whenever the gameplay
pause flag flips via an explicit engine.paused write. Callback
receives (newPaused, oldPaused) as booleans. Returns a watcher id.
Pause is independent of engine.mode: pausing play mode returns the
editor authoring surface (free camera + EditorOnly entities) over the
frozen play world, and resuming hides it again. Mode-driven pause
resets (the edit=paused / play=running defaults applied on a mode flip)
are delivered through onModeChange, not this hook.
Parameters
callback(boolean, boolean) -> ()— Function invoked as(newPaused: boolean, oldPaused: boolean).
Returns number
local id = engine.onPauseChange(function(paused)
print(paused and "frozen" or "running")
end)
globals/engine/onWorldLoaded
engine.onWorldLoaded(callback: () -> ()) -> number
Register a callback fired (no args) when the world is fully
LOADED — its .world_entrypoint.luau ran AND its onWorldLoad
returned (the startup scene loaded, defaults seeded, editor UI
mounted). This is strictly AFTER onWorldReady (content synced):
ready = "bytes are in the VFS"; loaded = "the entrypoint has run".
LATCHED — a callback registered after the world is already loaded
fires immediately, so a late consumer never misses it and never has
to poll. Read the same state synchronously via engine.worldLoaded.
Parameters
callback() -> ()— Function invoked with no arguments.
Returns number
globals/engine/onWorldReady
engine.onWorldReady(callback: () -> ()) -> number
Register a callback fired (no args) when the bound world's
content has been synced into the VFS and the world is ready to
load. This is the race-free, user-space hook that drives the whole
world-VM lifecycle: the builtin world-entrypoint loader subscribes
to it and, when it fires, loadstring(vfs.read(...))s
/source/.world_entrypoint.luau and runs its onWorldLoad —
exactly the way a scene entrypoint loads. The trusted VM fires this
(via world.markReady()) ONLY once the bytes are in the VFS, so a
subscriber never sees a half-synced world. Returns a watcher id.
Parameters
callback() -> ()— Function invoked with no arguments.
Returns number
globals/engine/onWorldUnloading
engine.onWorldUnloading(callback: () -> ()) -> number
Symmetric teardown of engine.onWorldReady: register a callback
fired (no args) when the bound world is unbinding/swapping out. The
builtin loader runs the world entrypoint's onWorldUnload here, so
the world entrypoint has the same load/unload parity a scene
entrypoint has. Returns a watcher id.
Parameters
callback() -> ()— Function invoked with no arguments.
Returns number
globals/engine/paused
engine.paused -> boolean
Whether gameplay is paused: update(dt) component callbacks are gated off while editorUpdate(dt) keeps firing in edit mode.
Returns boolean
globals/engine/profile
engine.profile -> "editor" | "runtime"
The boot profile this process started under, editor or runtime. Read-only.
Returns "editor" | "runtime"
globals/engine/resetScriptingState
engine.resetScriptingState() -> { [string]: number }
Drop every world-event subscription, lifecycle watcher and
cached module registered since the last
engine.markScriptingBaseline(), leaving everything registered
before it in place — including the builtin world-entrypoint loader,
which subscribes at VM boot and so always sits below any mark.
Raises when no mark has been taken. Returns per-registry counts of
what was removed: worldEvents, modeWatchers,
worldReadyWatchers, worldUnloadingWatchers, pauseWatchers,
modules, and total.
Returns { [string]: number }
globals/engine/scriptingRegistryCounts
engine.scriptingRegistryCounts() -> { [string]: number }
How many subscriptions each scripting registry holds right now,
plus the size of the require cache and the generation of the mark in
force. Keys: worldEvents, modeWatchers, worldReadyWatchers,
worldUnloadingWatchers, pauseWatchers, modules, and
baselineGeneration (nil when no mark has been taken).
Returns { [string]: number }
globals/engine/setMode
engine.setMode(mode: string, options: { strict: boolean? }?) -> { mode: string, bypassed: { any } }
Change the engine mode with per-call control over the play gate, and
read back what the change went past. engine.mode = value is the same
flip with the defaults.
options.strict = false lets THIS call enter play while your own content
carries error-severity diagnostics. It settles with the call: the world's
lsp.strict_mode is untouched, so no other session and no later session
of the world sees a different gate. The returned bypassed array holds
the diagnostics the call went past — each { path, line, col, code, message, severity } — and the engine log carries the same list. An
error in content another session wrote never gates the flip, so it never
appears here; a push still refuses to publish while any of them stands.
Parameters
modestring—"edit"or"play".options{ strict: boolean? }(optional) —{ strict: boolean? }.strict = falsewaives the play gate for this call;trueor omitted honours the world'slsp.strict_mode.
Returns { mode: string, bypassed: { any } } — { mode, bypassed } — the mode now in force and the diagnostics this call entered play past (empty when it went past none).
local report = engine.setMode("play", { strict = false })
for _, d in ipairs(report.bypassed) do
print(("entered play past %s:%d — %s"):format(d.path, d.line, d.message))
end
globals/engine/timeScale
engine.timeScale -> number
The global time scale applied to the fixed-timestep accumulator and to update(dt): 1.0 is real time, 0.0 frozen, 2.0 double speed.
Returns number
globals/engine/vertexStride
engine.vertexStride -> number
Byte stride of the engine's standard GPU Vertex layout, which a mesh built from a compute buffer sizes and strides its writes to. Read-only.
Returns number
globals/engine/worldLoaded
engine.worldLoaded -> boolean
Whether the world entrypoint's onWorldLoad has run to completion. Read-only.
Returns boolean
globals/entity/__batchReadIds
entity.__batchReadIds()
Read a component field across many entities by id, in one crossing.
globals/entity/__batchReadToBuffer
entity.__batchReadToBuffer()
Read a component field into a typed substrate buffer.
globals/entity/__batchWriteBound
entity.__batchWriteBound()
Write a component field across a binding handle, in one crossing.
globals/entity/__batchWriteFromBuffer
entity.__batchWriteFromBuffer()
Write a component field from a typed substrate buffer.
globals/entity/__batchWriteIds
entity.__batchWriteIds()
Write a component field across many entities by id, in one crossing.
globals/entity/batchAddComponent
entity.batchAddComponent(targets: { string | entityRef }, type_name: string, data: table?) -> number
Add the same component type to many entities in one call. Returns the count of entities the component was added to — an entity already carrying an unnamed instance of the same type is skipped rather than double-added.
Parameters
targets{ string | entityRef }— Array of entity ids or entity proxies (e.g. the return ofentity.batchSpawnorentity.findAll).type_namestring— Component type to add to every entity.datatable(optional) — Init data table, applied identically to every entity — the same shape the second arg toentity(id).component.add(type, data)takes.
Returns number — How many entities had the component added.
local n = entity.batchAddComponent(ids, "Debris", { lifetime = 5 })
globals/entity/batchDespawn
entity.batchDespawn(targets: { string | entityRef }) -> number
Despawn many entities in one call. Locked or unresolvable entities are skipped. Returns the count queued for despawn.
Parameters
targets{ string | entityRef }— Array of entity ids, entity proxies, or display names (e.g. the return ofentity.batchSpawn/entity.findAll).
Returns number — Count of entities queued for despawn.
local n = entity.batchDespawn(ids)
globals/entity/batchProxy
entity.batchProxy(targets: { string | entityRef }) -> { entityRef? }
Resolve an array of entity ids to proxies in one call. Each output
slot is the standard entity(id) proxy; ids missing from the frame
cache surface as nil at that index. Use when iterating over a snapshot
of entities so per-id lookups don't dominate the hot path.
Parameters
targets{ string | entityRef }— Array of entity ids or entity proxies.
Returns { entityRef? } — Array of proxies (nil for missing ids).
local proxies = entity.batchProxy(ids)
globals/entity/batchRead
entity.batchRead(target: { string | entityRef } | binding, component: string?, field: string?, sink: buffer?) -> { any? } | number
Read a component-field across many entities in one call.
Polymorphic on the shape of target and sink:
entity.batchRead(ids)/(ids, comp)/(ids, comp, field)— returns one value per entity (a whole snapshot, one component table, or one field value). Missing entities/components/fields surface as nil at that slot.entity.batchRead(binding, comp, field, buffer)— reads each entity's field directly into a typed CPU substrate buffer (substrate.createBuffer({type="vec3"}), etc.) with no per-entity Lua table allocation. Returns the count of successful reads.targetaccepts an entity-id array or aecs.bindEntities(ids)handle. Buffer sinks require a binding — the typed kernel is binding-only.
Parameters
target{ string | entityRef } | binding— Array of entity ids or entity proxies, or a binding handle fromecs.bindEntities(ids).componentstring(optional) — Component type name (e.g. "Transform").fieldstring(optional) — Field name (e.g. "position").sinkbuffer(optional) — Typed CPU buffer fromsubstrate.createBuffer({...})to memcpy field values into. Required whentargetis a binding.
Returns { any? } | number — Per-entity values when reading into Lua tables; count of reads when reading into a buffer.
local snapshot = entity.batchRead(ids)
local positions = entity.batchRead(ids, "Transform", "position")
globals/entity/batchReadToBuffer
entity.batchReadToBuffer(binding: number, component: string, field: string, buffer: number) -> number
FFI primitive backing entity.batchRead(binding, ..., buffer).
Prefer the unified entity.batchRead, which auto-dispatches by
argument shape. Reads each entity's component field directly into a
typed CPU substrate buffer, with no per-entity Lua table allocation.
After the call, read the buffer via buf:read(0, count*stride).
Parameters
bindingnumber— Binding id fromecs.bindEntities(ids).id.componentstring— Component type name.fieldstring— Field name to read.buffernumber— Destination buffer id (must be the matching type).
Returns number — Count of successful reads.
entity.batchReadToBuffer(binding.id, "Transform", "position", buf.id)
globals/entity/batchSpawn
entity.batchSpawn(count: number, name_prefix: string?) -> { string }
Spawn count entities in one call. Returns an array of the new
entity ids in spawn order. Each entity is given a display name of
<name_prefix><i> (or entity<i> if the prefix is omitted). Prefer this
over looping entity.spawn when creating large entity counts.
Parameters
countnumber— How many entities to spawn (capped at 1,000,000).name_prefixstring(optional) — Display-name prefix appended with the 1-based index. Defaults to "entity".
Returns { string } — Array of newly-spawned entity ids.
local ids = entity.batchSpawn(100, "grass_")
globals/entity/batchWrite
entity.batchWrite(target: { string | entityRef } | binding, component: string, field: string, source: { any? } | buffer) -> number
Write a single component-field across many entities in one call.
Polymorphic on the shape of target and source:
entity.batchWrite(ids, comp, field, values)— per-call entity-id resolution;valuesis an array the same length asids(nil slots are skipped). Use for one-shot writes.entity.batchWrite(binding, comp, field, values)— binding handle fromecs.bindEntities(ids); skips per-call id resolution. Use for per-frame writes against a stable entity set.entity.batchWrite(binding, comp, field, buffer)— typed CPU buffer source (substrate.createBuffer({type="vec3"}), etc.), with no per-entity table allocation. Returns the count of successful writes. Buffer sources require a binding — the typed kernel is binding-only.
Parameters
target{ string | entityRef } | binding— Array of entity ids or entity proxies, or a binding handle fromecs.bindEntities(ids).componentstring— Component type name.fieldstring— Field name to write.source{ any? } | buffer— Per-entity values array (nil entries are skipped), or a typed CPU buffer fromsubstrate.createBuffer({...}). A buffer source requires a bindingtarget.
Returns number — Count of successful writes.
entity.batchWrite(ids, "Transform", "position", positions)
globals/entity/batchWriteBound
entity.batchWriteBound(binding: number, component: string, field: string, values: { any? }) -> number
FFI primitive backing entity.batchWrite(binding, ...) with a
per-entity values table. Prefer the unified entity.batchWrite, which
auto-dispatches by argument shape; this entry stays for power users /
debug code that wants to skip dispatch overhead.
Parameters
bindingnumber— Binding id fromecs.bindEntities(ids).id.componentstring— Component type name.fieldstring— Field name to write.values{ any? }— Per-entity source values (nil = skip). Length must match the binding's entity count.
Returns number — Count of successful writes.
entity.batchWriteBound(binding.id, "Transform", "position", values)
globals/entity/batchWriteFromBuffer
entity.batchWriteFromBuffer(binding: number, component: string, field: string, buffer: number) -> number
FFI primitive backing entity.batchWrite(binding, ..., buffer).
Prefer the unified entity.batchWrite, which auto-dispatches by
argument shape. Caller fills a typed substrate buffer
(substrate.createBuffer({type="vec3"})) once via buf:write(...),
then this memcpys 12 (vec3) or 16 (quat) bytes per entity into the
component field. Buffer count and binding count should match — a
mismatch processes the smaller of the two.
Parameters
bindingnumber— Binding id fromecs.bindEntities(ids).id.componentstring— Component type name.fieldstring— Field name to write.buffernumber— Buffer id fromsubstrate.createBuffer({type="vec3", len=N}).id.
Returns number — Count of successful writes.
entity.batchWriteFromBuffer(binding.id, "Transform", "position", buf.id)
globals/entity/capture
entity.capture(builder: () -> ()) -> ({ string }, any?, { string })
Run builder inside an entity capture scope and return the entity ids
it minted, in creation order, the error it raised (if any), and the ids
among them that a component the builder attached minted in its own
lifecycle. Every id minted while the builder runs is recorded — through
entity.spawn, entity.spawnSynced, entity.batchSpawn, and
entity.instantiate alike. Scopes nest: an id minted inside an inner
capture is recorded by that capture AND every enclosing one — the
innermost capture answers, so a nested build shapes its own entities,
not the ones around it. A builder that raises still returns its ids, so
the caller can despawn what a failed build left behind; the scope closes
either way and never outlives this call. While the builder runs, an
operation whose result cannot be composed into a record is refused
rather than applied, and so is any operation aimed at an entity the
builder did not mint — a builder that returned while something it did
was refused comes back with an error naming every refusal.
Parameters
builder() -> ()— Function run inside the scope; the entities it creates are what comes back.
Returns ({ string }, any?, { string }) — Entity ids minted while the builder ran, in creation order; the error it raised (or the refusals it hit), or nil; and the ids a component the builder attached minted in its own lifecycle.
local ids, err, reproduced = entity.capture(function() entity.spawn("chair") end)
globals/entity/despawn
entity.despawn(target: string | entityRef)
Despawn an entity and all its components. Pass an id string or an entity proxy to despawn that ONE entity. Pass a name to despawn EVERY entity with that name — names are not unique, so a name argument despawns all matches, not one arbitrary match. A despawned id becomes invalid after this call. Raises if no entity matches; for a bulk name despawn, locked entities are skipped with a logged summary and only raise if every match is locked.
Parameters
targetstring | entityRef— Entity id, name, or entity proxy. A name despawns all entities sharing that name.
entity.despawn(id)
entity.despawn("Enemy") -- despawns every entity named "Enemy"
globals/entity/duplicate
entity.duplicate(sourceId: string | entityRef, name: string?, opts: table?) -> string?
Duplicate an entity with all its components (transform, script
components, attributes, visuals, material) and its descendants. Returns
the new entity's id, or nil when sourceId names no live entity.
Descendants marked temporary are left out of the copy: they are
scaffolding whatever spawned them re-creates, so a component that
regenerates its own children rebuilds them on the copy rather than the
copy carrying a second set. includeTemporary copies them too, for the
hierarchy that IS the temporary thing.
Parameters
sourceIdstring | entityRef— Entity id or entity proxy of the source entity to clone.namestring(optional) — Display name for the copy (defaults to source name + " (copy)").optstable(optional) —{ includeTemporary?: boolean, name?: string }—nameis the same field thenameargument sets, and wins when both are given.
Returns string? — The new entity's id, or nil when the source is not live.
local copyId = entity.duplicate(id); if copyId then entity(copyId).position = { 1, 0, 0 } end
local copyId = entity.duplicate(id, "Turret", { includeTemporary = true })
globals/entity/exists
entity.exists(idOrProxy: string | entityRef) -> boolean
Check whether an entity currently exists in the scene. Accepts an
entity-id string or an entity proxy, matched by entity id — so it agrees
exactly with entity(id). A name is a different kind of identifier: a
string that misses as an id but names a live entity raises rather than
answering false, since false there is indistinguishable from absence.
Check by name with entity.find(name) ~= nil.
Parameters
idOrProxystring | entityRef— Entity id or an entity proxy.
Returns boolean — true if the entity exists.
if entity.exists(id) then ... end
globals/entity/find
entity.find(nameOrGlob: string) -> entityRef?
Find the first entity matching nameOrGlob. A plain string matches
an exact id or Name component; a string containing * (any run of
characters) or ? (any single character) matches Names as a glob, so
entity.find("enemy_*") is the first entity whose name starts with
enemy_. A glob addresses Names only, never ids. Same-frame pending
spawns are searched too, and anything queued for despawn in the same
frame is skipped. Names are NOT unique — use entity.findAll when every
match matters.
Parameters
nameOrGlobstring— Exact entity Name or id, or a*/?glob over Names.
Returns entityRef? — First matching entity proxy, or nil.
local e = entity.find("enemy_*")
globals/entity/findAll
entity.findAll(nameOrGlob: string?) -> { entityRef }
Enumerate entity proxies. With a nameOrGlob argument, returns every
entity whose Name component or id matches (names are not unique): a
plain string matches exactly, while a * / ? glob matches Names. With
no argument, returns every entity in the current snapshot —
findAll("") is the exact-match filter for the empty name, which
normally matches nothing. Same-frame pending spawns are included and
same-frame despawns filtered out. Elements are entity proxies, not id
strings — for ids, wrap the result: entity.ids(entity.findAll(...)).
Parameters
nameOrGlobstring(optional) — Exact entity Name or id to filter by, or a*/?glob over Names. Omit to enumerate every entity.
Returns { entityRef } — Array of entity proxies, possibly empty.
for _, e in entity.findAll("enemy_*") do e:despawn() end
globals/entity/getChildren
entity.getChildren(id: string | entityRef) -> { entityRef }
Get an array of the direct children as entity proxies. Each element
carries .name, .id, .position, .component, and the rest of the
per-entity surface — the same shape entity.findAll returns.
Parameters
idstring | entityRef— Entity id or entity proxy.
Returns { entityRef } — Array of child entity proxies, possibly empty.
for _, c in entity.getChildren(id) do c.internal = true end
globals/entity/getDescendants
entity.getDescendants(id: string | entityRef) -> { entityRef }
Get every descendant (children, grandchildren, and deeper) of the given entity as entity proxies in breadth-first order, excluding the entity itself. Resolves the whole subtree in one linear pass over the entity set, so a large subtree costs proportionally to the entity count rather than to the subtree size times the entity count.
Parameters
idstring | entityRef— Entity id or entity proxy.
Returns { entityRef } — Array of descendant entity proxies, possibly empty.
local all = entity.getDescendants(id)
globals/entity/getParent
entity.getParent(id: string | entityRef) -> entityRef?
Get the parent entity proxy, or nil if the entity is a root entity.
The returned proxy carries .name, .id, .position, .component,
and the rest of the per-entity surface — the same shape entity.find
returns.
Parameters
idstring | entityRef— Entity id or entity proxy.
Returns entityRef? — Parent entity proxy, or nil.
local p = entity.getParent(id)
globals/entity/hierarchy
entity.hierarchy()
Hierarchy helpers reached as entity.hierarchy.<member>.
globals/entity/ids
entity.ids(value: any) -> { string }
Coerce an entity, a list of entities, or nil into a list of entity id strings. Accepts id strings and entityRefs in any mix, so the entity-answering calls and a stored list of ids both feed it. A single entity becomes a one-element list and nil becomes an empty list. Order is preserved and duplicates are kept.
Parameters
valueany— An entity, a list of entities, or nil.
Returns { string } — Array of entity id strings.
local ids = entity.ids(root.getChildren())
local ids = entity.ids(entity.findAll())
globals/entity/instantiate
entity.instantiate(handle: number, count: number, fn: ((number) -> EntityInstantiateOverrides?)?) -> { string }
Spawn count instances of a template registered with
entity.template. Each instance gets a fresh entity id; the optional
fn(i) callback runs per instance (i in 1..=count) and may return an
overrides table. Override keys: name, position, rotation, scale,
parent, temporary / active / internal, attributes, components
(script components, merged over the template body's data for that type
— a type the template lacks is added fresh), and ecs (native
components, merged the same way). Each override supersedes the
template's shared config for that instance. The whole batch crosses in
one call and lands as a single deferred mutation the engine expands
into bulk work — per-instance cost drops from a full round trip to one
callback plus one mutation. Inside queue() the batch is deferred onto
the cross-frame ring; outside, it lands in the next frame's drain.
Returns the array of newly-minted entity ids in spawn order.
Parameters
handlenumber— Template handle fromentity.template.countnumber— Number of instances to spawn (capped at 1,000,000).fn((number) -> EntityInstantiateOverrides?)(optional) — Per-instance override callback(i) -> table?.
Returns { string } — Array of newly-spawned entity ids in spawn order.
local ids = entity.instantiate(h, 50, function(i) return { position = { i, 0, 0 } } end)
globals/entity/proxies
entity.proxies(value: any) -> { entityRef }
Coerce an entity, a list of entities, or nil into a list of entityRefs.
Parameters
valueany— An entity, a list of entities, or nil.
Returns { entityRef } — Array of entityRefs.
globals/entity/refs
entity.refs(value: any) -> { entityRef }
Coerce an entity, a list of entities, or nil into a list of entityRefs —
the form carrying .position, .component, and the rest of the per-entity
surface. Accepts id strings and entityRefs in any mix. A single entity
becomes a one-element list and nil becomes an empty list. An id whose
entity has despawned raises, naming its position.
Parameters
valueany— An entity, a list of entities, or nil.
Returns { entityRef } — Array of entityRefs.
for _, e in entity.refs(savedIds) do e.position = { 0, 1, 0 } end
globals/entity/spawn
entity.spawn(nameOrOpts: (string | SpawnOpts)?, opts: SpawnOpts?) -> entityRef
Spawn a new entity and return its PROXY (the same value entity(id)
yields) — act on it immediately (entity.spawn(name).component.add(...),
.localPosition = ...) with no second entity(id) round trip. The proxy
still exposes .id for the rare site that needs the raw string. An
entity with no components carries only a Transform and is invisible;
pass components to give it the components that make it visible in the
same call — entity.spawn { name = "crate", components = { Model = { model = "cube" } } } — or add them afterwards through the returned
proxy. Mirrors entity.find / entity.findAll, which also return
proxies. The options table can be passed on its own with the name inside
it — entity.spawn { name = "turret", position = { 1, 2, 3 } } is the
same call as entity.spawn("turret", { position = { 1, 2, 3 } }).
Parameters
nameOrOpts(string | SpawnOpts)(optional) — Display name for the entity, or the options table itself.optsSpawnOpts(optional) — Options:components= component types to attach to the new entity, keyed by type name with each value the component's init table (attached in sorted type order; a failing add raises),internal= take the entity out of the default entity listings (it still renders —entity(id):hide()stops the draw),parent= parent entity id or proxy,temporary= skip this entity (and descendants) from scene/world saves,position/rotation/scale= place the entity's Transform at spawn,id= restore a previously-assigned entity id (scene_loader use; leave unset for a normal spawn). An unrecognised key is rejected loudly.
Returns entityRef — Proxy for the new entity (carries .id, .component, transform properties, etc.).
local e = entity.spawn("crate", { components = { Model = { model = "cube" } } })
local e = entity.spawn { name = "turret", position = { 1, 2, 3 } }
globals/entity/spawnSynced
entity.spawnSynced(nameOrOpts: (string | SpawnOpts)?, opts: SpawnOpts?) -> entityRef
Spawn an entity already flagged multiplayer-synced at the root — the
explicit form of entity.spawn for SHARED, host-authoritative content.
The entity's existence broadcasts to every peer; joiners receive it from
the relay snapshot instead of spawning their own copy. Use this (NOT
entity.spawn) for anything that must be the SAME object on all
clients: enemies, pickups, projectiles, dynamic world props. Call it
ONLY where exactly one client runs the code — a scene's onHostLoad
(host-only) phase, or behind multiplayer.isHost(). Calling it in
all-client code makes every client spawn+sync its own copy — the
double-spawn "explosion". Equivalent to
entity.spawn(name, { synced = true }); identical in every other
respect.
Parameters
nameOrOpts(string | SpawnOpts)(optional) — Display name for the entity, or the options table itself.optsSpawnOpts(optional) — Same options asentity.spawn(syncedis already implied).
Returns entityRef — Proxy for the new synced entity.
if multiplayer.isHost() then entity.spawnSynced("Goblin") end
globals/entity/template
entity.template(def: EntityTemplateDef) -> number
Construct a reusable spawn template. Captures a shared entity config
ONCE and returns a stable handle for entity.instantiate(handle, count, fn?) — one call per batch instead of one per entity. def keys:
components (script components, { [type] = init-data }), ecs
(array of native ecs.X{...} components), temporary (instances skip
scene/world saves), active (spawn state), internal (instances are
taken out of the default entity listings; they still render),
attributes ({ key = value } applied to every instance). Every value
is a shared default; a per-instance entity.instantiate override
supersedes it. The template body is captured by value — later edits to
the source table do not affect templates already created.
Parameters
defEntityTemplateDef— Template definition:components/ecs/temporary/active/internal/attributes. Per-instancename/position/rotation/scale/parentand any override go through theinstantiatecallback.
Returns number — Stable template handle for entity.instantiate.
local h = entity.template({ components = { Model = { model = "cube" } } })
globals/entity/tree
entity.tree(opts: { [string]: any }?) -> { [string]: any }
A windowed, lean view over the scene's entity tree, in one crossing.
Rows carry id, name, parentId, depth, childCount, active, sceneLayer and
componentNames — names only, never component values — so the call costs
the rows it answers with rather than the size of the scene. Entities group
under scene layers, per-layer roots and children name-sorted; internal
entities and their subtrees stay out. expanded names the ids whose
children unfold, and a collapsed node still reports its childCount;
filter keeps the rows whose name or id contains the needle plus every
ancestor on a path to one, auto-unfolded, with the actual matches flagged
matched. offset / limit window the flattened rows, layer scopes the
window and its total to one layer while layers still reports every
layer's row count, and revision echoes
getEntitiesRevision("structure"), which moves only on structural change.
Parameters
opts{ [string]: any }(optional) —{ layer?, expanded?, filter?, offset?, limit? }.
Returns { [string]: any } — { rows, total, layers, revision }.
local view = entity.tree({ filter = "crate", limit = 50 })
globals/environment/capture
environment.capture(x: number, y: number, z: number) -> boolean
Bake the scene into the environment from (x, y, z) as the single global
reflection (slot 0 + one full-coverage probe). Every PBR surface reflects it.
Queued — takes effect on the next frame. For multiple proximity-blended
probes use the reflectionProbe system instead.
Parameters
xnumber— World X of the capture position.ynumber— World Y of the capture position.znumber— World Z of the capture position.
Returns boolean — True — the capture was queued.
environment.capture(0, 2, 0)
globals/environment/captureSky
environment.captureSky(x: number?, y: number?, z: number?) -> boolean
Render the SKY alone into the environment's sky slot from (x, y, z) and
arm the sky fallback. A reflective surface no probe covers then reflects the
sky rather than black, and a partially covered one blends the shortfall
against it. The capture holds whatever the scene's sky draws — a gradient, a
physical atmosphere, a skybox material — with no geometry in it, so it stays
correct wherever the camera goes. Once captured, the slot follows the sky
the scene draws: a sky that changes is recaptured from the same position.
Queued — takes effect on the next frame.
Parameters
xnumber(optional) — World X of the capture position. Defaults to 0.ynumber(optional) — World Y of the capture position — the altitude a height-dependent atmosphere is sampled at. Defaults to 0.znumber(optional) — World Z of the capture position. Defaults to 0.
Returns boolean — True — the sky capture was queued.
environment.captureSky()
globals/environment/captureSlot
environment.captureSlot(slot: number, x: number, y: number, z: number) -> boolean
Bake the scene into reflection-probe slot from (x, y, z).
Renders the FULL scene (geometry + sky) six times from that
point into that slot. Register the probe's position+radius via setProbes
so surfaces blend it by proximity. Queued — takes effect next frame.
Parameters
slotnumber— Reflection-probe slot (0-based).xnumber— World X of the capture position.ynumber— World Y of the capture position.znumber— World Z of the capture position.
Returns boolean — True — the capture was queued.
environment.captureSlot(0, 0, 2, 0)
globals/environment/captureSlotToAsset
environment.captureSlotToAsset(name: string, slot: number, x: number, y: number, z: number, timeoutFrames: number?) -> (string?, string?)
Bake the scene into reflection-probe slot from (x, y, z) AND persist
the 6 rendered faces into a faces6 .texture cubemap asset at
/source/<name>.texture/ (px/nx/py/ny/pz/nz PNGs + a cube.yaml sidecar).
Survives an engine restart and syncs like any other texture. Yields a few
frames while the bake + GPU readback complete; must be called from a
task/coroutine context (component hook, task.spawn, or execute). NATIVE
only — the wasm async-readback path is a tracked follow-up.
Parameters
namestring— Destination asset identity (writes/source/<name>.texture/).slotnumber— Reflection-probe slot (0-based).xnumber— World X of the capture position.ynumber— World Y of the capture position.znumber— World Z of the capture position.timeoutFramesnumber(optional) — Optional max frames to wait for the readback (default 180).
Returns (string?, string?) — The asset path on success, or (nil, errorMessage) on failure.
environment.captureSlotToAsset("probe_lobby", 0, 0, 2, 0)
globals/environment/captureToAsset
environment.captureToAsset(name: string, x: number, y: number, z: number) -> (string?, string?)
Bake the single global reflection AND persist it to a faces6 .texture
asset (slot 0). Yields a few frames; call from a task/coroutine context.
Parameters
namestring— Destination asset identity (writes/source/<name>.texture/).xnumber— World X of the capture position.ynumber— World Y of the capture position.znumber— World Z of the capture position.
Returns (string?, string?) — The asset path on success, or (nil, errorMessage) on failure.
environment.captureToAsset("env_main", 0, 2, 0)
globals/environment/ensureSkyFallback
environment.ensureSkyFallback() -> boolean
Ensure the scene's sky is in the environment's sky slot: a reflective surface no probe covers then reflects the sky rather than black, and a partially covered one blends the shortfall against it. Queues a capture when the sky slot holds none, and re-arms the fallback when a capture is there but switched off. The engine's own state answers both questions, so everything that stands a sky up can call this and one capture is shared between them. Once captured, the slot follows the sky the scene draws on its own.
Returns boolean — True if a capture was queued, false if the sky slot already holds one.
environment.ensureSkyFallback()
globals/environment/loadFromAsset
environment.loadFromAsset(name: string) -> (boolean, string?)
Load a persisted global reflection asset into slot 0 and make it the active single reflection (one full-coverage probe).
Parameters
namestring— Source asset identity (reads/source/<name>.texture/).
Returns (boolean, string?) — True on success, or (false, errorMessage) on failure.
environment.loadFromAsset("env_main")
globals/environment/loadSlotFromAsset
environment.loadSlotFromAsset(name: string, slot: number) -> (boolean, string?)
Load a persisted faces6 .texture cubemap (written by
captureSlotToAsset) into reflection-probe slot WITHOUT re-rendering the
scene. Reads the 6 face PNGs from /source/<name>.texture/ and uploads them
into the slot's cube layers. How a persisted probe restores its baked
environment on reload.
Parameters
namestring— Source asset identity (reads/source/<name>.texture/).slotnumber— Reflection-probe slot (0-based).
Returns (boolean, string?) — True on success, or (false, errorMessage) on failure.
environment.loadSlotFromAsset("probe_lobby", 0)
globals/environment/setProbes
environment.setProbes(probes: { any }) -> boolean
Set the active reflection probes' blend data. probes is an array of
{ x, y, z, radius } (or { position = {x,y,z}, radius = r }); index i is
probe slot i. Surfaces blend the probe slots by proximity to these
positions, gathering the highest priority first — each rank takes the
coverage the ranks above it left, so a small interior probe ranked above a
large exterior one wins outright wherever it reaches full weight. Coverage
left over reflects the sky once captureSky has run. Queued for next frame.
Parameters
probes{ any }— Array of{ x, y, z, radius, priority? }, one per active probe slot.prioritydefaults to 0.
Returns boolean — True — the probe data was queued.
environment.setProbes({ { x = 0, y = 2, z = 0, radius = 12 } })
globals/environment/setSkyFallback
environment.setSkyFallback(active: boolean) -> boolean
Arm or disarm the sky fallback against the sky already captured, with no
recapture. Disarmed, reflections come from the probes alone. Arming is
refused while the sky slot holds no capture (captureSky fills it), since
an uncaptured slot reflects black; renderer.reflectionEnvironment()
reports whether the fallback ended up armed.
Parameters
activeboolean— Whether reflections fall back to the captured sky.
Returns boolean — True — the change was queued.
environment.setSkyFallback(false)
globals/error
error(message, level?)
Raise an error. Halts execution.
globals/font/glyph
font.glyph(name: string, codepoint: number) -> any
Read one glyph's vectorized outline from a registered font, in font
units (resolution-independent — scale by fontSize / unitsPerEm).
Parameters
namestring— Registered family name.codepointnumber— Unicode codepoint (e.g.string.byte("A")).
Returns any — { advance, unitsPerEm, bbox = {xMin,yMin,xMax,yMax}, contours } where each contour is { start = {x,y}, segments = { {kind="line|quad|cubic", ...} } }, or nil if the font isn't registered.
local g = font.glyph("Inter", string.byte("A"))
globals/font/list
font.list() -> { string }
List every registered font family name.
Returns { string } — Array of family-name strings.
for _, fam in font.list() do print(fam) end
globals/font/observe
font.observe() -> { any }
What the text system is holding for fonts: one row per family the
shaper can resolve, with its face count, the numeric weights those faces
carry, whether any of them is slanted, and whether the family arrived
through a registration rather than from the platform. weights is what a
style's weight can name for that family. The same rows are fonts in
text.observe().
Returns { any } — Array of { family, faces, weights, italic, loaded }.
for _, f in ipairs(font.observe()) do print(f.family, #f.weights) end
globals/font/parse
font.parse(bytes: buffer | string) -> string?
Parse a font file (TTF / OTF raw bytes) ONCE into the baked, vectorized
glyph format (ZFNT): per-glyph vector outlines + metrics + character map,
plus the original bytes. Heavy — run at import time (the .font assetType's
onCreate / the font importer), then store the result as the asset payload.
font.register loads it cheaply.
Parameters
bytesbuffer | string— Raw font-file bytes (binary-safe) — TTF / OTF.
Returns string? — Baked ZFNT payload (binary-safe string), or nil if the bytes don't parse as a font.
local zfnt = font.parse(vfs.read("/zero/source/Inter.ttf"))
globals/font/reconcile
font.reconcile() -> { any }
Every family the text shaper can resolve, held against what the shaper
does with it. family is the name, faces how many faces of it the font
database holds, weights the numeric weights those faces carry, loaded
whether it arrived through a registration rather than from the platform,
registered whether content registered the name, selectable whether some
style naming the family reaches it, matched whether fontFamily = family
on its own reaches it — the family name at the default weight over Latin
text — weight the weight it needs when the default is not it, shapedWith
the face that answered, and reason why when it is not the one asked for. A
family is probed at its own weights and over content from several scripts,
so a family reachable only at one weight or covering only one script is
reported selectable, with matched false and weight naming what the style
must carry. Every probe object is destroyed again, so the live text-object
count is where it was.
Returns { any } — Array of { family, faces, weights, loaded, registered, selectable, matched, weight, shapedWith, reason }.
for _, f in ipairs(font.reconcile()) do if f.selectable and not f.matched then print(f.family, f.weight) end end
globals/font/register
font.register(name: string, zfnt: string, opts: table?) -> any
Register a baked font (ZFNT from font.parse) under name, making it
usable on every text surface via fontFamily = "<name>". Loads the
vectorized glyph data into the runtime store (for font.glyph /
font.textMesh) and feeds the embedded face to the 2D text and egui UI
systems. Passing raw font bytes still works but logs a slow-path warning —
bake with font.parse at import. Re-registering the same name replaces it.
opts groups several weight/style faces under one CSS family and maps
web-font names onto it: opts.family is the shared group key, opts.role
is "regular" | "bold" | "italic" | "bolditalic", and opts.aliases is a
list of extra selectable names (web fonts + CSS generics like "Arial",
"sans-serif") that resolve to this group, matched case-insensitively.
With a group set, font-weight / font-style on a font-family pick the
real metric-compatible face instead of a synthesized one.
Parameters
namestring— Family name to register under.zfntstring— BakedZFNTpayload fromfont.parse(binary-safe string).optstable(optional) —{ family: string?, role: string?, aliases: {string}? }— group key, weight/style role, and case-insensitive selectable aliases.
Returns any — { family, faces, glyphCount } on success, or nil on failure.
local info = font.register("Inter", font.parse(vfs.read("/zero/source/Inter.ttf")))
globals/font/textMesh
font.textMesh(name: string, text: string, opts: table?) -> any
Tessellate a string into renderable mesh geometry from a registered
font's glyph outlines — true 3D text, laid out left-to-right by advance
(newlines drop a line). Hand the result to renderer.mesh.create() (GPU)
or asset.create("mesh") (persistable).
Parameters
namestring— Registered family name.textstring— String to lay out.optstable(optional) —{ size?=1, depth?=0 (extrude, EM units), tolerance?=0.0015, letterSpacing?=0, lineHeight?=0 }.
Returns any — { positions, indices, normals, uvs } as flat float / u32 arrays, or nil if the font isn't registered or the string is all whitespace.
local geom = font.textMesh("Inter", "Hello", { size = 1, depth = 0.1 })
globals/frameStream/attach
frameStream.attach(texture: string, stream: string, opts: AttachOpts?) -> (string?, string?)
Carry an image the GPU drew out to an open byte stream, frame
after frame. texture is the guid of the render target it was
drawn into — renderer.texture.create({ width = W, height = H })
makes one, and a Camera component draws into it as its
textureHandle; the session reads that target back when a frame
comes due, so what the camera drew last reaches the far end.
stream is a handle from stream.open. What reaches the stream
is one frame's pixels then the next frame's, with nothing between
them: a frame is width * height * bytesPerPixel bytes of tight
rows, written in a single call so a consumer reads a whole frame
or none of it. Each frame is read back off the render thread, so
the stream never holds the renderer up. fps caps how often a
frame is taken and defaults to one per rendered frame; format
accepts "rgb24" (3 bytes per pixel, the default) or "rgba8"
(4) — a call with a format outside those two raises, naming both;
flipY writes the last texture row first. Returns the session
handle, or nil and the reason an empty texture, a handle naming no
open stream, a stream another session already carries, or a
non-positive fps was refused with.
Parameters
texturestring— Guid of the render target the image was drawn into (a Camera's textureHandle).streamstring— Stream handle from stream.open.optsAttachOpts(optional) — Rate, pixel layout and row order (optional).
Returns (string?, string?) — Session handle, or nil and the refusal reason.
local session = frameStream.attach(rt.guid, handle, { fps = 30 })
globals/frameStream/detach
frameStream.detach(handle: string) -> boolean
End the session and free the staging buffers it read frames back through. The stream stays open — whoever opened it closes it.
Parameters
handlestring— Session handle from frameStream.attach.
Returns boolean — True if a session was ended, false if handle already named none.
frameStream.detach(session)
globals/frameStream/list
frameStream.list() -> { string }
Every live session handle, in a stable order.
Returns { string } — Array of session handles.
for _, h in frameStream.list() do frameStream.detach(h) end
globals/frameStream/status
frameStream.status(handle: string) -> FrameStreamStatus?
Report what the session has carried and lost. frames counts
the frames the stream accepted and bytes the bytes they
carried. dropped counts the frames it refused, of which
droppedBackpressure is the part refused because the consumer was
behind; stalledReadbacks counts the frames that came due while
every staging buffer still held a copy on its way from the GPU.
achievedFps is the rate the accepted frames arrived at, across
the span from the first to the most recent, and reads 0 until two
have been accepted — compare it against requestedFps to see a
display running slower than it was asked to. lastOutcome names
what became of the most recent frame offered. nil when handle
names no live session.
Parameters
handlestring— Session handle from frameStream.attach.
Returns FrameStreamStatus? — Session status, or nil when handle names no live session.
local s = frameStream.status(session); print(s.frames, s.dropped, s.achievedFps)
globals/getLookHitTable
getLookHitTable() -> table | nil
Get the physics raycast hit at screen center (crosshair).
Returns table | nil — Hit result {entityId, point, normal, distance} or nil
globals/getPointerHitTable
getPointerHitTable() -> table | nil
Get the physics raycast hit under the mouse pointer.
Returns table | nil — Hit result {entityId, point, normal, distance} or nil
globals/getTime
getTime() -> number
Seconds since the engine started, stamped once per frame — so every script in a frame reads the same now and the frame's logic is consistent with itself. This is the clock to drive gameplay and animation from. Because it holds still for the whole frame, it cannot measure a duration INSIDE one: two reads with no yield between them return the same number, and os.clock() is the live monotonic clock for that. One epoch on every platform. Frames are a third quantity again — getFrame() counts the frames the engine ran and renderer.drawnFrames() the frames it drew — and how many of either a second buys is decided by the machine, so waiting for engine work counts frames rather than seconds.
Returns number — Seconds since the engine started, as of this frame
globals/getViewportSize
getViewportSize() -> { width: number, height: number }
Get the viewport dimensions in pixels as a single table { width, height }. Returns one table, not two numbers.
Returns table — { width = number, height = number } in pixels
globals/getmetatable
getmetatable(table) -> mt | nil
Get a table's metatable.
globals/http/get_bytes
http.get_bytes(url: string, headers: Headers?) -> PromiseId
Async HTTP GET returning raw bytes (binary-safe string).
Suitable for piping into vfs.write to download a file.
Parameters
urlstring— Request URL.headersHeaders(optional) — Header key-value pairs (optional).
Returns PromiseId — Promise handle for task.await().
local bytes = task.await(http.get_bytes("https://example.com/sound.ogg"))
globals/http/get_json
http.get_json(url: string, headers: Headers?) -> PromiseId
Async HTTP GET returning JSON. Returns a promise handle — wrap
with task.await() to block until the response arrives.
Parameters
urlstring— Request URL.headersHeaders(optional) — Header key-value pairs (optional).
Returns PromiseId — Promise handle for task.await().
local data = task.await(http.get_json("https://api.example.com/info"))
globals/http/post_bytes
http.post_bytes(url: string, headers: Headers?, body: JsonBody?) -> PromiseId
Async HTTP POST returning raw bytes — use for APIs that accept JSON input but return binary output (audio, images).
Parameters
urlstring— Request URL.headersHeaders(optional) — Header key-value pairs (optional).bodyJsonBody(optional) — JSON body (optional).
Returns PromiseId — Promise handle for task.await().
local audio = task.await(http.post_bytes(ttsUrl, nil, { text = "hello" }))
globals/http/post_json
http.post_json(url: string, headers: Headers?, body: JsonBody?) -> PromiseId
Async HTTP POST returning JSON. Body is a Luau table; the FFI layer JSON-encodes it before the request goes out.
Parameters
urlstring— Request URL.headersHeaders(optional) — Header key-value pairs (optional).bodyJsonBody(optional) — JSON body (optional).
Returns PromiseId — Promise handle for task.await().
local r = task.await(http.post_json(url, nil, { name = "Alice" }))
globals/http/request
http.request(method: string, url: string, headers: Headers?, body: JsonBody?) -> PromiseId
Async HTTP request with an arbitrary verb (GET/POST/PUT/PATCH/ DELETE/…) returning JSON. Body is a Luau table; an empty 2xx response resolves to an empty table.
Parameters
methodstring— HTTP verb (case-insensitive).urlstring— Request URL.headersHeaders(optional) — Header key-value pairs (optional).bodyJsonBody(optional) — JSON body (optional).
Returns PromiseId — Promise handle for task.await().
local w = task.await(http.request("PATCH", url, hdrs, { description = "hi" }))
globals/http/request_raw
http.request_raw(method: string, url: string, headers: Headers?, body: buffer | string | nil?) -> PromiseId
Async HTTP request with an arbitrary verb and a RAW binary request body (a binary-safe string), for content-addressed blob uploads. The resolved value is the response body text.
Parameters
methodstring— HTTP verb (case-insensitive).urlstring— Request URL.headersHeaders(optional) — Header key-value pairs (optional).bodybuffer | string | nil(optional) — Raw binary request body (optional).
Returns PromiseId — Promise handle for task.await().
local r = task.await(http.request_raw("POST", blobsUrl, hdrs, pngBytes))
globals/httpServer/address
httpServer.address(path: string) -> (string?, string?)
The URL a path answers on — scheme, host, port and the /app mount,
ready to be fetched or printed for someone to open. Takes the same path
spelling route does, and reads the interface and port from the socket
routes answer on: the address listen opened while one is open, and the
engine's own server otherwise.
Parameters
pathstring— Path under the/appmount, e.g. "/status".
Returns (string?, string?) — The URL, or nil plus the reason there is none — this engine holds no address, or the path is not one a route can be registered at.
print(httpServer.address("/status")) --> http://127.0.0.1:7607/app/status
globals/httpServer/listen
httpServer.listen(target: string) -> (HttpListener?, string?)
Hold an interface and port of this world's own, and answer content routes on it.
The host in target is the interface bound, and the whole of what decides
who can reach those routes: "127.0.0.1:8080" answers programs on this
machine, "0.0.0.0:8080" answers any host that routes to this machine on
that port — a phone on the same wifi, and whatever else the network lets
through. Bind loopback unless you want that. A port of 0 asks the
operating system for a free one, which the returned record reports, and
http:// may be spelled out in front.
This address serves the routes registered under the /app mount. The
engine's own /engine/* tree answers on the loopback server it booted
with, whose interface stays what the boot bound.
The address belongs to the chunk that opened it and is released when that chunk runs again, so an edited module holds the address its current source names. Asking for the address already held is the same address back.
Parameters
targetstring— Interface and port to hold, e.g. "0.0.0.0:8080".
Returns (HttpListener?, string?) — The listener record, or nil plus the reason the target or the bind was refused.
local l = assert(httpServer.listen("0.0.0.0:8080"))
globals/httpServer/route
httpServer.route(method: string, path: string, handler: HttpHandler, options: HttpRouteOptions?) -> (number?, string?)
Serve one method and path from this engine, answering each matching
request with handler.
The path is relative to the /app mount, and a trailing /* segment
matches the rest of the path — "/files/*" answers /app/files/a/b, with
"a/b" in request.wildcard. An exact path answers ahead of a wildcard,
and among wildcards the longest one wins.
One method and path is served by one handler. Registering an address
another chunk serves returns nil and a reason naming the handle and the
chunk holding it; httpServer.routes() finds that handle and
httpServer.unroute frees the address. Registering an address this same
chunk already serves takes it back and releases the handler it replaces,
so a chunk that runs twice serves the handler it just built.
The handler runs on the script thread. Raising inside it answers 500 and writes the error to the engine log; returning something that is not a response table or a string answers 500 saying what arrived.
Parameters
methodstring— HTTP verb, e.g. "GET" or "POST".pathstring— Path under the/appmount, e.g. "/status" or "/files/*".handlerHttpHandler— Called with the request table; returns a response table or a body string.optionsHttpRouteOptions(optional) —{ timeoutMs? }— how long a request waits for this handler.
Returns (number?, string?) — The route handle, or nil plus the reason it was not registered.
local h = httpServer.route("GET", "/status", function(req)
globals/httpServer/routes
httpServer.routes() -> { HttpRoute }
Every route this engine currently serves, in registration order — handle, method, registered path, the address it answers on, its full URL, the chunk that registered it, and how long a request for it waits.
Returns { HttpRoute } — An array of route records.
for _, r in ipairs(httpServer.routes()) do print(r.method, r.url, r.owner) end
globals/httpServer/status
httpServer.status() -> HttpServerStatus
Whether this engine serves content routes, on which interface, port
and mount, who can reach them, and how many routes and waiting requests it
holds. host, port, url and reach are read from the socket routes
answer on — the one listen opened while one is open, and the engine's
own server otherwise — and listeners carries every address, each with
its own reach. When supported is false, reason says why: a browser tab
answers HTTP requests and holds no address of its own.
Returns HttpServerStatus — { supported, reason?, host?, port?, url?, reach?, prefix, routeCount, pending, listeners }.
local s = httpServer.status(); print(s.url, s.reach)
globals/httpServer/unlisten
httpServer.unlisten() -> boolean
Release the address listen opened. Returns once the socket is free,
so the same port binds again straight after.
Returns boolean — True when an address was held.
httpServer.unlisten()
globals/httpServer/unroute
httpServer.unroute(handle: number) -> boolean
Stop serving a route and release its handler. The address is free for another registration once this returns true.
Parameters
handlenumber— The handlehttpServer.routereturned.
Returns boolean — True when a route with this handle was registered.
httpServer.unroute(h)
globals/ipairs
ipairs(table) -> iterator
Iterate array portion of table (1, 2, 3...).
globals/jobs/find
jobs.find(name: string) -> JobHandle?
Look up a registered job by name. Returns a JobHandle or nil for anonymous / unknown names. Single FFI crossing — returns the id directly, no registry snapshot.
Parameters
namestring— Job name supplied tojobs.register.
Returns JobHandle? — JobHandle or nil.
local job = jobs.find("animation_blend_main")
globals/jobs/inspect
jobs.inspect(target: JobHandle | string) -> JobInfo?
Return a snapshot row by job handle or by name without retrieving a full handle. Single FFI crossing — pulls only the matching row.
Parameters
targetJobHandle | string— JobHandle, or job name string.
Returns JobInfo? — Snapshot row or nil.
local info = jobs.inspect("animation_blend_main")
globals/jobs/list
jobs.list(phase: string?) -> { JobInfo }
List registered job summaries. Pass a phase name to filter to a single phase. Single FFI crossing — only the requested rows cross the bridge.
Parameters
phasestring(optional) — Optional phase filter — nil returns every job.
Returns { JobInfo } — Array of JobInfo rows.
local rows = jobs.list("main")
globals/jobs/register
jobs.register(descriptor: table) -> JobHandle?
Register a substrate job. Returns a JobHandle on success, nil on validation failure. Dispatches by executor.kind:\n - "kernel" / "stub" → standard __jobs.register (JSON-only descriptor).\n - "luau" → __jobs.register_luau(descriptor, executor.run) so the Luau function survives the JSON crossing as a stable registry ref. The dispatcher invokes the run closure once per frame; the closure captures any bindings/buffers it needs.\n - "compute" → the shader reference resolves to its registration key, and the job queues one dispatch per frame.\n\norigin is auto-filled with the VFS path of the calling script unless the descriptor already supplies one — surfaced under /zero/runtime/jobs/<phase>/<key>/origin.txt for agent traceability. Single FFI crossing — auto-origin runs Rust-side via lua_getinfo, no separate stack-inspection trip.
Parameters
descriptortable— Job declaration with theJobDescriptorshape —name?,phase,reads?,writes?,executor,ordering?,pure?,origin?,metadata?. Param is typed astablerather thanJobDescriptorbecause the LSP doesn't yet narrow string literals to their literal types in record fields, so aJobDescriptorannotation rejects the tagged-unionexecutordiscriminator on every call site (literalkind = "kernel"infers askind: string, doesn't subtypeKernelExecutor.kind: "kernel"). Runtime validation in__jobs.registerenforces the actual structure; see theJobDescriptortype alias above for the canonical shape.
Returns JobHandle? — JobHandle or nil.
local job = jobs.register({ phase = "main", executor = { kind = "kernel", kernel = "copy_buffer" }, reads = {{resource={kind="buffer",id=src.id},mode="r"}}, writes = {{resource={kind="buffer",id=dst.id},mode="w"}} })
local job = jobs.register({ phase = "main", executor = { kind = "luau", run = function() print("tick") end } })
local job = jobs.register({ phase = "main", executor = { kind = "compute", shader = asset.resolve("carve", "computeShader"), buffers = { "heights" }, workgroups = { 64 } } })
globals/layers/active
layers.active -> any
The root scene's proxy, re-resolved on every read.
Returns any
globals/layers/active/clearDirty
layers.active:clearDirty(opts?) -> boolean
Discard this layer's unsaved edits and respawn the live scene from canonical. Must run in a coroutine.
Parameters
optstable(optional) — Options
Returns boolean — True if overlay state was removed.
globals/layers/active/hasDirty
layers.active:hasDirty(opts?) -> boolean
True iff any unsaved dirty state exists for this layer — the manifest or at least one per-entity overlay file.
Parameters
optstable(optional) — Options
Returns boolean — True when unsaved dirty state exists.
globals/layers/active/load_additive
layers.active:load_additive(ref) -> SceneProxy
Load a scene as an additive overlay on top of this one, routed through the canonical load path (v6 player/camera config, lighting, entrypoint discovery).
Parameters
refstring— Scene asset reference to overlay
Returns SceneProxy — The new overlay layer's proxy.
globals/layers/active/offReady
layers.active:offReady(handle) -> boolean
Remove an onReady subscription by its handle.
Parameters
handlenumber— Handle returned by onReady
Returns boolean — True if the subscription was removed.
globals/layers/active/off_edit_load
layers.active:off_edit_load(handle) -> boolean
Remove an on_edit_load subscription by its handle.
Parameters
handlenumber— Handle returned by on_edit_load
Returns boolean — True if the subscription was removed.
globals/layers/active/off_load
layers.active:off_load(handle) -> boolean
Remove an on_load subscription by its handle.
Parameters
handlenumber— Handle returned by on_load
Returns boolean — True if the subscription was removed.
globals/layers/active/off_load_play
layers.active:off_load_play(handle) -> boolean
Remove an on_load_play subscription by its handle.
Parameters
handlenumber— Handle returned by on_load_play
Returns boolean — True if the subscription was removed.
globals/layers/active/off_unload
layers.active:off_unload(handle) -> boolean
Remove an on_unload subscription by its handle.
Parameters
handlenumber— Handle returned by on_unload
Returns boolean — True if the subscription was removed.
globals/layers/active/onReady
layers.active:onReady(cb) -> number
Subscribe to this scene reaching the 'ready' state. LATCHED — a callback registered after the scene is already ready fires immediately.
Parameters
cbfunction— Called with the SceneProxy once the scene is ready
Returns number — Subscription handle for offReady.
globals/layers/active/on_edit_load
layers.active:on_edit_load(cb) -> number
Subscribe to THIS scene's load, firing only when it loads with engine.mode == "edit".
Parameters
cbfunction— Called when this scene loads in edit mode
Returns number — Subscription handle for off_edit_load.
globals/layers/active/on_load
layers.active:on_load(cb) -> number
Subscribe to THIS scene's load event (fires only for this layer, unlike the global layers.onLoad).
Parameters
cbfunction— Called on this scene's load
Returns number — Subscription handle for off_load.
globals/layers/active/on_load_play
layers.active:on_load_play(cb) -> number
Subscribe to THIS scene's load, firing only when it loads with engine.mode == "play".
Parameters
cbfunction— Called when this scene loads in play mode
Returns number — Subscription handle for off_load_play.
globals/layers/active/on_unload
layers.active:on_unload(cb) -> number
Subscribe to THIS scene's unload event (fires only for this layer).
Parameters
cbfunction— Called on this scene's unload
Returns number — Subscription handle for off_unload.
globals/layers/active/promoteDirty
layers.active:promoteDirty(opts?) -> string?
Promote the dirty overlay to canonical — compose canonical + overlay, write canonical, then clear the dirty state. Raises when the canonical write is refused, leaving the overlay holding this session's edits.
Parameters
optstable— { to?
Returns string — Canonical path on success; nil when nothing to promote.
globals/layers/active/reload
layers.active:reload(opts?) -> SceneBuildReport?
Round-trip reload — unload then reload the same scene ref, firing the full unload / before-load / load lifecycle, and running the scene's build.luau against what it resolves now. Must run in a coroutine.
Parameters
optstable— { rebuild?
Returns table — What the scene's build.luau did — { built = true, content, editorOnly }, carrying { refused, message } when an operation the build ran was refused, or { built = false, reason, message }.
globals/layers/active/root
layers.active:root() -> SceneProxy
Return the root SceneProxy — walks up the additive-overlay parent chain to the non-additive root layer.
Returns SceneProxy — The root layer's proxy.
globals/layers/active/save
layers.active:save(opts?) -> string
Publish the layer's current state to canonical scene.json — the ONLY path that writes canonical (edit/play cycles never save). Additive overlays cannot be saved.
Parameters
optstable— { to?
Returns string — Full VFS path of the canonical scene.json written.
globals/layers/active/set_visible
layers.active:set_visible(v)
Show or hide this layer's entities.
Parameters
vboolean— Whether the layer's entities are shown
globals/layers/active/unload
layers.active:unload()
Unload this layer: despawn every entity attributed to it and remove its slot from SceneLayers.
globals/layers/active/writeDirty
layers.active:writeDirty(opts?) -> string
Force-drain every pending edit into the dirty overlay directory (deltas only — changed-entity files + manifest); never touches canonical.
Parameters
optstable(optional) — Write options
Returns string — Absolute VFS path of the dirty manifest.
globals/layers/camera
layers.camera -> any
The active root scene's camera handle, the same value layers.active.camera answers.
Returns any
globals/layers/cost
layers.cost() -> { SceneLayerCost }
What each loaded scene's per-frame tick costs, attributed to the layer
that owns it — the update / editorUpdate its entrypoint declares,
timed where it runs. totalMs is a SUM across the window
layers.observe().window reports, so divide by calls (or read avgMs)
for the per-tick figure; a tick that runs every frame makes that the
per-frame figure. Call layers.resetCostWindow() first to time a
particular stretch. A layer whose entrypoint declares no tick is absent.
Returns { SceneLayerCost } — An array of SceneLayerCost.
layers.resetCostWindow(); task.wait(1); for _, c in layers.cost() do print(c.name, c.avgMs) end
globals/layers/find
layers.find(ref: AssetRef<scene> | string) -> any?
The loaded layer for a scene, matched on guid — the canonical identity, since display names can collide and paths drift when assets move. A layer torn down but not yet pumped out of the engine's loaded list reads as gone.
Parameters
refAssetRef<scene> | string— A sceneAssetRef, or an identity string resolved throughasset.ref.
Returns any? — The scene proxy, or nil when that scene has no loaded layer.
local layer = layers.find("scenes.arena")
globals/layers/fireBeforeLoad
layers.fireBeforeLoad(proxy: any?) -> nil
Announce that a scene layer is about to load: clears any pending
unload for that layer slot, marks the proxy loading, and fans out to every
layers.onBeforeLoad subscriber. The scene-load pipeline calls this.
Parameters
proxyany(optional) — The scene proxy about to load.
Returns nil
layers.fireBeforeLoad(sceneProxy)
globals/layers/fireLoad
layers.fireLoad(proxy: any?) -> nil
Announce that a scene layer has loaded, fanning out to every
layers.onLoad subscriber. The layer is pinned as the active one for the
duration of the fan-out, so entities a subscriber spawns are attributed to
it rather than landing orphaned. The scene-load pipeline calls this.
Parameters
proxyany(optional) — The loaded scene proxy.
Returns nil
layers.fireLoad(sceneProxy)
globals/layers/fireUnload
layers.fireUnload(proxy: any?) -> nil
Announce that a scene layer is unloading: fans out to every
layers.onUnload subscriber, then drops the layer's cached proxy and
per-layer state so the next load of that scene rebuilds from disk. The
unload path calls this.
Parameters
proxyany(optional) — The scene proxy being unloaded.
Returns nil
layers.fireUnload(sceneProxy)
globals/layers/install
layers.install() -> nil
Install the layers global. layers.active is exposed as a property
whose every read resolves the current root scene, so it tracks scene
changes without manual invalidation; other keys resolve against this
module. The prelude calls this once at boot.
Returns nil
layers.install()
globals/layers/inventory
layers.inventory() -> { SceneLayerInventory }
What each loaded layer holds: the entities the engine attributes to it,
whether it came up whole, and how many failures it carries. unattributed
in layers.observe().totals counts what exists in the world that no layer
claims.
Returns { SceneLayerInventory } — An array of SceneLayerInventory.
for _, l in layers.inventory() do print(l.name, l.entities, l.ok) end
globals/layers/is_loaded
layers.is_loaded(ref: AssetRef<scene> | string) -> boolean
Whether a scene currently has a loaded layer — the boolean form of
layers.find. A scene counts as loaded from the frame the engine holds a
layer slot for it — the same slot its entities are attributed to — until
an unload is issued against that slot. So a gate like
if layers.is_loaded(ref) then layers.unload(ref) end sees the layer on
the frame its entities exist.
Parameters
refAssetRef<scene> | string— A sceneAssetRef, or an identity string.
Returns boolean — True when the scene is loaded as a layer.
if not layers.is_loaded("scenes.hud") then layers.load("scenes.hud", { additive = true }) end
globals/layers/lastLoad
layers.lastLoad() -> SceneLoadReport?
The most recent load's report: what it loaded, what root it replaced and which overlays went with it, the entity counts on each side, how long each phase took, and every failure it produced. Nil on an engine that has loaded nothing — which is how "nothing has loaded" reads differently from a load that changed nothing.
Returns SceneLoadReport? — A SceneLoadReport, or nil.
local r = layers.lastLoad(); print(r.name, r.outcome, r.entities.added)
globals/layers/lastUnload
layers.lastUnload() -> SceneUnloadReport?
The most recent unload's report: the layer it took down under the name it was loaded with, the overlays it cascaded, and the entities that went with them. A guid no longer resolves to a name once its layer is gone, so this is where that name survives.
Returns SceneUnloadReport? — A SceneUnloadReport, or nil.
local u = layers.lastUnload(); print(u.name, u.entities.removed)
globals/layers/list
layers.list() -> { any }
Every loaded scene layer as a proxy, root and additive alike, in the order the engine reports them.
Returns { any } — Array of scene proxies — empty before any scene is loaded.
for _, layer in ipairs(layers.list()) do print(layer.name, layer.additive) end
globals/layers/load
layers.load(ref: AssetRef<scene> | string, opts: LoadOpts?) -> any
Load a scene into the root non-additive slot ("main") OR as
an additive overlay alongside it. Identity is ref-based: pass an
AssetRef<scene> envelope (preferred — caught at the callsite
by the LSP) or an identity string (resolved via asset.ref at
entry, hard-error if no stable guid comes back). For non-additive,
idempotency is by guid: re-loading the same scene logs and
returns the existing proxy without tearing anything down.
Different guid → unloads the current root + cascades every
additive overlay it spawned + transitions the multiplayer room +
loads the new scene. Logs every step at info level so a silent
no-op is impossible.
Parameters
refAssetRef<scene> | string—AssetRef<scene>envelope (preferred) or scene identity string.optsLoadOpts(optional) — Optional load options — additive overlay flag, slot name, persistence flag, world-origin offset, and whether to rebuild.
Returns any — SceneProxy for the loaded layer (the cached instance the module also returns from layers.active / layers.find). Its lastBuild field says what the scene's build.luau did on this load — whether it ran, and what stood in the way when it did not.
layers.load(asset.ref("@builtin::scenes.test_arena", "scene"))
layers.load(myAssetRef, { additive = true, name = "hud_overlay" })
globals/layers/loadHistory
layers.loadHistory() -> { SceneLoadReport }
Every load report the engine still holds, oldest first. Bounded — old reports fall off the front, so a long session's memory does not grow with how many times a scene was swapped.
Returns { SceneLoadReport } — An array of SceneLoadReport.
for _, r in layers.loadHistory() do print(r.name, r.durationMs) end
globals/layers/loadInFlight
layers.loadInFlight() -> number
Returns the number of scene loads currently in flight (queued
but not yet visible via onLoad dispatch). Returns 0 when the
engine is in a stable load state. Used by engine.mode = ... to
block flips while a load is mid-air; agents can read this to wait
for a load to finish before driving the next operation.
Returns number
globals/layers/localPlayer
layers.localPlayer -> any
The active root scene's local player handle, the same value layers.active.players.localPlayer answers.
Returns any
globals/layers/observe
layers.observe() -> SceneObservation
What every scene load did, and what each loaded scene costs. One read covering the last load's report (what it produced, what it replaced, what it failed to produce and why, and how long each phase took), the load and unload history, a per-layer inventory of what the engine attributes to each layer, and the per-frame cost of each layer's entrypoint tick. Answers in edit mode as well as play.
Returns SceneObservation — A SceneObservation.
local o = layers.observe(); print(o.lastLoad.outcome, o.lastLoad.durationMs)
for _, c in layers.observe().cost do print(c.name, c.avgMs) end
globals/layers/offBeforeLoad
layers.offBeforeLoad(h: number) -> boolean
Cancel a layers.onBeforeLoad subscription.
Parameters
hnumber— The handlelayers.onBeforeLoadreturned.
Returns boolean — True when a subscription was removed.
layers.offBeforeLoad(h)
globals/layers/offEntityChanged
layers.offEntityChanged(h: number) -> boolean
Remove a subscription made with layers.onEntityChanged.
Parameters
hnumber— The handle returned bylayers.onEntityChanged.
Returns boolean — True when the subscription existed and was removed.
layers.offEntityChanged(handle)
globals/layers/offLoad
layers.offLoad(h: number) -> boolean
Cancel a layers.onLoad subscription.
Parameters
hnumber— The handlelayers.onLoadreturned.
Returns boolean — True when a subscription was removed.
layers.offLoad(h)
globals/layers/offUnload
layers.offUnload(h: number) -> boolean
Cancel a layers.onUnload subscription.
Parameters
hnumber— The handlelayers.onUnloadreturned.
Returns boolean — True when a subscription was removed.
layers.offUnload(h)
globals/layers/onBeforeLoad
layers.onBeforeLoad(cb: (any) -> ()) -> number
Run a callback just before a scene layer loads, while the previous layer's entities are still present.
Parameters
cb(any) -> ()— Receives the scene proxy about to load.
Returns number — A handle to pass to layers.offBeforeLoad.
local h = layers.onBeforeLoad(function(scene) print("loading", scene.name) end)
globals/layers/onEntityChanged
layers.onEntityChanged(cb: (any) -> ()) -> number
Subscribe to authored entity changes. The callback runs once per
frame with every entity edited since the previous frame, batched by
layer as { { scene = string, entities = { string } } } — a moved
transform, an edited component field, a spawn, or a despawn (the id
of a despawned entity arrives with entity.exists already false).
Any number of subscribers can watch the same edits.
Scope: authored edits in edit mode — what lands in the scene's dirty
overlay. Mutations a component makes from its own update are runtime
behavior and do not appear, so a subscriber that rebuilds derived data
cannot re-trigger itself.
Parameters
cb(any) -> ()— Called with the change batch.
Returns number — A handle for layers.offEntityChanged.
layers.onEntityChanged(function(batch)
for _, row in ipairs(batch) do
for _, id in ipairs(row.entities) do rebuild(id) end
end
end)
globals/layers/onLoad
layers.onLoad(cb: (any) -> ()) -> number
Run a callback once a scene layer has loaded — the point where its entities exist and player / camera spawners can attach to them.
Parameters
cb(any) -> ()— Receives the loaded scene proxy.
Returns number — A handle to pass to layers.offLoad.
local h = layers.onLoad(function(scene) spawnPlayerFor(scene) end)
globals/layers/onUnload
layers.onUnload(cb: (any) -> ()) -> number
Run a callback as a scene layer unloads, while its entities are still addressable — the place to release anything keyed to them.
Parameters
cb(any) -> ()— Receives the scene proxy being unloaded.
Returns number — A handle to pass to layers.offUnload.
local h = layers.onUnload(function(scene) releaseHandlesFor(scene) end)
globals/layers/problems
layers.problems(ref: (AssetRef<scene> | string | any)?) -> { SceneLoadFailure }
What a layer failed to produce, and why. Each entry names the phase it happened in, one reason from the closed set, and the engine's own words — plus the entity, component or lifecycle hook it is about when it is about one.
Parameters
ref(AssetRef<scene> | string | any)(optional) — A sceneAssetRef, an identity string, or a scene proxy. Omit for the active root layer.
Returns { SceneLoadFailure } — An array of SceneLoadFailure — empty for a layer that came up whole.
for _, f in layers.problems() do print(f.reason, f.entity, f.message) end
globals/layers/rebuildInFlight
layers.rebuildInFlight() -> boolean
Whether the engine is rebuilding the live scene right now — a scene load is carrying entities in, or an edit↔play flip's transition is materialising the layer set. A flip unloads the root layer and loads it again for the new mode across many frames, and each mode materialises a different set of entities, so the live entities are a stage of a scene being built while this reads true. A caller whose answer belongs to the settled scene — a test taking a root, a validator judging the live tree — polls it down to false first.
Returns boolean — true while a load or a mode-flip transition is converging.
if not layers.rebuildInFlight() then judge(layers.active) end
globals/layers/reload
layers.reload(ref: (AssetRef<scene> | string)?) -> any?
Unload and re-load a scene layer in place, so an edited scene asset
takes effect without rebuilding the surrounding layer stack. The scene's
build.luau runs against what it resolves right now, so a build script
whose inputs moved — a component that now exists, an asset that now
resolves — produces the scene it describes today.
Parameters
ref(AssetRef<scene> | string)(optional) — A sceneAssetRef, or an identity string. Omit to reload the active root scene.
Returns any? — What the scene's build.luau did — { built = true, content, editorOnly } with the entity counts each half placed, carrying refused and a message reading them out when an operation the build ran was refused, or { built = false, reason, message } naming what stood in the way. Nil when no layer matched, which is a no-op.
layers.reload("scenes.arena")
globals/layers/resetCostWindow
layers.resetCostWindow() -> nil
Open a new cost window, discarding what the previous one measured. Call this before timing a stretch of frames; the load history is untouched.
Returns nil
layers.resetCostWindow()
globals/layers/unload
layers.unload(refOrProxy: (AssetRef<scene> | string | any)?) -> nil
Unload a scene layer. Unloading the root cascades through its additive overlays first, most-recently-loaded first, so none is left as a layer the engine still lists after its entities are gone; persistent additive layers survive the cascade. A scene with no loaded layer is a no-op.
Parameters
refOrProxy(AssetRef<scene> | string | any)(optional) — A sceneAssetRef, an identity string, or a scene proxy. Omit to unload the active root scene.
Returns nil
layers.unload("scenes.hud")
layers.unload() -- the active root, plus its non-persistent overlays
globals/layers/whyPartial
layers.whyPartial(ref: (AssetRef<scene> | string | any)?) -> (string?, string?)
Why a layer is not whole. Returns nil when it IS — everything the scene
declared was produced — and otherwise the nearest cause from the closed set
loaderRaised, entrypointCompileFailed, entrypointBodyRaised,
entrypointRaised, buildRaised, entityFailed, parentMissing,
parentRefused, parentAbandoned, componentUnresolved,
componentRefused, subscriberRaised, updateRaised. A second return
carries the engine's own words for that cause.
Parameters
ref(AssetRef<scene> | string | any)(optional) — A sceneAssetRef, an identity string, or a scene proxy. Omit for the active root layer.
Returns (string?, string?) — (reason, detail).
local why, detail = layers.whyPartial(); if why then print(why, detail) end
globals/library/has
library.has(path: string) -> boolean
Check if a library asset exists at the given path.
Parameters
pathstring— Library asset path (e.g. "@builtin/models/Sample/DamagedHelmet").
Returns boolean — True if the asset exists in the library.
assert(library.has("@builtin/models/Cube"))
globals/library/import
library.import(namespace: string, worldRef: string) -> LibraryImport
Import another world as a library under the given namespace.
Resolves the world, pins its current commit, and writes the
library marker at /source/libs/@<namespace>. Once the engine has
fetched the pinned commit, the imported tree answers to
require("@<namespace>::path"), is listed by library.list(), and
is readable under /zero/source/libs/@<namespace>/.
Parameters
namespacestring— Library namespace, with or without the leading@(e.g."@mylib"or"mylib").worldRefstring— The upstream world's guid, or its name as it appears inworld.list().
Returns LibraryImport — Record describing the import: the local name, the marker path, the upstream world_guid, and the pinned commit as version.
library.import("@mylib", "my-shared-world")
globals/library/list
library.list(assetType: string?) -> { LibraryAsset }
List all available library assets. Optionally filter by asset
type — call asset.categories() for the live set.
Parameters
assetTypestring(optional) — Asset type filter (optional).
Returns { LibraryAsset } — Array of asset descriptors. path is the @builtin::* identity, type is one of the valid type strings, format is the original file extension (e.g. "glb", "luau", "wgsl").
for _, a in ipairs(library.list("model")) do print(a.path) end
globals/loadstring
loadstring(code, chunkname?) -> function | (nil, error)
Compile a Luau source string into a callable function. Returns the compiled function on success, or nil + error message on failure. The returned function can be called to execute the code.
Parameters
codestring— Luau source code to compilechunknamestring(optional) — Name for the chunk (shown in errors). Default: '=(loadstring)'
Returns function | (nil, string) — Compiled function, or nil + error string
globals/logs/clear
logs.clear() -> boolean
Drop all buffered log entries. Lifetime per-level counts
(logs.count) are preserved.
Returns boolean — True on success.
logs.clear()
globals/logs/count
logs.count(opts: LogQueryOpts?) -> LogCounts
Aggregate counters for the log ring. Lifetime counts survive
eviction, so errors reflects the total seen even if the lines
have scrolled out of the buffer. opts takes the same filter table
as logs.query, and matched is how many held entries it selects,
counted without materialising them — limit and newest_first bound
and order what a query RETURNS, so they leave matched alone. mcp
is how many held entries record your own tool traffic; a query leaves
those out, so with no opts, matched + mcp is everything held.
last_seq is the cursor for
incremental polling: read it before an action, then pass it as
logs.query({ since = <that> }) afterwards to see only what the
action logged.
Parameters
optsLogQueryOpts(optional) — Filter options, aslogs.querytakes.
Returns LogCounts — Counts summary table.
print("errors:", logs.count().errors)
local before = logs.count().last_seq
globals/logs/errors
logs.errors(limit: number?) -> { LogEntry }
Most-recent ERROR-level entries (newest first). limit
defaults to 100.
Parameters
limitnumber(optional) — Maximum entries to return.
Returns { LogEntry } — Array of ERROR log-entry tables.
for _, e in ipairs(logs.errors(20)) do print(e.message) end
globals/logs/find
logs.find(text: string, limit: number?) -> { LogEntry }
Case-insensitive substring search over log messages. limit
defaults to 200 (keeps the most recent matches). Searches what the
engine logged, so looking for a marker cannot return the call that
looked for it; logs.query({ contains = ..., include_mcp = true })
searches your own tool traffic too.
Parameters
textstring— Substring to search for.limitnumber(optional) — Maximum entries to return.
Returns { LogEntry } — Array of matching log-entry tables in chronological order.
local hits = logs.find("MY_MARKER")
globals/logs/query
logs.query(opts: LogQueryOpts?) -> { LogEntry }
Query the engine's in-memory log ring — the filtered view of what
also reads as plain text at /zero/runtime/logs/engine. Answers about what the
engine logged: the MCP record of your own tool traffic is left out,
because the call carrying the query is one of those records and an
unqualified search would match itself. type = "MCP" selects them;
include_mcp = true mixes them in with everything else. On a world
several sessions share, origin = "local" narrows the answer to the
lines this session's own authoring caused.
Parameters
optsLogQueryOpts(optional) — Filter options.
Returns { LogEntry } — Array of matching log-entry tables.
logs.query({ entity = "guard-1", limit = 20 })
logs.query({ level = "error", context = 3 })
for _, e in ipairs(logs.query({ level = "warn", limit = 50 })) do print(e.message) end
globals/logs/tail
logs.tail(limit: number?) -> { LogEntry }
Most-recent entries of any level in chronological order.
limit defaults to 100.
Parameters
limitnumber(optional) — Maximum entries to return.
Returns { LogEntry } — Array of the most recent log-entry tables.
for _, e in ipairs(logs.tail(20)) do print(e.level, e.message) end
globals/logs/template
logs.template(message: string) -> string
Normalize a message to its template — the same line with the parts that vary between occurrences (numbers, hashes, entity ids) masked out. Two messages that differ only in those parts share a template, which is what turns "this error repeated 400 times" into one row instead of 400. The engine keys its own error retention by the same normalization, so grouping built on this agrees with what survives ring eviction.
Parameters
messagestring— Log message to normalize.
Returns string — The message template.
local key = logs.template(entry.message)
globals/logs/warnings
logs.warnings(limit: number?) -> { LogEntry }
Most-recent WARN+ entries (newest first). limit defaults
to 100.
Parameters
limitnumber(optional) — Maximum entries to return.
Returns { LogEntry } — Array of WARN+ log-entry tables.
print(#logs.warnings(), "warnings")
globals/lsp/check
lsp.check(path: string, opts: CheckOpts?) -> DiagnosticsResult
Validate a single .luau file in the VFS and return its
diagnostics. A path the check could not read comes back as one
lsp-check-* error naming the path and the reason, so errors == 0
means a code body was read and is clean.
Parameters
pathstring— VFS path.optsCheckOpts(optional) —{ severity?, limit?, context? }.
Returns DiagnosticsResult — Array of diagnostic tables.
local diags = lsp.check("/zero/source/main.luau")
globals/lsp/checkAll
lsp.checkAll(opts: CheckAllOpts?) -> CheckAllResult
Validate the user's Luau scripts and return an aggregate
summary plus diagnostic list. opts.scope = "user" (default)
skips library mounts; "all" includes them. The sweep is
time-budgeted (ZERO_EXECUTE_INLINE_BUDGET_MS, ~20s by default): if
the budget elapses it returns the partial result gathered so far
with budgetExceeded = true rather than blocking the engine.
Parameters
optsCheckAllOpts(optional) —{ scope?, severity?, limit? }.
Returns CheckAllResult — { filesChecked, errors, warnings, info, hints, budgetExceeded, diagnostics }.
globals/lsp/checkCode
lsp.checkCode(source: string, opts: CheckOpts?) -> DiagnosticsResult
Validate inline Luau source without a backing file. Useful for checking code before writing it to disk.
Parameters
sourcestring— Luau source.optsCheckOpts(optional) —{ severity?, limit?, context? }.
Returns DiagnosticsResult — Array of diagnostic tables.
globals/lsp/checkDirty
lsp.checkDirty() -> DiagnosticsResult
Drain the dirty-file set populated by the hot-reload hook, validate each, and return the combined diagnostic list.
Returns DiagnosticsResult — Array of diagnostic tables.
globals/lsp/describe
lsp.describe(path: string, opts: DescribeOpts?) -> DocEntry?
Inspect a single documented entry. Returns the full doc
table (signature, args, returns, examples, level), or nil.
The path is resolved independently of which root the doc is
registered under and of separator style, so the spelling that reads
off the API surface (renderer.texture.create) finds the entry
registered as globals/renderer/texture/create. A path naming a
binding the engine registered internally answers with the entry a Luau
module publishes over it where there is one, so the signature is the
call content makes; opts.includeInternal answers with the internally
registered entry itself. When a path does not resolve,
lsp.describePaths says what the registry holds near it.
Parameters
pathstring— Doc path (e.g."asset/resolve","renderer.texture.create").optsDescribeOpts(optional) — Optional{ includeInternal? }— default prefers the published entry.
Returns DocEntry? — Full doc table or nil.
local doc = lsp.describe("renderer.texture.create")
globals/lsp/describePaths
lsp.describePaths(path: string) -> { string }
List the registered doc paths related to path. A path that names
an entry returns every root it is registered under (the first is what
lsp.describe resolves to); a path that names a namespace returns the
entries registered under it. Empty when the registry holds nothing
near the path — so a lookup that returns nil can always be turned into
the list of what does exist.
Parameters
pathstring— Doc path in any spelling ("renderer.texture","ecs/query").
Returns { string } — Array of registered doc paths, most canonical first.
for _, p in ipairs(lsp.describePaths("renderer.texture")) do print(p) end
globals/lsp/describeTool
lsp.describeTool(path: string) -> string?
Return the full documentation text for a code-mode tool.
Parameters
pathstring— Tool path (e.g."scene/spawnLight").
Returns string? — Full tool docs or nil.
globals/lsp/docsByKind
lsp.docsByKind(kind: string) -> { MethodSummary }
List every doc whose registration kind matches kind.
Valid: "binding", "runtime_tool", "module", "component",
"library", "lua_export".
Parameters
kindstring— Registration kind.
Returns { MethodSummary } — Array of doc summary tables.
globals/lsp/getStrictMode
lsp.getStrictMode() -> StrictMode
Return the current strict mode.
Returns StrictMode — "off" | "soft" | "strict".
globals/lsp/isStrict
lsp.isStrict() -> boolean
Is the pre-execute LSP gate fully strict? False when off or in soft mode.
Returns boolean — True when fully strict.
globals/lsp/lastCheckGen
lsp.lastCheckGen() -> number
Generation counter — bumped each time the cache is rebuilt. UI polls this to know when to redraw.
Returns number — Generation number.
globals/lsp/methods
lsp.methods(namespace: string, opts: MethodsOpts?) -> { MethodSummary } | { string }
List every documented method / entry under a namespace. A broad
namespace (ui, renderer) returns a large dump by default, so two
options narrow it: opts.filter keeps only methods whose name (or
doc path) contains the substring, case-insensitively; opts.namesOnly
returns a plain list of method-name strings instead of the full
per-method summary tables — much smaller, and nothing to unwrap. The
listing answers with the surface content calls: an entry registered
internally is left out where its signature spells the __ binding or a
Luau module publishes the same member, and opts.includeInternal lists
every registered entry instead.
Parameters
namespacestring— Namespace name (e.g."entity","modules/Transform").optsMethodsOpts(optional) — Optional{ filter?, namesOnly?, includeInternal? }.
Returns { MethodSummary } | { string } — Array of method summary tables, or plain name strings when namesOnly is set (empty when the namespace is unknown or nothing matches the filter).
for _, name in ipairs(lsp.methods("ui", { namesOnly = true })) do print(name) end
lsp.methods("renderer", { filter = "shadow" })
globals/lsp/modules
lsp.modules() -> { ModuleEntry }
List every Luau library module the engine currently knows
about — discovered via --!module headers, library scans, and
manually-recorded docs.
Returns { ModuleEntry } — Array of module summary tables.
globals/lsp/namespaces
lsp.namespaces(opts: NamespacesOpts?) -> { NamespaceEntry }
List the documentation namespaces reachable from Luau. By
default only namespaces exposing at least one PUBLIC method are
returned, so the list matches what you can actually call — internal
FFI plumbing (e.g. pause, native_entity), whose public surface
lives elsewhere (engine.paused, the entity proxy, …), is left
out. Pass { includeInternal = true } to list every namespace,
internal ones included.
Parameters
optsNamespacesOpts(optional) — Optional{ includeInternal? }— default lists public only.
Returns { NamespaceEntry } — Array of namespace summary tables.
for _, ns in ipairs(lsp.namespaces()) do print(ns.name) end
globals/lsp/readDirectives
lsp.readDirectives(source: string) -> DirectiveBlock
Parse the leading --! directive block of a Luau source
string. Used by UIs that audit which files have skip directives
and what they suppress.
Parameters
sourcestring— Luau source text.
Returns DirectiveBlock — { mode, codes? }.
globals/lsp/search
lsp.search(query: string, opts: SearchOpts?) -> { MethodSummary }
Case-insensitive substring search across every registered
doc's path, signature, and description. Hits answer with the surface
content calls: an entry registered internally is left out where its
signature spells the __ binding or a Luau module publishes the same
member, and opts.includeInternal searches every registered entry.
Parameters
querystring— Substring to search for.optsSearchOpts(optional) —{ limit? = 50, includeInternal? }.
Returns { MethodSummary } — Array of method summary tables.
globals/lsp/setStrict
lsp.setStrict(enabled: boolean) -> boolean
Toggle the pre-execute LSP gate. Returns true when the change
was persisted to .world_settings, false when the play-mode write
lock blocked the write.
Parameters
enabledboolean— True = strict, false = off.
Returns boolean — Persistence signal.
globals/lsp/setStrictMode
lsp.setStrictMode(mode: StrictMode) -> boolean
Set the pre-execute strict gate's mode. Returns true when the
change was persisted to .world_settings, false when the
play-mode write lock blocked the write.
Parameters
modeStrictMode—"off"|"soft"|"strict".
Returns boolean — Persistence signal.
globals/lsp/summary
lsp.summary() -> Summary
Counts only — does not re-run validation.
Returns Summary — Counts of cached diagnostics by severity.
globals/lsp/tools
lsp.tools() -> { ToolEntry }
List every code-mode tool registered in the VFS under
/zero/docs/tools/<category>/<tool>.
Returns { ToolEntry } — Array of tool summary tables.
globals/lsp/typeOf
lsp.typeOf(expr_source: string, context_path: string?) -> TypeDescriptor
Infer the static type of a Luau expression. When
context_path is given, the file is loaded and walked so the
inference env contains every local + alias in scope at its end.
Parameters
expr_sourcestring— Luau expression source (no surrounding chunk).context_pathstring(optional) — VFS path whose scope should be visible.
Returns TypeDescriptor — Type descriptor table.
local td = lsp.typeOf("Transform.lookAt", "/zero/source/main.luau")
globals/luau_profile/begin
luau_profile.begin(name: string) -> number
Open a named manual region. Returns an opaque integer id;
pass it back to end_region(id) to close and record elapsed
wall-clock under name.
Parameters
namestring— Region name; aggregated across opens.
Returns number — Region id.
local id = luau_profile.begin("walk"); ...; luau_profile.end_region(id)
globals/luau_profile/dump
luau_profile.dump(path: string) -> DumpResult
Write the folded-stack dump to path on the HOST filesystem,
one line per stack as <ticks> <stack_csv> — the format
upstream Luau emits and tools/perfgraph.py consumes
unchanged. A path naming the engine filesystem (/zero/..., or
a bare root such as /source/...) is refused, and says so:
luau_profile.folded() with vfs.write puts the dump there.
Parameters
pathstring— Absolute host filesystem path to write.
Returns DumpResult — { ok, path, samples, stacks, bytes }.
local r = luau_profile.dump("/tmp/profile.folded")
globals/luau_profile/dump_regions
luau_profile.dump_regions(path: string) -> DumpRegionsResult
Write per-region stats to path as JSON, on the HOST
filesystem. A path naming the engine filesystem is refused, the
same way dump refuses one.
Parameters
pathstring— Absolute host filesystem path to write.
Returns DumpRegionsResult — { ok, path, regions, bytes }.
luau_profile.dump_regions("/tmp/regions.json")
globals/luau_profile/end_region
luau_profile.end_region(id: number)
Close a region previously opened by begin(name). Records
elapsed wall-clock under the region's name. Silently no-ops on
unknown id (typically a double-close or swapped-out VM).
Parameters
idnumber— Region id returned bybegin().
luau_profile.end_region(id)
globals/luau_profile/folded
luau_profile.folded() -> string
The folded-stack dump as a string, one line per stack as
<ticks> <stack_csv> — the format upstream Luau emits and
tools/perfgraph.py consumes unchanged. The same bytes dump
writes, handed back instead of written, so the profile can go
wherever the caller keeps it: vfs.write puts it in the engine
filesystem, where bash and vfs.read reach it.
Returns string — Folded-stack text; empty when nothing was sampled.
vfs.write("/source/tmp/sample.folded", luau_profile.folded())
globals/luau_profile/is_running
luau_profile.is_running() -> boolean
True iff the background sampler is currently running.
Returns boolean — Sampler running state.
if luau_profile.is_running() then luau_profile.stop() end
globals/luau_profile/reset
luau_profile.reset()
Clear every accumulated sample and region stat. The sampler keeps running if it was already on; only the data is wiped.
luau_profile.reset()
globals/luau_profile/sampling_available
luau_profile.sampling_available() -> boolean
True on platforms where the background sampler can run (native targets), false on WASM. Manual regions work everywhere — only the sampler is platform-gated.
Returns boolean — Whether start() would actually spawn a sampler.
if luau_profile.sampling_available() then luau_profile.start() end
globals/luau_profile/snapshot
luau_profile.snapshot(top_n: number?) -> Snapshot
Snapshot the current accumulator without touching the
filesystem — cheap enough for per-frame UI polling. top_n
truncates stacks to the N hottest entries; omitting it
returns all stacks sorted descending by self_us. regions
is always returned in full (sorted by total_us).
Parameters
top_nnumber(optional) — Truncate stacks to this many entries; omit for all.
Returns Snapshot — Profile snapshot.
local snap = luau_profile.snapshot(10)
globals/luau_profile/start
luau_profile.start(hz: number?) -> StartResult
Start the background Luau sampling profiler at hz samples
per second (default 1000, clamped to [1, 100000]). Idempotent
— calling while already running is a no-op. Returns
{ available, hz } — available = false on WASM (no
std::thread). Manual regions work regardless.
Parameters
hznumber(optional) — Sampling rate in Hz.
Returns StartResult — Effective sampler state.
luau_profile.start(500)
globals/luau_profile/stop
luau_profile.stop()
Stop the background sampler. Blocks until the sampler thread
joins (typically <1ms). Safe when not running. Does not clear
the accumulator — call reset() to drop samples.
luau_profile.stop()
globals/mathx/addScaledVec3
mathx.addScaledVec3(dstBuffer: Substrate.TypedBuffer, srcBuffer: Substrate.TypedBuffer, count: number, scale: number) -> boolean
dst[i] += src[i] * scale for count vec3 elements. Both
buffers must hold at least count * 3 floats. Useful for
particle integration (position += velocity * dt) and accumulator
passes.
Parameters
dstBufferSubstrate.TypedBuffer— The buffer written into.srcBufferSubstrate.TypedBuffer— The buffer read from.countnumber— Number of vec3 elements.scalenumber— Multiplier applied to every src element.
Returns boolean — True on success.
mathx.addScaledVec3(positions, velocities, n, dt)
globals/mathx/dampScalar
mathx.dampScalar(buffer: Substrate.TypedBuffer, offset: number, count: number, target: number, smoothTime: number, dt: number) -> boolean
Critically-damped exponential approach toward target for
count scalars at buffer[offset .. offset+count]. smoothTime
is the time constant (~ 0.16 ⇒ ~63% per frame at 60 Hz). Pass
smoothTime <= 0 to snap to the target.
Parameters
bufferSubstrate.TypedBuffer— The buffer to operate on.offsetnumber— Starting f32 index.countnumber— Number of scalars.targetnumber— Target value all scalars approach.smoothTimenumber— Time constant (≤ 0 snaps to target).dtnumber— Frame time in seconds.
Returns boolean — True on success, false on a bad handle or out-of-range slice.
mathx.dampScalar(buf, 0, 16, 0.0, 0.16, dt)
globals/mathx/lerpVec3
mathx.lerpVec3(buffer: Substrate.TypedBuffer, offset: number, count: number, tx: number, ty: number, tz: number, t: number) -> boolean
Element-wise linear blend of count vec3s in
buffer[offset .. offset+count*3] toward (tx, ty, tz) by t.
Parameters
bufferSubstrate.TypedBuffer— The buffer to operate on.offsetnumber— Starting f32 index.countnumber— Number of vec3 elements.txnumber— Target X.tynumber— Target Y.tznumber— Target Z.tnumber— Blend amount (0..1).
Returns boolean — True on success.
mathx.lerpVec3(buf, 0, n, 0, 1, 0, 0.5)
globals/mathx/normalizeQuat
mathx.normalizeQuat(buffer: Substrate.TypedBuffer, offset: number, count: number) -> boolean
Re-normalise count quaternions in place. Zero-length quats
become identity (0, 0, 0, 1) so downstream code never sees NaN.
Parameters
bufferSubstrate.TypedBuffer— The buffer to operate on.offsetnumber— Starting f32 index.countnumber— Number of quaternions.
Returns boolean — True on success.
mathx.normalizeQuat(buf, 0, n)
globals/mathx/slerpQuat
mathx.slerpQuat(buffer: Substrate.TypedBuffer, offset: number, count: number, tx: number, ty: number, tz: number, tw: number, t: number) -> boolean
Slerp count quaternions (xyzw) at buffer[offset..] toward
(tx, ty, tz, tw) by t. Falls back to nlerp+normalize for
very-close quats. Always picks the shortest-arc path.
Parameters
bufferSubstrate.TypedBuffer— The buffer to operate on.offsetnumber— Starting f32 index.countnumber— Number of quaternions.txnumber— Target quat X.tynumber— Target quat Y.tznumber— Target quat Z.twnumber— Target quat W.tnumber— Slerp amount (0..1).
Returns boolean — True on success.
mathx.slerpQuat(buf, 0, n, 0, 0, 0, 1, 0.25)
globals/mathx/transformVec3
mathx.transformVec3(buffer: Substrate.TypedBuffer, offset: number, count: number, mat16: { number }) -> boolean
Treat each vec3 in buffer[offset..] as a position (w = 1),
multiply by the 4x4 column-major matrix mat16 (16-element
array), write .xyz of the result back. Layout matches glam,
wgpu, and GLSL conventions.
Parameters
bufferSubstrate.TypedBuffer— The buffer to operate on.offsetnumber— Starting f32 index.countnumber— Number of vec3 elements.mat16{ number }— Column-major 4x4 matrix as a 16-element array.
Returns boolean — True on success.
mathx.transformVec3(positions, 0, n, worldMatrix)
globals/mcpLog/clear
mcpLog.clear() -> boolean
Clear all entries from the engine's MCP log ring buffer.
Returns boolean — True on success.
mcpLog.clear()
globals/mcpLog/query
mcpLog.query(limit: number?) -> { McpLogEntry }
Return the most-recent MCP tool-call entries from the engine's
MCP log ring buffer (newest last). Pass limit to cap how many
entries are returned — omit for the full ring (up to 500 entries).
Parameters
limitnumber(optional) — Maximum number of entries to return.
Returns { McpLogEntry } — Array of tool-call entry tables.
for _, e in ipairs(mcpLog.query(50)) do print(e.tool_name, e.status) end
globals/microphone/awaitRunning
microphone.awaitRunning(timeout: number?) -> (MicState, string?)
Wait until the capture settles out of starting and
permissionPending, and report where it landed. Returns as soon as the
state settles, or when timeout seconds have passed, whichever comes
first — a browser permission prompt nobody answers never settles, so
the wait is always bounded.
Parameters
timeoutnumber(optional) — Seconds to wait at most. Defaults to 10.
Returns (MicState, string?) — The state reached, and its reason where it has one.
microphone.start(); local state, why = microphone.awaitRunning()
globals/microphone/devices
microphone.devices() -> { MicDevice }
Every input device the platform offers. id is what
microphone.start takes to select one and is stable across reboots
where the platform provides a stable identifier; name is the label a
person recognises.
An empty list is a legitimate answer, not a failure: a machine with no input hardware offers none, and a browser names none until microphone access has been granted at least once — the labels are part of what the permission protects.
Returns { MicDevice } — Array of { id, name, default }.
for _, d in ipairs(microphone.devices()) do print(d.name, d.default) end
globals/microphone/frequencies
microphone.frequencies() -> { number }
The frequency each spectrum bin is centred on, in Hz, as an array
parallel to microphone.spectrum(). Derived from the capture's rate and
transform size, so it changes only when a capture is started with
different ones. Empty while no capture is running.
Returns { number } — Array of centre frequencies, one per bin.
local hz = microphone.frequencies(); print(hz[#hz]) -- the Nyquist frequency
globals/microphone/level
microphone.level() -> number
Loudness of the most recent analysis window, as an RMS amplitude in 0..1. A full-scale sine reads about 0.707 and silence reads 0.
Measured over only the samples that have arrived, so a capture that has just started reports the loudness of what it holds rather than a level diluted by a window it has not filled yet. 0 while no capture is running.
Returns number — RMS amplitude, 0..1.
if microphone.level() > 0.05 then print("someone is talking") end
globals/microphone/peak
microphone.peak() -> { [string]: number }?
The bin carrying the most energy and what it says: the frequency it is centred on, its amplitude, and the loudness of the whole window. A capture reading silence answers with amplitude 0 at bin 1.
Returns { [string]: number }? — { bin, hz, amplitude, level }, or nil while no capture is running.
local p = microphone.peak(); if p and p.amplitude > 0.05 then print(p.hz) end
globals/microphone/samples
microphone.samples(max: number?) -> buffer?
Captured mono PCM no caller has taken yet, oldest sample first, as a
buffer of little-endian f32 read with buffer.readf32. The samples are
removed, so successive calls walk forward through the capture and a
caller doing its own analysis sees every frame once.
nil while no capture is running, and a zero-length buffer when the
capture is running and nothing new has arrived. Samples nobody takes
are discarded once the queue fills, and status().overruns counts
every one.
Parameters
maxnumber(optional) — How many samples to take at most. Omitted, everything held comes back.
Returns buffer? — Buffer of f32 samples, or nil when no capture is running.
local pcm = microphone.samples(); if pcm then print(buffer.len(pcm) // 4) end
globals/microphone/spectrum
microphone.spectrum() -> { number }
Amplitude per frequency bin over the most recent analysis window:
fftSize / 2 + 1 numbers, DC at index 1 through the Nyquist frequency
at the last. Bin i covers (i - 1) * status().binHz Hz.
Each value is an amplitude estimate rather than a raw transform magnitude, so a full-scale tone sitting on a bin centre reads about 1.0 and the numbers stay comparable across transform sizes.
The window is multiplied by a Hann taper before the transform. An untapered window ends abruptly at both edges and the transform reads that as energy spread across every bin, smearing one tone into a skirt that buries quieter tones beside it. Hann trades a slightly wider main lobe — a tone occupies about three bins rather than one — for sidelobes that fall away steeply, which is what lets neighbouring tones be told apart. Read a peak as "a tone near here", not "a tone exactly here".
Reading this takes no samples away from microphone.samples(). Empty
while no capture is running.
Returns { number } — Array of amplitudes, one per bin.
local bins = microphone.spectrum(); print(#bins, bins[1])
globals/microphone/start
microphone.start(opts: MicOpts?) -> (MicState?, string?)
Open an input device and begin capturing. Returns the state the
capture reached — "running" once a device is delivering, or
"permissionPending" where the platform must ask for access first,
which is the browser's normal path. Poll microphone.status() from
there, or use microphone.awaitRunning().
A request that cannot be made at all returns nil and the reason: an
fftSize that is not a whole power of two between 64 and 16384, a
device no machine here offers, a rate the device does not capture at,
or a capture that is already running.
Omitting device opens the platform default. Omitting sampleRate
takes the device's own rate, which is what avoids a resample.
fftSize is how many samples one analysis window covers and defaults
to 1024 — at 48 kHz that spans ~21 ms and resolves ~47 Hz per bin.
Parameters
optsMicOpts(optional) —{ device, sampleRate, fftSize }.
Returns (MicState?, string?) — The state reached, or nil and the reason the request was refused.
local state, why = microphone.start({ fftSize = 2048 })
globals/microphone/status
microphone.status() -> MicStatus
Where the capture stands.
reason carries the platform's own message: the refusal for denied,
the device's message for failed, what is being waited on for
permissionPending. binHz is the width of one spectrum bin and
bins how many microphone.spectrum() returns.
framesCaptured counts every mono frame the device delivered whether
or not anything drained it, so a silent room reads differently from a
stalled device. overruns counts samples discarded because a consumer
did not keep up — it standing still is what says the readings are
continuous, and it climbing is why a caller sees gaps.
Returns MicStatus — { state, reason, device, sampleRate, fftSize, binHz, bins, framesCaptured, overruns }.
local s = microphone.status(); print(s.state, s.framesCaptured, s.overruns)
globals/microphone/stop
microphone.stop() -> boolean
Stop the capture and release the device. True when a capture was open or being opened at call time. The device is let go before this returns, so a stop followed by a start opens it again rather than finding it held.
Returns boolean — Whether a capture was active.
microphone.stop()
globals/modelImport/decompose
modelImport.decompose(bytes: buffer | string, format: string) -> string
Parse raw model bytes on a background thread. format is the real source
extension ("fbx", "obj", "dae", "gltf", "glb", "stl", "ply",
"3ds", …), forwarded to assimp as the format hint. Returns a promise
handle: task.await it, then read the data with result(handle).
Parameters
bytesbuffer | string— Raw model file bytes (fromvfs.readAsync).formatstring— The source file extension (lowercase, no dot).
Returns string — Promise handle for task.await.
local h = modelImport.decompose(bytes, "obj"); task.await(h)
globals/modelImport/decomposeFiles
modelImport.decomposeFiles(files: { ModelFile }, mainName: string) -> string
Parse a model plus its companion files on a background thread, so assimp
resolves the model's external references (a .gltf's external .bin and
image files, an .obj's .mtl colors/textures, MD5's .md5anim, …).
files is an array of { name = basename, bytes = <bytes> } that MUST
include the model file itself; mainName is that file's basename. Returns a
promise handle: task.await it, then read the data with result(handle) —
the same shape decompose produces.
Parameters
files{ ModelFile }— Array of{ name, bytes }: the model file plus its companions.mainNamestring— Basename of the model file to import (one offiles' names).
Returns string — Promise handle for task.await.
local h = modelImport.decomposeFiles(files, "CesiumMilkTruck.gltf"); task.await(h)
globals/modelImport/extractAnimation
modelImport.extractAnimation(sourcePath: string, clipName: string) -> string
Read a model source file and extract one animation clip to its .zanim
payload, stashed for retrieval. The source extension decides the parser, so
this is format-agnostic. Returns a promise handle: task.await it, then
extractAnimationResult(handle) returns the bytes.
Parameters
sourcePathstring— VFS path to the source model file.clipNamestring— Clip name as returned byresult(handle).animations[i].name.
Returns string — Promise handle for task.await.
globals/modelImport/extractAnimationResult
modelImport.extractAnimationResult(handle: string) -> string?
After awaiting an extractAnimation handle, return the extracted
.zanim bytes (binary-safe), consuming them. The bytes to hand to
asset.create("animation", name, { bytes }). Returns nil on failure or if
already taken.
Parameters
handlestring— Promise handle fromextractAnimation.
Returns string? — The clip's .zanim payload bytes, or nil.
globals/modelImport/result
modelImport.result(handle: string) -> any?
Read the decomposed model after decompose's handle has been awaited.
Every format decomposes into the same shape, so this is format-agnostic.
Runs on the main thread; consumes the stored result.
Parameters
handlestring— Promise handle fromdecompose.
Returns any? — { nodes, meshes, materials, textures, animations, hasSkeleton, skeletonRootNode?, skeleton? }, or nil.
globals/modelImport/retryHandle
modelImport.retryHandle(makeHandle: () -> any, retries: number?, yield: (() -> ())?) -> string?
Call makeHandle — which returns a promise-handle string, or a falsy
value on a transient failure (e.g. a source read that raced a pending
write during a parallel import) — up to retries + 1 times, yielding via
yield between attempts so a pending write can land before the next try.
Returns the handle string once one is produced, or nil when every attempt
failed. Callers task.await the result only when it is non-nil, so a
transient miss never reaches task.await as a non-string.
Parameters
makeHandle() -> any— Returns a promise-handle string, or a falsy value on failure.retriesnumber(optional) — Extra attempts after the first (default 3).yield(() -> ())(optional) — Called between attempts (defaulttask.wait).
Returns string? — The handle string, or nil when every attempt failed.
globals/modelImport/rigFromMeshSkin
modelImport.rigFromMeshSkin(meshBytes: buffer | string) -> string?
Lift the skeleton out of a skinned .mesh (ZMSH) payload and return it
as a .rig JSON document: bones (hierarchy, rest pose, inverse-bind), the
auto-derived humanoid profile, and the humanoid classification. The source
rig a skinned mesh's clips retarget through. Returns nil when the bytes are
not a mesh or carry no skin.
Parameters
meshBytesbuffer | string— Raw ZMSH mesh bytes carrying a skin.
Returns string? — .rig JSON document, or nil.
globals/modelImport/rigFromSkeleton
modelImport.rigFromSkeleton(skeleton: AnimationSkeleton) -> string?
Build a .rig JSON document from the skeleton an animation-only file was
authored on — result(handle).skeleton, the bones its clips drive with
their local rest transforms. Forward kinematics over the locals resolves
globals + inverse-bind; the humanoid profile + classification are derived as
for rigFromMeshSkin. The source rig a standalone clip retargets through.
Returns nil on malformed input.
Parameters
skeletonAnimationSkeleton—{ names, parents, locals }(adecomposeresult'sskeleton).
Returns string? — .rig JSON document, or nil.
local rigJson = modelImport.rigFromSkeleton(data.skeleton)
globals/multiplayer/beginOperation
multiplayer.beginOperation(description: string)
Begin recording an undoable operation. All mutations until
commitOperation() are grouped into one undo entry.
Parameters
descriptionstring— Human-readable label.
multiplayer.beginOperation("move cube")
globals/multiplayer/canRedo
multiplayer.canRedo() -> boolean
Check if this client has any redoable operations.
Returns boolean — True if redo is available.
globals/multiplayer/canUndo
multiplayer.canUndo() -> boolean
Check if this client has any undoable operations.
Returns boolean — True if undo is available.
globals/multiplayer/cancelOperation
multiplayer.cancelOperation()
Cancel the current operation and restore all properties to their values at begin time.
multiplayer.cancelOperation()
globals/multiplayer/claimOwnership
multiplayer.claimOwnership(entityId: (string | entityRef)?) -> boolean
Request ownership of an entity. Returns true if the claim was tentatively granted (relay confirmation pending).
Parameters
entityId(string | entityRef)(optional) — Entity id or proxy to claim.
Returns boolean — True if the claim was tentatively accepted.
globals/multiplayer/clearHistory
multiplayer.clearHistory()
Drop this client's whole undo/redo history — for boundaries where old edits stop being meaningful (a scene load, a test rig reset).
multiplayer.clearHistory()
globals/multiplayer/commitOperation
multiplayer.commitOperation()
Finalize the current operation and push it onto the undo stack. Only changes that actually differ from the start state are recorded.
multiplayer.commitOperation()
globals/multiplayer/connect
multiplayer.connect(relayUrl: string)
Connect to a multiplayer relay server for the current world.
Uses the loaded world's world_id as the room prefix for scene
isolation. A world must be loaded before connecting.
Parameters
relayUrlstring— Relay server URL.
multiplayer.connect("https://relay.example.com")
globals/multiplayer/disconnect
multiplayer.disconnect()
Disconnect from the multiplayer relay server.
multiplayer.disconnect()
globals/multiplayer/explain
multiplayer.explain(entityId: (string | entityRef), componentType: string, property: string) -> DeliveryVerdict
Why a synced property is not reaching the peers this client shares its entity's room with. Answers from the engine's own registry, so a name the component never registered is reported as such instead of inferred from a second client's silence.
Parameters
entityId(string | entityRef)— Entity id or proxy.componentTypestring— Component type name, e.g. "Health".propertystring— Property name as written in the component'ssync {}block.
Returns DeliveryVerdict — arriving true when the property is on its way; otherwise reason names the cause and property carries the registry's record of it when one exists.
local v = multiplayer.explain(player, "Health", "hp")
if not v.arriving then print(v.reason) end
globals/multiplayer/getDiagnostics
multiplayer.getDiagnostics() -> SyncDiagnostics
Get sync diagnostics — traffic counts, bandwidth, link quality,
peer count. The counts — bytesSent/Received,
datagramsSent/Received, rpcsSent/Received, ownershipChanges —
are running totals for the session, so a sparse event stays readable
long after it happened; subtract two samples for the rate over the
interval between them. bytesSentPerSec / bytesReceivedPerSec are
averages over the last completed ~1 second window.
rttMs is the smoothed round-trip time to the relay and
packetLoss the fraction (0..1) of packets lost over the last 5
seconds; both read 0 until the transport has sampled a live
connection. messagesAwaitingEntity counts the sync messages this
peer is holding for an entity it has not received yet — each waits
for the spawn that names it, applies the moment it arrives, and is
released once its wait runs out.
Returns SyncDiagnostics — Diagnostics table.
local d = multiplayer.getDiagnostics()
if d.packetLoss > 0.05 then warnThePlayer(d.rttMs) end
print(d.messagesAwaitingEntity .. " message(s) waiting for their entity")
globals/multiplayer/getPeerId
multiplayer.getPeerId() -> number?
Get this client's peer ID in the current session.
Returns number? — Local peer ID, or nil if not connected.
globals/multiplayer/getPeers
multiplayer.getPeers() -> { PeerInfo }
Get a list of all connected peers in the current session.
Returns { PeerInfo } — Array of peer info tables.
for _, p in ipairs(multiplayer.getPeers()) do print(p.name) end
globals/multiplayer/getRoomPeers
multiplayer.getRoomPeers(roomKey: string) -> { PeerInfo }
Get the peers this client shares the given room with, ordered by
peer id. getPeers answers for the whole session — the union of every
room this client is in — while this answers for one room, so a peer
that leaves this room while staying in another disappears from here
and remains in getPeers.
Parameters
roomKeystring— Fully-qualified room key ({worldGuid}/{profile}/{mode}/{sceneGuid}).
Returns { PeerInfo } — Array of peer info tables for that room.
for _, p in ipairs(multiplayer.getRoomPeers(key)) do print(p.id) end
globals/multiplayer/getRooms
multiplayer.getRooms() -> { string }
The relay rooms this client has joined, sorted. A broadcast reaches only the peers that share one of these.
Returns { string } — Room keys.
for _, key in ipairs(multiplayer.getRooms()) do print(key) end
globals/multiplayer/getTickRate
multiplayer.getTickRate() -> number
Get the current sync tick rate (network updates per second).
Returns number — Sync ticks per second (default 20).
globals/multiplayer/heldMessages
multiplayer.heldMessages() -> { HeldMessage }
The sync messages this peer is holding for entities it has not
received — what getDiagnostics().messagesAwaitingEntity counts, one
entry each, with the entity sync id it names, its age and the grace it
is held against.
Returns { HeldMessage } — Held messages.
for _, m in ipairs(multiplayer.heldMessages()) do print(m.kind, m.ageMs) end
globals/multiplayer/isConnected
multiplayer.isConnected() -> boolean
Check if a multiplayer session is active and connected to a relay.
Returns boolean — True if connected.
globals/multiplayer/isHost
multiplayer.isHost() -> boolean
Whether THIS client is the host (authoritative owner) of the
current scene's play room — the relay room CREATOR, or offline /
single-player. Host code spawns the shared synced world (via
entity.spawnSynced or a scene's onHostLoad) and runs authoritative
simulation; a non-host (JOINER) receives that content from the relay
snapshot and must NOT re-create it. Gate ANY code that spawns synced
entities or owns shared state with this so it runs on exactly one
client — running it on every peer is the double-spawn 'explosion'.
Returns boolean — True on the host / offline / single-player; false on a confirmed joiner. Defaults to true when the role isn't known yet (degrade to host so single-player and pre-join code still run) — pair with a scene's onHostLoad hook when exact one-shot timing matters.
if multiplayer.isHost() then enemy = entity.spawnSynced("enemy") end
globals/multiplayer/isOwner
multiplayer.isOwner(entityId: (string | entityRef)?) -> boolean
Check if the local client owns the given entity (or the current entity if called from a component). Only the owner can modify synced properties directly.
Parameters
entityId(string | entityRef)(optional) — Entity id or proxy to check (defaults toself.entityIdin component context).
Returns boolean — True if the local client is the owner.
globals/multiplayer/isRoomCreator
multiplayer.isRoomCreator(roomKey: string) -> boolean?
Whether this client created the given room — it was the FIRST peer to join it (race-free; the relay assigns it on join). In play mode the creator instantiates the scene's entities (synced) and every other joiner receives them from the relay snapshot, so the scene is never double-instantiated.
Parameters
roomKeystring— Fully-qualified room key ({worldGuid}/{profile}/{mode}/{sceneGuid}).
Returns boolean? — True if this client created the room, false if it joined an existing one, nil if the relay hasn't reported a role yet (offline / not joined).
if multiplayer.isRoomCreator(key) ~= false then layers.load(scene) end
globals/multiplayer/joinRoom
multiplayer.joinRoom(roomKey: string)
Join a relay room. Room keys are built as
{worldGuid}/{profile}/{mode}/{sceneGuid} — four segments, the
{profile} one keeping a runtime peer (published content) and an
editor peer (live content) in separate rooms even when both are in
play mode. Rooms partition the relay's fan-out: only peers in the
same room receive each other's broadcasts. getRooms() reports the
keys this client is already in and roomFor(entity) the one an
entity broadcasts into, so a key can be read rather than rebuilt.
No-op when not connected or already joined.
Parameters
roomKeystring— Fully-qualified room key.
multiplayer.joinRoom(worldGuid .. "/" .. engine.profile .. "/play/" .. sceneGuid)
globals/multiplayer/leaveRoom
multiplayer.leaveRoom(roomKey: string)
Leave a relay room. The key is reported under
observe().withdrawnRooms until joinRoom names it again. No-op
when not connected or not joined.
Parameters
roomKeystring— Fully-qualified room key.
globals/multiplayer/loopback
multiplayer.loopback() -> { [string]: any }
Loopback testing harness. Returns a table with enable(),
disable(), flush(), receive() methods for testing sync
without a relay server.
Returns { [string]: any } — Loopback API table.
globals/multiplayer/observe
multiplayer.observe() -> ReplicationObservation
Report what this peer is replicating and why a property is not
arriving. Carries the rooms this client joined, one record per entity
with a sync id — its owner, the room it broadcasts into, how many
other peers share that room, and every REGISTERED synced component
with its declared property names, wire indices, public/private table
and dirty bits — the messages held for entities that have not arrived,
and the registry's totals. Every property carries notArriving: one
name from reasons, or nil when it is on its way. Answers in edit
mode as well as play mode, for what the relay carries in each: in
edit mode scene content is left out of the sync-id pass, so its
changes travel to the other clients with the source they are
written into and it reads entityNotSynced here.
Returns ReplicationObservation — The engine's current replication observation.
local o = multiplayer.observe()
print(#o.entities .. " entities, " .. o.totals.properties .. " synced properties")
globals/multiplayer/observeComponent
multiplayer.observeComponent(entityId: (string | entityRef), componentType: string) -> SyncedComponent?
The registered synced component of the named type on an entity's
record. Matches a fully-qualified type (@builtin::components.Model)
and the leaf name it ends in (Model) alike.
Parameters
entityId(string | entityRef)— Entity id or proxy.componentTypestring— Component type name or its leaf.
Returns SyncedComponent? — The registered component instance, or nil when none of that type is registered on the entity.
local c = multiplayer.observeComponent(player, "Health")
print(#c.properties .. " declared properties")
globals/multiplayer/observeEntity
multiplayer.observeEntity(entityId: (string | entityRef)) -> EntityReplication?
The replication record for one entity — its sync id, owner, room, and the synced components registered on it.
Parameters
entityId(string | entityRef)— Entity id or proxy.
Returns EntityReplication? — The entity's record, or nil when the engine holds no sync record for it.
local r = multiplayer.observeEntity(e)
for _, c in ipairs(r.components) do print(c.componentType) end
globals/multiplayer/on
multiplayer.on(channel: string, callback: (number, ...any) -> ())
Subscribe to a custom message channel. The callback runs as
callback(fromPeerId, ...args) whenever another peer calls
multiplayer.send(channel, ...). Multiple callbacks per channel fire
in registration order.
Parameters
channelstring— Channel name to listen on.callback(number, ...any) -> ()—function(fromPeerId: number, ...)— the sender's peer id then the sent args.
globals/multiplayer/recordSpawn
multiplayer.recordSpawn(entityId: string)
Adopt an existing entity into the open operation as its spawn — for flows that create an entity before the operation opens (a drag preview adopted on drop). Undoing the operation despawns it.
Parameters
entityIdstring— Entity id to record as spawned by this operation.
multiplayer.recordSpawn(id)
globals/multiplayer/redo
multiplayer.redo() -> boolean
Redo this client's last undone operation.
Returns boolean — True if an operation was redone.
globals/multiplayer/releaseOwnership
multiplayer.releaseOwnership(entityId: (string | entityRef)?) -> boolean
Release ownership of an entity.
Parameters
entityId(string | entityRef)(optional) — Entity id or proxy to release.
Returns boolean — True if ownership was released.
globals/multiplayer/roomFor
multiplayer.roomFor(entityId: (string | entityRef)) -> string?
The room key an entity's spawns and property deltas broadcast into.
Parameters
entityId(string | entityRef)— Entity id or proxy.
Returns string? — The room key, or nil when the engine has established no scene context for the entity.
local key = multiplayer.roomFor(player)
if key then multiplayer.joinRoom(key) end
globals/multiplayer/send
multiplayer.send(channel: string, ...: any?)
Broadcast a message on a named channel to every OTHER peer in the
room. The relay forwards it transparently; peers receive it via
multiplayer.on. Arguments may be any synced value (nil, boolean,
number, string, Vec3, entity/component proxy, or table) and are
delivered to listeners in order. No-op when not connected.
Parameters
channelstring— Channel name listeners subscribe to viamultiplayer.on....any(optional) — Zero or more values delivered to each listener after the sender's peer id.
globals/multiplayer/syncTotals
multiplayer.syncTotals() -> SyncTotals
What the sync registry holds across every entity: entities with a registered synced component, component instances, declared properties, declared functions, and the component instances holding a dirty property this tick.
Returns SyncTotals — The registry totals.
print(multiplayer.syncTotals().properties .. " synced properties registered")
globals/multiplayer/undo
multiplayer.undo() -> boolean
Undo this client's last edit-mode operation.
Returns boolean — True if an operation was undone.
globals/next
next(table, key?) -> key, value
Raw table iteration.
globals/notices/post
notices.post(template: string, params: { [string]: any }?, opts: NoticeOpts?)
Post a notice. template is a fixed sentence used to collapse
repeats; put varying values in params. opts.severity defaults to
"info"; opts.includeLocation attaches the emitting call site.
Parameters
templatestring— Fixed sentence identifying the notice.params{ [string]: any }(optional) — Optional table of named values rendered alongside the template.optsNoticeOpts(optional) — Optional table: severity ("info" | "warn" | "error"), includeLocation (boolean).
notices.post("wave complete", { wave = 3 })
notices.post("save slot corrupted, using defaults", { slot = id }, { severity = "warn" })
globals/nx/add
nx.add(dst: NxBuffer, x: NxScalarOrBuffer) -> boolean
b[i] += x (x scalar) or b[i] += x[i] (x buffer).
Dispatches on type(x). For interleaved-stride writes use
nx.addStrided.
Parameters
dstNxBuffer— Target buffer (mutated).xNxScalarOrBuffer— Either a scalar or a same-shaped buffer.
Returns boolean — true on success, false on type / handle errors.
nx.add(b, 1.5)
nx.add(dst, src)
globals/nx/addStrided
nx.addStrided(b: NxBuffer, scalar: number, stride: number, offset: number?) -> boolean
Strided add: buf[k * stride + offset] += scalar for every
valid k.
Parameters
bNxBuffer— Target buffer (mutated).scalarnumber— Per-element addend.stridenumber— Element stride.offsetnumber(optional) — Optional element offset (default 0).
Returns boolean — true on success, false on unknown handle.
nx.addStrided(buf, 1.0, 3, 1)
globals/nx/addStridedFrom
nx.addStridedFrom(dst: NxBuffer, src: NxBuffer, scale: number?, dst_stride: number, dst_off: number?, src_stride: number, src_off: number?) -> boolean
Strided BLAS-axpy from src into dst:
dst[k * dst_stride + dst_off] += scale * src[k * src_stride + src_off].
Parameters
dstNxBuffer— Target buffer (mutated).srcNxBuffer— Source buffer.scalenumber(optional) — Optionalsrcscale factor (default 1.0).dst_stridenumber— Destination element stride.dst_offnumber(optional) — Optional destination offset (default 0).src_stridenumber— Source element stride.src_offnumber(optional) — Optional source offset (default 0).
Returns boolean — true on success, false on shape mismatch / unknown handle.
nx.addStridedFrom(dst, src, 1, 3, 0, 3, 0)
globals/nx/applyAbs
nx.applyAbs(b: NxBuffer) -> boolean
In-place b[i] = abs(b[i]).
Parameters
bNxBuffer— Target buffer.
Returns boolean — true on success, false on unknown handle.
nx.applyAbs(b)
globals/nx/applyCeil
nx.applyCeil(b: NxBuffer) -> boolean
In-place b[i] = ceil(b[i]).
Parameters
bNxBuffer— Target buffer.
Returns boolean — true on success, false on unknown handle.
nx.applyCeil(b)
globals/nx/applyCos
nx.applyCos(b: NxBuffer) -> boolean
In-place b[i] = cos(b[i]).
Parameters
bNxBuffer— Target buffer.
Returns boolean — true on success, false on unknown handle.
nx.applyCos(b)
globals/nx/applyExp
nx.applyExp(b: NxBuffer) -> boolean
In-place b[i] = exp(b[i]).
Parameters
bNxBuffer— Target buffer.
Returns boolean — true on success, false on unknown handle.
nx.applyExp(b)
globals/nx/applyFloor
nx.applyFloor(b: NxBuffer) -> boolean
In-place b[i] = floor(b[i]).
Parameters
bNxBuffer— Target buffer.
Returns boolean — true on success, false on unknown handle.
nx.applyFloor(b)
globals/nx/applyFract
nx.applyFract(b: NxBuffer) -> boolean
In-place b[i] = fract(b[i]) (fractional part).
Parameters
bNxBuffer— Target buffer.
Returns boolean — true on success, false on unknown handle.
nx.applyFract(b)
globals/nx/applyLog
nx.applyLog(b: NxBuffer) -> boolean
In-place b[i] = ln(b[i]).
Parameters
bNxBuffer— Target buffer.
Returns boolean — true on success, false on unknown handle.
nx.applyLog(b)
globals/nx/applyLog2
nx.applyLog2(b: NxBuffer) -> boolean
In-place b[i] = log2(b[i]).
Parameters
bNxBuffer— Target buffer.
Returns boolean — true on success, false on unknown handle.
nx.applyLog2(b)
globals/nx/applyNeg
nx.applyNeg(b: NxBuffer) -> boolean
In-place b[i] = -b[i].
Parameters
bNxBuffer— Target buffer.
Returns boolean — true on success, false on unknown handle.
nx.applyNeg(b)
globals/nx/applyRecip
nx.applyRecip(b: NxBuffer) -> boolean
In-place b[i] = 1 / b[i].
Parameters
bNxBuffer— Target buffer.
Returns boolean — true on success, false on unknown handle.
nx.applyRecip(b)
globals/nx/applyRecipSqrt
nx.applyRecipSqrt(b: NxBuffer) -> boolean
In-place b[i] = 1 / sqrt(b[i]).
Parameters
bNxBuffer— Target buffer.
Returns boolean — true on success, false on unknown handle.
nx.applyRecipSqrt(b)
globals/nx/applyRound
nx.applyRound(b: NxBuffer) -> boolean
In-place b[i] = round(b[i]).
Parameters
bNxBuffer— Target buffer.
Returns boolean — true on success, false on unknown handle.
nx.applyRound(b)
globals/nx/applySign
nx.applySign(b: NxBuffer) -> boolean
In-place b[i] = sign(b[i]) (returns -1, 0, or +1).
Parameters
bNxBuffer— Target buffer.
Returns boolean — true on success, false on unknown handle.
nx.applySign(b)
globals/nx/applySin
nx.applySin(b: NxBuffer) -> boolean
In-place b[i] = sin(b[i]).
Parameters
bNxBuffer— Target buffer.
Returns boolean — true on success, false on unknown handle.
nx.applySin(b)
globals/nx/applySqrt
nx.applySqrt(b: NxBuffer) -> boolean
In-place b[i] = sqrt(b[i]).
Parameters
bNxBuffer— Target buffer.
Returns boolean — true on success, false on unknown handle.
nx.applySqrt(b)
globals/nx/applySquare
nx.applySquare(b: NxBuffer) -> boolean
In-place b[i] = b[i] * b[i].
Parameters
bNxBuffer— Target buffer.
Returns boolean — true on success, false on unknown handle.
nx.applySquare(b)
globals/nx/applyTan
nx.applyTan(b: NxBuffer) -> boolean
In-place b[i] = tan(b[i]).
Parameters
bNxBuffer— Target buffer.
Returns boolean — true on success, false on unknown handle.
nx.applyTan(b)
globals/nx/applyTrunc
nx.applyTrunc(b: NxBuffer) -> boolean
In-place b[i] = trunc(b[i]).
Parameters
bNxBuffer— Target buffer.
Returns boolean — true on success, false on unknown handle.
nx.applyTrunc(b)
globals/nx/applyWindow
nx.applyWindow(signal: NxBuffer, window: NxBuffer) -> boolean
Element-wise signal[i] *= window[i] in place. Operates over
the shorter of the two — passing a longer window to window a
shorter clip is intentional, not an error.
Parameters
signalNxBuffer— Signal buffer (mutated).windowNxBuffer— Window buffer.
Returns boolean — true on success, false on unknown handle.
nx.applyWindow(signal, hann)
globals/nx/axpby
nx.axpby(dst: NxBuffer, a: number, src: NxBuffer, b: number) -> boolean
BLAS axpby: dst[i] = a*dst[i] + b*src[i].
Parameters
dstNxBuffer— Target buffer (mutated).anumber— Scale applied todst.srcNxBuffer— Source buffer.bnumber— Scale applied tosrc.
Returns boolean — true on success, false on stride mismatch / unknown handle.
nx.axpby(y, 0.5, x, 2.0)
globals/nx/clamp
nx.clamp(b: NxBuffer, min_v: number, max_v: number) -> boolean
In-place clamp: b[i] = clamp(b[i], min_v, max_v).
Parameters
bNxBuffer— Target buffer (mutated).min_vnumber— Lower bound.max_vnumber— Upper bound.
Returns boolean — true on success, false on unknown handle.
nx.clamp(b, 0.0, 1.0)
globals/nx/copy
nx.copy(dst: NxBuffer, src: NxBuffer) -> boolean
Copy every record from src into dst (memcpy fast path).
Parameters
dstNxBuffer— Target buffer (mutated).srcNxBuffer— Source buffer.
Returns boolean — true on success, false on shape mismatch or unknown handle.
nx.copy(dst, src)
globals/nx/copyStridedFrom
nx.copyStridedFrom(dst: NxBuffer, src: NxBuffer, scale: number?, dst_stride: number, dst_off: number?, src_stride: number, src_off: number?) -> boolean
Strided copy from src into dst with optional scaling:
dst[k * dst_stride + dst_off] = scale * src[k * src_stride + src_off].
Parameters
dstNxBuffer— Target buffer (mutated).srcNxBuffer— Source buffer.scalenumber(optional) — Optionalsrcscale factor (default 1.0).dst_stridenumber— Destination element stride.dst_offnumber(optional) — Optional destination offset (default 0).src_stridenumber— Source element stride.src_offnumber(optional) — Optional source offset (default 0).
Returns boolean — true on success, false on shape mismatch / unknown handle.
nx.copyStridedFrom(dst, src, 1, 3, 0, 3, 0)
globals/nx/create
nx.create(type_: NxType, n: number) -> NxBuffer?
Allocate a CPU buffer of type × len records. Thin alias for
substrate.createBuffer({type=type_, len=n, kind="cpu"}) — kept here so the
public nx library is the canonical entry point and users never
need to import buffer separately.
Parameters
type_NxType— Element layout ("f32","vec3","vec4","quat","mat4").nnumber— Record count.
Returns NxBuffer? — Freshly allocated buffer handle, or nil on failure.
local b = nx.create("vec3", 1024)
globals/nx/div
nx.div(dst: NxBuffer, x: NxScalarOrBuffer) -> boolean
b[i] /= x (x scalar) or b[i] /= x[i] (x buffer).
Parameters
dstNxBuffer— Target buffer (mutated).xNxScalarOrBuffer— Either a scalar or a same-shaped buffer.
Returns boolean — true on success, false on type / handle errors.
nx.div(b, 2)
nx.div(dst, src)
globals/nx/dot
nx.dot(a: NxBuffer, b: NxBuffer) -> number?
Reduction: dot product of two same-shaped buffers.
Parameters
aNxBuffer— First buffer.bNxBuffer— Second buffer.
Returns number? — Scalar dot product, or nil on shape mismatch / unknown handle.
local d = nx.dot(a, b)
globals/nx/fft1d
nx.fft1d(re: NxBuffer, im: NxBuffer, inverse: boolean?) -> boolean
In-place 1D FFT over parallel re / im CPU buffers.
inverse=true runs the inverse transform scaled by 1/N (so
ifft(fft(x)) ≈ x).
Parameters
reNxBuffer— Real-component buffer (mutated).imNxBuffer— Imaginary-component buffer (mutated).inverseboolean(optional) — Whentrueruns the inverse transform.
Returns boolean — true on success, false on length mismatch / invalid handle.
nx.fft1d(re, im)
nx.fft1d(re, im, true)
globals/nx/fft2d
nx.fft2d(re: NxBuffer, im: NxBuffer, width: number, height: number, inverse: boolean?) -> boolean
In-place 2D FFT over row-major parallel re / im buffers
of length width*height. inverse=true is scaled by
1 / (width * height).
Parameters
reNxBuffer— Real-component buffer (mutated).imNxBuffer— Imaginary-component buffer (mutated).widthnumber— 2D width in samples.heightnumber— 2D height in samples.inverseboolean(optional) — Whentrueruns the inverse transform.
Returns boolean — true on success, false on length mismatch / invalid handle.
nx.fft2d(re, im, w, h)
globals/nx/fill
nx.fill(b: NxBuffer, value: number?) -> boolean
Fill the buffer with value (default 0.0). Equivalent to the
scalar form of nx.add against a zeroed buffer, but skips the
type-dispatch.
Parameters
bNxBuffer— Target buffer (mutated).valuenumber(optional) — Fill value (default 0.0).
Returns boolean — true on success, false on unknown handle.
nx.fill(b, 3.5)
globals/nx/fillRandomNormal
nx.fillRandomNormal(b: NxBuffer, mean: number?, stddev: number?, seed: NxSeed) -> boolean
Fill the buffer with Gaussian samples (Box-Muller), with the given mean and standard deviation, using a splitmix-keyed PRNG.
Parameters
bNxBuffer— Target buffer (mutated).meannumber(optional) — Optional mean (default 0.0).stddevnumber(optional) — Optional standard deviation (default 1.0).seedNxSeed— Optional seed — number,"frame", ornil(0).
Returns boolean — true on success, false on unknown handle.
nx.fillRandomNormal(b, 0, 1, 42)
globals/nx/fillRandomUniform
nx.fillRandomUniform(b: NxBuffer, min_v: number?, max_v: number?, seed: NxSeed) -> boolean
Fill the buffer with uniform-random samples in [min, max),
using a splitmix-keyed deterministic PRNG.
Parameters
bNxBuffer— Target buffer (mutated).min_vnumber(optional) — Optional lower bound (default 0.0).max_vnumber(optional) — Optional upper bound (default 1.0).seedNxSeed— Optional seed — number,"frame"(per-frame value), ornil(0).
Returns boolean — true on success, false on unknown handle.
nx.fillRandomUniform(b, -1, 1, "frame")
globals/nx/fillStrided
nx.fillStrided(b: NxBuffer, value: number, stride: number, offset: number?) -> boolean
Strided fill: buf[k * stride + offset] = value for every
valid k.
Parameters
bNxBuffer— Target buffer (mutated).valuenumber— Per-element value.stridenumber— Element stride.offsetnumber(optional) — Optional element offset (default 0).
Returns boolean — true on success, false on unknown handle.
nx.fillStrided(buf, 0, 3, 2)
globals/nx/fromTable
nx.fromTable(arr: { number }, type_: NxType?) -> NxBuffer?
Build a CPU buffer from a Lua table of numbers. The table is
written through buf:write in a single FFI crossing — no
per-element Lua loop. For large arrays, prefer one of the
nx.* constructors + a kernel pass over building a Lua array
first.
Parameters
arr{ number }— Lua array of numbers.type_NxType(optional) — Optional element layout (default"f32").
Returns NxBuffer? — Newly allocated buffer holding arr, or nil on failure.
local b = nx.fromTable({ 0.1, 0.2, 0.3 })
globals/nx/full
nx.full(n: number, type_: NxType?, value: number?) -> NxBuffer?
Allocate a buffer of type × len records and fill with value.
Parameters
nnumber— Record count.type_NxType(optional) — Optional element layout (default"f32").valuenumber(optional) — Fill value (default 0.0).
Returns NxBuffer? — Buffer initialised to value.
local b = nx.full(1024, "f32", -1.0)
globals/nx/ifft1d
nx.ifft1d(re: NxBuffer, im: NxBuffer) -> boolean
Convenience: nx.fft1d(re, im, true).
Parameters
reNxBuffer— Real-component buffer (mutated).imNxBuffer— Imaginary-component buffer (mutated).
Returns boolean — true on success, false on length mismatch / invalid handle.
nx.ifft1d(re, im)
globals/nx/ifft2d
nx.ifft2d(re: NxBuffer, im: NxBuffer, width: number, height: number) -> boolean
Convenience: nx.fft2d(re, im, w, h, true).
Parameters
reNxBuffer— Real-component buffer (mutated).imNxBuffer— Imaginary-component buffer (mutated).widthnumber— 2D width.heightnumber— 2D height.
Returns boolean — true on success, false on length mismatch / invalid handle.
nx.ifft2d(re, im, w, h)
globals/nx/integratePosition
nx.integratePosition(pos: NxBuffer, vel: NxBuffer, dt: number) -> boolean
Per-vec3: pos[i] += vel[i] * dt — the position half of an
Euler step. Both buffers must be vec3 (stride 3). The velocity step
is the caller's: update vel BEFORE this call for semi-implicit
Euler; updating it after gives forward Euler, which gains energy on
stiff systems.
Parameters
posNxBuffer— Position buffer (mutated).velNxBuffer— Velocity buffer.dtnumber— Time step.
Returns boolean — true on success, false on shape mismatch / unknown handle.
nx.integratePosition(pos, vel, dt)
globals/nx/irfft1d
nx.irfft1d(re: NxBuffer, im: NxBuffer) -> NxBuffer?
Real-output inverse 1D FFT. Input re / im are length
N/2 + 1. Returns a fresh CPU f32 buffer of length
2 * (N/2 + 1 - 1) = N real samples.
Parameters
reNxBuffer— Real-component buffer.imNxBuffer— Imaginary-component buffer.
Returns NxBuffer? — Real-valued output buffer on success, nil on failure.
local out = nx.irfft1d(re, im)
globals/nx/irfft2d
nx.irfft2d(re: NxBuffer, im: NxBuffer, width: number, height: number) -> NxBuffer?
Real-output inverse 2D FFT. Input re / im are
(width/2 + 1) * height row-major. Returns a fresh f32 buffer
of length width * height.
Parameters
reNxBuffer— Real-component buffer.imNxBuffer— Imaginary-component buffer.widthnumber— 2D width.heightnumber— 2D height.
Returns NxBuffer? — Real-valued output buffer on success, nil on failure.
local out = nx.irfft2d(re, im, w, h)
globals/nx/lerpTo
nx.lerpTo(dst: NxBuffer, src: NxBuffer, t: number) -> boolean
dst[i] += t * (src[i] - dst[i]) — element-wise lerp toward
src by t.
Parameters
dstNxBuffer— Target buffer (mutated).srcNxBuffer— Source buffer.tnumber— Interpolation factor.
Returns boolean — true on success, false on stride mismatch / unknown handle.
nx.lerpTo(current, target, 0.1)
globals/nx/max
nx.max(b: NxBuffer) -> number?
Reduction: maximum of all elements.
Parameters
bNxBuffer— Source buffer.
Returns number? — Scalar max, or nil on unknown handle.
local m = nx.max(b)
globals/nx/maxOp
nx.maxOp(dst: NxBuffer, x: NxScalarOrBuffer) -> boolean
Element-wise b[i] = max(b[i], x) (scalar) or
b[i] = max(b[i], x[i]) (buffer).
Parameters
dstNxBuffer— Target buffer (mutated).xNxScalarOrBuffer— Scalar or same-shaped buffer.
Returns boolean — true on success, false on type / handle errors.
nx.maxOp(b, 0.0)
globals/nx/mean
nx.mean(b: NxBuffer) -> number?
Reduction: arithmetic mean of all elements.
Parameters
bNxBuffer— Source buffer.
Returns number? — Scalar mean, or nil on unknown handle.
local m = nx.mean(b)
globals/nx/min
nx.min(b: NxBuffer) -> number?
Reduction: minimum of all elements.
Parameters
bNxBuffer— Source buffer.
Returns number? — Scalar min, or nil on unknown handle.
local m = nx.min(b)
globals/nx/minOp
nx.minOp(dst: NxBuffer, x: NxScalarOrBuffer) -> boolean
Element-wise b[i] = min(b[i], x) (scalar) or
b[i] = min(b[i], x[i]) (buffer).
Parameters
dstNxBuffer— Target buffer (mutated).xNxScalarOrBuffer— Scalar or same-shaped buffer.
Returns boolean — true on success, false on type / handle errors.
nx.minOp(b, 1.0)
globals/nx/mul
nx.mul(dst: NxBuffer, x: NxScalarOrBuffer) -> boolean
b[i] *= x (x scalar) or b[i] *= x[i] (x buffer).
Parameters
dstNxBuffer— Target buffer (mutated).xNxScalarOrBuffer— Either a scalar or a same-shaped buffer.
Returns boolean — true on success, false on type / handle errors.
nx.mul(b, 2)
nx.mul(dst, src)
globals/nx/mulStrided
nx.mulStrided(b: NxBuffer, scalar: number, stride: number, offset: number?) -> boolean
Strided multiply: buf[k * stride + offset] *= scalar for
every valid k.
Parameters
bNxBuffer— Target buffer (mutated).scalarnumber— Per-element multiplier.stridenumber— Element stride.offsetnumber(optional) — Optional element offset (default 0).
Returns boolean — true on success, false on unknown handle.
nx.mulStrided(buf, 2, 4, 0)
globals/nx/normL1
nx.normL1(b: NxBuffer) -> number?
Reduction: L1 norm — sum(|b[i]|).
Parameters
bNxBuffer— Source buffer.
Returns number? — Scalar L1 norm, or nil on unknown handle.
local n = nx.normL1(b)
globals/nx/normL2
nx.normL2(b: NxBuffer) -> number?
Reduction: L2 norm — sqrt(sum(b[i]^2)).
Parameters
bNxBuffer— Source buffer.
Returns number? — Scalar L2 norm, or nil on unknown handle.
local n = nx.normL2(b)
globals/nx/normalizeVec3
nx.normalizeVec3(b: NxBuffer) -> boolean
Normalise each vec3 in-place. Vectors below 1e-8 are left untouched.
Parameters
bNxBuffer— Vec3 buffer (mutated).
Returns boolean — true on success, false on stride mismatch / unknown handle.
nx.normalizeVec3(directions)
globals/nx/ones
nx.ones(n: number, type_: NxType?) -> NxBuffer?
Allocate a buffer of type × len records and fill with 1.0.
Parameters
nnumber— Record count.type_NxType(optional) — Optional element layout (default"f32").
Returns NxBuffer? — Buffer initialised to one.
local b = nx.ones(1024)
globals/nx/pow
nx.pow(b: NxBuffer, p: number) -> boolean
In-place b[i] = b[i] ^ p (scalar exponent).
Parameters
bNxBuffer— Target buffer (mutated).pnumber— Scalar exponent.
Returns boolean — true on success, false on unknown handle.
nx.pow(b, 2.2)
globals/nx/quatFromYaw
nx.quatFromYaw(dst: NxBuffer, yaw: NxBuffer) -> boolean
Per-quat: dst[i] = (0, sin(yaw[i]/2), 0, cos(yaw[i]/2)) —
the pure-Y axis-angle quaternion for each yaw value. dst must
be stride-4 (quat); yaw must be stride-1 (f32).
Parameters
dstNxBuffer— Quaternion buffer (mutated).yawNxBuffer— Source yaw scalars buffer.
Returns boolean — true on success, false on shape mismatch / unknown handle.
nx.quatFromYaw(quats, yaws)
globals/nx/rfft1d
nx.rfft1d(signal: NxBuffer?) -> (NxBuffer?, NxBuffer?)
Real-input forward 1D FFT. Allocates two new CPU f32 buffers
of length N/2 + 1 holding the (re, im) parts of the
Hermitian-symmetric spectrum (same convention as NumPy
np.fft.rfft).
Parameters
signalNxBuffer(optional) — Real-valued input buffer.
Returns (NxBuffer?, NxBuffer?) — (re_buf, im_buf) on success, nil otherwise.
local re, im = nx.rfft1d(signal)
globals/nx/rfft2d
nx.rfft2d(signal: NxBuffer, width: number, height: number) -> (NxBuffer?, NxBuffer?)
Real-input forward 2D FFT. Input signal is row-major
width*height. Returns (re_buf, im_buf) of length
(width/2 + 1) * height each (matches np.fft.rfft2 layout).
Parameters
signalNxBuffer— Real-valued input buffer (row-major).widthnumber— 2D width.heightnumber— 2D height.
Returns (NxBuffer?, NxBuffer?) — (re_buf, im_buf) on success, nil on failure.
local re, im = nx.rfft2d(image, w, h)
globals/nx/scale
nx.scale(b: NxBuffer, s: number) -> boolean
In-place b[i] *= s.
Parameters
bNxBuffer— Target buffer (mutated).snumber— Scalar multiplier.
Returns boolean — true on success, false on unknown handle.
nx.scale(b, 0.5)
globals/nx/sinCosTo
nx.sinCosTo(src: NxBuffer, sin_dst: NxBuffer, cos_dst: NxBuffer) -> boolean
Compute sin_dst[i] = sin(src[i]) and cos_dst[i] = cos(src[i])
in one pass using cheaper paired-trig argument reduction.
Parameters
srcNxBuffer— Source angles buffer.sin_dstNxBuffer— Destination buffer for the sine values.cos_dstNxBuffer— Destination buffer for the cosine values.
Returns boolean — true on success, false on stride mismatch / unknown handle.
nx.sinCosTo(angles, s, c)
globals/nx/sub
nx.sub(dst: NxBuffer, x: NxScalarOrBuffer) -> boolean
b[i] -= x (x scalar) or b[i] -= x[i] (x buffer).
Parameters
dstNxBuffer— Target buffer (mutated).xNxScalarOrBuffer— Either a scalar or a same-shaped buffer.
Returns boolean — true on success, false on type / handle errors.
nx.sub(b, 0.5)
nx.sub(dst, src)
globals/nx/sum
nx.sum(b: NxBuffer) -> number?
Reduction: sum of all elements.
Parameters
bNxBuffer— Source buffer.
Returns number? — Scalar sum, or nil on unknown handle.
local s = nx.sum(b)
globals/nx/wanderYaw
nx.wanderYaw(args: NxWanderArgs) -> boolean
Fused per-entity wander step. Each entity's yaw[i] walks by
a uniform random delta in [-yawDelta, +yawDelta], then pos[i]
advances forward in the (sin yaw, cos yaw) direction by step.
Optional rot quat output writes a pure-Y axis-angle rotation.
Replaces the per-entity Luau loop pattern (~12 ms / 5000 in
interpreter) with a single Rust pass (~0.2 ms / 5000).
Buffer requirements: pos vec3 (stride 3), yaw f32 (stride 1),
rot optional quat (stride 4). All counts should match (kernel
walks min(count_i)).
Parameters
argsNxWanderArgs— Table with{ pos, yaw, rot?, step?, yawDelta?, seed }.
Returns boolean — true on success, false on stride / shape failures.
nx.wanderYaw({ pos = pos, yaw = yaw, step = 0.5, yawDelta = 0.2, seed = "frame" })
globals/nx/window/blackman
nx.window.blackman(n: number) -> NxBuffer?
Allocate a fresh CPU f32 buffer of length n filled with a
symmetric Blackman window (NumPy np.blackman convention).
Parameters
nnumber— Number of samples in the window.
Returns NxBuffer? — Buffer holding the window samples, or nil on failure.
local w = nx.window.blackman(1024)
globals/nx/window/hamming
nx.window.hamming(n: number) -> NxBuffer?
Allocate a fresh CPU f32 buffer of length n filled with a
symmetric Hamming window (NumPy np.hamming convention).
Parameters
nnumber— Number of samples in the window.
Returns NxBuffer? — Buffer holding the window samples, or nil on failure.
local w = nx.window.hamming(1024)
globals/nx/window/hann
nx.window.hann(n: number) -> NxBuffer?
Allocate a fresh CPU f32 buffer of length n filled with a
symmetric Hann window (NumPy np.hanning convention).
Parameters
nnumber— Number of samples in the window.
Returns NxBuffer? — Buffer holding the window samples, or nil on failure.
local w = nx.window.hann(1024)
globals/nx/zeros
nx.zeros(n: number, type_: NxType?) -> NxBuffer?
Allocate a buffer of type × len records and fill with 0.0.
Parameters
nnumber— Record count.type_NxType(optional) — Optional element layout (default"f32").
Returns NxBuffer? — Buffer initialised to zero.
local b = nx.zeros(2048)
globals/packages/list
packages.list() -> { PackageEntry }
List every registered package across scopes.
Returns { PackageEntry } — Array of package entries.
for _, p in ipairs(packages.list()) do print(p.name, p.scope) end
globals/packages/lookup
packages.lookup(name_or_scope: string, name: string?) -> PackageEntry?
Look up a single package by name (any scope) or by exact
(scope, name).
Parameters
name_or_scopestring— Package name, or scope if a second arg is given.namestring(optional) — Package name when the first arg is a scope.
Returns PackageEntry? — Package entry or nil.
local p = packages.lookup("@builtin", "audio")
globals/pairs
pairs(table) -> iterator
Iterate all key-value pairs.
globals/particles/create
particles.create(spec: table?) -> any
Create a GPU particle system from a spec, allocating its buffers and
registering it with the auto-update driver. The handle it returns carries
:emit, :update, the setters, :observe, and :getCreator.
Parameters
spectable(optional) —{ maxCount, rate, lifetime, speed, shape, ... }— every field optional, each falling back to the emitter's default.ownerandnamesay whose the emitter is:owneris the keylist(owner)matches, so a creator reaches exactly its own emitters after it has lost their handles, andnamesays which of them this one is. An emitter that states neither is still attributed to the module and line it was created from. optional, each falling back to the emitter's default. A field that names one of a closed set takes a name from it and raises with the whole set otherwise; a key the spec does not define is reported on its own, naming the key that writes what it was written for.man particles.createlists every key the spec defines.
Returns any — The particle system handle.
local fire = particles.create({ maxCount = 2000, rate = 100 })
local star = particles.create({ owner = "starfield", name = "shell" })
globals/particles/list
particles.list(filter: (string | ParticleCreatorFilter)?) -> { any }
Every particle system this VM has created and not destroyed, in creation order — or, given a filter, the ones whose creator matches it. Answered from the emitter registry, so finding an emitter costs nothing per entity in the scene.
Called with nothing it answers with every emitter in the VM, which is what
makes it the way to reach one whose creator has lost its handle, and
:getCreator() on an entry says whose that one is. A filter narrows it to
one creator's own, so a module clears what a previous load of it left
behind and leaves every other emitter in the world standing.
Parameters
filter(string | ParticleCreatorFilter)(optional) — Optional. A string matches theownerkey a creator stated; a table matches every one ofowner,nameandsourcethat it names.
Returns { any } — table Array of particle system handles.
for _, sys in ipairs(particles.list()) do print(sys:getActiveCount()) end
for _, sys in ipairs(particles.list("starfield")) do sys:destroy() end
local mine = particles.list({ source = debug.info(1, "s") })
globals/particles/observe
particles.observe(system: any?) -> { [string]: any }
What the engine is simulating and drawing for particles right now.
With no argument, every live emitter plus the totals they sum to; with an
emitter, that one's reading. An engine holding no emitters answers
count = 0 with an empty list, which reads differently from an engine
whose emitters are all silent (count > 0, silent = count).
Parameters
systemany(optional) — Optional particle system handle to read on its own.
Returns { [string]: any } — table The observation.
local o = particles.observe(); print(o.count, o.alive, o.silent)
local r = particles.observe(fire); print(r.alive, r.bytes.total)
globals/particles/silenceReasons
particles.silenceReasons() -> { { reason: string, means: string } }
The closed set of reasons an emitter can be producing nothing, in the
order a reading resolves them — nearest cause first — each with what it
means. Every observe().reason is one of these.
Returns { { reason: string, means: string } } — table Array of { reason, means }.
for _, r in ipairs(particles.silenceReasons()) do print(r.reason, r.means) end
globals/particles/whySilent
particles.whySilent(system: any?) -> (string?, string?)
Why one emitter is producing nothing, from the closed set
silenceReasons() enumerates — or nil when it is producing. The second
return is the detail line naming what the reason is about.
Parameters
systemany(optional) — The particle system handle to ask about.
Returns (string?, string?) — string? The reason, or nil. string? The detail line for that reason.
local why, detail = particles.whySilent(fire)
globals/pcall
pcall(fn, ...) -> ok, result
Protected call. Returns false + error on failure.
globals/physics
physics: any
Lowercase alias of Physics. Same identity (#565). Prefer this in new code so the convention matches entity, scene, cam, ...
globals/physics/COLLIDER_COMPONENTS
physics.COLLIDER_COMPONENTS()
Every collider component, in the order a lookup walks them. Shape is the component's identity, so code that works on "whatever collider this entity has" walks this list rather than guessing a shape.
for _, name in ipairs(Physics.COLLIDER_COMPONENTS) do ... end
globals/physics/addCollider
physics.addCollider(entityId: string | entityRef, component: string, config: table?)
Add a collider component to an entity, naming the shape you want.
Parameters
entityIdstring | entityRef— Target entity id.componentstring— One ofPhysics.COLLIDER_COMPONENTS.configtable(optional) — The component's own fields, e.g.{ radius = 0.5 }for a sphere.
Physics.addCollider(id, "SphereCollider", { radius = 0.5 })
globals/physics/addConstraint
physics.addConstraint(entityId: string | entityRef, opts: table?)
Add a transform constraint to an entity.
Parameters
entityIdstring | entityRef— Target entity id.optstable(optional) — Optional constraint description (targetEntityId, position, rotation, scale, lookAt, targetPosition, axes, weight).
Physics.addConstraint(id, { targetEntityId = parent, position = true })
globals/physics/addJoint
physics.addJoint(entityIdA: string | entityRef, entityIdB: string | entityRef, opts: table?)
Add a Joint component connecting two entities. Accepts either
vec3-style anchor inputs (localAnchor = {x,y,z}) or pre-split
scalar keys (localAnchorX/Y/Z).
Parameters
entityIdAstring | entityRef— Entity that hosts the Joint component.entityIdBstring | entityRef— Connected entity.optstable(optional) — Optional joint description (kind, anchors, axis, stiffness, damping, restLength, maxDistance, breakForce, breakTorque).
Physics.addJoint(a, b, { kind = "fixed" })
Physics.addJoint(a, b, { kind = "hinge", axis = {x=0,y=1,z=0} })
Physics.addJoint(a, b, { kind = "rope", maxDistance = 8 })
Physics.addJoint(a, b, { kind = "fixed", breakForce = 1200, breakTorque = 800 })
globals/physics/addVelocity
physics.addVelocity(a: string | entityRef | number | vec3, b: (number | vec3)?, c: number?, d: number?)
Add to the linear velocity of an entity. Same call shapes as
setVelocity.
Parameters
astring | entityRef | number | vec3— dx, a{x, y, z}delta vector, or an entity id (explicit target).b(number | vec3)(optional) — dy, dx, or the delta vector depending on call form.cnumber(optional) — dz or dy depending on call form.dnumber(optional) — Optional dz when targeting an explicit entity.
Physics.addVelocity(0, 5, 0)
Physics.addVelocity(entityId, 0, 5, 0)
Physics.addVelocity(entityId, {x=0, y=5, z=0})
globals/physics/addWheelCollider
physics.addWheelCollider(entityId: string | entityRef, config: table?)
Add a WheelCollider to an entity. The entity must be a child (or descendant) of a rigid body — the system walks up the hierarchy to find the Physics component.
Parameters
entityIdstring | entityRef— Target entity id.configtable(optional) — Optional wheel configuration (radius?,suspensionDistance?,springRate?,damperRate?,motorTorque?,brakeTorque?,steerAngle?,forwardFriction?,sidewaysFriction?,is2D?).
Physics.addWheelCollider(id, { radius = 0.35, motorTorque = 500 })
globals/physics/applyForce
physics.applyForce(entityIdOrForce: string | entityRef | vec3, force: vec3?)
Apply a force to an entity's rigid body for the next physics step — call every frame for continuous thrust. With one argument the script-context entity is targeted; with two args the explicit entity id wins.
Parameters
entityIdOrForcestring | entityRef | vec3— Entity id (when paired withforce) OR a force vector for the script-context entity.forcevec3(optional) — Optional force vector when targeting an explicit entity.
Physics.applyForce({x=0, y=10, z=0})
Physics.applyForce(entityId, {x=0, y=10, z=0})
globals/physics/applyForceAtPoint
physics.applyForceAtPoint(entityId: string | entityRef, force: vec3, point: vec3)
Apply a force at a specific world-space point — generates the matching torque from the lever arm.
Parameters
entityIdstring | entityRef— Target entity id.forcevec3— Force vector.pointvec3— World-space application point.
Physics.applyForceAtPoint(id, {x=0,y=10,z=0}, {x=1,y=0,z=0})
globals/physics/applyImpulse
physics.applyImpulse(entityIdOrImpulse: string | entityRef | vec3, impulse: vec3?)
Apply an instantaneous impulse (one-shot velocity change). With one argument the script-context entity is targeted; with two args the explicit entity id wins.
Parameters
entityIdOrImpulsestring | entityRef | vec3— Entity id (withimpulse) OR an impulse vector for the script-context entity.impulsevec3(optional) — Optional impulse vector when targeting an explicit entity.
Physics.applyImpulse({x=0, y=5, z=0})
Physics.applyImpulse(entityId, {x=0, y=5, z=0})
globals/physics/applyTorque
physics.applyTorque(entityIdOrTorque: string | entityRef | vec3, torque: vec3?)
Apply a torque to an entity's rigid body for the next physics step — call every frame for continuous spin-up. With one argument the script-context entity is targeted; with two args the explicit entity id wins.
Parameters
entityIdOrTorquestring | entityRef | vec3— Entity id (withtorque) OR a torque vector for the script-context entity.torquevec3(optional) — Optional torque vector when targeting an explicit entity.
Physics.applyTorque({x=0, y=1, z=0})
Physics.applyTorque(entityId, {x=0, y=1, z=0})
globals/physics/bodyState
physics.bodyState(entityId: string | entityRef) -> PhysicsBodyState?
Everything the solver holds for one body — its type, mass, centre of mass, inertia, gravity scale, damping, lock flags, CCD, collision groups, sleep state, velocities, the force and torque queued for the next step, its colliders, contacts, joints and transform constraints, and why it is not moving.
Parameters
entityIdstring | entityRef— Entity id or proxy.
Returns PhysicsBodyState? — A PhysicsBodyState — with exists = false for an entity that carries no rigid body — or nil when nothing in the scene answers to that id.
local b = Physics.bodyState(id); print(b.bodyType, b.mass, b.stillness)
if not Physics.bodyState(id).exists then print("no body was built") end
globals/physics/boxCast
physics.boxCast(origin: vec3, halfExtents: vec3, direction: vec3, maxDistance: number?, exclude: (string | {string})?) -> table?
Cast a box along a direction and return the first hit.
Parameters
originvec3— Box center at the start of the cast.halfExtentsvec3— Half the size of the box on each axis.directionvec3— Cast direction.maxDistancenumber(optional) — Optional distance limit.exclude(string | {string})(optional) — Optional entity id, or array of entity ids, to exclude from hits. Applied while sweeping rather than to the answer, so a cast that starts inside an excluded collider reports what is behind it.
Returns table? — Hit table { entityId, point, normal, distance, startedInside }, or nil if nothing hit. startedInside is false for a surface the query crossed on its way there, and true when the query's own start already lay inside that collider — where distance is 0, point is the start itself, and normal is the shortest way out of the collider. normal is a unit vector either way.
local hit = Physics.boxCast(o, {x=0.5,y=0.5,z=0.5}, dir, 10)
local hit = Physics.boxCast(o, {x=0.5,y=0.5,z=0.5}, dir, 10, { selfId, carriedId })
globals/physics/capsuleCast
physics.capsuleCast(origin: vec3, radius: number, halfHeight: number, direction: vec3, maxDistance: number?, exclude: (string | {string})?) -> table?
Cast an upright capsule along a direction and return the first hit.
This is the sweep that answers whether a body of that shape fits through
a passage: a capsule of radius r reports a hit on anything that leaves
it less than 2 * r of clearance.
Parameters
originvec3— Capsule centre at the start of the cast.radiusnumber— Capsule radius.halfHeightnumber— Distance from the centre to either cap centre. The capsule standshalfHeight + radiustall in each direction.directionvec3— Cast direction.maxDistancenumber(optional) — Optional distance limit.exclude(string | {string})(optional) — Optional entity id, or array of entity ids, to exclude from hits. Applied while sweeping rather than to the answer, so a cast that starts inside an excluded collider reports what is behind it.
Returns table? — Hit table { entityId, point, normal, distance, startedInside }, or nil if nothing hit. startedInside is false for a surface the query crossed on its way there, and true when the query's own start already lay inside that collider — where distance is 0, point is the start itself, and normal is the shortest way out of the collider. normal is a unit vector either way.
local hit = Physics.capsuleCast(o, 0.3, 0.6, dir, 0.5)
local hit = Physics.capsuleCast(o, 0.3, 0.6, dir, 0.5, selfId)
globals/physics/colliderCount
physics.colliderCount() -> number
How many colliders the physics world holds. Zero means no ray, cast or overlap fired into this world can hit anything, so it is what separates a query that MISSED from a query fired into a world that holds nothing to hit. Read off the collider set itself, so it costs the same whatever the world holds.
Returns number — colliders across the whole physics world.
if Physics.colliderCount() == 0 then print("nothing here is solid") end
globals/physics/colliderGeometry
physics.colliderGeometry(options: table?) -> table?
Read the physics world as drawable triangles: every collider triangulated in world space into one indexed mesh, in GPU buffers ready to draw.
Box, sphere, capsule, cylinder, cone, convex, triangle-mesh and heightfield
colliders return their real surface, and a compound returns its children
folded together; a shape with no triangulation returns its bounding box and
reports exact = false.
options.colors is POSITIONAL over colliderManifest() — entry i colours
collider i — so you can colour by role, shape, entity or anything else you
read there. A position you leave out takes options.defaultColor.
The returned buffers are yours: destroy them when you replace them.
Parameters
optionstable(optional) —{ tessellation = "low"|"medium"|"high", colors = { {r,g,b,a}, ... }, defaultColor = {r,g,b,a} }.
Returns table? — { vertices, indices, vertexCount, indexCount, colliders } where each entry of colliders is { entity, colliderName?, shapeType, role, exact, firstIndex, indexCount }.
local geo = Physics.colliderGeometry({ tessellation = "high" })
globals/physics/colliderManifest
physics.colliderManifest() -> table
List every physics collider in the world with what it is and what it takes part in — no geometry, so it is the cheap read to make before deciding what to do with each one.
role is one of static, dynamic, kinematic, sensor. A sensor
is a collider the simulation holds as one, reported ahead of the body type
behind it, and a collider with no rigid body is static. exact says whether colliderGeometry would return this
collider's true surface or its bounding box.
Every collider of one entity shares its entity, so this is what to key
per-object decisions on. The order is stable across calls over an unchanged
world, which is what makes colliderGeometry's positional colours usable.
Returns table — Array of { entity, colliderName?, shapeType, role, exact }.
for _, c in ipairs(Physics.colliderManifest()) do print(c.entity, c.role) end
globals/physics/colliderOn
physics.colliderOn(entityId: string | entityRef) -> string?
Which collider component an entity carries, or nil when it carries none.
Parameters
entityIdstring | entityRef— Target entity id.
Returns string? The component name, e.g. "SphereCollider".
local which = Physics.colliderOn(id)
globals/physics/colliderShapes
physics.colliderShapes(entityId: string | entityRef) -> table
Read an entity's resolved physics collider shape(s) as the physics engine sees them, including auto-sized colliders.
shapeType is one of box, sphere, capsule, convex, mesh,
heightfield, compound, other — the shape the simulation is
running, so a mesh collider reads mesh.
params carries half-extents for a box, radius for a sphere, radius
and half-height for a capsule, and the collider's bounding half-extents
for the shapes that have no parametric description. A convex collider
reports its outline in linePoints instead.
Parameters
entityIdstring | entityRef— Target entity id.
Returns table — Array of resolved collider shapes (empty if none): { shapeType, position, rotation, params, linePoints, name? }.
local shapes = Physics.colliderShapes(id)
globals/physics/contacts
physics.contacts(entityId: string | entityRef) -> { PhysicsContact }
Every contact one body's colliders are in right now, with the other entity, the normal, how deeply the two interpenetrate, the impulse the last step applied, and each contact point.
Parameters
entityIdstring | entityRef— Entity id or proxy.
Returns { PhysicsContact } — An array of PhysicsContact — empty when the body touches nothing, or when the entity carries no rigid body.
for _, c in Physics.contacts(id) do print(c.other, c.deepestPenetration) end
globals/physics/getAngularVelocity
physics.getAngularVelocity(entityId: (string | entityRef)?) -> vec3?
Read the angular velocity of an entity's rigid body.
Parameters
entityId(string | entityRef)(optional) — Target entity id or proxy; resolves from script context when omitted.
Returns vec3? — Angular velocity in rad/s, or nil if the entity has no rigid body.
local w = Physics.getAngularVelocity(id)
globals/physics/getGravity
physics.getGravity() -> vec3
Read the current world gravity vector.
Returns vec3 — Gravity vector in m/s² (negative y is "down" in the default world).
local g = Physics.getGravity()
globals/physics/getVelocity
physics.getVelocity(entityId: (string | entityRef)?) -> vec3?
Read the linear velocity of an entity's rigid body.
Parameters
entityId(string | entityRef)(optional) — Target entity id or proxy; resolves from script context when omitted.
Returns vec3? — Velocity in m/s, or nil if the entity has no rigid body.
local v = Physics.getVelocity(id)
globals/physics/getWheelState
physics.getWheelState(entityId: string | entityRef) -> table?
Read a wheel collider's runtime state. Reads the native component the wheel system writes after each physics step.
Parameters
entityIdstring | entityRef— Target entity id (must carry a WheelCollider component).
Returns table? — { isGrounded, compression, angularVelocity }, or nil if the component is absent.
local state = Physics.getWheelState(id)
globals/physics/hasLineOfSight
physics.hasLineOfSight(fromId: string, toId: string) -> boolean
Check whether two entities have line-of-sight between their origins.
Parameters
fromIdstring— Viewer entity id.toIdstring— Target entity id.
Returns boolean — true when no collider sits between them (including coincident origins), false otherwise.
if Physics.hasLineOfSight(a, b) then ... end
globals/physics/ignoreCollision
physics.ignoreCollision(entityIdA: string | entityRef, entityIdB: string | entityRef, ignore: boolean?)
Toggle ignored-collision state between two specific entities.
Parameters
entityIdAstring | entityRef— First entity id.entityIdBstring | entityRef— Second entity id.ignoreboolean(optional) — Whentrue(default) collisions between the pair are skipped.
Physics.ignoreCollision(a, b, true)
globals/physics/isSleeping
physics.isSleeping(entityId: (string | entityRef)?) -> boolean?
Whether an entity's rigid body is currently asleep (at rest and not simulating). A body sleeps once it stops moving, to save simulation cost.
Parameters
entityId(string | entityRef)(optional) — Target entity id or proxy; resolves from script context when omitted.
Returns boolean? — true if asleep, false if awake, or nil if the entity has no rigid body.
if Physics.isSleeping(id) then Physics.wakeUp(id) end
globals/physics/jointBreaks
physics.jointBreaks() -> table
Every joint that has broken since the last call to this function.
A joint breaks when the reaction it carries exceeds the breakForce
(newtons of linear reaction) or breakTorque (the angular row of the same
reaction) its joint was given; each joint
reports once and its constraint is already released when the record
arrives. The 256 most recent are kept: a structure that comes apart while
nothing reads them drops the oldest beyond that, as the engine's own queue
does beyond 1024.
Returns table — Array of { entityId, connectedEntityId, kind, impulse, angularImpulse, force, torque, position }, oldest first.
for _, e in ipairs(Physics.jointBreaks()) do print(e.entityId, e.force) end
globals/physics/jointReaction
physics.jointReaction(entityId: string | entityRef) -> table?
The load an entity's joint is carrying right now, as the constraint
solver resolved it on the last physics step. This is the same quantity a
break threshold is measured against, so it is what to size breakForce
and breakTorque from.
Parameters
entityIdstring | entityRef— Entity carrying the Joint component.
Returns table? — { impulse, angularImpulse, force, torque, position }, or nil when the entity owns no joint.
local r = Physics.jointReaction(id); print(r and r.force)
globals/physics/observe
physics.observe(entityId: (string | entityRef)?, opts: table?) -> PhysicsObservation?
Read the solver's own state — the world's accounting, and what it
holds for each body plus why it is not moving one. Every value comes off
the simulation rather than the Physics component, so a write the solver
refused or clamped reads back as what it kept. Answers in edit mode as
well as play mode.
Parameters
entityId(string | entityRef)(optional) — Report on this one entity. Omit for every body in the world.optstable(optional) —{ bodies: boolean?, contactPoints: boolean? }—bodies = falsebuilds the world accounting alone, andcontactPoints = falsekeeps each contact pair's normal, depth, impulse and point count while leaving out the individual points. Both default to true.
Returns PhysicsObservation? — A PhysicsObservation, or nil when entityId names nothing in the scene. bodies is an array, not a table keyed by entity id — each entry names its own entity in entity.
local o = Physics.observe(); for _, b in o.bodies do print(b.entity, b.stillness) end
local o = Physics.observe(id); print(o.bodies[1].stillness, o.bodies[1].stillnessDetail)
globals/physics/onJointBreak
physics.onJointBreak(fn: (table) -> ()) -> () -> ()
Call fn for every joint that breaks from now on, with the same record
jointBreaks returns.
Parameters
fn(table) -> ()— Receives one break record per broken joint.
Returns () -> () — A function that removes this listener.
local off = Physics.onJointBreak(function(e) print(e.kind, e.force, e.position) end)
globals/physics/overlapSphere
physics.overlapSphere(center: vec3, radius: number) -> table
Find every entity id whose colliders overlap a sphere.
Parameters
centervec3— Sphere center in world space.radiusnumber— Sphere radius.
Returns table — Array of overlapping entity ids.
local ids = Physics.overlapSphere({x=0,y=0,z=0}, 5)
globals/physics/pumpJointBreaks
physics.pumpJointBreaks()
Deliver every joint break the simulation has recorded to the registered
listeners. An enabled Joint component calls this each tick, so listeners
fire on their own wherever joints come from that component. A joint made by
writing ecs.PhysicsJoint directly has no such tick behind it — call this
each frame, or poll jointBreaks, to deliver its breaks.
Physics.pumpJointBreaks()
globals/physics/raycast
physics.raycast(origin: vec3, direction: vec3, maxDistance: number?, exclude: (string | {string})?) -> table?
Cast a ray and return the first hit. Answers from COLLIDERS ALONE: a
mesh that renders but carries no collider is not in the physics world, so a
ray fired through it reports the same nil a ray through open air does.
renderer.raycast answers the same ray against the geometry the renderer
DRAWS, which is what reads the surface of a terrain, a procedurally
generated mesh, or any plain Model.
Parameters
originvec3— Ray origin in world space.directionvec3— Ray direction (does not need to be unit-length; the engine normalises).maxDistancenumber(optional) — Maximum distance along the ray (defaults to 1000).exclude(string | {string})(optional) — Optional entity id, or array of entity ids, to exclude from hits.
Returns table? — Hit table { entityId, point, normal, distance, startedInside }, or nil if nothing hit. startedInside is false for a surface the query crossed on its way there, and true when the query's own start already lay inside that collider — where distance is 0, point is the start itself, and normal is the shortest way out of the collider. normal is a unit vector either way. A nil says the ray met no COLLIDER, which Physics.colliderCount() separates from a world that holds none for it to meet.
local hit = Physics.raycast({x=0,y=2,z=0}, {x=0,y=-1,z=0})
local hit = Physics.raycast(origin, dir, 50, { selfId, carriedId })
if Physics.colliderCount() == 0 then hit = renderer.raycast(eye, down, 200) end
globals/physics/raycastAll
physics.raycastAll(origin: vec3, direction: vec3, maxDistance: number?, maxHits: number?, exclude: (string | {string})?) -> table
Cast a ray and return every hit up to maxHits. Answers from COLLIDERS
ALONE, so a rendered mesh with no collider is absent from the result;
renderer.raycastAll answers the same ray against the geometry the
renderer draws.
Parameters
originvec3— Ray origin in world space.directionvec3— Ray direction.maxDistancenumber(optional) — Optional distance limit along the ray.maxHitsnumber(optional) — Optional cap on the number of hits returned.exclude(string | {string})(optional) — Optional entity id, or array of entity ids, to exclude from hits.
Returns table — Array of hit tables { entityId, point, normal, distance, startedInside } — empty when nothing was hit. startedInside is false for a surface the query crossed on its way there, and true when the query's own start already lay inside that collider — where distance is 0, point is the start itself, and normal is the shortest way out of the collider. normal is a unit vector either way.
local hits = Physics.raycastAll(origin, dir, 50, 4)
globals/physics/raycastBetween
physics.raycastBetween(fromId: string, toId: string, maxDistance: number?) -> table?
Cast a ray from one entity toward another and return the first hit.
Parameters
fromIdstring— Origin entity id.toIdstring— Target entity id.maxDistancenumber(optional) — Optional distance cap (default 1000).
Returns table? — Hit table, or nil if the entities are coincident or nothing was hit.
local hit = Physics.raycastBetween(a, b)
globals/physics/raycastScreen
physics.raycastScreen(sx: number, sy: number, maxDistance: number?, exclude: (string | {string})?) -> table?
Cast a ray from a screen pixel into the scene and return the first hit. Unprojects the pixel with screenToRay, then casts with raycast.
Parameters
sxnumber— Screen X in viewport-local pixels (the space ofinput.mouse_positionandscreenToRay).synumber— Screen Y in viewport-local pixels.maxDistancenumber(optional) — Maximum distance along the ray (defaults to 1000, matchingraycast).exclude(string | {string})(optional) — Optional entity id, or array of entity ids, to exclude from hits.
Returns table? — Hit table { entityId, point, normal, distance, startedInside }, or nil on a miss or when no camera has rendered yet. startedInside is false for a surface the query crossed on its way there, and true when the query's own start already lay inside that collider — where distance is 0, point is the start itself, and normal is the shortest way out of the collider. normal is a unit vector either way.
local m = input.mouse_position; local hit = Physics.raycastScreen(m[1], m[2])
globals/physics/removeCollider
physics.removeCollider(entityId: string | entityRef) -> string?
Remove whichever collider component an entity carries.
Parameters
entityIdstring | entityRef— Target entity id.
Returns string? The component that was removed, or nil when there was none.
Physics.removeCollider(id)
globals/physics/removeConstraint
physics.removeConstraint(entityId: string | entityRef, index: number?)
Remove transform constraints from an entity (if any are present).
Parameters
entityIdstring | entityRef— Target entity id.indexnumber(optional) — Optional constraint index (currently ignored — the whole component is removed).
Physics.removeConstraint(id)
globals/physics/removeJoint
physics.removeJoint(entityId: string | entityRef)
Remove the Joint component from an entity (if present).
Parameters
entityIdstring | entityRef— Target entity id.
Physics.removeJoint(id)
globals/physics/removeWheelCollider
physics.removeWheelCollider(entityId: string | entityRef)
Remove the WheelCollider component from an entity (if present).
Parameters
entityIdstring | entityRef— Target entity id.
Physics.removeWheelCollider(id)
globals/physics/setAngularDamping
physics.setAngularDamping(entityIdOrDamping: string | entityRef | number, damping: number?)
Set angular damping on an entity's rigid body. One-arg form targets the script-context entity.
Parameters
entityIdOrDampingstring | entityRef | number— Entity id (withdamping) OR damping value (script-context entity).dampingnumber(optional) — Optional explicit damping when targeting another entity.
Physics.setAngularDamping(0.1)
Physics.setAngularDamping(entityId, 0.1)
globals/physics/setAngularVelocity
physics.setAngularVelocity(a: string | entityRef | number | vec3, b: (number | vec3)?, c: number?, d: number?)
Set the angular velocity of an entity (radians/sec). Same call
shapes as setVelocity.
Parameters
astring | entityRef | number | vec3— x-component, a{x, y, z}vector, or an entity id (explicit target).b(number | vec3)(optional) — y-component, x-component, or the vector depending on call form.cnumber(optional) — z-component or y-component depending on call form.dnumber(optional) — Optional z-component when targeting an explicit entity.
Physics.setAngularVelocity(0, 0, 1)
Physics.setAngularVelocity(entityId, 0, 0, 1)
Physics.setAngularVelocity(entityId, {x=0, y=0, z=1})
globals/physics/setBodyType
physics.setBodyType(entityId: string | entityRef, bodyType: string)
Change a rigid body's type at runtime. Mass, colliders, and joints are preserved — only the body's response to forces and position writes changes.
Parameters
entityIdstring | entityRef— Target entity id.bodyTypestring— One of"dynamic","kinematic","static".
Physics.setBodyType(entityId, "kinematic")
globals/physics/setCcdEnabled
physics.setCcdEnabled(entityIdOrEnabled: string | entityRef | boolean, enabled: boolean?)
Enable or disable continuous collision detection on an entity's rigid body. One-arg form targets the script-context entity.
Parameters
entityIdOrEnabledstring | entityRef | boolean— Entity id (withenabled) OR boolean (script-context entity).enabledboolean(optional) — Optional explicit boolean when targeting another entity.
Physics.setCcdEnabled(true)
Physics.setCcdEnabled(entityId, true)
globals/physics/setCollisionGroups
physics.setCollisionGroups(entityId: string | entityRef, membership: number, filter: number)
Set the collision-group membership and filter bitmasks on an
entity's colliders. Adds a CollisionGroup component if missing.
Parameters
entityIdstring | entityRef— Target entity id.membershipnumber— Bitmask: which groups this collider belongs to.filternumber— Bitmask: which groups this collider can collide with.
Physics.setCollisionGroups(id, 0x0001, 0xFFFF)
globals/physics/setGravity
physics.setGravity(gravity: vec3)
Replace the world gravity vector.
Parameters
gravityvec3— New gravity vector in m/s².
Physics.setGravity({x=0, y=-9.81, z=0})
globals/physics/setGravityScale
physics.setGravityScale(entityIdOrScale: string | entityRef | number, scale: number?)
Set the per-entity gravity scale (1.0 = normal, 0.0 = no gravity). One-arg form targets the script-context entity.
Parameters
entityIdOrScalestring | entityRef | number— Entity id (withscale) OR scale value (script-context entity).scalenumber(optional) — Optional explicit scale when targeting another entity.
Physics.setGravityScale(0.5)
Physics.setGravityScale(entityId, 0.5)
globals/physics/setJointMotor
physics.setJointMotor(entityId: string | entityRef, targetVelocity: number, maxForce: number)
Set a motor on an entity's joint.
Parameters
entityIdstring | entityRef— Target entity id (must carry a Joint component).targetVelocitynumber— Desired joint velocity.maxForcenumber— Maximum force the motor can apply.
Physics.setJointMotor(id, 5.0, 1000)
globals/physics/setLinearDamping
physics.setLinearDamping(entityIdOrDamping: string | entityRef | number, damping: number?)
Set linear damping on an entity's rigid body (0 = no damping). One-arg form targets the script-context entity.
Parameters
entityIdOrDampingstring | entityRef | number— Entity id (withdamping) OR damping value (script-context entity).dampingnumber(optional) — Optional explicit damping when targeting another entity.
Physics.setLinearDamping(0.05)
Physics.setLinearDamping(entityId, 0.05)
globals/physics/setMass
physics.setMass(entityIdOrMass: string | entityRef | number, mass: number?)
Set the mass of an entity's rigid body (kg). One-arg form targets the script-context entity.
Parameters
entityIdOrMassstring | entityRef | number— Entity id (withmass) OR mass value (script-context entity).massnumber(optional) — Optional explicit mass when targeting another entity.
Physics.setMass(10)
Physics.setMass(entityId, 10)
globals/physics/setRotationLocks
physics.setRotationLocks(entityId: string | entityRef, x: boolean, y: boolean, z: boolean)
Lock or unlock rotation on specific axes.
Parameters
entityIdstring | entityRef— Target entity id.xboolean— Lock rotation about the world X axis.yboolean— Lock rotation about the world Y axis.zboolean— Lock rotation about the world Z axis.
Physics.setRotationLocks(id, false, true, false)
globals/physics/setTranslationLocks
physics.setTranslationLocks(entityId: string | entityRef, x: boolean, y: boolean, z: boolean)
Lock or unlock translation on specific axes.
Parameters
entityIdstring | entityRef— Target entity id.xboolean— Lock translation along the world X axis.yboolean— Lock translation along the world Y axis.zboolean— Lock translation along the world Z axis.
Physics.setTranslationLocks(id, false, false, true)
globals/physics/setVelocity
physics.setVelocity(a: string | entityRef | number | vec3, b: (number | vec3)?, c: number?, d: number?)
Set the linear velocity of an entity. Accepts (x, y, z) or a
{x, y, z} vector for the script-context entity, or the same
prefixed with an explicit entityId.
Parameters
astring | entityRef | number | vec3— x-component, a{x, y, z}vector, or an entity id (explicit target).b(number | vec3)(optional) — y-component, x-component, or the vector depending on call form.cnumber(optional) — z-component or y-component depending on call form.dnumber(optional) — Optional z-component when targeting an explicit entity.
Physics.setVelocity(0, 10, 0)
Physics.setVelocity(entityId, 0, 10, 0)
Physics.setVelocity(entityId, {x=0, y=10, z=0})
globals/physics/sphereCast
physics.sphereCast(origin: vec3, radius: number, direction: vec3, maxDistance: number?, exclude: (string | {string})?) -> table?
Cast a sphere along a direction and return the first hit.
Parameters
originvec3— Sphere center at the start of the cast.radiusnumber— Sphere radius.directionvec3— Cast direction.maxDistancenumber(optional) — Optional distance limit.exclude(string | {string})(optional) — Optional entity id, or array of entity ids, to exclude from hits. Applied while sweeping rather than to the answer, so a cast that starts inside an excluded collider reports what is behind it.
Returns table? — Hit table { entityId, point, normal, distance, startedInside }, or nil if nothing hit. startedInside is false for a surface the query crossed on its way there, and true when the query's own start already lay inside that collider — where distance is 0, point is the start itself, and normal is the shortest way out of the collider. normal is a unit vector either way.
local hit = Physics.sphereCast(o, 0.5, dir, 10)
local hit = Physics.sphereCast(o, 0.5, dir, 10, selfId)
globals/physics/stepCost
physics.stepCost() -> PhysicsStepCost?
What the last physics step cost, stage by stage — the same figures
worldState().step carries, for a caller that wants only these. Each
covers that one step rather than a window of them, and consecutive steps
over the same resting scene vary by tens of percent, so several samples
averaged is the honest read of what a step costs.
Returns PhysicsStepCost? — A PhysicsStepCost, or nil on a frame where the pipeline did not step — a paused simulation, or a world still bootstrapping.
local c = Physics.stepCost(); if c then print(c.stepMs, c.narrowPhaseMs) end
globals/physics/stillnessReasons
physics.stillnessReasons() -> { string }
Every reason whyStill can answer with, in the order the engine
considers them. Read from the engine, so the list is the one the answers
come from.
Returns { string } — An array of reason names.
for _, reason in Physics.stillnessReasons() do print(reason) end
globals/physics/touching
physics.touching(entityId: string | entityRef, otherId: string | entityRef) -> (boolean, number, { PhysicsContactPoint })
Whether two entities are touching, and how deeply.
Parameters
entityIdstring | entityRef— Entity id or proxy.otherIdstring | entityRef— The other entity id or proxy.
Returns (boolean, number, { PhysicsContactPoint }) — (touching, deepestPenetration, points) — deepestPenetration is in metres and 0 for surfaces that meet without overlapping.
local hit, depth = Physics.touching(a, b); print(hit, depth)
globals/physics/wakeUp
physics.wakeUp(entityId: (string | entityRef)?)
Wake an entity's sleeping rigid body so it resumes simulating. The
motion setters (applyImpulse, setVelocity, setAngularVelocity) wake
the body for you; call this to wake one explicitly.
Parameters
entityId(string | entityRef)(optional) — Target entity id or proxy; resolves from script context when omitted.
Physics.wakeUp(id)
globals/physics/whyStill
physics.whyStill(entityId: string | entityRef) -> (string?, string?)
Why the solver is not moving a body. Returns nil when it IS moving
it, and otherwise one of noBody, simulationNotStepping, disabled,
static, kinematic, infiniteMass, translationLocked,
gravityDisabled, asleep, outsideIsland, resting, aboutToMove —
the nearest cause, so the answer names the thing to change. A second return
carries the detail: which collider it rests on and how deeply, what its
effective gravity works out to, and so on.
Parameters
entityIdstring | entityRef— Entity id or proxy.
Returns (string?, string?) — (reason, detail).
local why, detail = Physics.whyStill(id); if why then print(why, detail) end
globals/physics/worldState
physics.worldState() -> PhysicsWorldState
How many bodies, colliders, joints and contacts the simulation holds
right now, with world gravity, the timestep, whether the pipeline is
stepping at all, and what the last step cost. Counted off the solver, so
a body that failed to build is absent here while its Physics component
still exists.
Returns PhysicsWorldState — A PhysicsWorldState.
local w = Physics.worldState(); print(w.bodies.awake .. "/" .. w.bodies.total .. " awake")
print(Physics.worldState().contacts.touchingPairs .. " pairs touching")
globals/postprocess/add
postprocess.add(name: string, shader: string | AssetRef, opts: PostprocessOpts?) -> boolean
Register a fullscreen post-process effect. This call is what
puts a pass into the frame — a post-process .shader asset defines an
effect, and renders only once registered here. The chain applies the
registration on the caller's own stack and the returned boolean is its
answer, so a setProperty or setTexture naming the effect in the same
call finds it. shader is a .shader asset reference whose shader.wgsl
provides fn fragment(in: PostInput) -> vec4<f32> and whose
properties.yaml declares the effect's properties; the engine generates the
group(0) framework + schema-driven group(1) from that schema. Editing that
shader afterwards recompiles this effect in place, keeping its enabled
state, priority, layer and tuned property values. WGSL text is also
accepted, and then opts.properties is the whole schema. Effects run in
priority order (lower first, default 100).
A registered effect runs over the live viewport's frame AND over every
offscreen one — a capture from a world-space station, one orbiting an
entity, one of a named camera, a render-to-texture camera. In each of those
the effect's engine.view_proj / engine.prev_view_proj /
engine.inv_view_proj are the camera THAT render was drawn from and
engine.resolution is that target's own size, so a pass reconstructing
world space from zero_scene_depth(uv) reconstructs against the station
and lens the capture asked for. An offscreen capture is therefore an oracle
for an authored grade: it photographs a chosen station without taking the
on-screen camera from whoever else is driving the scene, and a capture's
postProcessing = false is the one control that takes the chain off the
frame it returns. An offscreen render keeps no view history of its own, so
engine.prev_view_proj there holds that same matrix rather than the frame
before it, and a pass taking camera motion from the two reads none.
Parameters
namestring— Unique effect name.shaderstring | AssetRef— A resolvedshaderasset reference, or author WGSL (fn fragment(in: PostInput)only).optsPostprocessOpts(optional) —{ priority = 100, enabled = true, layer = "all"|"scene", properties = {{name, type, default?, min?, max?, textureDefault?}} }— with a shader asset,propertieslayers over the asset's own schema.layerpicks which composited image the effect grades:"scene"runs it before the UI is drawn, so it grades the rendered picture and leaves every widget on screen as authored, and"all"(the default) runs it after the UI has landed, so the interface is graded along with the picture — an effect that belongs to the world's look wants"scene", since a screen another author drew is otherwise graded by it too.textureDefaultis what atype = "texture"property samples while nothing is bound to it:"white"(1,1,1,1 — the default),"black"(0,0,0,1),"normal"(0.5,0.5,1,1) or"transparent"(0,0,0,0). An effect that lays its texture over the scene wants"transparent", so the frame is untouched untilsetTexturebinds a texture that exists.
Returns boolean — True when the chain registered the effect; false when it refused it. A shader that does not compile draws nothing at any property value, so it is not registered and postprocess.list() never names it — the compiler's message is in the engine log. A call made from inside queue() or batch(), where the engine has not run the registration by the time the call returns, answers true for the queued request.
postprocess.add("vignette", asset.resolve("@builtin::shaders.post.vignette", "shader"))
postprocess.add("vignette", VIGNETTE_WGSL, { properties = {{ name = "intensity", type = "float", default = 0.5 }} })
globals/postprocess/describe
postprocess.describe(name: string) -> PostprocessDescription?
One effect by name, read in full: the chain state
postprocess.status() lists for it, and on top of that properties — the
schema the effect declared, each entry { name, type, default?, min?, max?, textureDefault? } in the shape add takes — and values, what each of
those properties currently holds. A property's value is the one the last
setProperty wrote, or the schema's own default where nothing has written
one, and it comes back as a number for a scalar and as the array for a
wider value, which is what setProperty takes, so a property read here is
written straight back.
This is the read-back for a property write. setProperty answers whether
the uniform took the value; this answers what the effect holds now, which
is the reading a pass that writes its properties every frame needs and the
one that tells a mistyped property name from an effect that is not
grading. The schema and the values are the engine's own record of the
effect — the schema it was registered with and every write the chain
accepted into its uniform, the same record /runtime/fx/<name>/meta.json
is serialized from. A write the chain refused is not in it, and neither is
one made against a property the schema does not declare.
Before an effect is registered its schema lives on the .shader asset it
will render: asset.resolve("@builtin::shaders.post.bloom", "shader"):getProperties() names what that shader declares.
Parameters
namestring— Effect name.
Returns PostprocessDescription? — The effect's state, schema and live values, or nil when nothing is registered under the name. An effect the renderer registers itself declares no properties of its own, and its properties and values are empty.
local e = postprocess.describe("vignette")
print(if e then e.values.intensity else "not registered")
for _, p in ipairs(postprocess.describe("bloom").properties) do
print(p.name, p.type, p.min, p.max)
end
globals/postprocess/list
postprocess.list() -> { string }
List all registered post-process effect names in renderer priority order (lower priority runs first).
Returns { string } — Array of effect names.
for _, n in ipairs(postprocess.list()) do print(n) end
globals/postprocess/remove
postprocess.remove(name: string) -> boolean
Queue removal of a post-process effect. Takes effect on the next frame. Removing a name that isn't registered is a silent no-op.
Parameters
namestring— Effect name to remove.
Returns boolean — True — the mutation was queued.
postprocess.remove("vignette")
globals/postprocess/setEnabled
postprocess.setEnabled(name: string, enabled: boolean) -> boolean
Queue an enable/disable toggle on a registered post-process effect. Targeting an unknown name is a silent no-op.
Parameters
namestring— Effect name.enabledboolean— True to enable, false to disable.
Returns boolean — True — the mutation was queued.
postprocess.setEnabled("bloom", false)
globals/postprocess/setProperty
postprocess.setProperty(name: string, prop: string, value: (number | { number })) -> boolean
Set a named material property on a registered post-process
effect. The property must be declared in the effect's properties
schema; read in WGSL as material.<prop>. value is a number or a
number array (vec/color).
Parameters
namestring— Effect name.propstring— Declared property name.value(number | { number })— Number or array of numbers.
Returns boolean — True when the effect's uniform took the value; false when it did not — an effect that is not registered, or one that declares no property by that name, is named in a WARN in the engine log. A call made from inside queue() or batch(), where the engine has not run the write by the time the call returns, answers true for the queued request.
postprocess.setProperty("vignette", "intensity", 0.6)
globals/postprocess/setSampler
postprocess.setSampler(name: string, opts: { [string]: any }) -> boolean
Configure the per-effect user sampler shared by the effect's declared texture properties. opts.filter = "linear" (default) or "nearest". opts.wrap (alias .address) = "clamp" (default), "repeat", or "mirror" — applied to all axes.
Parameters
namestring— Effect name.opts{ [string]: any }—{ filter = "linear"|"nearest", wrap = "clamp"|"repeat"|"mirror" }.
Returns boolean — True — the mutation was queued.
postprocess.setSampler("blur", { filter = "linear", wrap = "clamp" })
globals/postprocess/setTexture
postprocess.setTexture(name: string, prop: string, path: string) -> boolean
Bind a texture to one of an effect's declared texture properties.
Declare it in properties ({ name = "noise", type = "texture" }) and
sample in WGSL as textureSample(noise, noise_sampler, in.uv). path
is any TextureCache-resolvable spec (@builtin::textures.foo,
color:1,0,0, default:white, a render-target name, ...). A path whose
texture has not reached the GPU yet — one this same script created — is
held and bound as soon as it does; postprocess.status() reports it under
pendingTextures until then.
Parameters
namestring— Effect name.propstring— Declared texture-property name.pathstring— Texture path / spec.
Returns boolean — True when the slot took the binding, including one held until its texture reaches the GPU; false when it did not — an effect that is not registered, or one that declares no texture property by that name, is named in a WARN in the engine log. A call made from inside queue() or batch(), where the engine has not run the binding by the time the call returns, answers true for the queued request.
postprocess.setTexture("__vsky_composite", "cloud", "cloud_target")
globals/postprocess/status
postprocess.status() -> { PostprocessStatus }
Every registered effect in chain order with the state that decides
whether it reaches the frame — enabled flag, priority, layer, the
shader's compile error when it has one, the .shader asset it renders
when it was registered from one, the texture each declared slot is bound
to (textures) and the bindings still waiting for their texture
(pendingTextures). This is what the renderer draws with, so a survey of
the chain answers "is this one affecting the picture right now?" without
capturing a frame and reading pixels.
An effect this script has just registered is listed with pending = true until the renderer publishes it, since a registration is queued
for the next frame.
Returns { PostprocessStatus } — Array of per-effect state, in the order the chain runs them.
for _, e in ipairs(postprocess.status()) do
print(e.name, e.enabled, e.layer, e.error)
end
globals/preset
preset: any
Preset namespace — apply named tuning bundles to components. Auto-injected by the prelude from @builtin::modules.preset.
globals/preset/create
preset.create(name: string, entityId: string, componentType: string, opts: PresetCreateOpts?) -> PresetCreateResult
Snapshot an existing component's public property table into a
preset asset on disk. Bare names land under
/source/presets/<name>.preset/preset.yaml; absolute paths must
point to a .preset directory or its inner preset.yaml.
Parameters
namestring— Either a bare preset name (lands under/source/presets/) or an absolute path to a.presetasset.entityIdstring— Source entity id.componentTypestring— Component to snapshot (e.g."CharacterController").optsPresetCreateOpts(optional) — Optional creation options (currently just{ name = displayName }).
Returns PresetCreateResult — Metadata about the written preset: { path, entityId, component, properties }.
preset.create("my_locomotion", entityId, "CharacterController")
globals/preset/load
preset.load(source: AssetRef<preset>, overrides: table?) -> { [string]: any }
Load a preset asset and return the plain component property table.
Parameters
sourceAssetRef<preset>— Ref-like input that must resolve to a preset asset before the function body runs.overridestable(optional) — Optional table merged onto the loaded properties.
Returns { [string]: any } — Plain table suitable for entity(id).component.add(type, data).
preset.load("@builtin::systems.characterController.presets.synty")
preset.load("/source/presets/my_locomotion.preset", { walkSpeed = 3.0 })
globals/print
print(...)
Print values to the engine console. Concatenates all arguments with tabs.
Returns nil
globals/profiler/begin
profiler.begin(name: string)
Start a named profiling block. Call profiler.finish(name) to
record the duration. Blocks appear in profiler.stats() under
"script.<name>" and inside captures.
Parameters
namestring— Block name (e.g. "MyComponent.update").
profiler.begin("MyComponent.update"); ...; profiler.finish()
globals/profiler/disableRing
profiler.disableRing()
Disable the ring buffer and clear its history.
profiler.disableRing()
globals/profiler/enableRing
profiler.enableRing(seconds: number?) -> boolean
Enable the always-recording ring buffer, retaining the last
seconds of per-frame data (default 20). Query it AFTER the fact
with profiler.retro() — latency-immune, since the data is
historical. Editor profile only: returns false in the runtime
profile. The enable is the gate; the ring costs nothing until on.
Parameters
secondsnumber(optional) — Seconds of history to retain (default 20).
Returns boolean — True if enabled, false if refused (runtime profile).
if profiler.enableRing(30) then ... end
globals/profiler/finish
profiler.finish(name: string?) -> number?
Finish a profiling block and record the elapsed duration as
"script.<name>". Without an argument, closes the most-recently-
begun block (LIFO stack). With a name, closes the most recent
block whose name matches — useful when blocks of different names
are nested.
Parameters
namestring(optional) — Block name to finish. Omit to pop the top of the stack.
Returns number? — Elapsed milliseconds, or nil if no matching block was active.
local ms = profiler.finish("MyComponent.update")
globals/profiler/gpuFrame
profiler.gpuFrame() -> GpuFrameReport
Label-aggregated GPU pass timings over the last window_frames
resolved frames, measured with GPU timestamp queries. supported
is false when the device lacks timestamp queries — spans stays
empty. Each span covers every render/compute pass recorded under
one label — compute.<shader> per compute dispatch, scene.* for
the scene passes, post.<effect> per post-process effect,
feature.* for render-feature passes: ms is the median of its
per-frame totals, min_ms/max_ms the range that median sits in,
count the passes per frame and frames how much of the window
carried it. at_floor marks a label whose every sample landed
within a few ticks of the device's timestamp counter (tick_ms) —
those passes ran and the device resolved no duration for them,
which is not the same as a measured zero. ran is whether the
label recorded a measured pass in the newest resolved frame, and
last_frame the newest frame that did. The window outlives the
work it describes, so a pass that stops being recorded leaves a row
standing for up to window_frames frames carrying the median of
the frames it did run in: read ran to answer whether a pass is
running, frame - last_frame for how many resolved frames ago it
last did, and ms as the cost of the frames it ran in.
frame_span_ms (first pass begin to last pass end) and
total_ms are medians too, so
rows do not sum to total_ms, and the GPU may overlap passes so
total_ms can exceed frame_span_ms. The readback is
asynchronous: the window lags the live frame by a few frames.
Returns GpuFrameReport — GPU timing window, spans ranked by median ms descending.
local g = profiler.gpuFrame(); print(g.frame_span_ms, g.spans[1] and g.spans[1].label)
globals/profiler/hits
profiler.hits(label: string?) -> string?
Drain the watchdog's recorded hit frames into a capture stored
under label (default "watch_hits") and clear the buffer.
Returns the capture JSON (same shape as stopCapture), or nil if
there were no hits.
Parameters
labelstring(optional) — Capture label to store under (default "watch_hits").
Returns string? — Capture JSON of the hit frames, or nil if none.
local json = profiler.hits()
globals/profiler/isCapturing
profiler.isCapturing() -> boolean
Check if a profiler capture is currently active.
Returns boolean — True if a capture is in progress.
if profiler.isCapturing() then ... end
globals/profiler/lastCapture
profiler.lastCapture() -> string?
Get the most recent completed capture result as a JSON string.
Same shape as profiler.stopCapture(). Returns nil if no capture
has been completed yet.
Returns string? — JSON string of the last capture, or nil.
local last = profiler.lastCapture()
globals/profiler/retro
profiler.retro(seconds: number?, label: string?) -> { [string]: any }?
Retroactively aggregate the last seconds of the ring (default:
the whole ring). The full per-frame capture is retained under label
(default "retro") for in-engine drill-down (the frame / hotspots
tools);
this RETURNS a compact structured aggregate table (frame-time distribution
- per-system summary), never the raw per-frame array — bounded, so it is
safe over the ZeroMind bridge. Code-facing primitive; the
retrotool renders the agent-facing report. Latency-immune: the data is historical.
Parameters
secondsnumber(optional) — How many seconds back to include (default: whole ring).labelstring(optional) — Capture label to store under (default "retro").
Returns { [string]: any }? — A compact aggregate { label, source, frames, seconds, exclude_agent, agent_frames, dt = { avg, p50, p90, p99, max, min }, summary = {...} }, or nil if the ring holds nothing.
local agg = profiler.retro(8, "collapse")
globals/profiler/ringStatus
profiler.ringStatus() -> string
Ring buffer status as a JSON string:
{ enabled, frames, capacity, span_seconds }.
Returns string — JSON status string.
local s = profiler.ringStatus()
globals/profiler/startCapture
profiler.startCapture(label: string?) -> boolean
Start recording per-frame profiler data. Each frame's system
timings are captured until stopCapture() is called. Results are
accessible via profiler.lastCapture() and VFS at
/zero/runtime/profiler/<label>.json.
Parameters
labelstring(optional) — Capture label (default"capture").
Returns boolean — True if capture started, false if a capture is already active.
if profiler.startCapture("frame-spike") then ... end
globals/profiler/stats
profiler.stats(pattern: string?) -> { ProfilerStat }
Get current EMA profiling statistics from the SystemProfiler.
Optional glob pattern filters by metric name (supports * and ?
wildcards). Each entry carries two averages: avg_ms averages one
RUN of the block and is folded when the block runs, so a block that
has stopped running keeps the last value it saw; avg_frame_ms
averages one FRAME and is folded every frame, including the frames
the block did not run in, so it is the block's share of the current
frame and falls back to zero once the block stops running.
Parameters
patternstring(optional) — Filter pattern (e.g. "schedule.", "system.schedule.render.").
Returns { ProfilerStat } — Array of profiler block stats.
for _, s in ipairs(profiler.stats("schedule.*")) do print(s.name, s.avg_frame_ms) end
globals/profiler/stopCapture
profiler.stopCapture() -> string?
Stop the active profiler capture and return its result as a
JSON string. The capture is also saved to VFS at
/zero/runtime/profiler/<label>.json. Top-level fields: label,
frame_count, started_at, ended_at, frames, summary.
Compute duration as ended_at - started_at.
Returns string? — JSON capture result, or nil if no capture was active.
local json = profiler.stopCapture()
globals/profiler/unwatch
profiler.unwatch()
Disarm the watchdog. Recorded hits are kept for a final
profiler.hits().
profiler.unwatch()
globals/profiler/watch
profiler.watch(ceilingMs: number, mode: string?, excludeAgent: boolean?, maxHits: number?) -> boolean
Arm the frame-time watchdog. When a frame's EFFECTIVE time
(total minus agent-injected execute cost) crosses ceilingMs,
mode "record" logs every offending frame (read with
profiler.hits()), and mode "pause" pauses gameplay ONCE to
freeze the bad state, then disarms. Editor profile only: returns
false in the runtime profile. excludeAgent (default true) keeps
the agent's own calls from tripping it.
Parameters
ceilingMsnumber— Effective frame-time ceiling in ms.modestring(optional) — "record" (default) or "pause".excludeAgentboolean(optional) — Subtract agent cost before comparing (default true).maxHitsnumber(optional) — Max frames retained in record mode (default 240).
Returns boolean — True if armed, false if refused (runtime profile).
if profiler.watch(50, "pause") then ... end
globals/profiler/watchStatus
profiler.watchStatus() -> string
Watchdog status as a JSON string: { armed, ceiling_ms, mode, exclude_agent, hits, dropped_hits, tripped }.
Returns string — JSON status string.
local s = profiler.watchStatus()
globals/queue
queue(fn, opts?): (boolean, string?)
Defer FFI write paths inside fn to be applied across frames by the engine drainer. Returns (true, nil) on success or (false, err) if the body throws — partial batch is cleared so it never lands. Nested queue() is re-entrant (depth counter). Reads inside the body are not yet fenced (Stage 2). See modules.queue and docs/plans/2026-05-06-luau-queue-deferred-mutations.md.
Parameters
fnfunction— Body whose FFI writes get queued across framesoptstable(optional) — Reserved for future opts (currently { onError = "halt" })
Returns (boolean, string?) — (true, nil) on success; (false, err) if the body errored.
globals/rawequal
rawequal(a, b) -> boolean
Compare without metamethods.
globals/rawget
rawget(table, key) -> value
Get without metamethods.
globals/rawlen
rawlen(table) -> number
Length without metamethods.
globals/rawset
rawset(table, key, value)
Set without metamethods.
globals/reflectionProbe/add
reflectionProbe.add(x: number, y: number, z: number, opts: { [string]: any }?) -> string
Add a reflection probe at (x, y, z) in one call: spawns a probe entity
carrying a ReflectionProbe component (which registers it and, unless
opts.bake == false, bakes it). The probe is an editor gizmo — invisible in
play mode. Returns the probe entity id.
Parameters
xnumber— World X.ynumber— World Y.znumber— World Z.opts{ [string]: any }(optional) — Optional{ radius = 12, probeId = "...", name = "..." }.probeIdis the STABLE asset identity (so a re-created probe reloads the same baked cube); defaults to the entity id. The probe does NOT bake on add — callbakeAll()once the scene is built (baking is an authoring step).
Returns string — The probe entity id.
reflectionProbe.add(0, 3, 0, { radius = 15, probeId = "lobby" })
globals/reflectionProbe/apply
reflectionProbe.apply() -> number
Push the current active-probe blend data (live positions + radii) to the renderer. Builds a dense slot array so each probe's data lands at its cube slot; freed/missing slots become inert placeholders. Called automatically by add / bake / remove; call it directly after moving a probe entity.
Returns number — The number of active probes applied.
globals/reflectionProbe/bake
reflectionProbe.bake(id: string) -> (string?, string?)
Bake the scene into probe id's cube slot from its current position AND
persist it to a faces6 .texture asset (so it survives reload + syncs),
then re-apply the probe set. Yields a few frames; call from a task/coroutine
context (component hook via task.spawn, bakeAll, or execute).
Parameters
idstring— Probe entity id.
Returns (string?, string?) — The asset path on success, or (nil, errorMessage) on failure.
globals/reflectionProbe/bakeAll
reflectionProbe.bakeAll() -> { baked: number, failed: number, errors: { string } }
Bake EVERY registered probe in the active layers, in one call. Captures
the sky into the fallback slot, then each probe's scene from its position
into its slot, persists it, and applies the full probe set. The agent/editor
one-liner. Yields; call from a task/coroutine context (execute, a tool, or
task.spawn).
Returns { baked: number, failed: number, errors: { string } } — { baked = N, failed = M, errors = { ... } }.
reflectionProbe.bakeAll()
globals/reflectionProbe/count
reflectionProbe.count() -> number
Number of registered probes.
Returns number
globals/reflectionProbe/ensureSkyFallback
reflectionProbe.ensureSkyFallback() -> boolean
Ensure the scene's sky is in the environment's sky fallback: a reflective surface no probe covers then reflects the sky rather than black, and a partially covered one blends the shortfall against it. Queues a capture when the sky slot holds none, and re-arms the fallback when a capture is there but switched off. The engine's own state answers both questions, so calling this on every probe that comes up costs one capture between them, and a scene that lost its fallback gets it back. Once captured, the fallback follows the sky the scene draws on its own.
Returns boolean — True if a capture was queued, false if the sky slot already holds one.
reflectionProbe.ensureSkyFallback()
globals/reflectionProbe/list
reflectionProbe.list() -> { any }
List every registered probe: { { id, slot, radius, priority, asset, position }, ... }.
Returns { any } — The probe list.
globals/reflectionProbe/loadBaked
reflectionProbe.loadBaked(id: string) -> boolean
Load probe id's PERSISTED baked cube (probe_<key>.texture) into its
slot WITHOUT re-rendering the scene — the runtime path. A probe bakes once at
authoring time and loads the asset on every subsequent scene load. Returns
false (not an error) when no baked asset exists yet.
Parameters
idstring— Probe entity id.
Returns boolean — True if a baked asset was loaded, false if none exists / load failed.
globals/reflectionProbe/register
reflectionProbe.register(id: string, radius: number, key: string?) -> number?
Register a reflection probe for entity id with influence radius.
Assigns a free cube slot and applies the updated probe set. Idempotent — a
re-register keeps the same slot and just updates the radius. Called by the
ReflectionProbe component's awake; rarely called directly.
Parameters
idstring— Probe entity id.radiusnumber— Influence radius (world units) — surfaces within blend it.keystring(optional) — Optional STABLE asset identity (the probe's probeId). Defaults toid. The baked cube persists atprobe_<key>.textureso an authored probe keeps the same asset across reloads even though its runtime entity id changes.
Returns number? — The assigned cube slot, or nil if all MAX_PROBES slots are taken.
globals/reflectionProbe/setPriority
reflectionProbe.setPriority(id: string, priority: number)
Set a probe's blend rank against the probes it overlaps, and re-apply. Probes are gathered highest rank first and each rank takes the coverage the ranks above it left, so a small interior probe ranked above the large exterior one it sits inside wins outright wherever it reaches full weight, while probes of equal rank crossfade by proximity as before.
Parameters
idstring— Probe entity id.prioritynumber— Blend rank. Defaults to 0 on every probe.
reflectionProbe.setPriority(interiorId, 1)
globals/reflectionProbe/setProxy
reflectionProbe.setProxy(id: string, kind: string, x: number, y: number, z: number)
Anchor a probe's reflections to a proxy volume and re-apply. A cube records the environment from one point, so sampling it along the raw reflection vector puts everything it recorded at infinity and the reflection slides across a surface as the camera moves. Sizing a proxy to the geometry the probe recorded — a room's walls, say — keeps the reflection anchored to what it depicts.
Parameters
idstring— Probe entity id.kindstring— "box" (sized by all three half-extents), "sphere" (sized byx), or "none" to sample along the raw reflection vector.xnumber— Half-extent along X, in world units — the sphere radius for "sphere".ynumber— Half-extent along Y.znumber— Half-extent along Z.
reflectionProbe.setProxy(id, "box", 5, 3, 4) -- a 10x6x8 room
globals/reflectionProbe/setRadius
reflectionProbe.setRadius(id: string, radius: number)
Update a probe's influence radius and re-apply.
Parameters
idstring— Probe entity id.radiusnumber— New influence radius.
globals/reflectionProbe/unregister
reflectionProbe.unregister(id: string)
Unregister entity id's probe, freeing its cube slot, and re-apply.
Parameters
idstring— Probe entity id.
globals/renderer/anisotropy
renderer.anisotropy() -> number
The maximum anisotropy material textures are sampled with right now — the requested level clamped to what this device honours.
Returns number — The effective level, 1 through 16.
if renderer.anisotropy() < 4 then ... end
globals/renderer/atmospherics/held
renderer.atmospherics.held() -> boolean
Whether a hold is standing on the air right now.
Returns boolean — True while at least one renderer.atmospherics.hold stands.
if renderer.atmospherics.held() then print("clear air") end
globals/renderer/atmospherics/hold
renderer.atmospherics.hold(share: number?) -> () -> ()
Hold the air between the camera and every surface at a stated share of what the scene authored, and return the release. At the default 0 the media contribute nothing and a surface renders in its own colour, which is what lets a reader judge an albedo, a tint or a material while another slice of a shared world drives the weather. The share reaches aerial perspective, height fog and volumetric light scattering; the sky, the sun and the light they put on a surface are untouched, because those are what the surface's colour is made of. Holds nest: the innermost names the share, and the authored air is back once the last release is called. Each release ends its own hold whatever order the releases come in, so two callers holding at once each end their own.
Parameters
sharenumber(optional) — How much of the authored air reaches the image, in [0, 1]. Defaults to 0 — no air at all.
Returns () -> () — A function that releases this hold. Calling it twice releases once.
local release = renderer.atmospherics.hold() ; local png = tools.use("capture", "fromPosition", { position = { 0, 2, 8 } }) ; release()
globals/renderer/atmospherics/onChange
renderer.atmospherics.onChange(listener: (number) -> ()) -> () -> ()
Register a listener called with the share now in force whenever it changes — a hold taken, a hold released — and return the unsubscribe. A system that packs a medium into a GPU buffer registers here and re-packs what it has already pushed, so the buffer carries the share before the frame the hold was taken on is drawn rather than a frame later.
Parameters
listener(number) -> ()— Called with the share now in force, in [0, 1].
Returns () -> () — A function that removes this listener.
local stop = renderer.atmospherics.onChange(function(share) pushParams() end)
globals/renderer/atmospherics/share
renderer.atmospherics.share() -> number
The share of the authored air that reaches the image: the innermost
hold's share while one stands, and 1 otherwise. A system that packs a
medium multiplies its extinction — aerial, a fog density — by this,
and a hold then reaches that medium however it is being driven.
Returns number — A number in [0, 1]. 1 when nothing holds.
local density = state.density * renderer.atmospherics.share()
globals/renderer/blendedBatching
renderer.blendedBatching() -> boolean
Whether blended neighbours sharing a draw key draw together.
Returns boolean
globals/renderer/bounds/clear
renderer.bounds.clear(id: string) -> boolean
Withdraw the box an entity published, so it stops contributing to the entity's reported extent.
Parameters
idstring— Entity id.
Returns boolean — True when there was a published box to withdraw.
renderer.bounds.clear(id)
globals/renderer/bounds/set
renderer.bounds.set(id: string, min: any?, max: any?) -> boolean
Publish the local-space box an entity's content-drawn geometry occupies.
entity:bounds() and entity:hierarchyBounds() union it with whatever
mesh geometry the entity has, each carried out of its own local space, so
framing a camera on the entity frames what a feature actually draws.
Parameters
idstring— Entity id.minany(optional) — Local-space minimum corner — a Vec3 table or a 3-element array.maxany(optional) — Local-space maximum corner, same shape asmin.
Returns boolean — True when the box was stored; false for a non-finite or inverted box.
renderer.bounds.set(id, cloud.boundsMin, cloud.boundsMax)
globals/renderer/captureView/channelId
renderer.captureView.channelId(name: string) -> number?
The debug channel a registered view draws on — what a feature passes as
its pass debugChannel. Nil when no view is registered under name.
Parameters
namestring— The view name.
Returns number? — The channel number or nil.
ctx.enqueue { ..., debugChannel = renderer.captureView.channelId("lightmap") }
globals/renderer/captureView/list
renderer.captureView.list() -> { any }
Every registered capture view as { name, channel, description } records
— what backs the discoverability of capture pass=<name> and the
unknown-view error's suggestion list.
Returns { any } — An array of view records.
for _, v in ipairs(renderer.captureView.list()) do ... end
globals/renderer/captureView/ready
renderer.captureView.ready(name: string) -> boolean
Whether a registered view can draw yet. A view's passes are enqueued from the moment its render feature first runs, but they are skipped while the materials they name have no pipeline — their shader is still compiling — so for the first frames of a session a camera bound to the view renders the ORDINARY view into its target, and the image gives no sign of it. This reports the difference, and reports it before any camera is on the view, so it is answerable for the first camera bound to one. False for an unregistered name.
Parameters
namestring— The view name.
Returns boolean — Whether this view's passes have resolved everything drawing needs.
repeat task.wait() until renderer.captureView.ready("zfighting")
globals/renderer/captureView/register
renderer.captureView.register(name: string, config: any?) -> number
Register (or update) a content capture view under name and return the
debug CHANNEL number assigned to it. A render feature gates its pass to this
channel (debugChannel = channel) so the pass draws only when a capture
selects the view. Idempotent: re-registering the same name keeps its channel.
Parameters
namestring— The view name, selected viacapture pass=<name>.configany(optional) —{ description?, ensure?, warmup?, renderLayers? }.ensureis called before a capture of this view so the feature that draws it is live (e.g. create it on demand).warmupis how many present frames a capture lets the view accumulate before it reads — set it when the feature retains prior-frame state (a temporal diff) so the first capture reads a warm result.renderLayersis the layer spec a capture of this view uses when the caller named none — a view that draws its own geometry and wants the scene's kept out of the frame (and out of the depth buffer it tests against) names only its own layer.
Returns number — The channel number assigned to the view.
local ch = renderer.captureView.register("lightmap", { description = "...", ensure = fn })
globals/renderer/captureView/resolve
renderer.captureView.resolve(name: string) -> any
Resolve a capture view by name to its { channel, ensure, description, warmup } record, or nil when no view is registered under name.
Parameters
namestring— The view name.
Returns any — The view record or nil.
local v = renderer.captureView.resolve("lightmap")
globals/renderer/captureView/unregister
renderer.captureView.unregister(name: string) -> boolean
Withdraw a capture view. A subsequent capture pass=<name> no longer
resolves to it (falls through to the unknown-view error).
Parameters
namestring— The view name.
Returns boolean — True when a view was registered under name.
renderer.captureView.unregister("lightmap")
globals/renderer/clearShadowHero
renderer.clearShadowHero() -> boolean
Release the hero caster, so the directional shadow is the cascades' alone again and the layer the hero view rendered into is given back.
Returns boolean — Whether a caster was registered.
renderer.clearShadowHero()
globals/renderer/clearShadowProxy
renderer.clearShadowProxy(mesh: string?) -> number
Stop proxying mesh, so it rasterizes its own geometry into shadow
views again. Called with no argument, drops every registration.
Parameters
meshstring(optional) — The mesh to stop proxying. Omit to clear all of them.
Returns number — How many registrations were removed.
renderer.clearShadowProxy(statueMesh)
print(renderer.clearShadowProxy(), "proxies dropped")
globals/renderer/collect
renderer.collect() -> RuntimeCollection
Release every runtime texture, material, mesh and render feature nothing holds: no handle a script still reaches, no live owner, no reference from live engine state, no asset backing it, no hold. A root scene load runs this once the new scene stands, so what the previous scene's content created and nothing still wears goes with that scene; calling it directly collects at any other moment. A session material's handle counts as reached while the entity it was keyed for stands, and stops counting once that entity is gone. It reaches the GPU textures the device holds beside the registry's own: a texture the cache loaded for an asset goes once nothing live names it and is read back from that asset the next time something asks for it, while one no asset answers for stays, there being nothing to read it back from — a render pass's own target, a colour swatch, an atlas the engine built. A texture the ASSET path uploaded and whose asset has since been removed has nothing to come back from either, and the collection decides about it from its holders the way it does about every other resource: a handle a script still reaches, a live owner, a reference from live engine state, a hold. Features go first, then materials, then meshes, then textures, so a texture only a released material named goes with the material. Runs a full garbage collection first, so a handle nothing reaches counts as let go, and yields for the frame the census runs on. A handle the calling function still has in a variable — or in a temporary it has not overwritten — is one a script reaches, so a resource created in the function that collects is let go by the next collection rather than this one.
Returns RuntimeCollection — { released = { texture, material, mesh, feature }, kept, entries } — the counts released per kind, how many stayed, and every resource's status with action = "released" | "kept".
local c = renderer.collect() print(c.released.texture, c.kept)
globals/renderer/compiledShaders
renderer.compiledShaders() -> { string }
Every name renderer.compiledSource answers for — one per name a
shader compile has run under this session, whether it succeeded or failed.
What makes the composed-source surface enumerable rather than something to
guess a key for.
Returns { string } — An array of shader names, sorted.
for _, name in renderer.compiledShaders() do print(name) end
globals/renderer/compiledSource
renderer.compiledSource(shader: string) -> string?
The WGSL the shader compiler received under one name, exactly as it
received it — the composed module, which is what a compile error's line
numbers and handle indices are positions in. Answers under any name a
compile ran under (identity, guid, alias, or a program from
renderer.shaderVariants()), for a shader that declares no features, and
for a shader whose compile FAILED, which is the case it exists for: a
message about a function body carries a position and nothing else, and the
text that position is in is this. The failed text stands for as long as
shaderRef:compileStatus() reports that failure under the same name.
Parameters
shaderstring— Any name a shader compiled under — identity, guid, alias, or ashaderVariants()program name.
Returns string? — The composed WGSL, or nil for a name no compile has run under.
local status = asset.resolve("myShader", "shader"):compileStatus()
if status.status == "failed" then
print(status.error)
print(renderer.compiledSource("myShader"))
end
globals/renderer/compositeSize
renderer.compositeSize() -> { width: number, height: number }
The size of the image the post-scene phases worked on in the last
presented frame — the target the UI composites onto, which every pass
after the scene reads as @scene.color and writes into, and which a
screenSpace = "composite" render target follows. While the renderer
presents the viewport itself that is the display's own size, whatever
fraction of it the scene rasterized at; while a UI viewport panel owns
the presentation it is the size the scene rasterized at, since the panel
draws the scene target at its own rect and nothing upscales before the
composite. Both read 0 before a frame has drawn.
Returns { width: number, height: number } in pixels.
local c = renderer.compositeSize()
globals/renderer/cullStats
renderer.cullStats() -> {
What the last completed frame decided to draw. total renderables went
into the frustum test, culled fell outside it and visible survived. Of
those, occlusion culling measured occlusionTested against the depth
pyramid and proved occlusionCulled were entirely behind other geometry —
both 0 while renderer.occlusionCulling() is false. A renderable the
pyramid has no say over — one that laid no depth in the pre-pass, one whose
bounds were never recorded, one straddling the near plane — is measured
against nothing and counted in neither, so the gap between visible and
occlusionTested reads how much of the frame the test could speak for.
This answers for the main camera. What a shadow view's own volume did with
the frame's casters is on that view's row in renderer.shadowViews().
Returns { total: number, culled: number, visible: number, occlusionTested: number, occlusionCulled: number }
local s = renderer.cullStats(); print(s.visible - s.occlusionCulled, "drawn")
globals/renderer/debugPass/builtins
renderer.debugPass.builtins() -> { string }
The built-in debug-pass names, one per channel in channel order — the engine's built-in pass vocabulary (final, albedo, normal, depth, …).
Returns { string } — An array of built-in pass names.
for _, n in ipairs(renderer.debugPass.builtins()) do ... end
globals/renderer/debugPass/channel
renderer.debugPass.channel(name: string) -> number?
The channel a debug-pass NAME renders on: a built-in pass, else a content
capture view registered via renderer.captureView. Nil when the name is
neither — the signal a selector uses to reject an unknown pass.
Parameters
namestring— A debug-pass name (e.g. "normal", "depth", "lightmap").
Returns number? — The channel number, or nil for an unknown name.
local ch = renderer.debugPass.channel("normal") -- 7
globals/renderer/debugPass/list
renderer.debugPass.list() -> { string }
Every selectable debug-pass name: the built-in passes plus every registered content capture view. What a debug-pass selector offers.
Returns { string } — An array of pass names.
local passes = renderer.debugPass.list()
globals/renderer/debugPass/name
renderer.debugPass.name(channel: number) -> string?
The canonical NAME for a debug channel: a built-in pass name for a built-in channel, else a registered capture view's name. Channel 0 is "final" (the lit image). Nil when no pass owns the channel.
Parameters
channelnumber— The channel number.
Returns string? — The pass name, or nil.
local name = renderer.debugPass.name(7) -- "normal"
globals/renderer/depthPrepass
renderer.depthPrepass() -> boolean
Whether the opaque depth pre-pass is currently enabled.
Returns boolean
globals/renderer/depthPrepassOrder
renderer.depthPrepassOrder() -> { runs: number, reordered: number }
What the last frame's depth pre-passes planned, and how far their
sequences were from near-to-far before they ordered. runs counts the
instanced draws planned; reordered counts the adjacent pairs the sort
moved past each other, taken before it ran. Both are summed over every
pre-pass the frame ran — the window plus each render-target camera, each
ordering against its own camera. Both read 0 while the pre-pass or the
ordering is off, and reordered reads 0 for a frame that already stood
in order. The ordering leaves no other trace — the draws, the depth and the
image are the same either way.
Returns { runs: number, reordered: number }
local o = renderer.depthPrepassOrder() -- o.reordered > 0 → it sorted
globals/renderer/depthPrepassOrdering
renderer.depthPrepassOrdering() -> boolean
Whether the depth pre-pass is submitted nearest-first.
Returns boolean
globals/renderer/destroy
renderer.destroy(handleOrKind: any?, id: string?) -> boolean
Free the GPU resource a renderer resource holds (the GPU-destroy verb).
Takes any of the forms that name it: the handle a create returned, routed
by its category so one call releases a mixed set of handles; the id a
listing hands out, whose kind is read back off what the renderer holds
under it — the runtime registry, the material definitions, the live
features, and the device itself for an asset's own texture or mesh; or the
kind with the id beside it, the shape renderer.hold and
renderer.references take, which is what names the kind for an id two of
them answer to. An id nothing holds anything under releases nothing and
answers false. The on-disk asset, if any, is untouched. A CPU handle's
:unload() frees the CPU copy separately.
Parameters
handleOrKindany(optional) — AMeshHandle,TextureHandle,MaterialHandleor feature handle; the id itself; or the kind ("texture","material","mesh","feature") with the id as the second argument.idstring(optional) — The guid or registry key, when the first argument is a kind.
Returns boolean true if a GPU resource was known under the id.
renderer.destroy(meshHandle); renderer.destroy(materialHandle)
renderer.destroy(renderer.texture.list()[1].guid)
renderer.destroy("mesh", guid)
globals/renderer/deviceGeneration
renderer.deviceGeneration() -> number
Which render device this process is on, counted from the first.
A render device is lost when a driver resets, when the GPU is taken away,
or when a browser reclaims a WebGPU context. The engine answers by building
another device and re-deriving this session's resources onto it, and this
number moves by one each time it does. Anything held across frames that was
built from a GPU resource records this beside it and remakes it when the two
differ; engine.onDeviceRebuilt is the hook that fires when it moves.
Returns number — The current device generation, counting from 1.
local generation = renderer.deviceGeneration()
engine.onDeviceRebuilt(function(g) print("device " .. g) end)
globals/renderer/deviceState
renderer.deviceState() -> string
Whether the render device this process draws through is the one it is using, one it is replacing, or one it has stopped trying to replace.
"ready" is a live device. "rebuilding" is the window between a device
reporting itself lost and another being in place: every GPU resource built
from the old one is invalid, the frames in that window draw nothing, and
anything reaching the GPU refuses. "abandoned" is after the engine gave
up — the adapter refused every attempt, so this session draws no more
frames.
Work that spans the device — build a render target, draw into it, read it
back — reads this to tell an operation that failed because the device went
out from under it, which is worth doing again once
renderer.deviceGeneration() moves, from one that failed on its own terms.
The loss is reported before the next device exists, so the two readings
answer different halves: this one says a replacement is coming, the
generation says it arrived.
Returns string — "ready" | "rebuilding" | "abandoned".
if renderer.deviceState() == "rebuilding" then return end
globals/renderer/drawDiagnostics
renderer.drawDiagnostics() -> { DrawDiagnostic }
Every renderable that is NOT drawing what its material says — the one
call for "why does this surface look wrong". Three states land here: a
surface rendering as the magenta placeholder (substituted), one the
renderer could bind nothing for at all (outcome = "skipped"), and one
drawing a program whose most recent compile FAILED (stale), which is what
a shader edited into brokenness looks like — the pipeline its last good
compile built keeps drawing, so the picture is intact and answers to none of
the edits since. Each row names the entity, the program asked for, the
program bound, programStatus — the compile gate's word about the program
the material NAMED — and the one cause
from shaderCompileFailed / shaderNotRegistered / shaderNotCompiledYet
/ noGbufferEntry / renderStateKeyNotBuilt / noPipelineForTarget /
unshaded, with the compiler's own message in detail or programError.
Covers every renderable the renderer holds, whether or not a camera reached
it: a row with observed = false and outcome = "notDrawn" carries the
renderer's own resolution for one this frame drew nowhere, so a broken
surface off-screen is reported the same as one in frame. An empty result
means every renderable the renderer holds is drawing the program its
material named and that program compiles. Answers on the deferred path as
well as forward, and in edit mode as well as play.
Returns { DrawDiagnostic }
for _, d in renderer.drawDiagnostics() do print(d.entity, d.reason, d.detail) end
if #renderer.drawDiagnostics() == 0 then print("every surface is drawing what it says") end
globals/renderer/drawStats
renderer.drawStats() -> {
What the last completed frame actually submitted. draws counts every
geometry draw call the frame issued — the camera's passes, each shadow
view a shadow-casting light adds, and whatever a render feature draws —
and instances counts the instances those draws covered. The pair is what
separates one draw carrying five hundred instances from five hundred draws
carrying one each, so it reads how well the scene batches rather than how
many objects are in it.
compacted is how many of those instances the frame planned through draws
whose instance count the GPU decides: the culler's own per-object answers
packed into a dense run, so an object it rejects is absent from the draw
instead of collapsing to nothing in the vertex stage. compactedDrawn is
how many of them survived, counted on the GPU as it packed them — a pass
that then skips a whole draw over its own layer or visibility answer
leaves that draw's instances in both numbers.
The plan is made over the populations the frame draws, and the tests
answer which of their instances the packing keeps. That packing runs
before any pass has resolved the depth occlusion culling is tested
against, so on its own it reads the frustum and screen-size answers
alone. With setOcclusionCulling armed the frame packs the same plan a
second time once the test has answered, and compactedDrawn then counts
what came through occlusion as well.
compactedDrawn comes back from the buffer the GPU wrote, so it describes
a frame that has finished while compacted describes the most recent
plan, and it holds the last count the GPU wrote until another arrives — a
frame that compacts nothing reads compacted 0 beside the count from the
last frame that did. In a scene standing still the gap between the two is
the front-end work culling removed.
materialBinds is how many times the frame's geometry passes set a
material's parameter group, and materialBindsElided how many times a
pass reached that decision and found the group already bound. Their sum
is how many times the decision was reached — once per unit of geometry
submitted, which sits at or below draws, since a mesh of several
primitives draws once per primitive under one set of binds. The ratio
inside the pair is what material binding costs the frame: the batched
opaque geometry is gathered into runs sharing a material, so a frame of
many such draws over few materials binds about once per material rather
than once per unit. materialExtraBinds and materialExtraBindsElided
are the same pair for the second group, the storage bindings a shader
declares for itself, which only the shaders that have them ever bind.
pipelineBinds and pipelineBindsElided are the same pair for the
pipeline itself: how many times the frame's geometry passes set one, and
how many times a pass reached that decision and found the pipeline it
wanted already bound. Which pipeline a unit needs follows its shader, its
material's render state and its mesh's vertex layout together, so a scene
whose units share all three costs one set for the run of them, while units
differing in any one of the three each pay their own. Their sum is how
many units reached the pipeline decision, which sits at or above what the
material pair reports: a unit the pass settles a pipeline for and then
abandons — one whose material group resolved to nothing — counts here and
never reaches the material decision.
Every figure here is the whole frame's, the main camera's draws and every
shadow view's summed together. renderer.shadowViews() splits compacted
and compactedDrawn across the views that made them, and carries the
camera's own share beside them.
Returns { draws: number, instances: number, compacted: number, compactedDrawn: number, materialBinds: number, materialBindsElided: number, materialExtraBinds: number, materialExtraBindsElided: number, pipelineBinds: number, pipelineBindsElided: number }
local d = renderer.drawStats(); print(d.instances / math.max(d.draws, 1), "instances per draw")
print(renderer.drawStats().compactedDrawn, "of", renderer.drawStats().compacted, "survived the cull")
local d = renderer.drawStats(); print(d.materialBinds, "material binds over", d.materialBinds + d.materialBindsElided, "units")
local d = renderer.drawStats(); print(d.pipelineBinds, "pipeline sets over", d.pipelineBinds + d.pipelineBindsElided, "units")
globals/renderer/feature/create
renderer.feature.create(ref: any?, guid: string?) -> any
Instantiate a render feature so the engine calls its render(ctx) hook
every frame. ref is an AssetRef<renderFeature> whose init.luau returns
{ setup?, render, teardown? }. Returns a live RenderFeatureHandle (its
guid is the stable id, same as mesh/texture handles); tear it down with
renderer:destroy(handle). Pass guid to assign a specific id.
Parameters
refany(optional) — AnAssetRef<renderFeature>, or a string identity/guid resolved viaasset.resolve(ref, "renderFeature").guidstring(optional) — Optional explicit handle guid (minted when omitted).
Returns any — A RenderFeatureHandle.
local h = renderer.feature.create(asset.resolve("acrylic", "renderFeature"))
local h = renderer.feature.create("acrylic")
globals/renderer/feature/destroy
renderer.feature.destroy(handleOrGuid: any?) -> boolean
Tear down a live render feature by its RenderFeatureHandle OR its guid
string — the by-id path for when the handle was lost (e.g. across execute
calls). Same effect as renderer.destroy(handle). Returns true if a feature
was live under that id.
Parameters
handleOrGuidany(optional) — ARenderFeatureHandleor itsguidstring.
Returns boolean
renderer.feature.destroy("eb623975-d8bd-47a1-a0a4-5c0e56238e2f")
globals/renderer/feature/list
renderer.feature.list() -> { { guid: string, identity: string } }
List every render feature currently live (running its render(ctx) each
frame). Each entry is { guid, identity } — the guid is the same id a
RenderFeatureHandle carries, so you can tear a feature down by guid even
after losing its handle (e.g. across separate execute calls).
Returns { { guid: string, identity: string } }
for _, f in renderer.feature.list() do print(f.identity, f.guid) end
globals/renderer/feature/shaded
renderer.feature.shaded() -> { [string]: number }
How many pixels each fragment pass a render feature enqueued shaded on
the last drawn frame, keyed by the pass's shader/effect name. A fragment
pass draws one triangle over its target, so it shades the whole screen
whatever its effect actually reaches — unless it declares bounds on the
pass spec, the world-space box its effect stays inside, in which case it
shades the rectangle that box projects into for the camera drawing it and
is skipped for a camera that cannot see the box at all. This is the reading
that says which of the two a pass is: it moves when the effect moves, and a
pass absent from it shaded nothing. Summed over every camera the frame drew.
Returns { [string]: number } — { [shader: string]: number } — pixels shaded, last drawn frame.
local px = renderer.feature.shaded(); for name, n in pairs(px) do print(name, n) end
globals/renderer/featureTexture/configure
renderer.featureTexture.configure(width: number, height: number, layers: number)
Size the shared feature-texture array — the layers a surface shader reads
through zero_feature_texture(uv, layer), and the layers a SpotLight
projects through its cone via cookieLayer. Layers are rgba16f.
A call for the size the array already has is left alone. One that changes
the size reallocates, and the replacement is zeroed — so it empties every
layer in the array, including the layers other features and other cookies
own. renderer.featureTexture.state() reports the extent and the layers
holding content, which is how a feature re-fills the layer a resize took
from it.
Parameters
widthnumber— Layer width in pixels.heightnumber— Layer height in pixels.layersnumber— How many layers the array holds.
renderer.featureTexture.configure(512, 512, 4)
globals/renderer/featureTexture/setLayer
renderer.featureTexture.setLayer(layer: number, textureKey: string, x: number, y: number)
Copy a texture already on the GPU into one layer of the shared array,
its top-left corner at (x, y) — GPU to GPU, with no readback. Several
small images pack into one layer by calling this once per image at
different offsets. The source must be rgba16f and fit at that offset.
Parameters
layernumber— Which layer of the array to write into.textureKeystring— The source texture's name — the one it was created under. Acompute.createStorageTexture2Dtarget, acompute.createTextureHistorypair (its current side), and a texture acompute.copyBufferToTexturewrote all answer to the name they were given.xnumber— Left edge of the destination rectangle, in pixels.ynumber— Top edge of the destination rectangle, in pixels.
compute.createStorageTexture2D("gobo", { width = 256, height = 256, format = "rgba16f" })
renderer.featureTexture.setLayer(0, "gobo", 0, 0)
globals/renderer/featureTexture/state
renderer.featureTexture.state() -> {
What the shared feature-texture array is right now: the extent every
layer carries, and filled, the ascending 0-based indices of the layers a
setLayer has landed in since the array was last sized. One array is
shared by every feature and every light cookie in the scene, and it has no
allocator, so this is the call that tells a feature whether the array it
sized and filled is still the array it is writing into — a configure that
changed the size reallocates and zeroes every layer, and the layer it
emptied leaves filled without it. Measured off the renderer at the end of
the last rendered frame, so a configure or setLayer issued this frame
reads back on a later one.
What this describes is the array a shader samples. The source texture a
setLayer copied FROM is a GPU resource of its own and keeps the bytes it
was written with for as long as it lives, so filled is the reading that
answers whether the layer behind a cookieLayer is live right now.
Returns { width, height, layers, filled }
local ft = renderer.featureTexture.state()
print(("feature textures: %dx%d over %d layers"):format(ft.width, ft.height, ft.layers))
-- Re-fill the cookie layer this module owns if anything emptied it.
if ft.width ~= myWidth or table.find(ft.filled, myLayer) == nil then
refillMyCookie()
end
globals/renderer/framePacing
renderer.framePacing() -> FramePacing?
How far the CPU is allowed to run ahead of the GPU, and what holding it there cost the frame just finished. Submitting work to the GPU returns before the GPU has done it, and everything that submission holds — its staging allocations, its bind groups, its command buffer — stays alive until it completes. A frame that asks for more work than the GPU finishes in a frame's time therefore leaves that behind it, and unbounded that is memory growth rather than a lower frame rate.
framesInFlight is how many submitted frames have not reported done
through the queue's completion signal, held under maxFramesInFlight: a
device that keeps up reads under the bound, one that is behind reads at it.
It counts submissions, which is its own quantity — how many presented
images the swapchain permits in flight is a separate setting.
mechanism names how that bound is enforced
here: submission-wait waits for the frame that many frames back and
reports the wait in waitMs, so a paced frame costs latency and still
draws; submitted-work-done counts outstanding frames off the queue's
completion signal and declines to start a frame while the bound is met,
counting those in pacedFrames and leaving the last presented image up.
submittedFrames counts the frames that were admitted and submitted, so it
rises for as long as the renderer is producing frames — which is what tells
a renderer running slowly under a tight bound from one that has stopped.
stalled reads true while that completion signal has stopped arriving and
the pacer stood down rather than hold the image indefinitely; it clears on
the first frame that finds the count back under the bound.
producing is whether the renderer is drawing frames at all. A headless
renderer draws into an offscreen framebuffer that nothing presents, so its
image reaches a reader only through something that copies it out: it draws
while a consumer is asking — an MCP call in flight, a queued texture
readback, a recording, a frame-egress session — and declines the frames
between two asks, counting them in idleSkippedFrames. Every other
renderer stat answers with the last frame that drew, so producing is what
separates a live reading from a frozen one. A windowed renderer presents
every frame it draws and reads producing = true throughout.
presentMode is what the surface presents with and presentModes what it
offers; both are empty of meaning on a headless renderer, which never
presents.
Returns FramePacing? — { framesInFlight, maxFramesInFlight, pacedFrames, submittedFrames, waitMs, mechanism, stalled, producing, idleSkippedFrames, presentMode, presentModes }, or nil before the renderer has drawn a frame
local p = renderer.framePacing()
print(("%d of %d frames in flight, paced by %s"):format(p.framesInFlight, p.maxFramesInFlight, p.mechanism))
globals/renderer/getRaytrace
renderer.getRaytrace() -> boolean
Whether ray tracing is currently enabled.
Returns boolean
globals/renderer/gpuMemory
renderer.gpuMemory() -> GpuMemory
Where the renderer's GPU memory went at the last completed frame — the call to reach for when something is holding memory and you do not know what.
Three figures answer three different questions, and they are meant to be read against each other:
- The categories —
shadow,textures,meshes,instances,compute, summing tocategorised— are the renderer's own accounting of what it asked for on purpose. Always present, on every backend. allocatoris the device allocator's ledger, with a row per creation label largest first, which is what names an allocation no category claims. It exceedscategorisedby the per-frame render targets and the scratch nothing categorises. The allocator hands memory out from blocks it reserves whole from the device and returns a block only once nothing is left in it, soreservedBytesruns aboveallocatedBytesby what those blocks hold unused;blockslists them emptiest first with the labels that keep each one alive, andemptyBytesplusslackBytesis that distance exactly — the pool held in empty blocks, and the room pinned inside blocks something still sits in.driver.deviceLocalBytesis what the graphics driver charges this process, out of the kernel's own accounting. It is the biggest of the three and the one that fills a card, because it also holds the swapchain, the images the driver keeps on the renderer's behalf, and the rounding to whole pages and heap blocks that neither figure above sees. Read it when the question is how much of the machine's GPU this engine is using; read the two above when the question is what the engine spent it on. A platform with no per-process accounting reportsavailable = falseand the reason.driver.outsideAllocatorBytesis that charge less everything the allocator reserved — what the driver holds on its own account, and the one figure here nothing releases: a dropped pipeline, another scene andrenderer.collect()all leave it where it is, and it falls when the device is destroyed. Read it when a session's device memory has grown and no ledger row accounts for the growth.
compute is what the compute subsystem holds; compute.observe() names
each of those resources and what it costs. renderTargets counts the
offscreen render targets the renderer holds at that frame, which is what
says a renderer.destroy has been applied rather than queued.
Returns GpuMemory — The accounting — see GpuMemory. The category figures are zeroed until the renderer has published its first frame; driver is read as the call runs and answers from the first.
local m = renderer.gpuMemory()
print(("shadows %.1f MiB"):format(m.shadow.total / 1024 / 1024))
-- how much of the card this engine is holding:
if m.driver.available then
print(("driver %.1f MiB"):format(m.driver.deviceLocalBytes / 1024 / 1024))
else
print("no driver figure: " .. m.driver.reason)
end
-- the biggest allocation in the engine:
local top = m.allocator and m.allocator.byLabel[1]
print(top and top.label, top and top.bytes)
-- the block held for the least, and what pins it:
local block = m.allocator and m.allocator.blocks[1]
if block and block.allocationCount > 0 then
print(block.size, block.allocatedBytes, block.byLabel[1].label)
end
globals/renderer/hold
renderer.hold(handleOrKind: any?, id: string?) -> boolean
Pin a runtime resource for the session. A held texture, material, mesh
or render feature survives every collection — the one a root scene load
runs and a direct renderer.collect() alike — until renderer.release
lets it go or its destroy frees it. It is the way to keep an ad-hoc
resource across the scenes that come and go under it. A hold keeps the
resource in the registry; a mesh's GPU buffers are governed by what draws
it, parked as a CPU definition when the last instance naming it goes and
brought back when one names it again, so renderer.mesh.isResident(guid)
is the separate question about the buffers.
Parameters
handleOrKindany(optional) — The resource's handle, or its kind ("texture","material","mesh","feature") with the guid or key as the second argument.idstring(optional) — The guid or key, when the first argument is a kind.
Returns boolean true when the registry knows the resource.
renderer.hold(tex)
renderer.hold("material", "swatch")
globals/renderer/instanceData/clear
renderer.instanceData.clear(target: string | entityRef)
Drop every lane of an entity's per-instance shader data, so its draws read zero again — how a feature releases a subject it is still holding. Despawning an entity releases its block too, so this is for a subject that stays. It takes an entity that has already gone, which is when a feature releasing its subjects often runs, and does nothing for an entity holding no block.
Parameters
targetstring | entityRef— The entity — a proxy fromentity(...)/entity.spawn(...), or an entity-id string.
renderer.instanceData.clear(subject)
globals/renderer/instanceData/laneCount
renderer.instanceData.laneCount() -> number
How many vec4 lanes each entity's per-instance block holds, so a lane
index runs 0 .. laneCount() - 1. The same count a surface shader indexes
input.shader_data against.
Returns number — lanes per entity.
for lane = 0, renderer.instanceData.laneCount() - 1 do
renderer.instanceData.set(subject, lane, 0)
end
globals/renderer/instanceData/set
renderer.instanceData.set(target: string | entityRef, lane: number, x: number, y: number?, z: number?, w: number?)
Write one vec4 lane of an entity's per-instance shader data — the
channel that lets ONE material serve many entities that differ in a value.
A surface shader reads the lane back as input.shader_data[lane], so a
dissolve at its own progress per subject, an effect at its own age per
firing, or a per-entity mask costs one material rather than one material
per entity.
The engine attaches no meaning to a lane: a feature picks the lane indices it owns and packs whatever its shader agrees they carry. Name those indices in the module that writes them, so the writer and the shader read the block the same way.
The write reaches the block where it is called, so the entity it names is the one holding that id at that point in the tick, and the value is on the draw from the next frame. It is held until the lane is written again, the entity's block is cleared, or the entity is despawned — a despawned entity releases its whole block. A lane an entity was never given reads zero.
Parameters
targetstring | entityRef— The entity — a proxy fromentity(...)/entity.spawn(...), or an entity-id string. It must name a live entity.lanenumber— Whichvec4lane to write,0 .. laneCount() - 1.xnumber— The lane's.x.ynumber(optional) — The lane's.y. Defaults to 0.znumber(optional) — The lane's.z. Defaults to 0.wnumber(optional) — The lane's.w. Defaults to 0.
-- One dissolve material, each subject at its own progress.
local DISSOLVE_LANE = 0
for _, subject in ipairs(dying) do
renderer.instanceData.set(subject.entity, DISSOLVE_LANE, subject.progress)
end
globals/renderer/loseDevice
renderer.loseDevice()
Destroy the render device on the next frame, so the engine meets a real device loss.
This is the one loss that can be caused on purpose, and it travels the same
path a driver reset does: frames draw nothing until the rebuild lands,
GET /engine/status reports the renderer as recovering while it does,
engine.onDeviceRebuilt fires afterwards, and renderer.deviceGeneration()
moves. Use it to prove that a world's content survives a device loss —
anything it holds only on the GPU has to be remade from the rebuild hook, and
this is how you find out whether it is.
renderer.loseDevice()
-- some frames later:
print(renderer.deviceGeneration()) -- one higher than before
globals/renderer/mainCameraView
renderer.mainCameraView() -> { number }?
The main camera's inverse view-projection (column-major, 16 numbers)
followed by its world position (3 numbers) — {m0..m15, px,py,pz} — for
reconstructing world positions from the depth buffer in a ray-tracing pass.
Nil before the first render.
Returns { number }? 19 numbers, or nil.
globals/renderer/material/animatedTexture
renderer.material.animatedTexture(texture: string | AssetRef, opts: { [string]: any }?) -> MaterialHandle
Build a material that PLAYS a layered texture: its layers bound as the
frames, its timing bound beside them, and the engine's animatedTexture
shader turning the clock into the layer showing now. One call from an
imported animated image to a material an entity can wear.
The layer showing is resolved per pixel against the texture's own schedule,
so frames of unequal length are shown for the lengths they were authored
with, and the sequence loops. speed scales the clock (2 plays twice as
fast, 0 holds the frame startTime lands in) and startTime offsets into
the sequence, so two surfaces sharing one texture can run out of phase.
The clock is the engine's, and it runs in edit mode as much as in play and
through a pause, so two screenshots of one surface taken moments apart are
two different frames of it. speed = 0 holds one frame for as long as it
is set, which is the state to compare two screenshots in.
The returned handle is what a surface wears — Model:applySessionMaterial
takes it, and so does a Model's material field. The handle's guid is
this material's REGISTRY KEY, the currency of setProperty, describe and
destroy; a component field resolves an asset, so a bare key in one leaves
the component waiting for an asset to register under that name.
The builtin plane mesh emits uv = (u, v) with v along its own +Z, so
a quad pitched +90° about X (Transform.eulerToQuat(0, math.pi / 2)) shows
the image upright to a camera on +Z, and -90° shows it first-row-last.
A texture whose layers carry no timing is rejected — there is nothing to
play. renderer.texture.info(bytes).animated is the test.
Parameters
texturestring | AssetRef— The texture — a guid, an identity, a name, a path, or a textureAssetRef.opts{ [string]: any }(optional) —{ key?, speed?, startTime?, alphaCutoff?, baseColor?, uvScale?, uvOffset? }.
Returns MaterialHandle
local mat = renderer.material.animatedTexture("banner.texture")
local id = entity.spawn("billboard", { rotation = { Transform.eulerToQuat(0, math.pi / 2) } })
entity(id).component.add("Model", { model = "plane" })
entity(id).component.get("Model"):applySessionMaterial(mat)
renderer.material.setProperty(mat.guid, "speed", 2)
globals/renderer/material/create
renderer.material.create(content: MaterialContent, key: string) -> MaterialHandle
Parameters
contentMaterialContentkeystring
Returns MaterialHandle
globals/renderer/material/describe
renderer.material.describe(key: string | { [string]: any } | AssetRef) -> any
The recoverable definition ({ shader, properties, textures, name })
this module registered under key via renderer.material.create, or nil
for keys registered elsewhere (e.g. material assets resolved by the
assetType). properties and textures carry the material's current values:
each setProperty / setTexture write lands on this record, a texture slot
under the GPU key the slot binds by — these are the WRITES, held here
whether or not the renderer took them up. renderer beside them is what the
renderer holds for the same key: the program its prepared bind group was
built against, the render state its draws are looked up under, whether a
pipeline exists for that key, and how many draws the observed frame gave
it. renderer is nil when the renderer holds no material under this key at
all, and resident states the same fact as a boolean. Writes reach the
screen through both halves: resident = false says the renderer holds
nothing to put them in, and renderer.draws = 0 on a resident material
says it holds them and no renderable is drawing with it. For a material
that is resident AND drawn and still looks wrong,
renderer.drawDiagnostics() names the renderable and the cause.
Parameters
keystring | { [string]: any } | AssetRef— The material's registry key, theMaterialHandlefromrenderer.material.create, or anAssetReffromasset.resolve.
Returns any — MaterialContent? with resident: boolean and renderer: MaterialObservation? fields
globals/renderer/material/destroy
renderer.material.destroy(key: string | { [string]: any } | AssetRef) -> boolean
Drop a runtime material registered via renderer.material.create: clears
its recoverable definition, unregisters its runtime-resource stamp so it is no
longer swept into the material freeze/save flow, and frees the GPU record. Use
for transient materials (e.g. a preview swatch) that must not outlive their use.
The on-disk asset, if any, is untouched.
The reach is the registry: after this, describe and list stop answering
for the key. A surface already wearing the handle goes on drawing what it
was given — Model:restoreSessionMaterial is what puts a Model back on its
authored material.
Parameters
keystring | { [string]: any } | AssetRef— The material's registry key (the one passed tocreate), theMaterialHandlecreatereturned, or anAssetReffromasset.resolve.
Returns boolean true when a definition was known under key.
renderer.material.destroy("__preview_swatch_" .. texGuid)
globals/renderer/material/list
renderer.material.list() -> { any }
Every runtime material currently registered, ordered by registry key.
Each entry carries the key, where it came from, and the shader it binds.
renderer.references("material", key) says what is still holding a row,
and renderer.collect() releases the rows nothing holds.
Returns { any } — Array of { guid, origin, owner?, shader? }.
for _, m in ipairs(renderer.material.list()) do print(m.guid, m.shader) end
globals/renderer/material/renderState
renderer.material.renderState(key: string | { [string]: any } | AssetRef) -> MaterialObservation?
What the renderer holds for a material, which is a different document
from the values written to it. shader is the program its prepared bind
group was built against, renderState the blend / cull / topology / queue /
depth key its draws are looked up under, keyBuilt whether a pipeline
exists for that key, and draws / instances / placeholderDraws /
binds / bindsElided what it cost in the frame the renderer last
observed — those five read 0 until something arms per-draw recording, which
renderer.materialCost() and
renderer.drawDiagnostics() do. nil means the renderer holds no material
under this key at all — the writes landed on a record nothing is drawing
with.
Parameters
keystring | { [string]: any } | AssetRef— The material's registry key, theMaterialHandlefromrenderer.material.create, or anAssetReffromasset.resolve.
Returns MaterialObservation?
local r = renderer.material.renderState("water"); print(r.renderState.blend, r.keyBuilt)
globals/renderer/material/sessionKeyFor
renderer.material.sessionKeyFor(entityId: string) -> string
The canonical registry key for an entity's SESSION material — the
runtime material a system (e.g. GI baking) shows on an entity in place
of its authored material for the lifetime of the engine session. One
session material per entity: create it under this key, hand the handle
to Model:applySessionMaterial, and the component re-adopts it across
VM reloads by probing this key with describe. The key names the entity
for as long as the entity stands: once it is gone the session store lets
the handle go, and a collection releases the material and whatever its
bindings were the last to hold.
Parameters
entityIdstring— The entity carrying the material.
Returns string — The registry key string.
local key = renderer.material.sessionKeyFor(entityId)
globals/renderer/material/setProperty
renderer.material.setProperty(key: string | { [string]: any } | AssetRef, name: string, value: any?) -> ()
Push one changed uniform property to a registered material's GPU record
(frame-fast incremental update; no re-register). Keyed by the material's
registry key. The value written becomes the material's current one: it is
what describe reports, and — for a property the material's shader
declares, which is what the uniform buffer is packed by — what a material
AssetRef reads back through getProperty / getProperties and what the
surface is drawn with. A write under any other name reaches the record
describe reports, which is where it reads back.
Parameters
keystring | { [string]: any } | AssetRef— The material's registry key, theMaterialHandlefromrenderer.material.create, or anAssetReffromasset.resolve.namestring— Property name.valueany(optional) — New value.
Returns ()
renderer.material.setProperty(asset.resolve("wall", "material"), "roughness", 0.2)
globals/renderer/material/setTexture
renderer.material.setTexture(key: string | { [string]: any } | AssetRef, slot: string, ref: string | { [string]: any } | AssetRef) -> ()
Push one changed texture slot to a registered material's GPU record. Keyed by the material's registry key.
Parameters
keystring | { [string]: any } | AssetRef— The material's registry key, theMaterialHandlefromrenderer.material.create, or anAssetReffromasset.resolve.slotstring— Texture slot name ("base_color_texture", …).refstring | { [string]: any } | AssetRef— Texture reference — a.textureguid / identity / name / path, the image path it was imported from, acolor:/default:form, a live GPU handle, or a textureAssetRefcarrying one. An asset reference is materialised (Disk→CPU→GPU) and bound by the key the upload lands under.
Returns ()
renderer.material.setTexture("sky", "sky_texture", "panorama.texture")
globals/renderer/materialCost
renderer.materialCost() -> { MaterialObservation }
What each material cost the frame the renderer last drew, and the state
it holds each one under. One row per material the renderer holds a prepared
bind group for — a material an author wrote and the renderer never prepared
is absent, which is itself the answer to "why is nothing I set reaching the
screen". draws and instances cover that one frame; placeholderDraws
is how many of those draws bound the magenta placeholder instead of this
material's own program; binds is how many material-owned bind groups the
frame's passes SET for it and bindsElided how many of its draws wanted a
group the pass already held, which is what draw-key sorting buys; a draw
that fell back to the placeholder bound the placeholder's group, so it
counts in placeholderDraws and in neither bind count. uniformBytes is
the GPU uniform buffer's own size,
which is the reflected property block raised to the 16-byte floor and
rounded up to the copy alignment. renderer.drawDiagnostics() names WHICH
renderable is not drawing what its material says, and why.
Returns { MaterialObservation }
for _, m in renderer.materialCost() do print(m.material, m.draws, m.binds, m.bindsElided) end
globals/renderer/materialIdentity
renderer.materialIdentity() -> MaterialIdentity
Which material each renderable draws with, as a number a shader can
carry. A material is authored and bound by name, and no shader can read a
string — so every renderable's per-instance record holds a material index
instead. slots is the name → index table those indices are drawn from: an
index is assigned the first time the renderer draws with that material and
does not move afterwards, so two renderables that differ only in material
read different indices, and one renderable reads the same index frame after
frame. It follows that the table keeps a row for every material name drawn
this session, whether or not anything still draws with it. renderables is
a row per renderable that owns a GPU slot — the entity it belongs to, that
slot, and the index the record at it carries; populations is the same for
an instanced draw, whose whole reserved run of slots carries the one
material its registration named. That index is what a shader reads as
instance_data[slot].material_index, and the row a ray hit resolves
through zeroMaterial(). A renderable draws with the material its entity
references, so one whose entity names none carries index 0.
Returns MaterialIdentity — { slots: { [string]: number }, renderables: { { entity: string, slot: number, index: number, material: string } }, populations: { { slot: number, count: number, index: number, material: string } } }
local id = renderer.materialIdentity()
for _, r in id.renderables do print(r.entity, r.slot, r.index, r.material) end
globals/renderer/materialIndex
renderer.materialIndex(name: string) -> number?
The index standing for a material, or nil for one the renderer has not
drawn with yet. Pass it to a shader (or compare it against what a shader
read out of instance_data[slot].material_index) to tell which material a
drawing instance carries.
Parameters
namestring—stringMaterial name, asrenderer.material.createfiled it.
Returns number?
local red = renderer.materialIndex("brick_red")
globals/renderer/maxAnisotropy
renderer.maxAnisotropy() -> number
The highest anisotropy this device honours: 16 on hardware that filters
anisotropically, 1 on hardware that does not, where a higher request would
be downgraded to trilinear regardless. Read it to report quality honestly —
renderer.setAnisotropy clamps for you, so a request never needs guarding.
Returns number — The device ceiling, 1 or 16.
local best = renderer.maxAnisotropy()
globals/renderer/mesh/boundsSource
renderer.mesh.boundsSource(mesh: string | { [string]: any } | AssetRef) -> string
Where this mesh's culling bounds come from. "compute" once a compute
pass has written its vertices: the engine reduces those vertices to an AABB
every frame, so the mesh is culled against the geometry the pass produced
wherever it puts it. "geometry" otherwise: the AABB of the geometry the
mesh was created with.
Parameters
meshstring | { [string]: any } | AssetRef— The mesh — aMeshHandle, aMeshCpuHandle, a guid, or a meshAssetRef.
Returns string — "compute" or "geometry".
print(renderer.mesh.boundsSource(mesh))
globals/renderer/mesh/buildClusters
renderer.mesh.buildClusters(mesh: string | { [string]: any } | AssetRef) -> string?
Build a cluster-LOD DAG (Nanite-style virtualized geometry) for the
static CPU mesh held under guid and return its serialized data.clusters
bytes. Returns nil when the mesh is degenerate, and on an engine whose
renderer.mesh.canBuildClusters reports false. Pair with
renderer.mesh.uploadClusters.
Parameters
meshstring | { [string]: any } | AssetRef— A mesh loaded into the CPU store (renderer.mesh.loadCpu) — theMeshCpuHandle, aMeshHandle, a guid, or a meshAssetRef.
Returns string? — Serialized cluster bytes, or nil.
local cb = renderer.mesh.buildClusters(cpu)
globals/renderer/mesh/canBuildClusters
renderer.mesh.canBuildClusters() -> boolean
Whether this engine bakes cluster-LOD hierarchies. It reads the binding
the running engine registered: every target the engine ships on carries the
builder, so a mesh loaded in a browser bakes its own clusters the same way
one loaded natively does, and an engine built without it reports false and
answers nil from renderer.mesh.buildClusters.
Returns boolean — True if renderer.mesh.buildClusters can bake on this platform.
if renderer.mesh.canBuildClusters() then ... end
globals/renderer/mesh/clusterBakeBudget
renderer.mesh.clusterBakeBudget(ms: number?) -> number
The wall time one frame may spend advancing scheduled cluster bakes, in
milliseconds — set first when ms is given. A slice always runs at least
one unit of the build, so the budget bounds what a frame spends by choice
and the largest single unit a mesh imposes sets the floor under it.
Parameters
msnumber(optional) — New per-frame budget in milliseconds, capped at 1000. A value that is not a positive, finite number raises.
Returns number — The budget in force after the call.
renderer.mesh.clusterBakeBudget(2)
globals/renderer/mesh/clusterBakes
renderer.mesh.clusterBakes() -> { [string]: any }
What the scheduled cluster bakes are costing. budgetMs is the slice a
frame may spend, pending how many bakes are queued, completed how many
have finished since the engine started, dropped how many left the queue
because the geometry they were scheduled over stopped being readable, and
heldBytes the source geometry the queue is holding across all of them —
the vertex pool and index run the bake at the head is reading, plus a copy
for each queued mesh the engine holds no definition for.
inFlight is one row per queued bake —
{ guid, cpuMs, frames, slices, bytes, state }: the wall time spent
advancing it, the frames it has been queued for, the slices it has been
advanced by, the geometry it is holding, and "baking" for the one being
advanced against "queued" for the ones waiting their turn.
Returns { [string]: any } — { budgetMs, pending, completed, dropped, heldBytes, inFlight }.
print(renderer.mesh.clusterBakes().heldBytes)
globals/renderer/mesh/clusterComponents
renderer.mesh.clusterComponents(clusterBytes: buffer | string) -> (ClusterComponents?, string?)
Split a cluster blob (from renderer.mesh.buildClusters) into its
GPU-ready component byte pools — the cluster vertex pool, the
geometry-addressing pool (every cluster's local→global vertex map, then
every cluster's triangle bytes), and the per-cluster record array — plus
their counts. A cluster's triangles address positions inside its own vertex
map one byte at a time, and a record's vertexOffset indexes the geometry
pool in u32 elements while its indexOffset indexes it in bytes, so ONE
binding resolves a corner. A pure decode (no GPU work): upload the pools
into buffers a compute shader owns (shaderRef:createBuffer +
buf:writeBytes) to drive a cluster draw from Luau.
Parameters
clusterBytesbuffer | string— Serialized cluster bytes (binary-safe).
Returns (ClusterComponents?, string?) — { vertices, geometry, records, vertexCount, vertexRefCount, triangleBytes, indexCount, clusterCount }, or (nil, err).
local c = renderer.mesh.clusterComponents(cb)
globals/renderer/mesh/clusters
renderer.mesh.clusters(mesh: string | { [string]: any } | AssetRef) -> { [string]: any }?
The shape of the cluster-LOD hierarchy the renderer holds for a mesh:
clusterCount across every level, levelCount with the finest counted as
one, and triangleCount across every cluster. The renderer keys one entry
per mesh that carries a hierarchy, so this answers whether the mesh has
clusters as well as what they are — nil for a mesh that carries none.
Parameters
meshstring | { [string]: any } | AssetRef— The mesh to read — aMeshHandle, aMeshCpuHandle, a guid, or a meshAssetRef.
Returns { [string]: any }? — { clusterCount: number, levelCount: number, triangleCount: number }?
local c = renderer.mesh.clusters(gpu) ; if c then print(c.levelCount) end
globals/renderer/mesh/create
renderer.mesh.create(src: any?, guid: string?) -> MeshHandle
Create (or fetch) a GPU mesh resource and return its MeshHandle. src:
a MeshCpuHandle from meshRef:load() (CPU→GPU upload under the asset's
guid, idempotent — returns the resident handle if already uploaded); raw
geometry {positions, indices, normals?, uvs?, colors?, uvs1?, unwrapUvs?, tangents?, skinning?, skins?} (a new runtime mesh — uvs1 is the lightmap
UV set, unwrapUvs generates one, skinning/skins bind a skeleton); GPU
compute buffers {vertexBuffer, indexBuffer, vertexCount, indexCount, aabbMin?, aabbMax?, prevVertexBuffer?} (size the vertex buffer at
vertexCount * engine.vertexStride bytes, the engine's standard Vertex
layout); or a MeshHandle (returned as-is). NEVER takes an AssetRef —
load the CPU first.
prevVertexBuffer is a second buffer of the same size and layout holding
those vertices as they stood on the previous frame. Naming it is what makes
geometry a compute pass moves report a motion vector: the surface
differences the two streams, so every consumer of screen-space velocity —
motion blur, temporal reprojection — sees the movement. The engine fills it
from the current vertices once per frame, ahead of that frame's compute
dispatches, so a frame in which the pass does not run leaves the two
streams equal and the geometry reports standing still.
morphTargets are the shapes the mesh can blend towards: a list of
{ name?, positions, normals? } records, each holding one offset per vertex
from the base geometry, in the mesh's own vertex order. An entity blends
them with ecs.MorphWeights, weight i scaling target i. A name makes
the shape addressable as itself — renderer.mesh.morphTargets reads the
names back and renderer.mesh.morphWeights drives them by name.
Raw geometry is read against the mesh type's conventions: indices count
vertices from 0, and a triangle's FRONT face is the one whose vertices turn
counter-clockwise as the viewer sees them — cross(v1 - v0, v2 - v0) points
out of it. A material culls its back faces by default, so a triangle wound
the other way draws nothing where it stands; reverse the index triple, or
give the material render = { cull = "none" }, to draw that side. normals
give the surface its outward direction and shade the face; the side that
draws comes from the index order alone. uvs sample (0,0) at the image's
top-left. Model space carries the world's basis: +X right, +Y up, -Z the
direction transform.forward points. guides { path = "types/mesh" } has
the whole table.
A geometry src carrying keepCpu = true also keeps its geometry in the
guid-keyed CPU store, so renderer.mesh.getVertices reads it and
renderer.mesh.setVertices rewrites its positions in place — the per-frame
deformation path, which sends positions alone where renderer.mesh.update
re-sends the whole geometry. renderer.mesh.unloadCpu(mesh) releases that
copy. Without it the geometry lives on the GPU alone and
renderer.mesh.readback(mesh) is what brings it back.
Parameters
srcany(optional) — A MeshCpuHandle, geometry, compute buffers, or a MeshHandle.guidstring(optional) — Optional v4 guid for a NEW runtime mesh (minted Luau-side when absent). Ignored for the CPU-handle path (the asset's guid is used).
Returns MeshHandle
local gpu = renderer.mesh.create(meshRef:load())
local gpu = renderer.mesh.create({ positions = {...}, indices = {...} })
-- a runtime mesh whose positions are rewritten in place each frame
local m = renderer.mesh.create({ positions = P, indices = I, keepCpu = true })
renderer.mesh.setVertices(m, P)
-- a runtime mesh carrying a lightmap UV set (unwrapped at creation)
local gpu = renderer.mesh.create({ positions = {...}, indices = {...}, unwrapUvs = true })
-- a mesh with one shape to blend towards, driven by ecs.MorphWeights
local gpu = renderer.mesh.create({ positions = P, indices = I, morphTargets = { { positions = D } } })
globals/renderer/mesh/decode
renderer.mesh.decode(zmsh: buffer | string) -> (MeshGeometry?, string?)
Decode engine-native ZMSH bytes back into a MeshGeometry. Inverse of
renderer.mesh.encode; each optional stream is present only when the blob
carries it. Takes the bytes themselves — the geometry of a mesh the engine
is holding comes from renderer.mesh.geometry(mesh).
Parameters
zmshbuffer | string— Engine-native ZMSH bytes (binary-safe).
Returns (MeshGeometry?, string?) the geometry, or (nil, errmsg).
local geom = renderer.mesh.decode(meshRef:getBytes())
globals/renderer/mesh/destroy
renderer.mesh.destroy(mesh: string | { [string]: any } | AssetRef) -> boolean
Release the GPU mesh mesh names, the release that pairs with
renderer.mesh.create. Takes every form that names a mesh — the
MeshHandle create returned, the guid renderer.mesh.list hands out, a
MeshCpuHandle or a mesh AssetRef — and routes through
renderer.destroy, the verb that releases any renderer resource by its
kind. The CPU copy, if one was loaded, is freed separately by the CPU
handle's :unload().
Parameters
meshstring | { [string]: any } | AssetRef— The mesh to release — aMeshHandle, a guid, aMeshCpuHandleor a meshAssetRef.
Returns boolean true if a GPU mesh was known under the guid.
local m = renderer.mesh.create(cpu); renderer.mesh.destroy(m)
renderer.mesh.destroy(renderer.mesh.list()[1].guid)
globals/renderer/mesh/drawInstanced
renderer.mesh.drawInstanced(mesh: string | { [string]: any } | AssetRef, opts: any?) -> InstancedDraw
Draw one mesh instanceCount times in a single call, each copy placed by
a world matrix read from a GPU buffer. The population is a renderable in its
own right — it goes through the mesh's ordinary pipeline and the material's
ordinary bind groups, so it appears in the deferred pass, the forward passes
and the shadow maps exactly as an entity-backed draw of that mesh does.
The buffer holds instanceCount column-major 4x4 matrices, 64 bytes
each, tightly packed — the layout a vertex shader reads as
array<mat4x4<f32>>, which puts each matrix's translation in its LAST four
floats (Lua indices 13/14/15 for x/y/z). Packing row-major transposes every
instance.
The matrices are COPIED into the engine's transform slots once per frame, which is what buys that full-pass parity. Rewrite the buffer between frames and the instances move — no re-registration, no re-upload.
material is what the population draws with, and it is required: a
MaterialHandle (matRef:handle()), an AssetRef, or a registry key.
instanceDataBuffer names a second buffer, holding 64 bytes per instance —
four vec4 lanes, tightly packed, in instance order. Those lanes arrive in
the fragment stage as zero_object_data(in.instance_id, lane), the same
read a per-entity __instancedata block answers, so the members of one
population can differ in whatever their material's shader agrees the lanes
carry. Copied every frame like the transforms, from a buffer a compute pass
writes: the values never touch the CPU. Omit it and the lanes read zero.
reserveCount sizes the reservation above instanceCount so
renderer.mesh.setInstanceCount can raise the drawn count later without
re-registering; both buffers must back the reservation, not just the count.
mobility states whether the copies stand still — "static", or
"movable" when it is left out. It is what a scene gather collecting
geometry for precomputed lighting admits a population on, the same
declaration Model.mobility makes for an entity: the transforms live in a
buffer anything may rewrite between frames, so a population that says
nothing is taken as one that moves.
Parameters
meshstring | { [string]: any } | AssetRef— The mesh the population draws — aMeshHandle, the guidrenderer.mesh.listhands out, aMeshCpuHandleor a meshAssetRef. A registration holds the mesh on the device for as long as it lives, and takes a mesh that is currently held off the device — one nothing displays — back onto it.optsany(optional) —{ transformBuffer, instanceCount, material, instanceDataBuffer?, reserveCount?, renderLayer?, castsShadows?, mobility? }.
Returns InstancedDraw — An InstancedDraw handle for instanceInfo / setInstanceCount / dropInstanced.
local m = renderer.mesh.create({ positions = ..., indices = ... })
local buf = substrate.createBuffer({
name = "crowd.xf", type = "mat4", len = 64, kind = "gpu",
})
-- Column-major: translation lives at indices 13/14/15.
local xf = {}
for i = 0, 63 do
local m4 = { 1,0,0,0, 0,1,0,0, 0,0,1,0, i * 2, 0, 0, 1 }
for _, v in ipairs(m4) do xf[#xf + 1] = v end
end
buf:write(xf)
local rock = asset.resolve("rock", "material"):handle()
local draw = renderer.mesh.drawInstanced(m, { transformBuffer = "crowd.xf", instanceCount = 64, material = rock })
globals/renderer/mesh/dropClusters
renderer.mesh.dropClusters(mesh: string | { [string]: any } | AssetRef) -> boolean
Detach a mesh's cluster-LOD hierarchy and cancel a bake still in flight
for it, so the renderer holds none for it. The inverse of
renderer.mesh.uploadClusters.
Parameters
meshstring | { [string]: any } | AssetRef— The mesh to detach — aMeshHandle, aMeshCpuHandle, a guid, or a meshAssetRef.
Returns boolean — True if a hierarchy was attached or a bake was in flight.
renderer.mesh.dropClusters(gpu)
globals/renderer/mesh/dropInstanced
renderer.mesh.dropInstanced(draw: InstancedDraw) -> boolean
Release an instanced-draw registration and the transform slots it
reserved. The mesh and the transform buffer outlive it — destroy those
through renderer.destroy and the buffer handle's :destroy().
Parameters
drawInstancedDraw— TheInstancedDrawto release.
Returns boolean — True if a registration was live under the handle.
renderer.mesh.dropInstanced(draw)
globals/renderer/mesh/encode
renderer.mesh.encode(geom: MeshGeometry) -> (string?, string?)
Encode raw geometry into engine-native ZMSH bytes (the on-disk mesh
payload). The CPU codec behind the mesh assetType's onCreate. Every stream
the format carries — including tangents, per-vertex skinning, and the
skeleton — round-trips back through renderer.mesh.decode. This pair moves
DATA the caller is holding; the geometry of a mesh the ENGINE is holding
comes from renderer.mesh.geometry(mesh).
Parameters
geomMeshGeometry—MeshGeometry— flat per-vertex float / u32 arrays plus optionalskinningandskins.
Returns (string?, string?) engine-native ZMSH bytes (binary-safe), or (nil, errmsg) naming what the geometry could not describe.
local bytes = renderer.mesh.encode({ positions = {...}, indices = {...} })
local bytes = renderer.mesh.encode(renderer.mesh.geometry(meshHandle))
globals/renderer/mesh/encodeCpu
renderer.mesh.encodeCpu(mesh: string | { [string]: any } | AssetRef) -> string
Encode a mesh's resident CPU copy into ZMSH bytes. Reads the ONE
guid-keyed CPU store — meshRef:load() populates it for assets, and
renderer.mesh.readback(mesh) populates it for a runtime mesh. Errors
loudly when the mesh has no resident CPU copy.
Parameters
meshstring | { [string]: any } | AssetRef— The mesh — aMeshHandle, aMeshCpuHandle, a guid, or a meshAssetRef.
Returns string ZMSH bytes.
local bytes = renderer.mesh.encodeCpu(handle)
globals/renderer/mesh/geometry
renderer.mesh.geometry(mesh: string | { [string]: any } | AssetRef) -> MeshGeometry
The complete geometry of a mesh the engine is holding, as a
MeshGeometry — the same shape renderer.mesh.create and
renderer.mesh.encode take, carrying every stream the mesh has
(positions, indices, and whichever of normals, uvs, colors,
uvs1, tangents, skinning, skins it was built with). The read that
pairs with create: hand it the MeshHandle create returned and get the
vertex data back. Reads the resident CPU copy when there is one; for a
runtime mesh that lives only on the GPU it reads the geometry back off the
GPU first (yielding a frame or two) and leaves CPU residency as it found it.
An optional stream is present only when the mesh carries one, so uvs1 == nil is the answer to whether it has a second UV set. The drawable mesh
the renderer holds carries the tangent basis its positions, uvs and normals
determine — supplied by the caller, or derived at the ingest that made it
drawable — and that is what the GPU read gives back. The CPU store answers
with the streams the bytes it decoded hold, so a .mesh written without a
tangent stream reads back tangents == nil for as long as a CPU copy of it
is resident.
Parameters
meshstring | { [string]: any } | AssetRef— The mesh — aMeshHandle, aMeshCpuHandle, a guid, or a meshAssetRef.
Returns MeshGeometry
local geom = renderer.mesh.geometry(renderer.mesh.create({ positions = P, indices = I }))
local tangents = renderer.mesh.geometry(handle).tangents
globals/renderer/mesh/getVertices
renderer.mesh.getVertices(mesh: string | { [string]: any } | AssetRef) -> { any }
Read the vertices of a mesh's resident CPU copy — one entry per vertex,
{ pos = {x,y,z}, normal = {x,y,z}, uv = {u,v} }. Reads the resident CPU
store directly (no re-decode). Errors when the mesh has no resident CPU copy
— renderer.mesh.geometry(mesh) is the read that works wherever the mesh
lives, and returns the tangent, colour and skinning streams too.
Parameters
meshstring | { [string]: any } | AssetRef— The mesh — aMeshHandle, aMeshCpuHandle, a guid, or a meshAssetRef.
Returns { any } — { { pos: {x,y,z}, normal: {x,y,z}, uv: {u,v} }, ... } Each record carries pos, normal, uv and uv1 as named channels ({ x = , y = , z = }). Geometry going the other way — into renderer.mesh.create — is parallel flat arrays (positions, normals, uvs), and create accepts this record list under vertices so a mesh read back here can go straight into a new one.
globals/renderer/mesh/instanceInfo
renderer.mesh.instanceInfo(draw: InstancedDraw) -> InstancedDrawInfo?
What a live instanced-draw registration is drawing: which mesh, which transform buffer, which per-instance data buffer if it named one, how many instances, and how many slots it reserved. Returns nil once the registration has been dropped.
status is what the renderer did with it. The fields above it are the
request, made a stage before the renderer sees it; status is the answer:
"drawing" for a registration the renderer is drawing, "refused" for one
it turned away — error carries its reason — and "pending" for the frame
between the call and the renderer answering. So a registration whose copies
are not being drawn says so here.
Parameters
drawInstancedDraw— TheInstancedDrawto report on.
Returns InstancedDrawInfo? — The registration record, or nil.
print(renderer.mesh.instanceInfo(draw).instanceCount)
local info = renderer.mesh.instanceInfo(draw)
if info.status == "refused" then error(info.error) end
globals/renderer/mesh/instanceTransforms
renderer.mesh.instanceTransforms(draw: InstancedDraw) -> any
Read back the world matrices a registration's drawn copies are placed
by: instanceCount matrices of 16 floats, column-major and tightly
packed, in the layout the transform buffer holds them. The read is of the
buffer as it stands when it runs, so a population a compute pass rewrites
every frame answers with the placement of the frame the read lands in.
Parameters
drawInstancedDraw— TheInstancedDrawwhose copies to locate.
Returns any — A Readback to poll — :ready() then :result() — or nil for a registration that is no longer live.
local pending = renderer.mesh.instanceTransforms(draw)
while not pending:ready() do task.wait() end
local floats = pending:result()
globals/renderer/mesh/isCpuResident
renderer.mesh.isCpuResident(mesh: string | { [string]: any } | AssetRef) -> boolean
True if this mesh has a resident CPU copy in the guid-keyed CPU store.
Parameters
meshstring | { [string]: any } | AssetRef— The mesh — aMeshHandle, aMeshCpuHandle, a guid, or a meshAssetRef.
Returns boolean
if renderer.mesh.isCpuResident(handle) then ... end
globals/renderer/mesh/isResident
renderer.mesh.isResident(mesh: string | { [string]: any } | AssetRef) -> boolean
True if a GPU mesh is resident under this mesh's guid — the device
holds its buffers, or the upload pass is still going to hand them over.
This is the store the draw paths are gated on, so a mesh this reports
resident is one renderer.mesh.drawInstanced and a Model can draw.
Parameters
meshstring | { [string]: any } | AssetRef— The mesh — aMeshHandle, aMeshCpuHandle, a guid, or a meshAssetRef.
Returns boolean
print(renderer.mesh.isResident(handle))
globals/renderer/mesh/list
renderer.mesh.list() -> { any }
Every mesh currently registered, ordered by guid — the ones a script
created and the ones that reached the device through an asset alike.
Answers "which mesh is this?" when all that is known is a size: each entry
carries the guid, the vertex/index counts it was created with, where it came
from (origin is "asset" for a mesh the asset path uploaded), whether the
GPU still holds it, and where its culling bounds come from (boundsFrom is
"compute" for a mesh a compute pass writes). A resident entry also carries
the bytes its buffers cost. bytes is the mesh's whole VRAM footprint and
is the sum of the THREE buffer columns beside it — vertexBytes + vertexStorageBytes + indexBytes, where the storage column is the same
vertices bound as a storage buffer for the passes that read them that way.
Summing only the vertex and index columns understates a mesh by its vertex
size. The bytes column is what sums to the meshes category of
renderer.gpuMemory().
renderer.references("mesh", guid) says what is still holding a row, and
renderer.collect() releases the rows nothing holds.
Returns { any } — Array of { guid, vertexCount?, indexCount?, origin, owner?, resident, boundsFrom, bytes?, vertexBytes?, vertexStorageBytes?, indexBytes?, primitives?, revision? }.
for _, m in ipairs(renderer.mesh.list()) do print(m.guid, m.bytes) end
globals/renderer/mesh/listInstanced
renderer.mesh.listInstanced() -> { InstancedDrawInfo }
Every instanced-draw registration this engine is drawing, in
registration order. Each record is what instanceInfo answers with, and
carries a draw handle of its own — so a population whose handle its
caller no longer holds is reached here and released, resized or read like
any other.
Returns { InstancedDrawInfo } — An array of registration records; empty when nothing is registered.
for _, pop in ipairs(renderer.mesh.listInstanced()) do
renderer.mesh.dropInstanced(pop.draw)
end
globals/renderer/mesh/loadCpu
renderer.mesh.loadCpu(ref: string | AssetRef) -> MeshCpuHandle
Load a .mesh asset's geometry into the ONE guid-keyed CPU store (the
Disk→CPU step) and return a CPU handle. The handle holds NO geometry — only
the guid plus counts and the per-handle read/encode/unload ops (which read
the Rust-side store). Called by meshRef:load(). DEFAULT lifecycle: upload
to the GPU then handle:unload(); the store is populated only by this call.
Parameters
refstring | AssetRef— A meshAssetRef(carries.guidand reads its primary via getBytes), or any stringasset.refresolves to one — the guidencodeCputakes, an identity, a name or a source path.
Returns MeshCpuHandle
local cpu = meshRef:load(); local gpu = renderer.mesh.create(cpu); cpu:unload()
local cpu = renderer.mesh.loadCpu(gpuMesh.guid)
globals/renderer/mesh/morphTargets
renderer.mesh.morphTargets(mesh: string | { [string]: any } | AssetRef) -> { string }
The names of the shapes this mesh blends towards, in the order an
entity's ecs.MorphWeights addresses them — weight i drives the target
named at i. An imported model carries the names its source file gave its
blend shapes, so content drives a face by the shape it means rather than by
the ordinal that shape happened to import at (which moves when the model is
re-exported). A target the source never named reads as an empty string.
Empty for a mesh with no morph targets. Errors when the mesh is neither
GPU- nor CPU-resident — materialise it first (meshRef:handle()).
Parameters
meshstring | { [string]: any } | AssetRef— The mesh — aMeshHandle, aMeshCpuHandle, a guid, or a meshAssetRef.
Returns { string } one name per morph target, in target order.
for i, name in renderer.mesh.morphTargets(meshRef) do print(i, name) end
globals/renderer/mesh/morphWeights
renderer.mesh.morphWeights(mesh: string | { [string]: any } | AssetRef, weights: { [string]: number }) -> { number }
Turn weights named by shape into the ordered weight array
ecs.MorphWeights takes — the drive-a-face-by-name call. Every target the
mesh carries gets a slot; the ones weights names take their value and the
rest are 0, so the returned array always describes the whole mesh and a
shape left out is a shape at rest.
A name the mesh does not carry is an error listing the names it does: a mistyped viseme that silently moved nothing would be indistinguishable from a rig that never had it.
Parameters
meshstring | { [string]: any } | AssetRef— The mesh — aMeshHandle, aMeshCpuHandle, a guid, or a meshAssetRef.weights{ [string]: number }—{ [string]: number }— how strongly to blend each named shape.
Returns { number } one weight per morph target, in target order.
local w = renderer.mesh.morphWeights(meshRef, { Eyes_Blink = 1, Mouth_Smile = 0.4 })
ecs.set(face, ecs.MorphWeights { weights = w })
globals/renderer/mesh/readback
renderer.mesh.readback(mesh: string | { [string]: any } | AssetRef) -> MeshCpuHandle
Read a runtime GPU mesh's geometry back to CPU and return a
MeshCpuHandle for it — the GPU→CPU half of the runtime-mesh freeze path. A
mesh made with renderer.mesh.create keeps no CPU copy, so persisting it
(:encode() → asset.create("mesh", …)) reads it back here first. Yields
until the readback completes (a frame or two). After it returns the geometry
is resident in the guid-keyed CPU store: :getTriangles, :getVertices,
:getBounds, :geometry, :encode, :unload all work. Errors if the mesh
never becomes resident in the vertex pool.
Parameters
meshstring | { [string]: any } | AssetRef— The mesh — theMeshHandlerenderer.mesh.createreturned, a guid, or a meshAssetRef.
Returns MeshCpuHandle
local cpu = renderer.mesh.readback(handle); local zmsh = cpu:encode(); cpu:unload()
globals/renderer/mesh/readbackPosed
renderer.mesh.readbackPosed(requests: { { entity: string, mesh: any } }) -> { [string]: MeshCpuHandle }
Read the POSED geometry of skinned entities back to CPU: for each request, the vertices the skinning pass wrote for that entity this frame, joined by the indices of the mesh it is posed from. A skinned surface's world-space triangles are produced on the GPU from the entity's joint matrices, so the mesh asset holds the bind pose and only this reads where the surface actually is. The posed vertices are in model space, so the entity's own world transform still places them — the same transform the raster draw uses.
Takes a LIST and answers a map, because the readbacks are queued together
and polled together: a scene's worth of characters costs the frames of one
readback rather than one entity's after another. Each posed mesh lands in
the CPU store under a guid of its own, derived from the entity, so
compute.buildBvh, meshcpu.* and every other guid-keyed reader takes it
like any other mesh. Call handle:unload() when done with it.
An entity the map omits holds no live pose — nothing skinned it this frame, which is also what makes its draws read the source mesh, so its bind-pose geometry is what stands for it.
Parameters
requests{ { entity: string, mesh: any } }—{ { entity = <id>, mesh = <mesh> } }— the entity to read, and the mesh it is posed from (a guid,MeshHandleor meshAssetRef).
Returns { [string]: MeshCpuHandle } — A map from entity id to the MeshCpuHandle holding that entity's posed geometry.
local posed = renderer.mesh.readbackPosed({ { entity = id, mesh = ecs.get(id, ecs.Mesh).mesh } })
local tris = posed[id]:getTriangles()
globals/renderer/mesh/scheduleClusters
renderer.mesh.scheduleClusters(mesh: string | { [string]: any } | AssetRef) -> boolean
Queue a cluster-LOD bake for the static CPU mesh held under guid, and
attach the DAG to the GPU mesh of that same guid on the frame it finishes.
The CPU mesh may be unloaded on the very next line; the DAG is then built
one bounded slice per frame, so a dense mesh virtualizes without the frame
loop stopping for the whole bake.
One bake is advanced per frame — the one at the head of the queue — and the
geometry is read on the frame a bake gets there, from the definition the
engine holds for the mesh. A queue of meshes the engine holds definitions
for therefore holds one mesh's geometry rather than one per mesh, whatever
its depth. A mesh the engine holds no definition for is copied into the
queue as it is scheduled, since the CPU store is then the only thing
holding it. renderer.mesh.clusterBakes().heldBytes reports what the queue
is holding, and its inFlight rows report which bakes it is holding for.
This is what the .mesh assetType materialisation path uses; reach for
renderer.mesh.buildClusters when you want the bytes in hand instead.
Scheduling the same mesh again replaces the bake already in flight for it.
Parameters
meshstring | { [string]: any } | AssetRef— A mesh loaded into the CPU store (renderer.mesh.loadCpu) — theMeshCpuHandle, aMeshHandle, a guid, or a meshAssetRef.
Returns boolean — True if a bake was queued.
renderer.mesh.scheduleClusters(cpu) ; cpu:unload()
globals/renderer/mesh/setInstanceCount
renderer.mesh.setInstanceCount(draw: InstancedDraw, count: number) -> InstancedDraw
Change how many of a registration's instances draw. Constant time — the
reservation, the transform buffer and the pipeline all stay put, so this is
the verb for a population whose size changes per frame. The new count must
fit the reservation drawInstanced was given.
Parameters
drawInstancedDraw— TheInstancedDrawto reconfigure.countnumber— Instances to draw, at least 1 and within the reservation.
Returns InstancedDraw — The same InstancedDraw.
renderer.mesh.setInstanceCount(draw, visibleCount)
globals/renderer/mesh/setInstanceRenderLayer
renderer.mesh.setInstanceRenderLayer(draw: InstancedDraw, renderLayer: number) -> InstancedDraw
Change which render layers a registration's copies belong to. Constant time — the reservation, the transform buffer and the pipeline all stay put, and the next frame drawn tests the copies against the new membership. It is the verb for a population that follows something whose membership moves: a camera or a capture including the layer draws the copies, one excluding it does not.
Parameters
drawInstancedDraw— TheInstancedDrawto reconfigure.renderLayernumber— The membership bitmask, the same valuedrawInstancedtakes asrenderLayer. At least one bit must be set.
Returns InstancedDraw — The same InstancedDraw.
renderer.mesh.setInstanceRenderLayer(draw, mask)
globals/renderer/mesh/setVertices
renderer.mesh.setVertices(mesh: string | { [string]: any } | AssetRef, positions: { number })
Replace a mesh's resident CPU vertex positions (flat { x,y,z, ... })
IN PLACE — indices, normals/uvs, and skinning are preserved, the AABB
recomputes, and the GPU re-fetches the new geometry so it shows on screen.
The positions alone travel, so this is the per-frame deformation path where
renderer.mesh.update re-sends the whole geometry. The mesh must be
CPU-resident: renderer.mesh.create({ ..., keepCpu = true }) keeps a copy
from the start, renderer.mesh.readback(mesh) recovers one from the GPU,
and meshRef:load() loads one for a .mesh asset. Errors with the reason
otherwise, or when the vertex count doesn't match.
Parameters
meshstring | { [string]: any } | AssetRef— The mesh — aMeshHandle, aMeshCpuHandle, a guid, or a meshAssetRef.positions{ number }— Flat{ x,y,z, ... }— one xyz per vertex; count must match the mesh.
local m = renderer.mesh.create({ positions = P, indices = I, keepCpu = true })
renderer.mesh.setVertices(m, P) -- P mutated in place each frame
globals/renderer/mesh/unloadCpu
renderer.mesh.unloadCpu(mesh: string | { [string]: any } | AssetRef)
Drop a mesh's resident CPU copy from the guid-keyed CPU store. The explicit release for a runtime geometry mesh's recoverable definition.
Parameters
meshstring | { [string]: any } | AssetRef— The mesh — aMeshHandle, aMeshCpuHandle, a guid, or a meshAssetRef.
globals/renderer/mesh/update
renderer.mesh.update(mesh: string | { [string]: any } | AssetRef, src: any?) -> MeshHandle
Overwrite the GPU resource mesh names IN PLACE, under the same guid,
from new geometry or compute buffers. Never writes a .mesh file — the
play-mode mutate path. A Model bound to the guid reflects the change with no
re-bind. Takes every form that names a mesh — the MeshHandle create
returned, the guid renderer.mesh.list hands out, a MeshCpuHandle or a
mesh AssetRef. Returns a handle carrying the bounds the new geometry has:
the handle it was given, refreshed, and a handle over the guid otherwise.
Parameters
meshstring | { [string]: any } | AssetRef— The mesh to update — aMeshHandle, a guid, aMeshCpuHandleor a meshAssetRef.srcany(optional) — New geometry{positions, indices, ...}or compute buffers{vertexBuffer, indexBuffer, vertexCount, indexCount, prevVertexBuffer?}.
Returns MeshHandle — A MeshHandle for the updated mesh.
globals/renderer/mesh/uploadClusters
renderer.mesh.uploadClusters(mesh: string | { [string]: any } | AssetRef, clusters: string) -> boolean
Attach a cluster-LOD DAG (bytes from renderer.mesh.buildClusters) to
the GPU mesh keyed by guid, enabling the continuous-cut cluster draw path
for that mesh.
Parameters
meshstring | { [string]: any } | AssetRef— The mesh the clusters belong to — aMeshHandle, aMeshCpuHandle, a guid, or a meshAssetRef.clustersstring— Serialized cluster bytes (binary-safe).
Returns boolean — True if the upload was queued.
renderer.mesh.uploadClusters(gpu, cb)
globals/renderer/minScreenSize
renderer.minScreenSize() -> number
The on-screen radius, in pixels, an object must reach to be drawn. 0
while the cutoff is off.
Returns number
local px = renderer.minScreenSize()
globals/renderer/morphStats
renderer.morphStats() -> {
The morph state the last frame drew with. A mesh carries the shapes it
can blend towards and an entity carries how strongly each is blended
(ecs.MorphWeights); where both are present, the vertex stage adds the
weighted deltas to the base geometry.
instances is how many render slots that happened at, and blends how
many single-target blends those slots carry between them: a slot
contributes one per target its weights move, or that they moved the frame
before, so the number of targets a mesh can be given is bounded by the
buffer the blends live in. meshes is how
many meshes hold a delta block and targets how many targets those blocks
cover between them; deltaBytes is what the shared buffer they are
appended into holds. A morph-target mesh whose weights are all zero reads a
meshes above zero beside an instances and blends of zero.
Returns { instances: number, blends: number, meshes: number, targets: number, deltaBytes: number }
local m = renderer.morphStats()
print(("%d instances carrying %d blends over %d meshes"):format(m.instances, m.blends, m.meshes))
globals/renderer/observe
renderer.observe() -> RenderObservation
Everything the renderer knows about the frame it last drew: what
program it bound for each renderable, the render state it holds each
material under, and what each program has cost in pipeline builds.
renderables is one row per renderable in the renderer's draw list,
carrying the program its material named (requestedProgram) beside the one
that was bound (boundProgram) — __error__ wherever the lookup missed
and the draw went ahead on the magenta placeholder — plus substituted,
the outcome (drew / drewPlaceholder / skipped / notDrawn), the
reason that forced it and the compiler's own detail for a failed
compile. observed says which of two answers a row is: true for a
resolution a geometry pass took as it drew, false for the renderer's own
resolution of a renderable this frame drew nowhere, which is what a
renderable outside every camera's frustum or layer mask reports.
materials is one row per
material the renderer holds a prepared bind group for; shaders is one row
per program pipelines have been built for. frame names the frame every
per-frame count covers; retainedFrames how many frames a resolution a
pass took is kept for after the last frame that drew it; window and
costWindow state both in the document itself. Recording is armed by the
first read, so this waits for the frame that first records rather than
answering empty.
Returns RenderObservation
local o = renderer.observe(); print(o.placeholderDraws, "renderables drew the placeholder in frame", o.frame)
globals/renderer/occlusionCulling
renderer.occlusionCulling() -> boolean
Whether occlusion culling is currently enabled.
Returns boolean
globals/renderer/passSchedule
renderer.passSchedule() -> {
The schedule check over this frame's enqueued render passes. Passes
declare what they read (inputs) and what they write (output /
outputs / storage), and the frame runs them in phase order and, inside
a phase, in order order. violations holds every input bound to a
resource the frame produces LATER: that read samples the resource as it
stands ahead of that pass, which is the previous frame's contents for a
render target that persists, an empty target for one just created, and the
scene draw's own output for a @scene.* buffer — and the pass renders
either way. The frame's own buffers are checked on the same terms as a
render target: bind @scene.motion at a phase ahead of the pass that
writes it and the read is reported, naming the buffer and its writer.
A pass reading a resource ahead of that write on purpose declares that slot
in its enqueue's readsPrevious and drops out of the list;
unboundPrevious holds declared slots the pass binds no such resource to,
which cover nothing.
A resource no queued pass writes is not reported — a camera rendering to
texture and compute.dispatch both fill targets outside the pass queue,
and the scene draw fills the @scene.* buffers every frame.
A read the frame has only one order for is not reported either: where the
writing pass consumes something the reading pass produces, the reader runs
first or the writer has nothing to write, which is what a pass reading a
buffer into a target of its own and a second pass copying that target back
over the buffer forms.
unreachable holds passes at a phase that does not run their kind: every
phase drains its fragment and compute passes, while afterLighting is the
one that draws geometry, draw and splat passes, so one of those enqueued
elsewhere sits in the queue and never runs.
Each finding is also stated in the engine log the first time it appears.
Returns { violations, unboundPrevious, unreachable }
local s = renderer.passSchedule()
for _, v in s.violations do print(v.message) end
globals/renderer/pipelineCache
renderer.pipelineCache() -> PipelineCache?
What the driver's compiled-pipeline store held, built, and wrote back. A pipeline is machine code the GPU driver compiles from the shader bound into it, and that compile is what a launch pays before the first frame drawing with each pipeline can appear. The store keeps that compiled code across runs, so a launch whose shaders have not changed reads back what the previous one compiled.
restoredBytes is what a previous run left for this GPU and this launch
read; pipelinesBuilt counts the pipelines built since startup and
buildMs is what they cost together, which is the number the store lowers.
saves and savedBytes describe writing it back — deferred until a burst
of builds settles, so one launch is one write — and dirty is true while
pipelines have been built that the file does not hold, including after a
write that failed, which lastError then names. path is the file, named
after the GPU it belongs to.
supported is false where the platform holds no store a program can carry:
a browser keeps its own and hands none out, and an adapter can lack the
capability. reason says which, and the build count and timing still read
true there. lastError names a read or write failure; a failed store costs
the saved compile and never the frame, since every pipeline is built from
its source either way.
pipelinesBuilt and buildMs are engine-wide totals; renderer.shaderCost()
is the same cost broken down per program, with each one's permutation count.
Returns PipelineCache? — { supported, reason, path, restoredBytes, pipelinesBuilt, buildMs, saves, savedBytes, dirty, lastError }, or nil before the renderer has drawn a frame
local c = renderer.pipelineCache()
print(("%d pipelines in %.1f ms, %d bytes restored"):format(c.pipelinesBuilt, c.buildMs, c.restoredBytes))
globals/renderer/pointShadowBudget
renderer.pointShadowBudget() -> PointShadowBudget
The point-light shadow pool now in force. A point light with
castsShadows renders an omnidirectional cube map, six faces of depth,
and slots is how many of them fit — a further caster is lit but throws
no shadow, and the engine log names how many were turned away. The slot
count is bought rather than authored: megabytes of VRAM at resolution
texels per face is what decides it.
Returns PointShadowBudget — The pool — see PointShadowBudget.
local p = renderer.pointShadowBudget()
print(("%d shadowed point lights, %.1f MiB"):format(p.slots, p.bytes / 1024 / 1024))
globals/renderer/projectionOffset
renderer.projectionOffset() -> (number, number)
The sub-pixel projection offset in force for the main camera, in NDC.
Returns (number, number) — The x and y offset, both 0 when the projection samples pixel centres.
local ox, oy = renderer.projectionOffset()
globals/renderer/raycast
renderer.raycast(origin: vec3, direction: vec3, maxDistance: number?, exclude: (string | { string })?) -> RenderRayHit?
Cast a ray against the geometry the renderer DRAWS and return the
nearest surface it meets. Every visible mesh answers, whether or not
anything gave it a rigid body — so a terrain, a procedurally generated
mesh, or any plain Model reports the surface at a point, which is what a
camera station, a prop, a sound source or a scatter standing on the ground
needs to know. The answer is the nearest triangle of the mesh, so a sloped
or terraced surface reports its height where it was asked rather than the
extent of its bounding box.
distance is measured from origin along the direction given, so it is a
world-space distance whenever that direction is a unit vector, and it is
directly comparable to a physics.raycast distance along the same ray.
normal is a unit vector turned to face back along the ray. exact is
true when the answer is a triangle and false when it is the object's
bounding box, which is what a mesh whose vertices live only in GPU buffers
answers with. The triangles are the mesh's own, placed by the entity's
transform and by the mesh's bind pose, so a surface a skinning or morph
pass deforms on the GPU answers as the geometry the mesh holds.
EVERYTHING drawn is in scope — the ground you meant, and equally a
character standing on it, a prop, a placeholder floor. The hit names its
entity in entityId, exclude steps over the ones you do not want, and
renderer.raycastAll hands back the whole column so you can pick the
surface yourself. A height you did not expect is usually a nearer surface
you did not mean to ask about, so read entityId before trusting a number.
Parameters
originvec3—vec3ray start in world spacedirectionvec3—vec3ray direction; any length, the engine normalisesmaxDistancenumber(optional) —number?how far the ray reaches, in world units. Default 1000exclude(string | { string })(optional) —(string | { string })?entity id, or ids, to step over
Returns RenderRayHit?
local hit = renderer.raycast({ x, 100, z }, { 0, -1, 0 }, 200)
if hit then camera.position = { x, hit.point.y + 1.7, z } end
globals/renderer/raycastAll
renderer.raycastAll(origin: vec3, direction: vec3, maxDistance: number?, maxHits: number?, exclude: (string | { string })?) -> { RenderRayHit }
Cast a ray against the geometry the renderer draws and return every
surface along it, nearest first. One entry per renderable the ray crosses —
the nearest intersection with each — so a stack of surfaces reads as the
order they stand in, and a caller after one particular surface finds it by
entityId rather than hoping it is the nearest. Each entry carries the
fields renderer.raycast returns.
Parameters
originvec3—vec3ray start in world spacedirectionvec3—vec3ray direction; any length, the engine normalisesmaxDistancenumber(optional) —number?how far the ray reaches, in world units. Default 1000maxHitsnumber(optional) —number?how many surfaces to return. Default 32exclude(string | { string })(optional) —(string | { string })?entity id, or ids, to step over
Returns { RenderRayHit }
for _, hit in renderer.raycastAll(eye, look, 500) do print(hit.entityId, hit.distance) end
globals/renderer/raytraceCapability
renderer.raytraceCapability() -> string
The active ray-tracing backend: "hardware" (GPU ray query) or
"compute" (software traversal — the path on devices without hardware ray
query, e.g. the web). The same ray-tracing features work on both.
Returns string "hardware" | "compute"
if renderer.raytraceCapability() == "hardware" then ... end
globals/renderer/raytraceStats
renderer.raytraceStats() -> { [string]: any }
What the ray-tracing acceleration structure holds, and what this
session's frames have spent building it. A ray walks a structure built over
the scene's geometry, and keeping it current is work a frame pays before it
traces anything. On the "compute" backend geometry that has stood still
long enough is filed under a static partition the frames after it leave
alone: staticTriangles + dynamicTriangles = triangles, nodes is the
hierarchy over them, fullRebuilds / partialRebuilds / reusedFrames
count what the session's frames did, and trianglesRebuilt is what those
rebuilds re-emitted, summed. On the "hardware" backend blas is the
bottom-level structures cached, blasBuilt how many the last frame built,
and tlasInstances what the top-level structure names. The counters are
cumulative — sample, run the scene, sample again.
Returns { [string]: any } — table {backend, triangles, staticTriangles, dynamicTriangles, nodes, fullRebuilds, partialRebuilds, reusedFrames, trianglesRebuilt, blas, blasBuilt, tlasInstances}
local before = renderer.raytraceStats().trianglesRebuilt
globals/renderer/references
renderer.references(handleOrKind: any?, id: string?) -> RuntimeResourceStatus?
What holds a runtime resource right now — the answer a root scene load
reads before releasing it. references names each live consumer the engine
found: { by = "entity", id } for an entity wearing the material or mesh,
"material" for a material whose slot names the texture, "instancedDraw",
"camera", "sky", "lightmap", "ui" (a screen drawing it) and
"postProcess" (an effect sampling it). handleHeld says whether a script
still reaches a handle to it, assetBacked whether an asset stands behind
it, ownerLive whether the component instance, scene load or feature that
created it still stands, and held whether a hold pins it. origin reads
"device" for a GPU texture the device holds that no script created — the
one the cache loaded for an asset, the atlas the engine built — whose
holders are the references, a handle and the asset. Runs a full garbage
collection first, the same one renderer.collect runs, so a handle nothing
reaches counts as let go and the row says what the next collection does
with the resource. Yields for the frame the census runs on.
Parameters
handleOrKindany(optional) — The resource's handle, or its kind with the id second.idstring(optional) — The guid or key, when the first argument is a kind.
Returns RuntimeResourceStatus? — The resource's status, or nil for a key the registry does not record and the device holds no texture under.
local s = renderer.references(mat) for _, r in s.references do print(r.by, r.id) end
globals/renderer/reflectionEnvironment
renderer.reflectionEnvironment() -> {
What a reflective surface is reflecting. probes is how many reflection
probes the shading blends; they are gathered highest priority first, each
rank taking the coverage the ranks above it left, so a small interior probe
ranked above the large exterior one it sits inside wins outright wherever it
reaches full weight. ranks is the priority each of those probe slots was
published with, in slot order. sky is whether the sky fallback is armed:
with it, coverage no probe claims reflects the captured sky, and without it
a surface outside every probe's radius falls back to the nearest probe
alone. skyCaptured is whether the sky slot holds a capture — arming is
refused until it does, since an uncaptured slot reflects black.
slots is how many cube slots the environment array holds right now: the
sky's alone, at index skySlot, until a probe is captured into it, then
that one plus one per probe. maxProbes is how many of them probes may
take, and resident whether the array has grown past the sky's single
slot. Capture the sky with environment.captureSky().
Returns { probes, ranks, sky, skyCaptured, resident, slots, skySlot, maxProbes }
local env = renderer.reflectionEnvironment()
print(("%d probes, sky fallback %s"):format(env.probes, tostring(env.sky)))
globals/renderer/release
renderer.release(handleOrKind: any?, id: string?) -> boolean
Let go of the hold renderer.hold placed. The resource stays until
nothing else holds it and a collection releases it — the one a root scene
load runs, or a direct renderer.collect().
Parameters
handleOrKindany(optional) — The resource's handle, or its kind with the id second.idstring(optional) — The guid or key, when the first argument is a kind.
Returns boolean true when the registry knows the resource.
renderer.release(tex)
globals/renderer/renderTargetLimits
renderer.renderTargetLimits() -> {
The size a render target may be on this device. maxDimension is the
device's own maximum 2D texture dimension — the largest either side of a
render target may take. maxPixels is how many pixels one render target
may hold, so the RGBA8 image it reads back as fits in a single buffer on
every platform the engine runs on, and maxSquare is the largest square
that budget buys. A capture, a renderer.texture.create({ width, height })
or a render-to-texture camera past either bound is refused at the call with
the reason, so ask here for the size to request.
Returns { maxDimension, maxPixels, maxSquare }
local lim = renderer.renderTargetLimits()
local w = math.min(want, lim.maxSquare)
globals/renderer/renderTargets
renderer.renderTargets() -> {
Every render target the renderer owns and what each one costs, measured
from the texture that is allocated. One row per target, each carrying its
name, whether it is resident, the bytes it holds while it is, its
width/height/layers/mipLevels, and onDemand.
An onDemand target exists only while something needs it: a target nothing
writes into reads resident = false and bytes = 0 and appears again the
frame something writes it, and one sized by content — the reflection-probe
cube array — holds the slots content asked for. The scratch the draws into
a render target have needed is reported as camera[<handle>].* rows:
depth and motion vectors under any rasterized pass, and the occlusion
channel and G-buffer over them under a camera's scene render. A draw builds
what it needs, and the set goes once no live camera names the target and
sixty frames have passed without a draw, so a target nothing draws into
carries no such row; the colour image drawn into belongs to the texture
cache and outlives every one of those releases.
totalBytes is what the resident targets hold together. Measured at the
end of the last rendered frame.
Returns { targets, totalBytes, residentCount }
local rt = renderer.renderTargets()
print(("render targets: %.1f MiB over %d resident"):format(rt.totalBytes / 1048576, rt.residentCount))
for _, t in rt.targets do
if t.onDemand then print(t.name, t.resident, t.bytes) end
end
globals/renderer/resolutionScale
renderer.resolutionScale() -> number
The fraction of the display resolution the scene is currently rendered
at. 1 until something sets it.
Returns number
local s = renderer.resolutionScale()
globals/renderer/setAnisotropy
renderer.setAnisotropy(level: number) -> number
Set the maximum anisotropy material textures are sampled with. Takes effect on the next frame for content already on screen — no reload, no texture re-upload. 1 is plain trilinear.
Parameters
levelnumber— One of 1, 2, 4, 8, 16. Any other value is an error.
Returns number — The EFFECTIVE level after clamping to renderer.maxAnisotropy(), so asking for more than the device offers reports what was actually applied.
renderer.setAnisotropy(16)
globals/renderer/setBlendedBatching
renderer.setBlendedBatching(enabled: boolean) -> ()
Whether neighbours in a view's back-to-front blended order draw
together. On by default: alpha-blended geometry is submitted farthest-first,
and a stretch of neighbours in that order sharing a mesh, a material, a
shader and a pose is submitted as one instanced draw over those neighbours,
which puts the same members on screen in the same order out of a single
submission. A run stops wherever a differently-drawn renderable sorts
between two of its members, and a mesh of several primitives keeps a draw
per renderable — both would otherwise move fragments through each other.
Off, every blended renderable draws on its own at its own slot, so a
transparent crowd costs a draw per member. The image is the same either way,
which is what makes this the comparison a frame suspected of being formed by
the batching is made against; renderer.drawStats().draws counts the
difference.
Parameters
enabledboolean—boolean
Returns ()
renderer.setBlendedBatching(false) -- a draw per blended renderable
globals/renderer/setDepthPrepass
renderer.setDepthPrepass(enabled: boolean) -> ()
Enable or disable the opaque depth pre-pass. While enabled the renderer resolves opaque depth in its own pass before shading, so each shaded pixel runs its material once instead of once per surface stacked behind it, and the resolved depth is what occlusion culling reads. Enabled by default.
Parameters
enabledboolean—boolean
Returns ()
renderer.setDepthPrepass(false) -- shade every layer, for comparison
globals/renderer/setDepthPrepassOrdering
renderer.setDepthPrepassOrdering(enabled: boolean) -> ()
Submit the depth pre-pass nearest-first. Renderables reach the pre-pass
in the order they were registered, which stands in no relation to where the
camera is: a scene built back-to-front makes every layer write depth and be
overwritten by the layer in front of it. Ordered, the nearest surface
writes first and the surfaces behind it are rejected by the depth test
before they write. The same draws go out either way and the depth that
comes out is the same, so scene.depth_prepass in profiler.gpuFrame() is
what moves. Enabled by default.
Parameters
enabledboolean—boolean
Returns ()
renderer.setDepthPrepassOrdering(false) -- submit in registration order
globals/renderer/setGpuMemoryTracking
renderer.setGpuMemoryTracking(frames: number?) -> number
Set how often the GPU allocator sampler reads — one reading every
frames frames — or turn it off with 0. It starts at 60, a reading a
second at 60 Hz, so renderer.gpuMemory().allocator answers without
anything arming it. Building the ledger walks every live allocation, which
is why it is sampled rather than read every frame; the category figures
cost nothing either way, and a reader between samples sees the most recent
ledger, so a slow interval still answers.
Called with no argument it reports the interval in force and changes nothing, which is how something that retimes the sampler puts it back afterwards instead of restoring a number it assumed was the default.
Parameters
framesnumber(optional) —number?Frames between readings; 0 turns the sampler off. Omit to read the interval without changing it.
Returns number — The interval now in force.
renderer.setGpuMemoryTracking(60)
local was = renderer.setGpuMemoryTracking()
renderer.setGpuMemoryTracking(1)
-- ... take a tight reading ...
renderer.setGpuMemoryTracking(was)
globals/renderer/setMaxFramesInFlight
renderer.setMaxFramesInFlight(frames: number) -> number
Set how many frames of GPU work may be outstanding before the renderer stops running ahead. One is the least overlap this can express — a frame's work is waited for as soon as the next frame has been submitted — which is the lowest latency and the lowest throughput; higher values let a slow frame build a longer backlog, and that backlog is memory. Takes effect on the next frame.
Answers the bound after clamping to [1, 8], so asking for more than the renderer honours reports what you actually got.
Parameters
framesnumber— number Frames of GPU work that may be outstanding, 1 through 8.
Returns number — The bound that took effect, after clamping.
renderer.setMaxFramesInFlight(1) -- lowest latency
print(renderer.setMaxFramesInFlight(99), "was clamped")
globals/renderer/setMinScreenSize
renderer.setMinScreenSize(pixels: number) -> ()
Stop drawing an object once its on-screen radius falls below this many
pixels. A few pixels across, an object carries no detail a viewer can
resolve while still costing a full vertex and submission pass, and the
cutoff drops it from the camera's draws entirely — 0, the default,
keeps every object however small it lands. Measured from the object's own
bounds against the camera's projection, so the same threshold means the
same apparent size at any distance or field of view. Shadow casters have
their own threshold in renderer.setShadowCasterCutoff.
Parameters
pixelsnumber—number— smallest on-screen radius still drawn; 0 disables.
Returns ()
renderer.setMinScreenSize(4) -- drop anything under 4 px of radius
print(renderer.drawStats().compactedDrawn, "instances survived it")
globals/renderer/setOcclusionCulling
renderer.setOcclusionCulling(enabled: boolean) -> ()
Enable or disable occlusion culling. While enabled the renderer reduces the pre-pass depth into a pyramid each frame and tests every renderable that cleared the frustum against it, dropping the ones another surface entirely covers before their geometry is submitted. The pyramid describes the frame being drawn, so an object that becomes visible this frame is never held back a frame. Requires the depth pre-pass.
Parameters
enabledboolean—boolean
Returns ()
renderer.setOcclusionCulling(true); print(renderer.cullStats().occlusionCulled)
globals/renderer/setPointShadowBudget
renderer.setPointShadowBudget(cfg: {
megabytes: number?,
resolution: number?,
}) -> number
Set how much VRAM the point-light shadow atlas may hold, and at what
per-face resolution. An omitted field keeps its current value. The atlas
is reallocated on the next frame, so renderer.pointShadowBudget().slots
reports the new pool one frame later; the returned number is what this
budget buys. Raising resolution sharpens every point shadow and spends
the same memory on fewer of them — doubling it quarters the slot count.
Values are clamped: megabytes [1, 1024], resolution [64, 4096], and the
pool never exceeds renderer.pointShadowBudget().maxSlots. One slot is
always granted, so a budget too small for a single cube shadows one light
and the pool costs what that slot costs rather than what was asked for —
{ megabytes = 1, resolution = 4096 } buys 384 MiB of ceiling. Read
pointShadowBudget().bytes back to see what a budget actually bought, and
renderer.shadowMemory().point to see what the scene has made resident.
Parameters
cfg{ megabytes: number?, resolution: number?, }— The fields to change —megabytesand/orresolution.
Returns number — Cube slots this budget buys.
renderer.setPointShadowBudget({ megabytes = 96 })
renderer.setPointShadowBudget({ resolution = 1024, megabytes = 96 })
globals/renderer/setPresentMode
renderer.setPresentMode(mode: string) -> string
Set how a presented frame reaches the display. fifo queues every frame
and shows it on a vertical blank, which never tears and never drops one;
mailbox replaces the queued frame with the newest, which does not tear
and does not hold the renderer to the refresh rate; immediate presents as
soon as a frame is ready and can tear; fifo_relaxed is fifo that tears
rather than stall when a frame misses its blank; auto_vsync and
auto_no_vsync leave the choice to the backend.
A surface that does not offer the mode presents fifo instead, so read
renderer.framePacing().presentMode for what took effect and
.presentModes for what this surface offers. Takes effect on the next
frame.
Parameters
modestring— string One of "fifo", "fifo_relaxed", "mailbox", "immediate", "auto_vsync", "auto_no_vsync".
Returns string — The canonical spelling of the request — renderer.framePacing().presentMode is what the surface presents with, and differs when the surface does not offer the request.
renderer.setPresentMode("mailbox")
print(renderer.framePacing().presentMode, "is what the surface took")
globals/renderer/setProjectionOffset
renderer.setProjectionOffset(x: number, y: number)
Offset the main camera's projection by a sub-pixel amount, in NDC, for
the frames until it is set again. The offset is in NDC because that is the
space it is constant in: one pixel is 2.0 / width across, so half a pixel
is 1.0 / width. Velocity (@scene.motion) is measured against the
offset-free projection, so a still scene reports no motion however the
samples are placed — and picking resolves a click to the same ray either
way. (0, 0) samples pixel centres.
Parameters
xnumber— Horizontal offset in NDC. One pixel is2.0 / width.ynumber— Vertical offset in NDC. One pixel is2.0 / height.
renderer.setProjectionOffset(0.5 * 2 / w, -0.25 * 2 / h)
globals/renderer/setRaytrace
renderer.setRaytrace(enabled: boolean) -> ()
Enable or disable GPU ray tracing. While enabled the engine builds the scene acceleration structure each frame so ray-tracing render features can trace against it; disabling stops the build (so it costs nothing until a ray-traced effect is active). Required before any ray-traced shadows / AO / reflections render.
Parameters
enabledboolean—boolean
Returns ()
renderer.setRaytrace(true); renderer.feature.create("rt_shadows")
globals/renderer/setResolutionScale
renderer.setResolutionScale(scale: number) -> number
Render the scene at a fraction of the display's resolution and present
it at the display's own size. Shading cost scales with pixel count and with
nothing else, so this trades sharpness for frame time without taking
anything out of the scene: at 0.5 the scene rasterizes a quarter of the
pixels. UI and text are unaffected — they are drawn after the scene is
brought back up to size. The scene rows in profiler.gpuFrame() are what
move.
Parameters
scalenumber—number— fraction of the display resolution, clamped to [0.25, 1].
Returns number — the scale in force after clamping.
renderer.setResolutionScale(0.7)
globals/renderer/setShadowCaching
renderer.setShadowCaching(enabled: boolean) -> ()
Whether a shadow map that nothing changed is kept rather than drawn again. On by default: a shadow view — one directional cascade, one atlas layer of spot tiles, one face of a point light's cube — is rasterized on the frames its own inputs change and holds the depth it drew on the ones they do not. Off, every view is drawn on every pass, which is what a shadow suspected of holding a stale image is compared against.
Parameters
enabledboolean—boolean
Returns ()
renderer.setShadowCaching(false) -- draw every shadow view, every frame
globals/renderer/setShadowCasterBatching
renderer.setShadowCasterBatching(enabled: boolean) -> ()
Whether a shadow view draws every caster of one mesh together. On by default: a view — one directional cascade, one atlas layer of spot tiles, one face of a point light's cube — submits one draw per geometry over every caster of it the view admits, wherever those casters sit in render order and whatever transform slots they hold. Off, a view draws the runs of render-order neighbours that share a mesh AND hold consecutive slots, so a scene that has spawned and despawned anything fragments into many more draws. The image is the same either way, which is what makes this the comparison a shadow suspected of being placed by the batching is made against.
Parameters
enabledboolean—boolean
Returns ()
renderer.setShadowCasterBatching(false) -- draw the runs the scene presents
globals/renderer/setShadowCasterCutoff
renderer.setShadowCasterCutoff(cfg: {
minRadiusPx: number?,
maxDistance: number?,
}) -> ShadowCasterCutoff
Set the shadow-caster cutoff. An omitted field keeps its current value,
so a call can adjust one threshold without restating the other. Both are
measured against the camera the frame draws from rather than against each
light, so one setting covers every cascade, spot and cube face, and a
caster that stops casting is one whose shadow the viewer could not have
resolved. maxDistance is measured to the near side of the caster's
bounding sphere, so a large object keeps casting while any part of it is in
range. 0 releases a threshold; releasing both draws the casters the frame
drew before either was set.
Parameters
cfg{ minRadiusPx: number?, maxDistance: number?, }— The fields to change —minRadiusPxand/ormaxDistance.
Returns ShadowCasterCutoff — The cutoff now in force.
renderer.setShadowCasterCutoff({ minRadiusPx = 2 })
renderer.setShadowCasterCutoff({ minRadiusPx = 1.5, maxDistance = 120 })
globals/renderer/setShadowConfig
renderer.setShadowConfig(cfg: {
resolution: number?,
cascades: number?,
distance: number?,
splitLambda: number?,
fadeFraction: number?,
softness: number?,
}) -> ShadowConfig
Set the directional shadow quality. Any omitted field keeps its current
value, so a call can adjust one knob without restating the rest. Values are
clamped: resolution [64, 8192], cascades [1, 4], distance >= 0, splitLambda
[0, 1], fadeFraction [0, 1], softness [0, 1]. Changing resolution or
cascades reallocates the depth array; the rest are per-frame values. A
distance of 0 hands the range to the frame — the splits are cut over the
depth its own shadow-taking renderables reach — and a positive one caps it,
which is what a scene bounding its shadow cost states.
Parameters
cfg{ resolution: number?, cascades: number?, distance: number?, splitLambda: number?, fadeFraction: number?, softness: number?, }— The fields to change — seeShadowConfig.
Returns ShadowConfig — The full config now in force.
renderer.setShadowConfig({ softness = 0.65, fadeFraction = 0.28 })
renderer.setShadowConfig({ cascades = 4, resolution = 2048, distance = 240 })
globals/renderer/setShadowHero
renderer.setShadowHero(entity: string, padding: number?) -> ()
Give one caster a directional shadow view of its own, fit to its world bounds.
A cascade covers the slab of world the camera sees, so its texels are spread
over tens of metres and one character standing in the middle of it is
resolved by a handful of them. The hero view is the same light and the same
depth range zoomed onto that entity's bounds, so the whole map goes into the
shadow it and the ground under it carry — renderer.shadowHero().zoom is
the factor its texel density gains.
It renders beside the cascades, into a layer of the same texture allocated while a hero is registered, and every surface inside it reads it in place of the cascade, crossing back at its edge. Nothing else about the shadow changes: the same casters reach it, at the same depth range, through the same filter.
Parameters
entitystring— The entity whose renderables the view is fit around.paddingnumber(optional) — How much room the fit leaves around those bounds — for a pose that leaves the bind-pose box and for the filter that samples outside a silhouette. 1.0 fits them exactly.
Returns ()
renderer.setShadowHero(player.id)
print(("hero shadow: %.1f texels/unit vs %.1f"):format(
renderer.shadowHero().texelsPerUnit, renderer.shadowHero().cascadeTexelsPerUnit))
globals/renderer/setShadowProxy
renderer.setShadowProxy(mesh: string, proxy: string) -> ()
Rasterize proxy in place of mesh in every shadow view. A shadow is a
silhouette resolved at the resolution of a shadow map, so the triangles that
carry a mesh's close-up detail write depth no reader can resolve — a
decimated version of the shape, a level of its own LOD chain, or a
hand-built hull casts the same shadow for a fraction of the geometry.
The registration is keyed by MESH, so one call covers every instance of it — entities and GPU-driven populations alike — and a crowd sharing that mesh stays one draw. The proxy is placed by whatever places the caster, its instance's own transforms, so it stands where the caster stands, at the caster's scale.
An entity caster keeps its own geometry where a stand-in could not be placed
or deformed correctly: it is skinned (it rasterizes the post-skinned
vertices written for its own mesh), it blends morph targets (whose deltas
describe its own mesh and are read by vertex id), or its proxy would be
placed by a different node of its model than the source mesh is. Either
caster keeps it where the renderer holds no geometry under the proxy's
guid. Each of those is counted in renderer.shadowProxies().
Nothing else in the scene draws a proxy, so this call is what brings it onto the GPU, and it raises where it cannot. A proxy already resident there is registered as it stands.
Parameters
meshstring— The mesh a caster draws, as a guid or any mesh reference.proxystring— The mesh it rasterizes into shadow views instead.
Returns ()
renderer.setShadowProxy(statueMesh, statueHullMesh)
print(renderer.shadowProxies().triangles, "vs", renderer.shadowProxies().sourceTriangles)
globals/renderer/setSkinnedBatching
renderer.setSkinnedBatching(enabled: boolean) -> ()
Whether skinned instances holding one pose draw together. On by default:
instances of one mesh wearing one material and posed alike read the same
post-skinned vertices, so the camera's colour passes submit them as a single
instanced draw, and so does each shadow view and the velocity pass while
renderer.shadowCasterBatching() is on — that switch is what makes a depth
view form its draws by geometry at all. The camera depth pre-pass submits
its casters nearest-first, which is a run per span of neighbours rather than
a draw per geometry, so a crowd costs a draw per member there. Off, each
skinned instance draws on its own at its own slot in every pass that
rasterizes it. The image is the same either way, which is what makes this
the comparison a frame suspected of being formed by the batching is made
against — renderer.drawStats().draws counts the difference and
renderer.skinningStats().poses says how many distinct poses it holds.
Parameters
enabledboolean—boolean
Returns ()
renderer.setSkinnedBatching(false) -- a draw per skinned instance
globals/renderer/setSkinningPoseHold
renderer.setSkinningPoseHold(enabled: boolean) -> ()
Whether a pose the skinning pass already wrote is read as it stands. On
by default: the pass produces an instance's vertices from its joint
matrices, its node transforms, its blend weight and its blend model, so the
slice holding a pose already holds what running the pass over those same
inputs would write. A frame binding a pose whose slice still holds it reads
the slice and dispatches nothing, and skinning costs what the frame's poses
CHANGED — a paused clip, a skeleton nothing drives, a cast standing in one
pose, each cost compute the frame the pose arrived and nothing after it.
Off, every pose a frame binds is dispatched again, which is the comparison a
frame suspected of reading a slice that no longer holds its pose is made
against; the image is the same either way and
renderer.skinningStats() counts the difference as dispatches against
held. A mesh whose vertices a compute pass writes is dispatched every
frame however this stands.
Parameters
enabledboolean—boolean
Returns ()
renderer.setSkinningPoseHold(false) -- dispatch every pose, every frame
globals/renderer/setSpotShadowBudget
renderer.setSpotShadowBudget(cfg: {
megabytes: number?,
resolution: number?,
}) -> number
Set how much VRAM the spot/area shadow atlas may hold, and the per-side
resolution of one layer. An omitted field keeps its current value. The
atlas is reallocated on the next frame, so renderer.spotShadowBudget()
reports it one frame later; the returned number is what this budget buys.
Raising resolution sharpens the lights that cover the most screen and
spends the same memory on fewer layers — doubling it quarters the layer
count. Raising megabytes buys layers, which is what lets several lights
hold a large tile at once. Values are clamped: megabytes [1, 1024],
resolution [64, 4096], and the atlas never exceeds
spotShadowBudget().maxLayers. One layer is always granted, so a budget
too small for one still shadows lights and the atlas costs what that layer
costs rather than what was asked for.
Parameters
cfg{ megabytes: number?, resolution: number?, }— The fields to change —megabytesand/orresolution.
Returns number — Atlas layers this budget buys.
renderer.setSpotShadowBudget({ megabytes = 64 })
renderer.setSpotShadowBudget({ resolution = 2048, megabytes = 64 })
globals/renderer/setTextureBudget
renderer.setTextureBudget(opts: TextureBudgetOpts) -> TextureBudget
Bound the VRAM a world's textures occupy, by keeping only the mip levels
the frame is actually sampling. Pass { megabytes = 256 }; 0 — the
default — leaves texture residency alone and every texture stays fully
resident the way it uploaded.
With a budget armed, each frame measures how many screen pixels ONE
traversal of a texture's coordinate range covers on the surface that spans
it widest, and asks for the mip level that serves that span one texel per
pixel — the level the GPU picks from the fragment's own derivatives. A
material with uvScale = 8 lays eight copies of its texture across a
surface, so each copy spans an eighth of the surface and asks for three
levels coarser than the surface's own size would. A shader that declares
// @uv_space: world advances its coordinate over world units rather than
over the mesh's UVs, so how many copies a surface carries follows how large
that surface is. The textures whose surfaces cover the fewest pixels give
up levels until the set fits. Detail climbs one level per frame, from the
image already on screen, so a surface the camera approaches sharpens rather
than popping, and no texture is taken below the level whose longest side is
64 texels.
bias shifts every measurement by whole mip levels either way — negative
for finer than the sampling implies, positive for coarser — over a world
whose look wants a different trade than one texel per pixel.
The plan moves a texture whose demand the frame can measure: one at least 256 texels on its narrowest side, worn by a surface an entity draws. A texture a UI image, a post-process property or a render feature holds a view of stays whole, because nothing measures how much of the screen those cover.
Which textures the budget governs follows the surfaces the frame draws. A
texture whose asset still holds its bytes is enrolled the frame a measured
surface wears it — whenever it loaded, and whenever the budget was armed —
because a level change reads the levels it needs back from the asset; when
the last such surface goes it leaves the set whole, at the level it
uploaded at, and a surface reaching it again takes it back up. A texture a
script uploaded has its pixels nowhere else, so one enrolled while it is
resident holds them in system memory
(renderer.textureMemory().streamSourceBytes) from the upload until a
surface has worn it and gone, and releases them then, which is what keeps
it out for the rest of the session; one whose pixels were already released
when the budget was armed is out from the start.
renderer.textureMemory().pinnedTextures counts those, together with the
textures whose asset could not be read back and the ones a UI image, a
post-process property or a render feature holds a view of. Disarming
returns every texture to the level it uploaded at, and arming again governs
the textures the frame's surfaces are wearing then.
Parameters
optsTextureBudgetOpts—{ megabytes: number?, bias: number? }
Returns TextureBudget — { megabytes, bias } — the budget now in force
renderer.setTextureBudget({ megabytes = 128 })
renderer.setTextureBudget({ megabytes = 128, bias = -1 }) -- one level finer everywhere
renderer.setTextureBudget({ megabytes = 0 }) -- leave residency alone
globals/renderer/setTransmissionShadows
renderer.setTransmissionShadows(enabled: boolean) -> ()
Let translucent casters tint the sunlight they block instead of blocking
it outright. A shadow map holds one depth per texel and is compared as a
yes-or-no test, so stained glass, water and thin fabric all project the same
black silhouette a wall does. With this on, a caster whose material declares
opacity (base_color alpha under a transparent blend) or transmission
also draws into a light-space transmittance map, and the colour it lets
through multiplies into the directional light reaching whatever stands
behind it. Stacked casters compose. Opaque casters are unaffected, and a
scene with no translucent caster allocates nothing and records no pass.
Parameters
enabledboolean—boolean
Returns ()
renderer.setTransmissionShadows(true) -- stained glass tints the floor
globals/renderer/shaderCache
renderer.shaderCache() -> ShaderCache?
What the shader compile gate's store of baked WGSL held, answered and
wrote back. Compiling a .shader wraps the author's body in its framework,
expands every #include, and hands the result to naga to parse and
validate — work that is a pure function of the text going in, and that a
launch would otherwise repeat for every shader it draws with. The store
keeps that baked text across launches.
restoredEntries and restoredBytes are what a previous launch left that
this one read back. hits counts the compiles answered out of the store
and misses those that ran in full; savedMs sums what each hit's own
recorded compile had cost, against compileMs, what the misses spent.
stale counts the misses whose key was held but whose #included modules
had changed underneath — an entry records every module its expansion
consumed, so editing a module invalidates exactly the shaders that included
it and leaves the rest.
entries and bytes are what the store now holds, evictions how many a
write dropped to stay inside its bounds, and saves / savedBytes /
dirty describe writing it back, deferred until a burst of compiles
settles. persistent is false where a launch has nowhere to keep
artifacts and reason says why; location is the file, or the browser
store, they are kept in. restoreState is how the read of what a previous
launch left has gone — pending while it is still out (a browser answers
through a promise, so a launch reaches its first frames before it lands),
restored once entries came back, empty when there were none to come
back, failed when what was there could not be read, and none where a
launch keeps nothing. A cold, missing or corrupt store leaves every
shader compiling from source with identical output, and lastError then
names what went wrong.
Returns ShaderCache? — { persistent, reason, restoreState, location, restoredEntries, restoredBytes, hits, misses, stale, entries, bytes, compileMs, savedMs, saves, savedBytes, dirty, evictions, lastError }, or nil on a build with no renderer
local c = renderer.shaderCache()
print(("%d hits, %d misses, %.1f ms saved"):format(c.hits, c.misses, c.savedMs))
globals/renderer/shaderCost
renderer.shaderCost() -> { ShaderCost }
What each program has cost in pipeline builds, beside the compile
gate's most recent word about it. variants is how many pipelines this
engine has built for it — one per (target format, vertex layout,
render-state key) permutation reached — and buildMs what those builds
cost, both summed since engine start. A pipeline the driver's own store
restored is not built and so is not counted, so a second launch on the same
adapter reports less than the first. status is compiled, failed or
pending, and error carries the compiler's message for a failure.
Ordered by cost, most expensive first.
Returns { ShaderCost }
local top = renderer.shaderCost()[1]; print(top.shader, top.variants, top.buildMs)
globals/renderer/shaderVariants
renderer.shaderVariants() -> { ShaderVariants }
Every shader that declares optional features, and the programs its
materials have made it compile. Each row carries the features the shader
declares, the base program it ships as, and one entry per variant with the
features that variant holds — so the permutation count a scene's materials
are spending is a number to read rather than something to infer from
compile time. A shader whose variants reach budget compiles no more; the
materials asking for further feature sets draw with the base program.
Returns { ShaderVariants } — An array of ShaderVariants, one per feature-declaring shader.
for _, s in renderer.shaderVariants() do print(s.shader, #s.variants, s.budget) end
globals/renderer/shadingOf
renderer.shadingOf(subject: string | { [string]: any }) -> ShadingReading
What the renderer is shading ONE subject with, taken from the document
the renderer publishes — the call a system holding a handle makes to find
out whether what reaches the screen is its own material or the magenta
placeholder standing in for it, without reading the engine log. subject is
an entity that draws or the registry key of a material. state reads
itsMaterial where the renderer bound the program the material names,
errorMaterial where it bound the placeholder instead, stalePipeline
where the pipeline drawing it was built before that program's most recent
compile, nothingBound where the renderer resolved no pipeline for it,
pending where this call is the one that armed per-draw recording and the
frame after it publishes, and unknown where the renderer holds a
resolution under no such subject. A fault state carries the renderer's own
reason from the closed set renderer.drawDiagnostics() names — plus
materialNotPrepared, which a material subject reads where the renderer
prepared nothing under that key — the compiler's detail, the program
the material asked for and the bound one; means states the reading in a
sentence. A material subject answers from the renderables drawing with it,
and from the renderer's record for the material itself where a draw
registered against the material carries no row of its own; a subject that
several renderables draw answers with a refused one wherever there is one.
The reading follows the renderer, so a program that compiles on a later
edit puts the subject back on itsMaterial from the frame the renderer
draws it with again.
Parameters
subjectstring | { [string]: any }— The entity — a proxy fromentity(...)or an entity-id string — or the material, as its registry key or theMaterialHandlerenderer.material.createreturned.
Returns ShadingReading
local s = renderer.shadingOf(emitter); if s.state ~= "itsMaterial" then print(s.means) end
globals/renderer/shadowCacheStats
renderer.shadowCacheStats() -> {
What the last frame did with the shadow maps it already had. A shadow
view — one directional cascade, one atlas layer of spot tiles, one face of
a point light's cube — is drawn again only when something it draws from
changed:
its light moved, a caster it can see moved or appeared or vanished, a
caster's geometry or material changed, a caster changed pose or moved the
nodes its parts are placed by, or the map it writes into was reallocated.
Anything else keeps the depth already in the texture, so a scene that stops
moving reads rendered 0 while cached keeps climbing. A mesh whose
vertices a compute pass writes — a population, or a mesh built from a
compute buffer — re-renders the views it stands in every frame. A shadowed
point light contributes six views, one per cube face, so a caster moving on
one side of it re-renders the face that can see it and leaves the other
five holding what they have. Counted per light kind, plus the totals across
all three.
These are totals over every view of a kind. renderer.shadowViews() is the
same frame one view at a time, each row naming the light that owns it and
what it drew.
Returns { directionalRendered, directionalCached, spotRendered, spotCached, pointRendered, pointCached, rendered, cached }
local s = renderer.shadowCacheStats()
print(("shadow views: %d drawn, %d cached"):format(s.rendered, s.cached))
globals/renderer/shadowCaching
renderer.shadowCaching() -> boolean
Whether a shadow view may keep the depth it already holds.
Returns boolean
globals/renderer/shadowCasterBatching
renderer.shadowCasterBatching() -> boolean
Whether a shadow view draws every caster of one mesh together.
Returns boolean
globals/renderer/shadowCasterCutoff
renderer.shadowCasterCutoff() -> ShadowCasterCutoff
How small, and how far away, a caster may get before it stops writing depth into any shadow view. A shadow view rasterizes a caster's whole triangle count whatever the shadow it produces ends up covering, so an object the viewer resolves a fraction of a pixel of, and one past the range the scene cares about, each cost a full depth pass per shadowed light for detail nothing reads. Both thresholds are 0 — released — until something sets them.
Returns ShadowCasterCutoff — The cutoff in force — see ShadowCasterCutoff.
local c = renderer.shadowCasterCutoff(); print(c.minRadiusPx, c.maxDistance)
globals/renderer/shadowConfig
renderer.shadowConfig() -> ShadowConfig
The directional shadow quality now in force. resolution and cascades
size the cascade depth array; distance and splitLambda place the splits
along the view; fadeFraction and softness shape how the result is
sampled.
Returns ShadowConfig — The full config — see ShadowConfig.
print(renderer.shadowConfig().cascades)
globals/renderer/shadowHero
renderer.shadowHero() -> ShadowHeroReport
The registered hero caster and what the last frame's fit produced. A
frame that fit nothing says why in decline.
Returns ShadowHeroReport — See ShadowHeroReport.
local h = renderer.shadowHero()
print(h.active, h.zoom, h.decline)
globals/renderer/shadowMemory
renderer.shadowMemory() -> {
How much GPU memory the shadow maps hold right now, in bytes, by the
light kind that owns them. The spot atlas and the point pool are sized to
the casters in the scene rather than to the budget, so spot and point
move as lights that cast shadows appear and leave, and a scene with one
shadowed light holds far less than one that fills every slot. A budget is
the ceiling they grow within — renderer.spotShadowBudget().layers and
renderer.pointShadowBudget().slots report that ceiling, unmoved by how
many casters exist. Raising shadow resolution costs the square of the
change across every cascade.
Returns { directional, spot, point, total } in bytes
local m = renderer.shadowMemory()
print(("shadows: %.1f MiB"):format(m.total / 1024 / 1024))
globals/renderer/shadowProxies
renderer.shadowProxies() -> ShadowProxyReport
The shadow proxies in force and what the last frame's shadow passes did
with them. triangles and sourceTriangles are what those passes
submitted and what they would have submitted from the source meshes — the
before/after of every registration, equal while nothing is proxied.
Returns ShadowProxyReport — See ShadowProxyReport.
local p = renderer.shadowProxies()
print(("shadow triangles: %d of %d"):format(p.triangles, p.sourceTriangles))
globals/renderer/shadowViews
renderer.shadowViews() -> ShadowViewReport?
Every shadow view the last rendered frame considered, and what each one cost.
A frame rasterizes a depth view per directional cascade, one for the hero
caster, one per shadow-casting spot and six per shadow-casting point.
renderer.shadowCacheStats() counts those views by light kind,
renderer.drawStats() sums their draws with the camera's, and
profiler.gpuFrame() carries one scene.shadow span across all of them.
This is the same frame read one view at a time.
Each row names the view and the light that owns it, says whether it drew or
kept the depth it already held, and carries the draws, the instances and the
casters that went into it. span is the label the view's pass is timed
under, so its GPU time is a lookup in profiler.gpuFrame(); every one of
those labels is a variant of scene.shadow, which still carries their
total. camera carries the same instance counters for the main camera, so
the camera's share of a frame-wide total is a read rather than a measurement
taken by turning every light's shadow off.
A cascade's near and far are where the split scheme cut its slice, not
the world it covers: the fit takes the bounding sphere of that slice and
rasterizes the ortho box around it, and both reach past far. What the
cascade covers is center and radius, with viewProj the exact test;
coversNear and coversFar read that volume back along one ray, the
camera's view axis. directional states the axis reading for the set —
how far it reaches (coversFar), the range the splits were run over
(distance), how far the camera draws (cameraFar), and the
depth past the reach the camera still draws (uncovered). A receiver
further along the axis than coversFar has no directional depth map over
it and is shaded as if the sun reached it, so uncovered is the room a
missing shadow has and a surface standing in that room is what makes one;
@builtin::systems.proxyOcclusion occludes past the cascades. The box is
bounded in every direction, so a receiver standing wide of the axis leaves
it at its own distance even where uncovered is 0 — viewProj is what
answers for that receiver.
The list is rebuilt every frame: a view whose light stopped casting is
absent from the next report rather than standing at the numbers it last had,
and a frame that drew no shadow view answers a report whose views is
empty. views grouped the way the shadow cache decides — a row per cascade,
per spot atlas layer, per point cube — counts what
renderer.shadowCacheStats() reports as rendered + cached.
The frame names its views only while something is reading them, so this call asks the frames after it to name theirs and waits out the first one. Nil on an engine that renders no frame at all.
Returns ShadowViewReport? — See ShadowViewReport.
local report = renderer.shadowViews()
print(("camera submitted %d of %d instances"):format(
report.camera.submittedInstances, renderer.drawStats().compactedDrawn))
for _, view in report.views do
print(("%s %d (%s): %d draws, %d instances, %s"):format(
view.role, view.index, view.light, view.draws, view.instances,
view.rendered and "drew" or "cached"))
end
local d = report.directional
if d ~= nil and d.uncovered > 0 then
print(("sun shadows stop at %.0f; the camera draws to %.0f"):format(
d.coversFar, d.cameraFar))
end
globals/renderer/skinnedBatching
renderer.skinnedBatching() -> boolean
Whether skinned instances holding one pose draw together.
Returns boolean
globals/renderer/skinningPoseHold
renderer.skinningPoseHold() -> boolean
Whether a pose already written into its slice skips its dispatch.
Returns boolean
globals/renderer/skinningStats
renderer.skinningStats() -> {
What the last frame's skinned instances cost. A skinned instance is
posed by a compute pass that writes its vertices into a shared pool, and
instances holding the same pose read one slice of that pool and the single
dispatch that fills it. instances is how many were posed, poses how
many distinct poses they held, and dispatches how many dispatches those
poses cost this frame — so a crowd whose members move together costs what
its poses cost rather than what its head count does, while members at
different animation times each hold their own pose and pay for it.
held is how many of the frame's poses cost no dispatch at all. The pass
produces a slice from what the pose is made of, so a slice an earlier frame
filled already holds what running it again would write, and a pose still
wearing that slice is read as it stands. Skinning is paid for by the poses
that CHANGED: a cast standing still reads dispatches 0 beside a held
equal to its poses, and the two add up to poses in any frame.
reusedSlices is how many of the frame's poses took a slice the pool
already held — one a retired pose gave back, or one a pose nothing has
asked for this frame was holding — rather than one cut from pool the
engine had never used. A scene whose poses keep changing reads a non-zero
count beside a poolBytes that stays where it was.
liveBytes is what the slices holding this frame's poses occupy, against
unsharedBytes — what the same instances would occupy with a slice each.
poolBytes is what the pool holds; a previous-position buffer of the same
size rides alongside it so skinned deformation reaches motion vectors.
Returns { instances: number, poses: number, dispatches: number, held: number, reusedSlices: number, liveBytes: number, unsharedBytes: number, poolBytes: number }
local s = renderer.skinningStats()
print(("%d instances over %d poses"):format(s.instances, s.poses))
print(("%d poses dispatched, %d read as they stood"):format(s.dispatches, s.held))
print(("skinned vertex storage: %d B of an unshared %d B"):format(s.liveBytes, s.unsharedBytes))
globals/renderer/splat/components
renderer.splat.components(bytes: any?, convention: string?) -> (SplatComponents?, string?)
Decode a Gaussian splat capture — a Niantic .spz (gzipped or raw) or a
3DGS .ply — into the GPU-ready byte pools a render feature uploads.
records is the packed splat array at recordBytes per splat (position,
log scale, quaternion, DC colour + opacity); sh is the quantized
higher-order spherical-harmonics pool at shStrideWords u32 words per
splat, empty at degree 0. A pure decode (no GPU work): upload the pools with
shaderRef:createBuffer + buf:writeBytes and draw them with a
kind = "splat", channel = "gaussian" pass.
Parameters
bytesany(optional) — Capture bytes —.spzor.ply, as abufferor a binary string.conventionstring(optional) — Source axis convention:"rightDownFront"(the default, what COLMAP-trained captures use) or"engineNative"for a capture already in engine space.
Returns (SplatComponents?, string?) — { records, sh, count, shDegree, shStrideWords, recordBytes, boundsMin?, boundsMax?, antialiased, format }, or (nil, err).
local c = renderer.splat.components(vfs.readBytes("captures/ceramic.spz"))
globals/renderer/spotShadowBudget
renderer.spotShadowBudget() -> SpotShadowBudget
The spot and area-light shadow atlas now in force. Each shadow-casting
spot is given a tile of it every frame, sized to what the camera can
resolve: a light filling the view gets a whole layer at resolution, one
far away gets a minResolution tile, and the atlas holds tiles of the
smallest kind. That is what lets one budget serve a close hero light and a
street of distant ones without either the memory or the sharpness being set
for the worst case.
Returns SpotShadowBudget — The atlas — see SpotShadowBudget.
local s = renderer.spotShadowBudget()
print(("%d layers of %d, %.1f MiB"):format(s.layers, s.resolution, s.bytes / 1024 / 1024))
globals/renderer/temporal/held
renderer.temporal.held() -> boolean
Whether a hold is pinning the per-frame clock right now.
Returns boolean — True while at least one renderer.temporal.hold stands.
if renderer.temporal.held() then print("frame is pinned") end
globals/renderer/temporal/hold
renderer.temporal.hold(at: number?, options: TemporalHoldOptions?) -> () -> ()
Pin the clock every per-frame effect draws itself against, and return
the release. While the hold stands, renderer.temporal.now answers at
instead of the running clock, so film grain and every other field redrawn
each frame is redrawn as the same field. Two renders taken under holds at
the same instant therefore agree pixel for pixel wherever the scene itself
has not moved, which is what makes one frame comparable with another.
Holds nest: the innermost names the instant, and the clock runs again once
the last release is called. Each release takes its own hold off the stack
whatever order the releases come in, so two callers holding at once — two
captures in flight together — each end their own hold and the clock runs
again when both have.
exclusive takes the clock for the owner key the call states: while
that hold stands, a hold is admitted only when it states the same key, and
every other one is refused with an error naming the key and the instant
holding it. That is what lets one caller wind the clock to the second it
means to photograph and keep it there while another agent drives the same
engine. The key is what an owner presents to take a nested hold of its
own, and what renderer.temporal.release hands the clock back by. A
capture taken while the hold stands renders at the held instant; a
deterministic capture takes a hold of its own that states no key, so it
runs once the clock is handed back.
Parameters
atnumber(optional) — The instant to pin the clock at, in seconds. Two holds that state the same instant produce the same field; the default 0 is that shared instant.optionsTemporalHoldOptions(optional) —owneris the key this hold is taken under, and an exclusive hold states one. A hold that states no key is labelled with the agent the call is attributed to, which is the account the caller presented a token for and is shared by every session driving this engine under it.exclusivetakes the clock for the stated key until the hold is released.
Returns () -> () — A function that releases this hold. Calling it twice releases once.
local release = renderer.temporal.hold() ; local png = tools.use("capture", "fromPosition", { position = { 0, 2, 8 } }) ; release()
local release = renderer.temporal.hold(46.0, { exclusive = true, owner = "stage-air" })
globals/renderer/temporal/now
renderer.temporal.now() -> number
The instant a per-frame effect should draw itself at: the innermost hold's instant while one stands, and seconds since boot otherwise. A system that redraws a field every frame reads this rather than the running clock, and a capture asking for a repeatable frame then gets one.
Returns number — Seconds — pinned while a hold stands, running otherwise.
local params = { grainTime = renderer.temporal.now() }
globals/renderer/temporal/onChange
renderer.temporal.onChange(listener: (number) -> ()) -> () -> ()
Register a listener called with the pinned instant whenever it changes — a hold taken, a hold released — and return the unsubscribe. A system whose shader reads the clock out of a GPU buffer registers here, so the buffer carries the pinned instant before the frame that hold was taken on is drawn rather than a frame later.
Parameters
listener(number) -> ()— Called with the instant now in force, in seconds.
Returns () -> () — A function that removes this listener.
local stop = renderer.temporal.onChange(function(t) pushClock(t) end)
globals/renderer/temporal/owner
renderer.temporal.owner() -> { id: string?, name: string?, at: number, exclusive: boolean }?
The hold naming the instant the clock answers right now: who took it,
what instant it pinned, and whether it took the clock exclusively. Several
agents drive one engine at once and a hold any of them takes moves the
clock every registered field is redrawn against, so this is how a caller
sees that another agent holds it before its own instant is quietly
replaced — and, when exclusive is true, id is the key a hold of its
own states to be admitted, and the key renderer.temporal.release hands
the clock back by. id and name are nil for a hold that stated no key
and that the engine attributes to no agent.
Returns { id: string?, name: string?, at: number, exclusive: boolean }? — { id, name, at, exclusive } for the standing hold, or nil when the clock is running.
local who = renderer.temporal.owner()
if who ~= nil and who.exclusive then print(who.name, "holds the clock at", who.at) end
globals/renderer/temporal/release
renderer.temporal.release(owner: string) -> number
Hand the clock back by the key its holds were taken under, and report how many came off. A hold stands until its release is called, and the release is a closure the call that took the hold holds: a caller that takes a hold in one call and comes back in another, and a task that ends between the two, both leave the clock pinned with nobody holding a release for it. Naming the key is how the clock runs again, and how a caller refused by an exclusive hold takes one over.
Parameters
ownerstring— The key the holds to release were taken under — whatownerstated when they were taken, whichrenderer.temporal.ownerreports.
Returns number — How many holds came off the stack.
renderer.temporal.release("stage-air")
globals/renderer/texture/capture
renderer.texture.capture(texture: string | { [string]: any } | AssetRef) -> string
Request a CPU readback of the GPU texture texture names (e.g. a
camera's rendered output). Returns a result key to pass to a
TextureCpuHandle's :encode() once the readback completes. Takes every form
that names a texture — the TextureHandle create returned, the guid
renderer.texture.list hands out, a TextureCpuHandle or a texture
AssetRef.
Parameters
texturestring | { [string]: any } | AssetRef— The texture to read back — aTextureHandle, a guid, aTextureCpuHandleor a textureAssetRef.
Returns string The capture result key.
local key = renderer.texture.capture(cameraTarget)
globals/renderer/texture/cpuCreate
renderer.texture.cpuCreate(width: number, height: number, fill: any?) -> TextureCpuHandle
Allocate a blank CPU image (RGBA8) filled with a solid colour and return a
TextureCpuHandle. Compose into it with canvas:blit(src, x, y, w, h), then
canvas:encodeJpeg() / :encodePng() for the bytes; :unload() drops it.
Parameters
widthnumber— number Canvas width in pixels.heightnumber— number Canvas height in pixels.fillany(optional) — Optional{ r, g, b, a }(0-255) solid fill; defaults to opaque white.
Returns TextureCpuHandle
local c = renderer.texture.cpuCreate(1024, 576, { 255, 255, 255, 255 })
globals/renderer/texture/cpuFromBytes
renderer.texture.cpuFromBytes(bytes: buffer | string, encodeOpts: any?) -> TextureCpuHandle
Load engine-native ZTEX bytes — or an encoded image (png / jpg / webp)
— into the CPU store under a fresh guid and answer the CPU handle, for
pixels that come from somewhere other than a texture asset: a data.ztex
read as a file, a payload held in memory. The pixels stay at the format
they were encoded in. DEFAULT: handle:unload() once done with them.
Parameters
bytesbuffer | string— The ZTEX or image bytes.encodeOptsany(optional) —{ format?, srgb?, generateMipmaps?, maxDimension? }applied when the bytes are an encoded image and need the engine-native encode.
Returns TextureCpuHandle
local cpu = renderer.texture.cpuFromBytes(vfs.read(path)); local png = cpu:encodePng(); cpu:unload()
globals/renderer/texture/create
renderer.texture.create(src: any?, guid: string?) -> TextureHandle
Create (or fetch) a GPU texture resource and return its TextureHandle.
src: a TextureCpuHandle from texRef:load() (CPU→GPU under the asset's
guid, idempotent); raw pixels {rgba, width, height, srgb?, format?} (a
flat widthheight4 byte payload, 0-255, row-major, top-to-bottom, RGBA —
a buffer, a binary string, or a number array; format = "rgba16f"
uploads an HDR texture instead, where rgba carries float channel
values); a TextureHandle (returned as-is);
or render-target dimensions {width, height, name?, format?} with no pixel
source — an empty GPU texture a render pass writes into (camera output,
UI surface) and that samples like any other texture. format names the
colour format the target is allocated in, and the passes drawing into it
are built for that format: "rgba8unorm" / "bgra8unorm" (the two
eight-bit channel orders, either of which a surface may carry),
"rgba16f" / "rgba32f", "rg16f" / "rg32f", "r16f" / "r32f".
Each also answers to its spelled-out width ("rgba16float", "r32float",
and so on), in any case. Omit it to take the surface's own. A float format
carries what eight bits quantize — positions, velocities, HDR. Any other
format raises an error naming every name that works, so a target is
allocated in the format it was asked for or not at all. A render target
takes filter the way raw pixels do: "nearest" keeps its own pixels square
wherever something draws it larger than it is — a viewport widget, a
magnified capture — which is what an image whose pixels ARE the subject
needs, since a 64x32 panel holds no detail between its pixels to
interpolate; "linear" (the default) smooths between them. It also
takes screen (the engine keeps it the size of the image being drawn),
screenScale (the fraction of that size it takes) and screenSpace
("scene", the default, or "composite" — the image the post-scene
phases draw into, which is the display's own resolution while the renderer
presents the viewport itself and the scene's size while a UI viewport panel
owns the presentation). A scene-space target is resized for every render
target drawn and cleared before an offscreen one; a composite-space target
follows the presented frame alone, which is what lets a pass keep an
accumulation in it. One scene-space screen target is therefore one
resource every render target draws through in turn, so its guid holds the
last one's image at the last one's size, and a value read back from it
belongs to whichever render target was drawn last. A reading that has to
be the viewport's own comes from screenSpace = "composite", or from a
target created without screen. NEVER takes an AssetRef — load the CPU
first.
Parameters
srcany(optional) — A TextureCpuHandle, raw pixels, a TextureHandle, or render-target dimensions.guidstring(optional) — Optional v4 guid for a NEW runtime texture — the asset identity the texture is filed under, which a material's texture slot resolves through. Minted when absent. Ignored for the CPU-handle and render-target paths.
Returns TextureHandle
local gpu = renderer.texture.create(texRef:load())
local gpu = renderer.texture.create({ rgba = pixels, width = 16, height = 16 })
local px = buffer.create(16 * 16 * 4); local gpu = renderer.texture.create({ rgba = px, width = 16, height = 16 })
local rt = renderer.texture.create({ width = 512, height = 256, name = "panel_rt" })
local hdr = renderer.texture.create({ width = 512, height = 256, name = "cam_rt", format = "rgba16f" })
local led = renderer.texture.create({ width = 64, height = 32, name = "panel", filter = "nearest" })
globals/renderer/texture/createFromAsset
renderer.texture.createFromAsset(ref: string | AssetRef, encodeOpts: any?, keepCpu: boolean?) -> TextureHandle
Put a .texture asset on the GPU under its own guid and answer its
handle at once. The asset's bytes are decoded off the frame and the
texture lands on the device when the decode finishes, a frame or more
later: a material naming the guid draws the shader's default for that
slot until then and rebinds when it arrives, and
renderer.texture.isResident reports the arrival. The decoded pixels are
dropped once uploaded unless keepCpu holds them in the CPU store for
textureRef:load()-style reads. An asset the device already holds is
answered from the shape the device reports, without reading the asset's
bytes and without a second decode.
Parameters
refstring | AssetRef— A textureAssetRef, or a string naming one (guid, identity, name or source path).encodeOptsany(optional) —{ format?, srgb?, generateMipmaps?, maxDimension?, filter? }applied when the primary is an encoded source image and needs the engine-native encode (a.ztexprimary is decoded as-is).keepCpuboolean(optional) — Keep the decoded pixels in the CPU store after the upload.
Returns TextureHandle
local h = renderer.texture.createFromAsset(texRef) -- bind h.guid on a material; it draws once resident
globals/renderer/texture/decode
renderer.texture.decode(bytes: buffer | string) -> (any, any, any, any)
Decode a texture payload to its pixel buffer. Takes the two shapes the renderer's own texture loader takes, told apart by their leading bytes:
- an engine-native
ZTEXpayload — handed back at the texel format the payload was written in, so a height field read back here keeps every bit it was authored with. AZTEXholding block-compressed or verbatim source-image levels decodes to"rgba8". - source image bytes — png, jpeg, gif or webp, straight off disk or out of
a
capture— decoded to"rgba8"at whatever colour type, bit depth or interlacing the file was written with. This is the call that reads the pixels of a screenshot.
The fourth return names the format the buffer came back in: "rgba8" (4
bytes/texel, channels 0-255), "rgba16" (8 bytes/texel, 16-bit unsigned
normalized channels 0-65535) or "rgba32f" (16 bytes/texel, float
channels).
Parameters
bytesbuffer | string— AZTEXpayload or source image bytes.
Returns (any, any, any, any) — (string?, number?, number?, string?) pixels, width, height, format — or (nil, errmsg) where errmsg is in the 2nd slot.
local pixels, w, h = renderer.texture.decode(vfs.read("/source/tmp/shot.png"))
globals/renderer/texture/destroy
renderer.texture.destroy(texture: string | { [string]: any } | AssetRef) -> boolean
Release the GPU texture texture names. For an empty render-into texture
(camera output, UI surface) this also frees its render scratch; for an
uploaded runtime texture it drops the GPU resource (and any CPU shadow).
After this, renderer.texture.list stops answering for the guid. Takes
every form that names a texture — the TextureHandle create returned, the
guid the listing hands out, a TextureCpuHandle or a texture AssetRef.
Parameters
texturestring | { [string]: any } | AssetRef— The texture to release — aTextureHandle, a guid, aTextureCpuHandleor a textureAssetRef.
Returns boolean true when a texture was known under the guid.
renderer.texture.destroy(rt)
renderer.texture.destroy(renderer.texture.list()[1].guid)
globals/renderer/texture/encode
renderer.texture.encode(rgba: any?, width: number, height: number, opts: any?) -> (string?, string?)
Encode raw pixels into an engine-native ZTEX payload (the on-disk
texture content). The CPU codec behind the texture assetType's onCreate.
opts.format selects the on-disk precision: "rgba8" / "srgb" (default,
8 bits/channel, rgba is widthheight4 bytes) or the high-precision data
formats "rgba16" (16-bit unsigned normalized, widthheight8 bytes) /
"rgba32f" (32-bit float, widthheight16 bytes) — for height/displacement
fields, baked lightmaps, and other data rasters an 8-bit format quantizes
visibly. The two high-precision formats store rgba verbatim and reject
opts.generateMipmaps / opts.maxDimension.
Parameters
rgbaany(optional) — Pixel payload atopts.format's native byte width — abuffer, a binary string, or a number array.widthnumber— numberheightnumber— numberoptsany(optional) —{ format?, srgb?, generateMipmaps?, maxDimension? }
Returns (string?, string?) ZTEX bytes, or (nil, errmsg).
globals/renderer/texture/encodeFromImage
renderer.texture.encodeFromImage(bytes: buffer | string, opts: any?) -> (string?, string?)
Encode source image bytes (png/jpg/webp/…) into an engine-native ZTEX
payload. Used by the texture importer / assetType onChange.
Parameters
bytesbuffer | string— source image bytes.optsany(optional) —{ format?, srgb?, generateMipmaps?, maxDimension? }
Returns (string?, string?) ZTEX bytes, or (nil, errmsg).
globals/renderer/texture/frameSchedule
renderer.texture.frameSchedule(texture: string | AssetRef) -> { number }?
The times at which each layer of a timed texture stops being shown, in seconds from the start of the sequence — the running total of the layer display times, so the last entry is the length of one pass.
This is the form a sampler reads a sequence through: a time is turned into
a layer by finding the first entry it has not passed, whatever the
individual layer times are. It is what the schedule slot of the builtin
animatedTexture shader holds, one entry per layer.
A texture whose layers carry no timing — a still image, a sprite sheet, a LUT stack — has no schedule and answers nil.
Parameters
texturestring | AssetRef— The texture — a guid, an identity, a name, a path, or a textureAssetRef.
Returns { number }? one cumulative end time per layer, or nil.
local ends = renderer.texture.frameSchedule(texRef) -- {0.1, 0.15, 0.35}
globals/renderer/texture/info
renderer.texture.info(ztex: buffer | string) -> (any, any)
Read the header of an engine-native ZTEX payload without copying the
pixels. Returns its format, dimensions, mip count, filter ("nearest"
or "linear" — the sampler baked into the blob from the asset's
settings.filter), and the payload's layer shape.
layers counts the array layers the payload carries and isArray is true
past one — the answer to "am I about to sample a texture_2d_array?",
available before anything samples it. animated is true when those layers
are a sequence in time; then frameDelaysMs lists each layer's display
time in milliseconds in display order, and durationMs totals one pass.
An animated image imports as one layer per frame, so layers is its frame
count. A still texture reports layers = 1, isArray = false.
Parameters
ztexbuffer | string— ZTEX bytes.
Returns (any, any) — (table?, string?) { format, width, height, mipCount, filter, layers, isArray, animated, frameDelaysMs?, durationMs? }, or (nil, errmsg).
local i = renderer.texture.info(bytes); if i.animated then print(i.layers, "frames", i.durationMs, "ms") end
globals/renderer/texture/isResident
renderer.texture.isResident(texture: string | { [string]: any } | AssetRef) -> boolean
True if a GPU texture is resident under this texture's guid.
Parameters
texturestring | { [string]: any } | AssetRef— The texture — aTextureHandle, aTextureCpuHandle, a guid, or a textureAssetRef.
Returns boolean
print(renderer.texture.isResident(handle))
globals/renderer/texture/list
renderer.texture.list() -> { any }
Every texture currently registered, ordered by guid — the ones a script
created and the ones that reached the device through an asset alike. Each
entry carries the guid, where it came from (origin is "asset" for a
texture the asset path uploaded), and whether the GPU still holds it. A
resident entry also carries the bytes it costs, its dimensions and its
texel format, so the listing sums to renderer.textureMemory(). A
streamable one carries streamOrigin — "asset" when a level change reads
the levels it needs back from the asset, "retained" when the cache holds
the pixels for it.
A script-created entry also carries held — whether renderer.hold pins
it for the session — and scene, the load that created it.
renderer.references("texture", guid) says what is still holding a row,
and renderer.collect() releases the rows nothing holds.
Returns { any } — Array of { guid, origin, owner?, scene?, held?, resident, bytes?, width?, height?, format?, compressed?, streamable?, streamOrigin? }.
for _, t in ipairs(renderer.texture.list()) do print(t.guid, t.bytes) end
globals/renderer/texture/loadCpu
renderer.texture.loadCpu(ref: string | AssetRef, encodeOpts: any?) -> TextureCpuHandle
Load a .texture asset's pixels into the ONE guid-keyed CPU store (the
Disk→CPU step) and return a CPU handle for per-pixel access (no GPU
readback). The handle holds NO pixels — only the guid, dims and texel
format plus the read/write/encode/unload ops (which read the Rust store).
The pixels stay at the format they were authored in: handle.format is
"rgba8", "rgba16" or "rgba32f", and :readPixel reports channels in
that format's own units. Called by texRef:load(). DEFAULT: upload to the
GPU then handle:unload().
Parameters
refstring | AssetRef— A textureAssetRef(carries.guid, reads its primary via getBytes), or any stringasset.refresolves to one — a guid, an identity, a name or a source path.encodeOptsany(optional) —{ format?, srgb?, generateMipmaps?, maxDimension? }applied when the primary is an encoded source image and needs the engine-native encode (a.ztexprimary is loaded as-is).
Returns TextureCpuHandle
local cpu = texRef:load(); local r,g,b,a = cpu:readPixel(3, 4); cpu:unload()
globals/renderer/texture/readback
renderer.texture.readback(texture: string | { [string]: any } | AssetRef) -> TextureCpuHandle
Read a runtime GPU texture's pixels back to CPU and return a
TextureCpuHandle for them — the GPU→CPU half of the runtime-texture freeze
path. A texture made with renderer.texture.create keeps no CPU copy, so
persisting it (:encode() → asset.create("texture", …)) reads it back
here first. Yields until the readback completes (a frame or two). After it
returns the pixels are resident in the guid-keyed CPU store: :readPixel,
:writePixel, :getInfo, :encode, :unload all work. Errors if the
texture never becomes GPU-resident.
A SCENE-space screen-sized render target is one resource shared by every
render target drawn — the viewport, an offscreen capture, a camera
rendering into a texture — resized and re-derived for each of them in
turn. The copy is taken ahead of all of them for the frame, so what a
readback of its guid answers is the content of the last frame the renderer
drew: the presented view's own image at the presented resolution, since
the presented view is the sink that draws last. A request made while the
renderer is holding frames back is carried to the next frame it draws
rather than being answered from a target another sink left standing, so a
readback can wait a frame longer than the copy itself takes.
Parameters
texturestring | { [string]: any } | AssetRef— The texture — theTextureHandlerenderer.texture.createreturned, a guid, or a textureAssetRef.
Returns TextureCpuHandle
local cpu = renderer.texture.readback(handle); local ztex = cpu:encode(); cpu:unload()
globals/renderer/texture/tone
renderer.texture.tone(histogram: any?) -> TextureTone
Reduce a histogram to what the picture's tone IS: where its darkest and brightest pixels sit, where the body of it sits, and how much of it is standing on the floor or the ceiling — all in code values on the 0-255 scale the pixels were delivered at.
span (max - min) is the whole range including a single stray pixel;
spread (p95 - p5) is the range the body of the picture occupies, which
is the reading that says whether a shot is legible. A frame whose subject is
modelled and shaded but delivered inside a few code values reads a large
mean and a tiny spread, and no mean alone can tell that apart from a
frame with a subject in it.
crushed and clipped are the shares of the picture at code 0 and at code
255, each 0..1 — what a shot loses to the floor and to the ceiling.
Parameters
histogramany(optional) — A histogram fromcpu:histogram().
Returns TextureTone
local t = cpu:tone(); if t.spread < 24 then error("the shot is flat") end
globals/renderer/texture/update
renderer.texture.update(texture: string | { [string]: any } | AssetRef, src: any?) -> TextureHandle
Overwrite the GPU texture texture names IN PLACE, under the same guid,
from new raw pixels. Never writes a .texture file — the play-mode mutate
path. Takes every form that names a texture — the TextureHandle create
returned, the guid renderer.texture.list hands out, a TextureCpuHandle
or a texture AssetRef. Returns a handle carrying the new dimensions: the
handle it was given, refreshed, and a handle over the guid otherwise.
Parameters
texturestring | { [string]: any } | AssetRef— The texture to update — aTextureHandle, a guid, aTextureCpuHandleor a textureAssetRef.srcany(optional) — New raw pixels{rgba, width, height, srgb?, format?}—rgbaas abuffer, a binary string, or a number array.
Returns TextureHandle — A TextureHandle for the updated texture.
globals/renderer/textureMemory
renderer.textureMemory() -> {
What the GPU texture cache holds, split by whether the texture is
block-compressed. compressedBytes and uncompressedBytes are what those
textures cost in VRAM, measured from each texture's own format and mip
chain — so a .texture whose settings name format = "bc7" appears in the
compressed columns at a quarter of what the same image costs as RGBA8.
blockCompressionSupported is whether this adapter can hold
block-compressed textures at all; where it is false a BC7 payload is
uploaded decoded and lands in the uncompressed columns instead, so the
texture is present everywhere and compressed where the hardware allows it.
Measured at the end of the last rendered frame.
streamableTextures is how many of them a texture budget can move the
base mip level of, split by where a level change reads the levels it needs
from: assetStreamedTextures are read back from the asset they came from
and hold nothing in system memory, retainedTextures hold the payload
because a script uploaded their pixels and the GPU copy is the only other
one there is. streamSourceBytes is what those held payloads occupy in
system memory — bytes that are not VRAM — so it is a reading on the
retained half alone. pinnedTextures counts the textures big enough to
stream that stand at a level nothing can move: their pixels were released
and no asset holds them, the asset behind them could not be read back, or a
UI image, a post-process property or a render feature holds a view of them.
A texture out of the streamable set only because no measured surface wears
it stands in neither count: a surface reaching it takes it back up, so its
level moves again as soon as there is a footprint to move it by. It reads 0
while no budget is armed.
Returns { blockCompressionSupported, compressedTextures, compressedBytes, uncompressedTextures, uncompressedBytes, streamableTextures, assetStreamedTextures, retainedTextures, pinnedTextures, streamSourceBytes }
local tm = renderer.textureMemory()
print(("%d compressed textures hold %.1f MiB"):format(tm.compressedTextures, tm.compressedBytes / 1048576))
print(("%d textures stream from their asset, %d hold %.1f MiB of pixels"):format(
tm.assetStreamedTextures, tm.retainedTextures, tm.streamSourceBytes / 1048576))
globals/renderer/textureStreaming
renderer.textureStreaming() -> TextureStreaming
What the last frame's texture-residency plan decided. budgetBytes is
the armed budget, and 0 means residency is left alone. streamable is
how many textures the plan can move. residentBytes is what those textures
occupy now, measured from the textures that are allocated; demandedBytes
is what the frame's demand alone would have cost, so the two part exactly
where the budget is doing something. starved counts the textures left
coarser than the frame asked for, promoted the ones that climbed a level
this frame, and changed the ones whose GPU texture was replaced. A camera
approaching a surface reads promoted above zero for a few frames and then
zero once it settles.
textures is one row per streamable texture, ordered by key, carrying the
level each one was asked for and the measurement that asked. Two byte
totals can agree while a single texture sits several levels off what its
surface samples, so read the row when the question is which level a texture
holds and why.
With budgetBytes at 0 nothing holds a level back, so residentBytes,
plannedBytes and demandedBytes all read the whole chain of every
texture still enrolled and textures is empty — which is how a session
that armed a budget and dropped it reads back that the levels came home.
Returns TextureStreaming — { budgetBytes, streamable, residentBytes, plannedBytes, demandedBytes, starved, promoted, changed, textures }
local ts = renderer.textureStreaming()
print(("textures: %.1f MiB resident of %.1f MiB demanded, %d starved"):format(
ts.residentBytes / 1048576, ts.demandedBytes / 1048576, ts.starved))
globals/renderer/transmissionShadows
renderer.transmissionShadows() -> boolean
Whether translucent casters tint the directional light they block.
Returns boolean
globals/renderer/uploadStats
renderer.uploadStats() -> {
What the last completed frame spent re-describing its renderables to the
GPU. Every renderable owns a slot in the per-instance data a draw reads —
its world matrix, the bounds the culler tests it by, and the flags that
decide which passes and which culling stages see it — and a frame uploads
only the slots whose contents changed. bytes is what those uploads
carried, fullBytes what re-sending every slot would have cost, and
writes how many buffer writes carried it. The three numbers cover that
per-renderable data alone, so a scene standing still reads bytes = 0
against a fullBytes that grows with the scene, and the ratio says how much
of it the scene's own churn — rather than its size — is paying for.
Returns { writes: number, bytes: number, fullBytes: number }
local u = renderer.uploadStats()
print(("instance upload: %d B of %d B in %d writes"):format(u.bytes, u.fullBytes, u.writes))
globals/renderer/variantSource
renderer.variantSource(program: string) -> string?
The WGSL one of the programs renderer.shaderVariants() lists holds,
exactly as the shader compiler received it. program is the program
field of a row's base or of one of its variants. Reading a base
alongside a variant shows what a feature set selected: each program's text
holds the code its own features guard. The variant-report spelling of
renderer.compiledSource, which answers the same for every other shader.
Parameters
programstring— Aprogramname fromrenderer.shaderVariants().
Returns string? — The compiled WGSL, or nil for a name no compile has run under.
local row = renderer.shaderVariants()[1]
local base = renderer.variantSource(row.base.program)
globals/require
require(path) -> module
Load a Lua module by VFS path. Cached after first load. Use for shared libraries.
Parameters
pathstring— VFS module path (e.g. '@libs/mylib')
Returns any — The module's return value
globals/retarget/animation
retarget.animation(clipRef: any?, targetMeshRef: any?, sourceMeshRef: any?) -> (boolean, string)
Retarget an animation clip onto a target rig, returning the VFS path of a
new .anim whose channels name the target skeleton's bones with bind-pose
corrected rotations. The source rig is the clip's embedded rig.zmsh (else
sourceMeshRef's skin, else the skinned mesh beside the clip in its bundle);
the target rig is targetMeshRef's skin. Play the result with
animGraph.addClip(entity, path). Pure asset transform — no entity/ECS state.
Parameters
clipRefany(optional) — Animation asset to retarget.targetMeshRefany(optional) — Target rig mesh whose skin defines the destination skeleton.sourceMeshRefany(optional) — Source rig mesh the clip was authored for; omit to use the clip's embedded rig.
Returns (boolean, string) — Success flag and the retargeted clip's VFS path (empty on failure).
local ok, path = retarget.animation(clipRef, targetMeshRef)
globals/retarget/extractRig
retarget.extractRig(meshBytes: buffer | string) -> string?
Strip a .mesh (ZMSH) payload to a lean skin-only rig: the skeleton with
geometry removed, re-encoded as a ZMSH whose only content is the skin. Returns
the rig bytes, or nil when the mesh carries no skin. A .animation composite
embeds this as rig.zmsh so a clip travels with its own source rig.
Parameters
meshBytesbuffer | string— Raw ZMSH mesh bytes carrying a skin.
Returns string? — Skin-only ZMSH rig bytes, or nil when the mesh has no skin.
local rig = retarget.extractRig(meshBytes)
globals/retarget/humanoidProfile
retarget.humanoidProfile(meshBytes: buffer | string) -> HumanoidHolder?
Derive the humanoid retarget holder for a rig from a .mesh (ZMSH)
payload, when that skeleton has the essential humanoid structure (a hips root,
a head or neck, at least one full arm chain and one full leg chain). Returns
nil for a rig that is not a humanoid — a prop, a plant whose leaves animate, a
quadruped — so a clip from it stays a plain clip rather than joining the shared
humanoid-animation pool. A rig whose bone hierarchy loops answers nil and a
message naming the bone edge that closes the loop, so a caller reading the
second return value can tell malformed input from a plain non-humanoid.
Parameters
meshBytesbuffer | string— Raw ZMSH mesh bytes carrying a skin.
Returns HumanoidHolder? — { base, boneCount, roles = { [role] = boneName } }, or nil when the rig is not a humanoid; nil and a message naming the closing bone edge when its hierarchy loops.
local holder = retarget.humanoidProfile(meshBytes)
globals/retarget/isHumanoid
retarget.isHumanoid(meshBytes: buffer | string) -> boolean
Whether a rig is a humanoid avatar — true when humanoidProfile resolves a
holder for it. Use this to tell a humanoid character apart from a generic
animated mesh (a prop, a plant, a quadruped) before treating its clips as
shareable humanoid animations.
Parameters
meshBytesbuffer | string— Raw ZMSH mesh bytes carrying a skin.
Returns boolean — True when the rig has the essential humanoid structure.
if retarget.isHumanoid(meshBytes) then ... end
globals/retarget/serializeProfile
retarget.serializeProfile(holder: HumanoidHolder) -> string
Serialize a humanoid holder to the humanoid.profile file body: an
editable YAML role -> bone-name map. Roles list hips-first head-to-toe through
the limbs, then any extras name-sorted, so the file reads top-down and diffs
stably. Edit a value to correct an auto-derived mapping.
Parameters
holderHumanoidHolder— A holder fromhumanoidProfile.
Returns string — The YAML body to store as humanoid.profile.
files["humanoid.profile"] = retarget.serializeProfile(holder)
globals/scopes/current
scopes.current() -> string?
The scope the calling code registers a resource under right now — the module whose body is running, the component instance whose lifecycle hook is on the stack, or the chunk of this call. Nil when the caller registers under no context.
Returns string? — The scope tag, or nil.
print("resources I register follow", scopes.current())
globals/scopes/list
scopes.list() -> { LiveResource }
Every live resource that follows an owning context, across every subsystem holding them. A resource registered with no context above it — the engine's own — is not listed, because no scope reaches it.
Returns { LiveResource } — Array of live resources.
for _, r in scopes.list() do print(r.scope, r.kind, r.detail) end
globals/scopes/release
scopes.release(scope: string) -> { Released }
End every resource registered under scope, across every subsystem.
Reaches contexts no seam does — the chunk of an execute call that
registered something and ended without releasing it.
Parameters
scopestring— A scope tag, as thescopefield of alist()row carries it.
Returns { Released } — One row per subsystem that ended something, with how many it ended.
local ended = scopes.release("exec:__exec_12")
globals/screenToPlanePoint
screenToPlanePoint(sx, sy, plane_y) -> x, y, z
Unproject screen coords to the intersection point on a horizontal Y-plane (the common cursor-on-ground case). Returns three numbers (x, plane_y, z) on hit, or no values when the ray is parallel to the plane, the plane is behind the camera, or no main camera has rendered yet — so local x, y, z = screenToPlanePoint(...) cleanly yields nil, nil, nil on miss.
Parameters
sxnumber— Screen X in pixelssynumber— Screen Y in pixelsplane_ynumber— World-space Y of the horizontal plane to intersect
Returns number, number, number — World-space hit point (x, plane_y, z). Returns no values on miss.
globals/screenToRay
screenToRay(sx, sy) -> { origin: vec3, direction: vec3 } | nil
Unproject screen coordinates to a world-space ray. Returns a single table { origin = {x,y,z}, direction = {x,y,z} } where direction is a unit vector, or nil if no main camera has rendered yet. The ray is computed against the main viewport, so offscreen passes (capture, shadow, post-process) do not disturb it.
Parameters
sxnumber— Screen X in pixelssynumber— Screen Y in pixels
Returns table | nil — Ray table { origin = {x,y,z}, direction = {x,y,z} }, or nil if unavailable
globals/select
select(index, ...) -> values
Select from varargs. select('#', ...) returns count.
globals/service/authenticated
service.authenticated() -> boolean
Whether a platform identity (JWT) is available to attach to service calls. Returns only a boolean — never the token.
Returns boolean — True if a caller identity is available.
if not service.authenticated() then error("link ZeroMind") end
globals/service/balance
service.balance() -> string?
Read the caller's credit balance from ZeroMind. Returns a
promise handle for task.await() resolving the balance JSON, or nil
when the gateway is unconfigured or no caller identity is available.
Returns string? — Promise handle for task.await(), or nil if not ready.
local h = service.balance(); local raw = h and task.await(h)
globals/service/gatewayConfigured
service.gatewayConfigured() -> boolean
Whether the ZeroMind service gateway has been configured.
Service handlers use this to distinguish "gateway not configured"
from "not signed in" when invoke returns nil.
Returns boolean — True if the gateway base URL is set.
if not service.gatewayConfigured() then error("no gateway") end
globals/service/invoke
service.invoke(offering: string, endpoint: string, opts: InvokeOpts?) -> string?
Invoke a provider offering's logical endpoint through ZeroMind.
Returns a promise handle for task.await() resolving the
InvokeResponse JSON, or nil when the gateway is unconfigured or no
caller identity is available. The JWT and real upstream URL are
never exposed to Luau.
Parameters
offeringstring— Fully-qualified offering identityprovider/name(e.g. "origozero/mesh_gen").endpointstring— Logical endpoint name (e.g. "create_preview").optsInvokeOpts(optional) —{ params?, headers?, body?, idempotency_key? }.
Returns string? — Promise handle for task.await(), or nil if not ready.
local h = service.invoke("origozero/mesh_gen", "create_preview", { body = { prompt = p } })
globals/service/jobStatus
service.jobStatus(jobId: string) -> string?
Poll a submitted service job. Returns a promise handle for
task.await() resolving the JobStatusResponse JSON { job_id, status, result?, error? }: status walks pending/running -> succeeded
(with result, the same InvokeResponse invoke returns) or failed
(with error). nil when the gateway is unconfigured or no caller
identity is available.
Parameters
jobIdstring— Job id returned bysubmitJob.
Returns string? — Promise handle resolving the job status JSON, or nil if not ready.
local h = service.jobStatus(jobId); local raw = h and task.await(h)
globals/service/submitJob
service.submitJob(offering: string, endpoint: string, opts: InvokeOpts?) -> string?
Submit a durable async invocation of an offering endpoint. Same
arguments as invoke, but the provider round-trip runs server-side
(off this connection), so a slow synchronous provider or a dropped
link no longer loses the result. Returns a promise handle for
task.await() resolving { job_id, status }; poll it with
jobStatus. nil when the gateway is unconfigured or no caller
identity is available.
Parameters
offeringstring— Fully-qualified offering identityprovider/name(e.g. "origozero/mesh_gen").endpointstring— Logical endpoint name (e.g. "create_preview").optsInvokeOpts(optional) —{ params?, headers?, body?, idempotency_key? }.
Returns string? — Promise handle resolving { job_id, status }, or nil if not ready.
local h = service.submitJob("origozero/mesh_gen", "create_preview", { body = { prompt = p } })
globals/setmetatable
setmetatable(table, mt) -> table
Set a table's metatable.
globals/settings/all
settings.all() -> { [string]: any }
Snapshot of the entire settings document (parsed). Modifying
the returned table does NOT propagate — call set or setMany
to persist. Useful for editors/inspectors that render every
section.
Returns { [string]: any } — A nested table mirroring the TOML document.
for section, keys in pairs(settings.all()) do
globals/settings/get
settings.get(key: string) -> any
Look up a value by dotted key. Returns whatever the file holds at that path — string / number / boolean / array / table — or nil if missing.
Parameters
keystring— Dotted-path key, e.g."render.culling_mode".
Returns any — The raw value at that key, or nil.
local mode = settings.get("render.culling_mode")
globals/settings/getBool
settings.getBool(key: string, default: boolean?) -> boolean
Boolean-typed accessor. Returns the value when present and
boolean-typed; falls back to default (or false) on missing key
or type mismatch.
Parameters
keystring— Dotted-path key.defaultboolean(optional) — Optional fallback boolean.
Returns boolean — The boolean value or the fallback.
if settings.getBool("render.shadows", true) then ... end
globals/settings/getNumber
settings.getNumber(key: string, default: number?) -> number
Number-typed accessor. Returns the value when present and
number-typed; falls back to default (or 0) on missing key or
type mismatch.
Parameters
keystring— Dotted-path key.defaultnumber(optional) — Optional fallback number.
Returns number — The number value or the fallback.
local g = settings.getNumber("physics.gravity", -9.81)
globals/settings/getString
settings.getString(key: string, default: string?) -> string
String-typed accessor. Returns the value when present and
string-typed; falls back to default (or "" if omitted) on
missing key or type mismatch.
Parameters
keystring— Dotted-path key.defaultstring(optional) — Optional fallback string.
Returns string — The string value or the fallback.
local mode = settings.getString("render.culling_mode", "gpu")
globals/settings/set
settings.set(key: string, value: any?)
Set a value by dotted key, then serialize and write the
file. In play mode the write fails like any other source-file
write — call wld.edit() first to unlock.
Parameters
keystring— Dotted-path key.valueany(optional) — Replacement value (string / number / boolean / table).
settings.set("render.culling_mode", "cpu")
globals/settings/setMany
settings.setMany(updates: { [string]: any })
Apply many key/value updates in one batched write — fewer
serialize+write round-trips than calling set per-key. Same
lock semantics as set.
Parameters
updates{ [string]: any }— A table of dotted-key → value pairs.
settings.setMany({
globals/shader/compile
shader.compile(keys: string | { string }, opts: { [string]: any }) -> boolean
Compile a zero-scaffolding SURFACE shader: the author wrote only
vertex() / fragment() and declared its material properties, and the
engine generates the group(1) material interface plus every render-mode
entry point. Compiles once and registers the result under every key.
Parameters
keysstring | { string }— One registration key, or the array of keys (guid, identity, aliases) the one compiled program answers to.opts{ [string]: any }—{ source, domain?, properties? }— the author's WGSL, its@domain, and the declared property schema.
Returns boolean — True when the compile was queued.
shader.compile({ ref.guid, ref.identity }, { source = wgsl, properties = props })
globals/shader/registerModule
shader.registerModule(keys: string | { string }, source: string) -> boolean
Register a block of WGSL other shaders include. Every key names the same source, so a shader includes it by whichever name it holds — its guid, its identity, or an alias. Registering again replaces it, and the shaders that include it recompile.
Parameters
keysstring | { string }— One key, or the array of keys this module answers to.sourcestring— The module's WGSL.
Returns boolean — True when the registration was queued.
shader.registerModule({ ref.guid, ref.identity }, wgsl)
globals/shader/status
shader.status(name: string) -> (string, string?)
A shader's latest compile outcome, without reading the engine log:
"compiled", "failed" (with the compiler error second), or "pending".
Compilation is async, so a "pending" straight after a write means ask
again next frame.
Parameters
namestring— Shader identity or guid — the key it compiled under.
Returns (string, string?) — Status, and the compiler error when it failed.
local status, err = shader.status(ref.guid)
globals/shell/run
shell.run(command: string) -> ShellResult
Execute a command in the engine's emulated Unix shell and
return once it has completed. This is the same shell as the MCP
bash tool — 60+ builtins (ls, cat, grep, find, echo, ...)
operating on the virtual scene filesystem. A command that runs
Luau (run, luau, zm, zero) needs the engine's frame loop,
so from a coroutine it is queued to run off the frame loop and
this yields until it finishes; everything else runs inline. That
queueing runs the whole line, so a line that also ran a command
of its own comes back with the explanation in stderr and
shell.runAsync as the way to run it whole.
Parameters
commandstring— Shell command to execute.
Returns ShellResult — Command result { stdout, stderr, exitCode, ok }.
local r = shell.run("ls /zero/source")
globals/shell/runAsync
shell.runAsync(command: string) -> string
Asynchronous version of shell.run. Returns a promise ID that
resolves to a JSON-encoded result string. Use with
task.await().
Parameters
commandstring— Shell command to execute.
Returns string — Promise ID — pass to task.await() to get the JSON result.
local json = task.await(shell.runAsync("find /zero -name '*.luau'"))
globals/skeleton/applyPose
skeleton.applyPose(sinkHandle: number, poseBuffer: Substrate.TypedBuffer) -> boolean
Snapshot the buffer's first layout.total_floats values and
queue a pending apply for the next ECS drain. Returns false on
unknown sink/buffer or buffer too small for the layout. The
Buffer is unchanged.
Parameters
sinkHandlenumber— Sink handle frombindPose.poseBufferSubstrate.TypedBuffer— The pose buffer to apply.
Returns boolean — True on success.
globals/skeleton/bindClip
skeleton.bindClip(zanimBytes: buffer | string, boneOrder: { string }) -> ClipBindInfo?
Decode a zanim payload and bind it to boneOrder,
precomputing which of the clip's channels feed each bone so
per-frame sampleClip is allocation-free. Returns
{ handle, matched, total, duration }, or nil on a malformed
payload / empty bone order. Check matched: 0 means the clip
drives none of these bones.
Parameters
zanimBytesbuffer | string— The clip'sdata.zanimpayload bytes (binary-safe).boneOrder{ string }— Output bone names — one stride-10 record per bone.
Returns ClipBindInfo? — { handle, matched, total, duration }, or nil.
globals/skeleton/bindPose
skeleton.bindPose(entityId: (string | entityRef)?, opts: SkeletonLayout) -> number?
Register a pose sink targeting entityId. The opts table
carries the layout: boneOrder is the bone-name array
({"hip", "spine", ...}), stride defaults to 10
(translation.xyz + rotation.xyzw + scale.xyz). Pass entityId
as nil to use the current component's owning entity.
Parameters
entityId(string | entityRef)(optional) — Engine entity id or proxy, or nil for the current entity.optsSkeletonLayout—{ boneOrder, stride }.
Returns number? — Sink handle, or nil.
local h = skeleton.bindPose(nil, { boneOrder = bones, stride = 10 })
globals/skeleton/clipBones
skeleton.clipBones(zanimBytes: buffer | string) -> { string }?
Decode a zanim payload and return its bone-name array. Pure:
build a bind order or a retarget map from a clip without binding a
sampler. Returns nil on bytes that aren't a valid zanim payload.
Parameters
zanimBytesbuffer | string— The clip'sdata.zanimpayload bytes (binary-safe).
Returns { string }? — Bone names referenced by the clip, or nil.
local names = skeleton.clipBones(vfs.read(path .. "/data.zanim"))
globals/skeleton/clipDecode
skeleton.clipDecode(zanimBytes: buffer | string) -> string?
Decode a zanim payload to its readable JSON form
({ name, duration, channels, bone_names }). The binary parse is
the engine's; json.decode the result to inspect or transform a
clip's channels (e.g. the retarget bake) in Luau. Returns nil on
bytes that aren't a valid zanim payload. Inverse of clipEncode.
Parameters
zanimBytesbuffer | string— The clip'sdata.zanimpayload bytes (binary-safe).
Returns string? — The clip as a JSON string, or nil.
local clip = json.decode(skeleton.clipDecode(bytes))
globals/skeleton/clipEncode
skeleton.clipEncode(jsonString: string) -> string?
Encode a clip's JSON form (the shape clipDecode returns) back
to a zanim payload — the bytes a .animation stores and
bindClip/sampleClip consume. Inverse of clipDecode. Returns
nil on invalid JSON.
Parameters
jsonStringstring— A clip JSON document.
Returns string? — The clip's zanim payload bytes, or nil.
local bytes = skeleton.clipEncode(json.encode(clip))
globals/skeleton/jointTransforms
skeleton.jointTransforms(entityId: string | entityRef) -> table
Read a skinned entity's per-joint world transforms for the current animated pose.
Parameters
entityIdstring | entityRef— Engine entity id or proxy of a skinned entity.
Returns table — Array of joint transforms: { position, matrix, parent, name }.
local joints = skeleton.jointTransforms(meshId)
globals/skeleton/sampleClip
skeleton.sampleClip(handle: number, time: number, poseBuffer: Substrate.TypedBuffer) -> boolean
Sample the bound clip at time (clamped to [0, duration])
and write one stride-10 pose record per bound bone into the
Buffer, starting at index 0. Bones the clip does not drive are
written as identity. Returns false on unknown handle/buffer or a
buffer too small for the bone count.
Parameters
handlenumber— Sampler handle frombindClip.timenumber— Sample time in seconds.poseBufferSubstrate.TypedBuffer— The stride-10 pose buffer written into.
Returns boolean — True on success.
globals/skeleton/unbindClip
skeleton.unbindClip(handle: number) -> boolean
Drop the bound clip sampler from the registry.
Parameters
handlenumber— Sampler handle to remove.
Returns boolean — True if the sampler existed.
globals/skeleton/unbindPose
skeleton.unbindPose(sinkHandle: number) -> boolean
Remove the sink from the registry.
Parameters
sinkHandlenumber— Sink handle to remove.
Returns boolean — True if the sink was present.
globals/sky/get
sky.get() -> { [string]: any }
Get all current sky configuration as a table. Returns the
same fields as sky.set accepts, plus read-only fields like
material_name and type. Color values are returned as
positional arrays [r, g, b].
Returns { [string]: any } — Full sky configuration table.
local cfg = sky.get(); print(cfg.time_of_day)
globals/sky/getTimeOfDay
sky.getTimeOfDay() -> number
Get the current time of day in hours (0-24).
Returns number — Current time of day.
local t = sky.getTimeOfDay()
globals/sky/preset
sky.preset(name: string)
Apply a named sky preset. Available: clear_day, sunset,
sunrise, overcast, night, studio, none. Raises a Luau
error for unrecognized names — wrap in pcall if uncertain.
Parameters
namestring— Preset name (case-sensitive).
sky.preset("sunset")
globals/sky/set
sky.set(opts: SkyOpts)
Configure the sky system. All fields are optional — only
provided fields are updated. Color fields accept both named
{x=r, y=g, z=b} and positional {r, g, b} forms. color is
an alias for solid_color.
Parameters
optsSkyOpts— Sky configuration properties.
sky.set({ type = "procedural", time_of_day = 14, sync_sun_to_light = true })
globals/sky/setSunDirection
sky.setSunDirection(dir: SkyColor)
Set an explicit sun direction and disable time-based sun positioning. The directional light is updated to match.
Parameters
dirSkyColor— Normalized sun direction vector.
sky.setSunDirection({ 0.5, -1, 0.3 })
globals/sky/setTimeOfDay
sky.setTimeOfDay(time: number)
Set the time of day (0-24 hours). 0 = midnight, 6 = sunrise, 12 = noon, 18 = sunset.
Parameters
timenumber— Time of day in hours.
sky.setTimeOfDay(18.5)
globals/spawn
spawn(...): any
High-level spawn helpers — auto-injected by the prelude from @builtin::modules.spawn. Provides spawn.cube, spawn.sphere, etc.
globals/stream/accept
stream.accept(listener: string) -> string?
Take the connection that has waited longest on the listener, as a
stream handle that reads, writes, and closes exactly like one
stream.open returned. Returns nil when nothing is waiting, so call
it in a loop each tick to take every peer that arrived.
stream.listenerStatus(listener).pending is how many are still
waiting.
Parameters
listenerstring— Listener handle from stream.listen.
Returns string? — The connection's stream handle, or nil when none is waiting.
while true do local h = stream.accept(listener); if not h then break end; table.insert(peers, h) end
globals/stream/close
stream.close(handle: string) -> boolean
Finish whatever handle names — a stream or a listener — and
drop it from the registry.
Closing a stream refuses every later write and carries the bytes
already queued to the peer before the connection ends, so a write and
a close in the same tick deliver — the shape a request answered with
one response has. A peer that has stopped reading altogether holds
that finish for thirty seconds; past that the connection ends and
what is still queued ends with it, so a caller that must know its
bytes went out watches stream.status(handle).pending reach zero
before it closes.
Closing a listener stops it answering new peers and closes the
connections nobody took; the connections stream.accept already
handed over keep running until they are closed themselves.
Parameters
handlestring— Stream handle from stream.open or stream.accept, or listener handle from stream.listen.
Returns boolean — True if a stream or listener was closed, false if handle already named none.
stream.write(peer, response); stream.close(peer)
globals/stream/listen
stream.listen(url: string, opts: StreamListenOpts?) -> string
Hold the address url names and answer the peers that dial it —
the other direction from stream.open, for when the thing you are
talking to starts the conversation and restarts on its own schedule.
Returns a promise handle: task.await() it to get the listener
handle once the address is held, or it raises the reason a malformed
url, a scheme that cannot listen, or a failed bind was refused with.
Take the connections with stream.accept.
The host in the url is the interface bound, and the whole of what
decides who can reach it. tcp://127.0.0.1:9000 answers only
programs on this same machine. tcp://0.0.0.0:9000 answers any host
that can route to this machine on that port — every device on the
wifi, and anything beyond it the network lets through. Write the one
you mean; there is no default, and stream.listenerStatus reports
which of the two you got. A port of 0 asks the operating system to
choose one, which that same status then reports.
opts.inboundCapacity and opts.outboundCapacity bound each
answered connection (65536 bytes each by default);
opts.backlog bounds the connections held for stream.accept
before the listener stops taking them from the operating system,
which leaves the rest queued in the kernel rather than answered and
forgotten (16 by default, and at least 1).
The listener belongs to the chunk that opened it — the chunk whose
own code called stream.listen, which is the module holding that
line even when something else called into it. When that chunk runs
again — a module hot-reload, a cleared require cache — the listener
and the connections it answered are closed, and the new run binds the
address for itself. Peers see the connection close and dial again.
stream.listeners() names that chunk as each entry's owner.
Parameters
urlstring— Listen URL — scheme://host:port.optsStreamListenOpts(optional) — Per-connection capacities and the accept backlog (optional).
Returns string — Promise handle for task.await().
local pending = stream.listen("tcp://127.0.0.1:9000"); local listener = task.await(pending)
globals/stream/listenerStatus
stream.listenerStatus(listener: string) -> ListenerStatus?
Report what the listener holds and has handed over. address is
the address the operating system resolved the bind to, port
included — the one to hand a peer. reach says who can connect to
it: "thisMachine" when it is a loopback address and only programs
on this machine can, "network" when any host that can route here
can. accepted counts the connections stream.accept handed over,
pending the ones still waiting, and capacity the value pending
may reach before the listener stops taking connections from the
operating system. nil when handle names no open listener.
Parameters
listenerstring— Listener handle from stream.listen.
Returns ListenerStatus? — Listener status, or nil when handle names no open listener.
local s = stream.listenerStatus(listener); print(s.address, s.reach, s.pending)
globals/stream/listeners
stream.listeners() -> { OpenListener }
Every listener this engine currently holds an address for, in the
order they were opened. Each entry is what stream.listenerStatus
reports about it, plus the handle it is addressed by and the owner
chunk its life follows.
This is how an address is reached again once nothing holds its handle:
filter on address for the port you want and close the entry by its
handle, rather than guessing at handles.
Returns { OpenListener } — An array of open listeners.
for _, l in stream.listeners() do if l.address == want then stream.close(l.handle) end end
globals/stream/open
stream.open(url: string, opts: StreamOpenOpts?) -> string
Open a byte stream at url (scheme://target[?k=v]). loopback
carries written bytes back out of the same stream and works on
every platform; tcp dials host:port; tty opens a serial
device node — /dev/ttyACM0 or /dev/ttyUSB0 for a USB CDC board
such as an ESP32, /dev/rfcomm0 for a Bluetooth controller paired
over classic SPP (both present as a tty on Linux, so one transport
serves either peer), COM5 on Windows. tty query parameters:
baud (default 115200), dataBits (5-8, default 8), parity
(none | odd | even, default none), stopBits (1 or 2,
default 1).
ble connects to a Bluetooth Low Energy device over GATT, on a
desktop engine and in a browser alike — the wireless transport a
web world reaches a device through:
ble://<device>?service=<uuid>&write=<uuid>¬ify=<uuid>. The
device is the name it advertises, * any device offering the
service, a trailing * a name prefix (Paw*). write is the
characteristic this engine writes to and notify the one it
subscribes to, which on a Nordic UART peripheral are that
peripheral's RX and TX; a module with one bidirectional
characteristic names it for both. UUIDs may be 16-bit (ffe0),
32-bit, or full. Optional: chunk (bytes per packet, 1-512 —
otherwise what the connection carries), writeMode
(withResponse | withoutResponse, default withResponse),
timeout (seconds to find and connect to the device, default
15).
opts bounds the stream's undrained inbound buffer
and in-flight outbound bytes (default 65536 each). Returns a
promise handle: task.await() it
to get the stream handle once the transport is open, or it raises
the reason a malformed url, an unknown or unsupported scheme, or a
failed connect was refused with. A ble stream resolves as soon as
it exists and reports the rest as state — watch
stream.status(handle).state go opening, permissionPending
while the browser asks the person at the machine to pick a device,
then open; writes made meanwhile are queued and go out when it
connects. Check stream.transports() first
to tell a mistyped scheme from one this build does not carry.
Parameters
urlstring— Stream URL — scheme://target[?k=v&k=v].optsStreamOpenOpts(optional) — Buffer capacities (optional).
Returns string — Promise handle for task.await().
local pending = stream.open("loopback://echo"); local handle = task.await(pending)
local paw = task.await(stream.open("ble://Paw*?service=ffe0&write=ffe1¬ify=ffe1"))
globals/stream/read
stream.read(handle: string, max: number?) -> string
Drain up to max buffered inbound bytes from the stream.
Parameters
handlestring— Stream handle from stream.open.maxnumber(optional) — Maximum bytes to drain (optional). Omit to drain everything buffered.
Returns string — Drained bytes, byte-safe. "" when none buffered or handle names no open stream.
local chunk = stream.read(handle)
globals/stream/serialPorts
stream.serialPorts() -> SerialPorts
Every serial device this machine has, for picking the one to open.
ports is an array ordered by path. Each entry carries the path the
device is at (/dev/ttyACM0 on Linux, COM3 on Windows), the url
that opens it, the kind of bus it attaches by, and — for a USB
device — the vendorId, productId, serialNumber, manufacturer
and product it advertises.
A device's path moves with enumeration order: a board that came up at
/dev/ttyACM0 is at /dev/ttyACM1 once something else is plugged in
first, and moves across COM3-COM5 on Windows. What the device
advertises holds still across those moves, so match on
vendorId/productId — or on serialNumber to tell two of the same
board apart — and open the url that entry carries, appending the
port settings stream.open documents.
Three answers are distinct. supported false with a reason means
this platform has no serial bus to enumerate at all. error set means
it has one and the operating system refused this enumeration, so a
later call may answer. An empty ports with neither means the machine
has no serial device attached, which is an ordinary result.
Returns SerialPorts — { supported, reason, error, ports } — the platform's answer, this enumeration's, and the devices it found.
for _, p in stream.serialPorts().ports do if p.vendorId == 0x303A then print(p.url, p.product) end end
globals/stream/status
stream.status(handle: string) -> StreamStatus?
Report what the stream has carried and lost. state is where
the stream is in its life: opening, permissionPending while the
platform asks the person at the machine to allow the connection,
open, denied when that permission was refused, and closed
when it is finished. pending is bytes
accepted and not yet handed to the peer; capacity is the value
pending may reach before a write is refused. error holds the
most recent transport failure and the refusal a denied stream
carries, retained for the life of the
stream. nil when handle names no open stream.
Parameters
handlestring
Returns StreamStatus? — Stream status, or nil when handle names no open stream.
local s = stream.status(handle); print(s.pending, s.capacity)
globals/stream/streams
stream.streams() -> { OpenStream }
Every open stream, dialled or answered, in the order they were
opened. Each entry is what stream.status reports about it, plus the
handle it is addressed by and the owner chunk its life follows —
a connection stream.accept handed over carries the owner of the
listener that answered it.
Returns { OpenStream } — An array of open streams.
for _, s in stream.streams() do print(s.handle, s.transport, s.pending, s.owner) end
globals/stream/transports
stream.transports() -> { [string]: TransportSupport }
Every stream scheme this build knows about — a capability
probe, in both directions. supported answers stream.open and
listen answers stream.listen, since a scheme can carry one and
not the other. Each reason is nil when its direction works,
otherwise it names why not: an unbuilt transport names its own
absence, a transport this platform lacks (tcp and tty on wasm;
ble in a browser without Web Bluetooth or with the radio off,
which the page itself answers) names that, and a loopback stream,
whose peer is itself, names that nothing dials it. A typo'd scheme
is absent from this table entirely, which is what tells it apart
from a real transport this build lacks.
Returns { [string]: TransportSupport } — Map of scheme name to { supported, reason, listen, listenReason }.
local t = stream.transports(); if not t.tcp.listen then warn(t.tcp.listenReason) end
globals/stream/write
stream.write(handle: string, bytes: string) -> WriteOutcome
Queue bytes for the stream's peer. Never blocks. "accepted"
means the bytes were queued. "full" means the outbound queue has
no room right now — backpressure, not failure: the peer is alive
and draining slower than this call is producing, so a retry after
it catches up can succeed. Compare pending against capacity on
stream.status() to see it coming before a write is refused.
"closed" means the stream is finished, or handle names no open
stream — reopen to continue, retrying never succeeds. "tooLarge"
means bytes is bigger than the stream's whole outbound capacity, so
it can never fit at any queue depth — retrying the same write
returns this again.
Parameters
handlestring— Stream handle from stream.open.bytesstring— Bytes to queue, byte-safe.
Returns WriteOutcome — "accepted" | "full" | "closed" | "tooLarge"
local outcome = stream.write(handle, data)
globals/streaming/cells
streaming.cells() -> { [string]: any }
What the spatial-streaming store has resident: the configured radii and budget, the counters the store keeps, and one row per cell with how many of its groups are standing, what it costs, and whether a release wrote it to a file it now reads back from.
Returns { [string]: any } — { config, stats, sources, cells, proxies }.
local s = streaming.cells()
globals/streaming/levels
streaming.levels(scene: any?) -> { [string]: any }
Which level every mesh-LOD receiver is drawing at, and the screen
fraction that selection was measured from.
A receiver whose entity the scene no longer holds is reported as
standing = false: the chain is registered and there is nothing left for
it to draw.
Parameters
sceneany(optional) — The scene walk to read against. Omitted, the call takes its own.
Returns { [string]: any } — { count, receivers }.
local l = streaming.levels()
globals/streaming/observe
streaming.observe() -> { [string]: any }
The whole reading in one document: terrain, voxel, streaming cells and mesh LOD, plus the totals those rows sum to.
Built by this call and published as its last act, so
/zero/runtime/observations/streaming serves the same document rather
than a second derivation of it.
Returns { [string]: any } — { terrain, voxel, cells, levels, totals }.
local r = streaming.observe()
globals/streaming/reasons
streaming.reasons() -> { string }
Every reason whyNotDrawn can answer with, so a caller can enumerate
the set rather than meeting it one failure at a time.
Returns { string } — Sorted array of reason names.
local r = streaming.reasons()
globals/streaming/terrain
streaming.terrain(scene: any?) -> { [string]: any }
What each terrain entity is drawing: whether a heightfield is bound to it, the LOD cut it settled on, what that cut costs in indices and in the vertex pool, and the eye the cut was refined under.
Parameters
sceneany(optional) — The scene walk to read against. Omitted, the call takes its own, which is what makes a whole reading one walk rather than four.
Returns { [string]: any } — { count, entities } — one row per entity carrying a Terrain.
local t = streaming.terrain()
globals/streaming/voxel
streaming.voxel(scene: any?) -> { [string]: any }
What became of every chunk of every voxel world: how many are meshed, queued, failed or empty, and one row per chunk carrying the state, the engine's reason when a build failed, and what the build reserved on the device.
Parameters
sceneany(optional) — The scene walk to read against. Omitted, the call takes its own.
Returns { [string]: any } — { count, worlds } — one entry per entity carrying a VoxelWorld.
local v = streaming.voxel()
globals/streaming/whyNotDrawn
streaming.whyNotDrawn(subject: any?) -> { [string]: any }
Why a piece of a world's detail is not on screen, as one reason from
the closed set streaming.reasons() enumerates, with a detail line naming
what that reason is about.
The subject picks which system answers:
- an entity ref, id or name — whichever of the four systems holds it
{ entity = ..., chunk = "cx_cy_cz" }— one chunk of a voxel world{ entity = ..., level = n }— one level of a mesh-LOD chain{ cell = "x_z" }— one cell of the spatial-streaming store
Parameters
subjectany(optional) — The entity, chunk, level or cell to answer about.
Returns { [string]: any } — { kind, reason, detail }.
local w = streaming.whyNotDrawn({ entity = "Vox", chunk = "0_0_0" })
globals/stringx/scanNumbers
stringx.scanNumbers(s: string, pos: number?) -> ({ number }, number)
Read the run of numbers starting at pos — separated by commas and/or
whitespace — and report where the run ended.
The run stops at the first character that neither continues a number nor
separates two of them (], }, a quote, a letter), and nextPos is that
character's index, so the caller's own parser resumes exactly there. A
token that is not a valid number also ends the run, with nextPos left ON
it rather than past it, so nothing is skipped without the caller seeing it.
Parameters
sstring— The text to read.posnumber(optional) — 1-based index to start at. Defaults to 1.
Returns ({ number }, number) — The numbers found, and the 1-based position just past them.
-- A JSON array of numbers, in one crossing instead of one per token.
local values, nextPos = stringx.scanNumbers(payload, afterBracket)
-- A whitespace-separated block (OBJ, PLY, a matrix dump).
local m = stringx.scanNumbers("1 0 0 0 0 1 0 0", 1)
globals/subscriptions/cancel
subscriptions.cancel(id: string) -> boolean
Cancel a subscription by id: disconnects the live connection immediately and marks the row cancelled. Returns true when a live subscription was cancelled, false for an unknown or already-disconnected id.
Parameters
idstring— Subscription id to cancel.
Returns boolean — True when a live subscription was disconnected.
subscriptions.cancel(conn.id)
globals/subscriptions/get
subscriptions.get(id: string) -> SubscriptionRow?
One subscription row by id, or nil when the id is unknown (never tracked, or evicted after its publisher was destroyed).
Parameters
idstring— Subscription id (conn.id, or a/zero/runtime/events/subscriptions/entry).
Returns SubscriptionRow? — The subscription row, or nil.
local row = subscriptions.get(conn.id); print(row and row.deliveries)
globals/subscriptions/list
subscriptions.list(filter: SubscriptionFilter?) -> { SubscriptionRow }
Every tracked subscription row, optionally filtered by publisher instance id, publisher entity id, event name, and/or connected state.
Parameters
filterSubscriptionFilter(optional) — Optional filter table.
Returns { SubscriptionRow } — Array of subscription rows.
for _, s in ipairs(subscriptions.list({ connected = true })) do print(s.id, s.event, s.deliveries) end
globals/subscriptions/publishers
subscriptions.publishers() -> { PublisherRow }
Every live event publisher: component instance, entity, and per-event fire stats (fires happen whether or not anyone subscribes) plus current subscriber ids.
Returns { PublisherRow } — Array of publisher rows.
for _, p in ipairs(subscriptions.publishers()) do print(p.component, p.entityName) end
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
globals/text/alive
text.alive(handle: any?) -> boolean
Whether the text system still holds this handle — true between
text.create and the text.destroy that released it.
Parameters
handleany(optional) — Text handle fromtext.create.
Returns boolean — True while the handle is live.
if not text.alive(h) then h = text.create({ content = "again" }) end
globals/text/count
text.count() -> number
How many text objects the text system is holding — the number that
moves when text.create and text.destroy are called.
Returns number — The live text-object count.
local before = text.count()
globals/text/create
text.create(options: table) -> any
Create a text handle from an initial content + style table. The handle
owns a runtime GPU texture (see text.textureGuid); pass it to every other
call.
Parameters
optionstable— Table ofcontentplus style fields (fontSize, color, alignment, richText, maxWidth, ...).
Returns any — An opaque text handle, or nil if the text system is unavailable.
local h = text.create({ content = "Hello", fontSize = 48 })
globals/text/destroy
text.destroy(handle: any?) -> boolean
Destroy a text handle and release its raster + glyph layout.
Parameters
handleany(optional) — Text handle fromtext.create.
Returns boolean — True when the text system held the handle and released it; false for a handle it did not have.
text.destroy(h)
globals/text/face
text.face(handle: any?) -> any
Which font face one handle actually shaped with, and whether that is
the family its style asked for. requested is what was asked, resolved
is the face that answered, matched says whether they agree and reason
says why when they do not — one of text.faceReasons(). A style that named
no family reports noFamilyRequested: it got the default because it asked
for nothing, so reason rather than matched is what an alert switches
on. faces lists
every face the shaper used, most glyphs first, so a fallback that covered
part of the string is visible alongside the face that covered the rest.
Parameters
handleany(optional) — Text handle fromtext.create.
Returns any — { requested, resolved, postScriptName, matched, reason, faces, glyphCount }, or nil for a handle the text system does not hold.
local r = text.face(h).reason; if r == "familyUnknown" or r == "familyNotSelectable" then print(r) end
globals/text/faceReasons
text.faceReasons() -> { string }
Every reason the face readings give for a label or a family not being
in the family a style named, nearest cause first. text.face gives them
for one label; font.reconcile() also gives familyCoveredNoGlyph, which
it can only reach by laying the family out under its own weights and over
several scripts.
Returns { string } — Array of reason strings.
for _, r in ipairs(text.faceReasons()) do print(r) end
globals/text/listFonts
text.listFonts() -> { string }
List the font families currently available to the text system.
Returns { string } — Array of font-family name strings.
local fonts = text.listFonts()
globals/text/loadFont
text.loadFont(ref: any?) -> any
Load a font from an asset reference so it becomes available to
setStyle's fontFamily.
Parameters
refany(optional) — Font asset reference or path.
Returns any — The loaded font-family name, or nil on failure.
text.loadFont(asset.ref("fonts.inter", "font"))
globals/text/measure
text.measure(handle: any?) -> any
Measure the rasterised text in pixels without producing a texture.
Parameters
handleany(optional) — Text handle fromtext.create.
Returns any — Table with width and height in pixels.
local size = text.measure(h)
globals/text/observe
text.observe() -> any
Everything the text system is holding right now. count is the live
text objects; objects is one row each, carrying its content, the style
it was laid out with, its measured extent, whether it is dirty, the face
the shaper actually used, the owner entity whose component created it
with whether that entity is still there, and the raster texture its last
rasterisation landed in with the bytes it costs. orphans is the subset
whose owning entity is gone, fonts the families the shaper can resolve,
and raster the glyph-raster bytes with the pool they belong to named.
Built when you ask, so it costs nothing per frame and reads the same in
edit mode as in play.
Returns any — { count, objects, orphans, fonts, dirty, raster }.
local live = text.observe().count
globals/text/orphans
text.orphans() -> { any }
The text objects whose owning entity no longer exists — a quad the
engine is still holding for something that has been despawned. Each row is
the same shape text.observe().objects carries.
Returns { any } — Array of text-object rows with a dead owner.
print(#text.orphans() .. " labels outlived their entity")
globals/text/rasterMemory
text.rasterMemory() -> any
The glyph-raster bytes, broken out of the runtime GPU texture pool.
bytes is summed off the same map renderer.gpuMemory().textures is
totalled from, so shareOfPool is a share of that number rather than a
second count of the same memory.
Returns any — { pool, bytes, textures, poolBytes, shareOfPool }.
local r = text.rasterMemory(); print(r.bytes .. " of " .. r.poolBytes)
globals/text/rasterize
text.rasterize(handle: any?, texture: any?, scale: number?) -> any
Rasterise the handle's current text + style into the given runtime GPU
texture. Bind that texture's guid as a material's base_color_texture to
display the text; re-rasterising the same texture overwrites it in place.
Parameters
handleany(optional) — Text handle fromtext.create.textureany(optional) — Destination GPU texture handle (renderer.texture.create) or its guid string — WHERE the raster lands.scalenumber(optional) — World/pixel scale factor for the raster (default 1.0).
Returns any — Table with width and height (in pixels), or nil if nothing rasterised.
local tex = renderer.texture.create({ width = 256, height = 64 })
local r = text.rasterize(h, tex, 1.0)
globals/text/setStyle
text.setStyle(handle: any?, style: table) -> boolean
Replace the handle's style. Fields not present keep their current value.
Parameters
handleany(optional) — Text handle fromtext.create.styletable— Style table (fontSize, color, alignment, outline, ...).
Returns boolean — True when the text system held the handle and took the style; false when it did not.
text.setStyle(h, { fontSize = 64, color = "yellow" })
globals/text/setText
text.setText(handle: any?, content: string) -> boolean
Replace the handle's text content.
Parameters
handleany(optional) — Text handle fromtext.create.contentstring— New text string.
Returns boolean — True when the text system held the handle and took the content; false when it did not, which is how a caller learns its handle went away.
if not text.setText(h, "HP: 100") then h = text.create({ content = "HP: 100" }) end
globals/text/textureGuid
text.textureGuid(handle: any?) -> string?
The runtime GPU texture guid this handle rasterises into — bind it as a
material texture (base_color_texture) to display the text.
Parameters
handleany(optional) — Text handle fromtext.create.
Returns string? — The texture guid string, or nil for a handle the text system does not hold.
entity(id).component.get("Material"):setTexture("base_color_texture", text.textureGuid(h))
globals/toml/encode
toml.encode(root: { [string]: any }) -> string
Encode a Luau table as canonical TOML bytes. Top-level string-keyed sub-tables become section headers ([name]); deeper string-keyed tables become dotted sections ([a.b]). Sequence tables are emitted as inline arrays, and string-keyed tables in value position (e.g. array elements) as inline tables ({ k = v }). Section + key order is alphabetical so the same input always produces the same bytes.
Parameters
root{ [string]: any }— The table to encode. Must be string-keyed at the root.
Returns string — A TOML-formatted string suitable for vfs.write.
local body = toml.encode({ render = { culling_mode = "gpu" } })
globals/toml/parse
toml.parse(src: string) -> { [string]: any }
Parse a TOML document into a nested Luau table. Sections ([a.b]) become nested tables; key/value pairs become entries on the current section (or root if before any section header). Throws with the line number on syntax errors.
Parameters
srcstring— TOML source bytes as a string.
Returns { [string]: any } — The parsed root table. Sub-tables are plain Luau tables; arrays are 1-indexed sequence tables.
local t = toml.parse('[a]\nx = 1\ny = "hi"\n')
globals/tonumber
tonumber(value, base?) -> number | nil
Convert value to number.
globals/tools/bind
tools.bind(identity: string, positional: { any }?, named: { [string]: any }?, opts: BindOpts?) -> BindResult
Resolve a call's arguments against a tool's declared parameters,
turning named arguments into the positional call the tool actually
takes. This is the binder behind zero <toolbox> <tool> --name value
and the use_tool MCP tool's named args, so a name resolves the
same way whichever surface the caller reached for. Reads the schema
from tools.get, matches each name to a parameter (exactly first,
then case-insensitively), and reports the first unresolvable name,
a name that a positional argument already filled, a call longer than
the signature, and a required parameter skipped over while a later
one is filled. Resolves the call only — running it is tools.use.
Parameters
identitystring— Tool identity ("<toolbox>.<name>", with or without the leadingtools.).positional{ any }(optional) — Arguments already given by position, filling slots from 1.named{ [string]: any }(optional) — Arguments given by name,{ [parameterName]: value }.optsBindOpts(optional) —order— the order to visitnamedin, so the first problem reported is the caller's first (defaults to sorted, for a stable answer).prefix— written in front of every argument name in the failure message,"--"for a shell flag.whole— readnamedas ONE table argument when not one of its keys names a parameter, and as the first argument written inline when only some of them do, for a surface whose payload is ambiguous between the two readings.positionalCount— how many slotspositionalfills, for a caller that passed an explicitniland so cannot be measured by length.
Returns BindResult — { ok, call, count, failure? }. Call the tool with table.unpack(call, 1, count); on failure call is empty and failure carries the reason.
bind("camera.lookAt", {}, { target = { 0, 5, 0 } })
bind("MaterialAuthor.fromColor", {}, { color = { 1, 0, 0 } }, { whole = true })
globals/tools/create
tools.create(args: { [string]: any }) -> { ok: boolean, path: string?, identity: string?, signature: string?, error: string? }
Create a new tool on disk inside an EXISTING toolbox.
Scaffolds the .tool/ folder via asset.create("tool", …),
then writes the supplied code wrapped in a documented typed
function into init.luau (the --!desc/--!arg/--!return/
--!example doc block + signature built from the structured
metadata), the tags into .metadata, and a brief README.md
— so the authored tool is indistinguishable from a builtin.
Errors cleanly if the parent toolbox doesn't exist — call
tools.createToolbox first so the toolbox starts out with a
real description instead of the placeholder template body.
The engine's normal hot-reload pipeline picks up the new
files and binds the tool's global on the next pass.
Every toolbox created via tools.createToolbox ships a
shared.module/ (the template default has ok/fail
result-envelope constructors; users can extend or replace it
via sharedCode at create time or by editing the module
later). The generated init.luau automatically declares
local shared = require(".shared") before your function
body, so the code you pass can reference shared.ok(...)
/ shared.fail(...) (or whatever the toolbox's custom
helpers expose) directly. If a toolbox doesn't have a
shared.module/ for some reason, the require is skipped so
there's no dangling import to fail.
Parameters
args{ [string]: any }— Structured tool definition. Required:name(leaf, e.g."hello"),toolbox(parent toolbox, e.g."mytools"),code(the Luau function body — not a full module, just the statements that will become the function's body),description(one or more sentences describing what the tool does). Optional but recommended:args(array of{name, type, description}argument records),returns({type, description}for the return value),examples(array of call-site code strings),tags(array of strings for search).
Returns { ok: boolean, path: string?, identity: string?, signature: string?, error: string? } — { ok, path?, identity?, signature?, error? }. path — the new .tool/ folder's VFS path. identity — the tool's <toolbox>.<name> identity. signature — the derived name(args) -> ret signature.
tools.create({ name = "hello", toolbox = "mytools", description = "Greet.", code = "return 'hi'" })
globals/tools/createToolbox
tools.createToolbox(args: { [string]: any }) -> { ok: boolean, path: string?, identity: string?, error: string? }
Create a new toolbox folder. A toolbox is the namespace
container for tools — .toolbox/ on disk; once tools are authored
inside it they are invoked as tools.use("<name>", "<toolName>", …).
This call scaffolds the .toolbox/ folder via
asset.create("toolbox", …) and overwrites the placeholder
README.md with a real description so the toolbox doesn't ship
the template stub. Optionally seeds shared.module/ if the
caller supplies cross-tool helper code. Authoring a tool inside
this toolbox is the separate tools.create call.
Parameters
args{ [string]: any }— Structured toolbox definition. Required:name(the toolbox's leaf name, e.g."mytools"— becomes<name>.toolbox/on disk and the namespace for tool identities),description(one or more sentences describing the surface this toolbox exposes — what problem space the tools cover and who calls them). Optional:sharedCode— replacement body for the toolbox'sshared.module/init.luau. The toolbox template ALWAYS ships ashared.module/with defaultok/failresult-envelope constructors so tools in this toolbox canrequire(".shared")from day one. SupplysharedCodeonly when you want to override that default with custom cross-tool helpers (parsers, registries, …); the module body is left untouched if you omit it.path— explicit VFS destination (/zero/source/...form). Defaults to/zero/source/tools/<name>if omitted; pass an explicit path to author a library-scoped or package-internal toolbox.
Returns { ok: boolean, path: string?, identity: string?, error: string? } — { ok, path?, identity?, error? }. path — the new .toolbox/ folder's VFS path. identity — the toolbox's registered identity (<name>).
tools.createToolbox({ name = "mytools", description = "Custom tooling for my workflow." })
globals/tools/delete
tools.delete(identity: string) -> { ok: boolean, path: string?, error: string? }
Remove a previously-authored tool by identity. Deletes the
.tool/ folder and its contents from the VFS via
vfs.remove(..., { recursive = true }). The engine drops the
tool's global on the next hot-reload pass. Refuses to operate
on a path that doesn't exist (returns { ok = false } with
an explanatory error).
Parameters
identitystring— Tool identity ("<toolbox>.<name>").
Returns { ok: boolean, path: string?, error: string? } — Table shaped { ok, path?, error? }.
tools.delete("mytools.hello")
globals/tools/get
tools.get(identity: string) -> ToolMeta?
Read back a tool's assembled metadata — description, typed
signature, per-argument docs, return, examples, and tags — gathered
from the four canonical sources in its .tool/ folder. Individual
tools aren't entries in the asset index (only their parent toolboxes
are), so this resolves the toolbox via asset.resolve(toolbox, "toolbox") and reads the tool relative to it. Returns nil when the
toolbox or tool is missing.
Parameters
identitystring— Tool identity ("<toolbox>.<name>", with or without the leadingtools.).
Returns ToolMeta? — The assembled metadata { name, signature?, description, args, varargs, returns?, examples, typeDefs, tags }, or nil if not found. Each entry in args is { name, type, description, optional } in signature order, so a caller can tell a required parameter from an optional one without parsing the rendered signature. varargs is true when the tool accepts trailing arguments beyond the named parameters. typeDefs carries { name, definition } for every named type the signature refers to, resolved transitively, so a signature reading spawn(opts: SpawnOpts) can be called without opening the tool's source.
local meta = tools.get("entityOps.modify"); print(meta.signature)
globals/tools/list
tools.list(tier: (number | string)?, toolbox: string?) -> { stdout: string, value: any }
Discover registered code-mode tools, grouped by toolbox.
Default tier returns just { <toolbox> = { name, name, … } }
plus a formatted stdout listing every toolbox on one line
with its tools — compact enough that listing the whole
catalogue doesn't flood agent context. Higher tiers enrich
each entry with its one-line description (tier 2) or its full
assembled metadata — signature, args, returns, examples (tier 3).
Pass a toolbox name to restrict the output to one toolbox.
Parameters
tier(number | string)(optional) — Verbosity level:1(default) toolbox summary,2adds one-line descriptions,3adds the full assembled metadata. Pass a string instead to restrict to that toolbox at tier 1.toolboxstring(optional) — Optional toolbox name to restrict the output to (e.g."entityOps"). Passing a string as the first arg also works.
Returns { stdout: string, value: any } — Table shaped { stdout: string, value: <grouped> }. For tier 1, value is { [toolbox] = { toolName, … } }. For tier ≥ 2, value is { [toolbox] = { { name, identity, signature?, description, … }, … } }.
tools.list() -- default: every toolbox, names only
tools.list(2) -- include first-line descriptions
tools.list("entityOps") -- only the `entityOps` toolbox at tier 1
tools.list("entityOps", 2) -- only `entityOps`, with descriptions
globals/tools/search
tools.search(query: string?, opts: { toolbox: string?, limit: number? }?) -> { stdout: string, value: any }
Search registered code-mode tools by relevance, the canonical
in-engine tool-discovery entry point — callable from execute
Luau so an agent can find the tool it needs without leaving the
engine. Enumerates every tool across every toolbox (reusing the
same filesystem discovery tools.list uses), then scores each
against the query: a token hit in the tool NAME weighs most,
then its DESCRIPTION, then its TAGS. Tools scoring above zero are
returned best-first. With an empty/omitted query and no
toolbox filter, returns the whole catalogue (name + signature
- toolbox) so the agent can browse. Each returned
nameis the tool's<toolbox>.<tool>identity; invoke it with theuse_toolMCP tool (toolbox+tool+args, an array in signature order or an object naming the parameters), the form each entry'sexamplesare rendered in.
Parameters
querystring(optional) — Free-text search string. Tokenized lowercase on non-alphanumeric boundaries; each token is matched against tool name, description, and tags. Empty or nil with notoolboxfilter lists every tool.opts{ toolbox: string?, limit: number? }(optional) — Optional filters table.toolbox— keep only tools whose owning toolbox exactly matches.limit— maximum entries to return (default10; pass a larger value to return more, up to every match — there is no upper cap).
Returns { stdout: string, value: any } — Table shaped { stdout: string, value: { SearchEntry } }. value is the ranked array; each entry is { name = "<toolbox>.<tool>", signature?, description, toolbox?, tags?, examples? }. stdout is a one-line human summary of the match count.
tools.search("spawn camera")
tools.search("material", { limit = 5 })
tools.search("", { toolbox = "physics" }) -- list a toolbox
tools.search("") -- list everything
globals/tools/toolboxes
tools.toolboxes() -> { stdout: string, value: { { toolbox: string, purpose: string, toolCount: number } } }
Discover the registered toolboxes as a grouped overview — one
row per toolbox with its one-line purpose and tool count. The
toolbox-first entry point to tool discovery: an agent navigates by
DOMAIN (which toolbox), then drills into a toolbox's tools with
tools.search("", { toolbox = "<name>" }). purpose is sourced from
the toolbox's README.md (its first descriptive line), falling back
to the .metadata description.
Returns { stdout: string, value: { { toolbox: string, purpose: string, toolCount: number } } } — Table shaped { stdout: string, value: { { toolbox, purpose, toolCount } } }. value is toolbox-name-sorted; each entry is { toolbox = "<name>", purpose = "<one line>", toolCount = <n> }.
tools.toolboxes()
globals/tools/tryUse
tools.tryUse(toolbox: string, tool: string, ...: any?) -> ToolCall
Invoke one code-mode tool and report the outcome as a value. Takes the
same arguments as tools.use and resolves the toolbox the same way, and
returns { ok, value, error, toolbox, tool } for every outcome — an
unknown toolbox, an unknown tool, a tool that reported failure, and a tool
that raised all arrive as ok = false with the reason in error. The
fields are the ones the use_tool MCP surface reports, so a script
comparing several calls in one pass reads the same names it would over
MCP, and reads them without wrapping each call in pcall.
Parameters
toolboxstring— The owning toolbox — an ambient toolbox by its name ("entityOps") or a library toolbox by its scoped identity ("@lib::ns").toolstring— The tool's leaf name within that toolbox ("spawn")....any(optional)
Returns ToolCall — { ok, value, error, toolbox, tool }. value carries the tool's own value when ok is true; error carries the reason when it is false.
tryUse("entityOps", "spawn", { Model = { model = "cube" } })
globals/tools/use
tools.use(toolbox: string, tool: string, ...: any?) -> ...any
Invoke one code-mode tool by naming its toolbox and tool explicitly —
the Luau-code counterpart to the use_tool MCP tool. The normal path is
the use_tool MCP tool; this is the escape hatch for editor panels and
shipped modules that script a tool from engine Luau. Resolves the toolbox
from the runtime store (builtin, library, and user-authored runtime
toolboxes all work), calls the named tool with the remaining args, and
returns the tool's value directly — unwrapping the ZmToolResult
envelope and RAISING a Luau error when the tool fails. Because every call
names both toolbox and tool, it can never read as a tools.<box>
namespace. tools.tryUse takes the same arguments and reports the
outcome as a value instead of raising.
Parameters
toolboxstring— The owning toolbox — an ambient toolbox by its name ("entityOps") or a library toolbox by its scoped identity ("@lib::ns").toolstring— The tool's leaf name within that toolbox ("spawn")....any(optional)
Returns ...any — Everything the tool returned, in the order it returned it — a tool declaring (path, reason, frame) hands back all three, so a second and third value the tool states are read the way the tool's own signature says. Raises on an unknown toolbox / tool or a tool-reported failure. These are the tool's OWN values, not the { ok, value } envelope the use_tool MCP surface reports — reach for tools.tryUse when the call's outcome is what you want.
use("entityOps", "spawn", { Model = { model = "cube" } }, { position = {0, 2, 0} })
globals/tostring
tostring(value) -> string
Convert value to string.
globals/transform
transform: any
Lowercase alias of Transform. Same identity. Prefer this in new code so the convention matches entity, scene, cam, physics, ...
globals/type
type(value) -> string
Returns type name of a value.
globals/typeof
typeof(value) -> string
Returns detailed type name.
globals/ui/blur
ui.blur()
Surrender keyboard focus from whichever widget currently holds it.
globals/ui/bringAreaToFront
ui.bringAreaToFront(id: string)
Raise a movable area to the top of the window stacking order —
the programmatic equivalent of clicking it. Areas sharing a stacking
band order by interaction, so this is the call that brings one forward
from code: use it when a taskbar button, focus change, or app launch
should raise a window. Moving a screen to a higher layer band raises
it over the bands below.
Parameters
idstring— Area widget id.
globals/ui/captureWindow
ui.captureWindow(screen: string, window: string, opts: CaptureOpts?) -> CaptureResult?
Render a single Window widget to its own offscreen texture
and write the result as PNG at
/runtime/render_surfaces/<rtHandle>.png. The screen does NOT
need to be visible. Returns { rtHandle, texturePath } or nil
on invalid inputs (width/height clamped to [1, 8192],
defaults 600x400).
Parameters
screenstring— Screen id containing the target Window.windowstring— Widget id of the Window.optsCaptureOpts(optional) —{ width, height }(optional).
Returns CaptureResult? — { rtHandle, texturePath } or nil.
globals/ui/click
ui.click(callbackId: string, value: any?)
Simulate a widget click / interaction by its callback id. The call carries no screen, so an id that names widgets on several screens reaches every component that declared it, once each.
Parameters
callbackIdstring— Callback id assigned to the widget.valueany(optional) — Optional value to pass with the callback.
globals/ui/defineStyle
ui.defineStyle(name: string, style: StyleProps)
Define a named style. Style keys follow
<widgetType>.<className> (e.g. "label.h1", "button.primary")
or bare <className> to apply across widget types. Widgets
reference styles via the classes (or class) prop.
Parameters
namestring— Style name.styleStyleProps— Style properties table.
globals/ui/defineStyles
ui.defineStyles(styles: { [string]: StyleProps })
Define multiple named styles at once.
Parameters
styles{ [string]: StyleProps }— Map of style name to style properties.
globals/ui/defineWidget
ui.defineWidget(name: string, builderFn: (WidgetTree, { WidgetTree }) -> WidgetTree)
Register a custom widget kind. When a tree contains
{ type = name, props = ..., children = ... }, the decoder
calls builderFn(props, children) at register / update time and
substitutes the returned widget table in place. Errors surface
through ui.lastValidation() with codes widget-builder-error
/ widget-builder-bad-return / decode-recursion-depth-exceeded.
Parameters
namestring— Custom widget kind name.builderFn(WidgetTree, { WidgetTree }) -> WidgetTree— Builder closure(props, children) -> widgetTable.
globals/ui/diagnose
ui.diagnose(widgetId: string) -> WidgetPaint?
Why one widget did or did not reach the last frame. Returns that
widget's row from ui.observe() — the same fields, resolved against the
same reading. An id no registered screen carries reads noSuchWidget,
which is how a misspelling separates from a widget whose screen is
hidden and from one the frame laid out no box for.
Parameters
widgetIdstring— The id the widget records layout under.
Returns WidgetPaint? — The widget's row, or nil before the UI has published a frame.
"hud-healthbar"
globals/ui/dragState
ui.dragState() -> { payload: string, x: number, y: number }?
The in-flight drag-and-drop payload while a dragPayload widget is
being dragged, else nil. x/y are the pointer's position in the
logical space ui.getLayoutInfo rects live in, so the reading resolves
directly against widget rects. Poll during a drag to drive live
feedback (a placement ghost following the cursor); the drop itself
still lands through the target's onDrop. Snapshotted each frame.
Returns { payload: string, x: number, y: number }? — { payload, x, y } during a drag, nil otherwise.
globals/ui/elementTree
ui.elementTree(screenName: string) -> ElementNode?
Introspect a screen's rendered widget hierarchy with each
element's layout rect. Every node the renderer draws appears, nested
exactly as the widgets nest, under the id it records layout against:
the id set on the node when the author gave it one, otherwise
<screen>/<type>@<path>. bounds is that element's rect — the same
table ui.getLayoutInfo(id) returns — and appears once the element has
been measured. Kinds registered through ui.defineWidget appear
expanded into the primitives they build. Feeds the gui.captureElement
tool: list the tree, pick the ids to frame, capture their region.
Parameters
screenNamestring— Screen id passed toui.registerScreen.
Returns ElementNode? — An ElementNode tree, or nil when no screen is registered under that name.
globals/ui/focus
ui.focus(widgetId: string)
Programmatically request keyboard focus on a widget. Queued
as a one-shot; the next render of the matching widget calls
response.request_focus().
Parameters
widgetIdstring— Widget id to focus.
globals/ui/focusedWidget
ui.focusedWidget() -> string?
Return the widget id of whichever widget currently holds keyboard focus, or nil. Snapshotted post-render each frame.
Returns string? — Focused widget id or nil.
globals/ui/getAreaPos
ui.getAreaPos(id: string) -> AreaPos?
Read the current pivot position of an area widget,
including any user drag deltas. Returns { x, y } or nil if
the area didn't render this frame.
Parameters
idstring— Area widget id.
Returns AreaPos? — { x, y } or nil.
globals/ui/getAreaSize
ui.getAreaSize(id: string) -> AreaSize?
Read the measured size of an area widget, including any user
resize-grip drags if the area is resizable. Returns { w, h }
or nil if the area didn't render this frame.
Parameters
idstring— Area widget id.
Returns AreaSize? — { w, h } or nil.
globals/ui/getDockLayout
ui.getDockLayout(id: string) -> string?
Read the current serialized layout (split/tab arrangement) of
a dockArea widget as a JSON string. Returns nil if the dockArea
didn't render this frame. Persist the string and pass it back via
the dockArea's layout prop to restore the arrangement.
Parameters
idstring— DockArea widget id.
Returns string? — Serialized DockState JSON string, or nil.
globals/ui/getLayoutInfo
ui.getLayoutInfo(widgetId: string?) -> LayoutInfo?
Get layout info (position, size, content bounds) for UI
containers. If widgetId is given, returns info for that
widget only; otherwise returns all.
Parameters
widgetIdstring(optional) — Optional widget id to query.
Returns LayoutInfo? — Layout info table or nil.
globals/ui/getScreenTree
ui.getScreenTree(screenName: string) -> WidgetTree?
Return the last widget tree table passed to
registerScreen / updateScreen for screenName.
Parameters
screenNamestring— Screen name to query.
Returns WidgetTree? — Widget tree or nil.
globals/ui/getTheme
ui.getTheme() -> string
Get the name of the currently active theme.
Returns string — Active theme name.
globals/ui/getToken
ui.getToken(name: string) -> string?
Look up a single design token value from the active theme.
Parameters
namestring— Token name (without$prefix).
Returns string? — Token value or nil.
globals/ui/getTokens
ui.getTokens() -> { [string]: string }
Get all design tokens from the active theme as a key-value map.
Returns { [string]: string } — Token map.
globals/ui/getWidgetProps
ui.getWidgetProps(typeName: string) -> { WidgetPropDescriptor }?
Get the property definitions for a widget type.
Parameters
typeNamestring— Widget type name.
Returns { WidgetPropDescriptor }? — Array of property descriptors, or nil if type not found.
globals/ui/getWidgetTypes
ui.getWidgetTypes() -> { string }
Get all available widget type names that can be used in widget trees.
Returns { string } — Array of widget type names.
globals/ui/hideScreen
ui.hideScreen(name: string) -> boolean
Hide a registered screen, and report whether a screen by that name
is registered. The engine applies the hide later in the frame;
listScreens reflects it from the next call onwards.
Parameters
namestring— Screen identifier to hide.
Returns boolean — True when a screen by this name is registered.
globals/ui/hitTest
ui.hitTest(x: number, y: number) -> PaintHitTest?
Which widget a pointer at (x, y) reaches, and the stack beneath it.
Coordinates are in the space ui.screenSize() reports — the same space
getLayoutInfo rects and gui.clickAt use.
Parameters
xnumber— Logical X.ynumber— Logical Y.
Returns PaintHitTest? — { widget, screen, stack, x, y } — widget nil when the point is over no UI — or nil before the UI has published a frame.
640, 360
globals/ui/invisibilityReasons
ui.invisibilityReasons() -> { string }
Every verdict ui.diagnose can report, as a closed list.
Returns { string } — The reason names.
globals/ui/lastRegistration
ui.lastRegistration() -> { name: string, layer: number? }?
The name and layer passed to the most recent ui.registerScreen
call, recorded synchronously at call time. A host that mounts a nested
app reads this immediately after the mount to learn which screen the
nested code registered, without intercepting the ui table.
Returns { name: string, layer: number? }? — { name, layer } for the last registration, or nil if none yet.
globals/ui/lastValidation
ui.lastValidation(screenName: string?) -> any
Validation diagnostics produced at the most recent
registerScreen / updateScreen, plus what the render stage
found while painting — unknown-font-family reports a
style.fontFamily that named no registered font family, once
per family per screen. With no args returns a
{ [screen] = entry } map; with a name returns that screen's
entry or nil. Validation gated by world setting
ui.validation = "off" | "warn" | "strict" (default "warn").
Parameters
screenNamestring(optional) — Optional screen name.
Returns any — Validation entry, full map, or nil.
globals/ui/listFonts
ui.listFonts() -> { FontFamilyInfo }
Every font family a style.fontFamily can select. Read from
the registry the UI text renderer resolves a family token
through, so a family this returns is one a label renders in.
family and every name in aliases are accepted as a
fontFamily, case-insensitively; aliases carries the
web-font names, CSS generic families and face names that
select the same group. faces names the concrete face in each
weight/style slot, so a fontWeight = 700 against a family
with no bold face gets a synthesised heavy. system = true
marks a family taken from the host OS — present on this
machine, absent on one without it, and absent on WASM — so a
UI that must look the same everywhere picks a family with
system = false. A fontFamily naming nothing in this list
is reported as an unknown-font-family warning through
ui.lastValidation(screen) once the screen paints, and the
text renders in the default proportional face.
Returns { FontFamilyInfo } — Array of { family, aliases, faces, system }, by family.
for _, f in ui.listFonts() do print(f.family) end
globals/ui/listScreens
ui.listScreens() -> { ScreenSummary }
List every registered screen with its current visibility, layer, and whether the screen has a populated root widget tree, including the register / show / hide / unregister calls the running script has already made. Sorted by layer ascending, then name.
Returns { ScreenSummary } — Array of screen summaries.
globals/ui/listThemes
ui.listThemes() -> { string }
List all registered theme names.
Returns { string } — Array of theme names.
globals/ui/observe
ui.observe(screenName: string?) -> PaintObservation?
What the last UI frame painted. Returns
{ generation, viewport, pointer, pointerOverUi, pointerWidget, widgets }
with one widgets row per widget any registered screen holds — its
layout box, the clip chain it painted under, the part of that box which
reached the frame (visible), the order it painted in (paintIndex),
and its reason from the closed set ui.invisibilityReasons() lists.
generation advances once per re-rendered frame, so two calls reporting
the same number describe the same frame.
Parameters
screenNamestring(optional) — Narrow the rows to one screen. Omit for every screen.
Returns PaintObservation? — The reading; nil before the UI has published a frame, and nil for a screenName no registered screen answers to.
"hud"
globals/ui/paintOrder
ui.paintOrder(a: string, b: string) -> number?
Which of two widgets paints later: -1 when a paints before b,
1 when after, 0 when level. This is what separates two widgets whose
rects are identical.
Parameters
astring— First widget id.bstring— Second widget id.
Returns number? — -1, 0, 1, or nil when the reading holds no row for one of them.
"panel-a", "panel-b"
globals/ui/pixelRatio
ui.pixelRatio() -> number
Physical pixels per logical point — the factor between the logical
space ui.screenSize() / getLayoutInfo rects live in and the physical
space input.mousePosition, the camera viewport rect and
input.simulateMouse* coordinates live in. Multiply a layout coordinate
by this to aim a simulated pointer at a widget.
Returns number — Physical pixels per logical point (1.0 when unscaled).
globals/ui/pointerWidget
ui.pointerWidget() -> PointerRead?
Whether the UI is consuming the pointer, and which widget holds it —
the pointer counterpart of ui.focusedWidget().
Returns PointerRead? — { x, y, overUi, widget, screen }, or nil before the UI has published a frame.
globals/ui/registerBackgroundShader
ui.registerBackgroundShader(shaderHandle: any?, width: number?, height: number?)
Register a screen-domain .shader as a UI background, drawn
via the backgroundShader style. Takes the shader's asset handle
from asset.resolve.
Parameters
shaderHandleany(optional) — The screen.shader's asset handle, fromasset.resolve.widthnumber(optional) — Render target width (default 1280).heightnumber(optional) — Render target height (default 720).
globals/ui/registerCallbackEnv
ui.registerCallbackEnv(key: string, env: { [string]: any })
Register an environment table to receive widget-callback
broadcasts: its global onCallback(id, value) fires for any widget
callback not owned by a specific component instance — the same
broadcast a component's onCallback receives. Keyed by key;
re-registering the same key replaces the previous env. A component
instance is folded into the callback dispatch automatically, so reach
for this from a non-component context that hosts a UI surface (a scene
entrypoint registering its own screen). Pair with
ui.unregisterCallbackEnv(key) so the ref is released.
Parameters
keystring— Stable identifier for this registration (re-register replaces).env{ [string]: any }— Environment table whoseonCallbackreceives the broadcasts.
globals/ui/registerScreen
ui.registerScreen(name: string, widgetTree: WidgetTree, layer: number?)
Register a named UI screen with a widget tree. Optional
layer controls z-ordering (higher = on top), in bands: below 0
behind everything, 0-99 ordinary app depth, 100-999 always-on-top
chrome, 1000+ menu and popup depth. A screen in a higher band
covers one in a lower band whatever their roots are; inside a band
a floating area or window root sits over ordinary content, and
a modal root sits over the whole stack. Tag-based
grouping lives in Z.tags (Z.tags.set(name, { "editor" })
after register).
Parameters
namestring— Unique screen identifier.widgetTreeWidgetTree— Root widget table.layernumber(optional) — Z-order layer (optional).
ui.registerScreen("hud", tree)
globals/ui/registerTheme
ui.registerTheme(name: string, theme: ThemeDefinition)
Register a theme from a flat Luau table. Most callers
should use Z.theme.register(name, table) which runs the
cascade for them.
Parameters
namestring— Theme name to register.themeThemeDefinition— Flat-resolved theme table.
globals/ui/removeScreen
ui.removeScreen(name: string) -> boolean
Alias for ui.unregisterScreen.
Parameters
namestring— Screen identifier to remove.
Returns boolean — True when a screen by this name was registered.
globals/ui/resetAreaSize
ui.resetAreaSize(id: string)
Clear a resizable area's remembered size (from a grip drag or
ui.setAreaSize) so its declared — or content — size takes over again.
Parameters
idstring— Area widget id.
globals/ui/response
ui.response(widgetId: string) -> WidgetResponse?
Per-widget interaction snapshot for the most recent frame.
Returns { clicked, hovered, focused, changed, value } where
clicked / changed mark transitions and hovered / focused
mark current state.
Parameters
widgetIdstring— The widget id (NOT the onClick / onChange callback id).
Returns WidgetResponse? — WidgetResponse or nil.
globals/ui/screen
ui.screen(name: string) -> { [string]: any }?
Get a screen proxy with methods like setResolution and
rasterize.
Parameters
namestring— Screen name.
Returns { [string]: any }? — Screen proxy table, or nil.
globals/ui/screenSize
ui.screenSize() -> { width: number, height: number }
The UI coordinate space as { width, height } (logical points). This is
the space area pos, anchors, and getLayoutInfo rects use — and it is
NOT the pixel size of a capture screenshot, which may be downscaled. Use
this for absolute area positioning (e.g. pinning a menu above a bottom
taskbar) instead of guessing the size from a capture image.
Returns { width: number, height: number } — { width, height } in logical UI points.
globals/ui/scroll
ui.scroll(deltaX: number, deltaY: number)
Simulate a mouse-wheel scroll event on the UI.
Parameters
deltaXnumber— Horizontal scroll delta.deltaYnumber— Vertical scroll delta.
globals/ui/setAreaPos
ui.setAreaPos(id: string, x: number, y: number)
Programmatically move a movable area widget to (x, y).
Applied for one frame; subsequent frames let drag tracking
take over.
Parameters
idstring— Area widget id.xnumber— Target pivot x (screen coords).ynumber— Target pivot y (screen coords).
globals/ui/setAreaSize
ui.setAreaSize(id: string, w: number, h: number)
Programmatically set a resizable area's size (the user-size
override) — for maximize / restore / tile. Persists until the area's
declared width/height changes or ui.resetAreaSize(id) clears it.
Parameters
idstring— Area widget id.wnumber— Target width (screen coords).hnumber— Target height (screen coords).
globals/ui/setDockWindowRect
ui.setDockWindowRect(dockId: string, panelId: string, x: number, y: number, width: number, height: number)
Place the floating window of a dockArea panel at (x, y) with
size (width, height). Applies once the panel occupies a window —
a request made before then waits for it.
Parameters
dockIdstring— DockArea widget id.panelIdstring— Id of the panel held by the window to place.xnumber— Window left edge (screen coords).ynumber— Window top edge (screen coords).widthnumber— Window width (screen coords).heightnumber— Window height (screen coords).
globals/ui/setScreenRenderLayer
ui.setScreenRenderLayer(name: string, mask: number)
Set a screen's render-layer membership bitmask. A screen draws into a
camera or capture only when this mask intersects the camera's include
mask — the same rule geometry follows. Content UI defaults to the ui
bit; the editor places its chrome on EditorUI so agent captures can
drop it. Masks come from __renderLayers.bit(name).
Parameters
namestring— Screen identifier.masknumber— Render-layer membership bitmask.
globals/ui/setScrollPosition
ui.setScrollPosition(widgetId: string, offsetY: number)
Set the scroll offset of a scrollArea widget.
Parameters
widgetIdstring— Scroll area widget id.offsetYnumber— Vertical scroll offset in pixels.
globals/ui/setShaderUniforms
ui.setShaderUniforms(name: string, uniforms: { [string]: number })
Set uniform values on a registered background shader.
Parameters
namestring— Shader name identifier.uniforms{ [string]: number }— Map of uniform name to number value.
globals/ui/setTheme
ui.setTheme(name: string)
Switch the active global theme by name.
Parameters
namestring— Theme name to activate.
globals/ui/showScreen
ui.showScreen(name: string) -> boolean
Make a registered screen visible, and report whether a screen by
that name is registered. The engine applies the show later in the
frame; listScreens reflects it from the next call onwards.
Parameters
namestring— Screen identifier to show.
Returns boolean — True when a screen by this name is registered.
globals/ui/unregisterCallbackEnv
ui.unregisterCallbackEnv(key: string)
Remove an environment registered with ui.registerCallbackEnv. Its
onCallback stops receiving broadcasts. No-op if key isn't registered.
Parameters
keystring— The key passed toui.registerCallbackEnv.
globals/ui/unregisterScreen
ui.unregisterScreen(name: string) -> boolean
Remove a screen from the registry entirely. Unlike
hideScreen, this deletes the entry so it no longer appears in
listScreens or render iteration.
Parameters
namestring— Screen identifier to unregister.
Returns boolean — True when a screen by this name was registered.
globals/ui/unregisterWidget
ui.unregisterWidget(name: string)
Drop a registered custom widget kind. Subsequent references
produce an unknown-widget-type diagnostic.
Parameters
namestring— Custom widget kind name.
globals/ui/updateScreen
ui.updateScreen(name: string, widgetTree: WidgetTree)
Replace the widget tree of an already-registered screen.
Parameters
namestring— Screen identifier to update.widgetTreeWidgetTree— New root widget table.
globals/ui/useStyles
ui.useStyles(themeName: string)
Apply a registered style file's classes additively without changing the active theme.
Parameters
themeNamestring— Name of the registered style / theme asset.
globals/ui/widgetState
ui.widgetState(widgetId: string, key: string, default: any?) -> any
Read per-widget cross-frame state. Returns the value
previously written via widgetStateSet, or default (or nil).
State is keyed by widget id and persists across re-renders
within a screen's lifetime; cleared automatically when the
owning screen is unregistered.
Parameters
widgetIdstring— Widget id whose state to read.keystring— State key.defaultany(optional) — Value to return when nothing has been written.
Returns any — Stored value, default, or nil.
globals/ui/widgetStateClear
ui.widgetStateClear(widgetId: string, key: string)
Remove a per-widget state entry.
Parameters
widgetIdstring— Widget id whose state to clear.keystring— State key.
globals/ui/widgetStateSet
ui.widgetStateSet(widgetId: string, key: string, value: any?)
Write per-widget cross-frame state. Replaces any existing
value under (widgetId, key). Tables are stored by reference.
Parameters
widgetIdstring— Widget id to scope the state under.keystring— State key.valueany(optional) — Value to store (must be non-nil).
globals/unpack
unpack(table, i?, j?) -> values
Unpack table elements as return values.
globals/userfile/pick
userfile.pick(opts: PickOpts?) -> PickResult
Open the user's system file picker and bring the chosen file(s)
into the engine. Yields until the user finishes (call from a coroutine /
task, like any task.await) and returns
{ cancelled, files = {{ name, mime, size, bytes?, vfsPath? }} }.
Without writeTo each file carries bytes (a binary-safe string);
with writeTo each carries vfsPath (read it with vfs.read).
Cancelling returns { cancelled = true, files = {} }; a genuine failure
(e.g. a lost browser user-activation gesture) raises an error.
Parameters
optsPickOpts(optional) — Picker options (optional): multiple, folder, title, filters, writeTo.
Returns PickResult — The decoded result table.
local r = userfile.pick({ filters = {{ name = "Images", extensions = {"png","jpg"} }} })
if not r.cancelled then vfs.write("/source/textures/wall.png", r.files[1].bytes) end
globals/userfile/pickFolder
userfile.pickFolder(opts: PickOpts?) -> PickResult
Convenience for userfile.pick({ folder = true }) — pick a whole
directory tree. Yields until the user finishes and returns the same
result table as pick. On the web this degrades to a multi-file selection.
Parameters
optsPickOpts(optional) — Picker options (optional);folderis forced true.
Returns PickResult — The decoded result table.
local r = userfile.pickFolder({ writeTo = "/source/imported/" })
globals/vfs/clearPlayShadow
vfs.clearPlayShadow() -> boolean
Forget the entire play-shadow set after a bulk promote or discard. Tracking only — never touches the bytes.
Returns boolean — Always true.
vfs.clearPlayShadow()
globals/vfs/copy
vfs.copy(src: string, dst: string) -> (boolean, string?)
Copy a file OR directory from src to dst, cp -r style. A
directory recurses — every descendant is replicated at the same
relative path under dst, .refs sidecars included. .meta
sidecars are minted fresh, so a copy is a distinct asset with its
own identity. Both paths are absolute. The source may live in any
layer (writable, library mount, builtin, runtime-generated); the
destination must be a writable route.
Parameters
srcstring— Source absolute VFS path (file or directory).dststring— Destination absolute VFS path.
Returns (boolean, string?) — True on success; (false, errmsg) on failure.
vfs.copy("/zero/runtime/recordings/take1.mp4", "/zero/source/clips/take1.mp4")
globals/vfs/currentAuthor
vfs.currentAuthor() -> { id: string, name: string? }?
The agent this call is attributed to — the author a /source write
made right now would be recorded under in the play shadow. Nil when the
call carries no actor identity, which is the case for engine-authored
work and for a caller that presented no token. Compare its id against
vfs.playShadowAuthors() to separate your own pending edits from a
co-author's.
Returns { id: string, name: string? }? — { id, name } for the acting agent, or nil when unattributed.
local me = vfs.currentAuthor()
print(if me ~= nil then me.id else "unattributed")
globals/vfs/evict
vfs.evict(path: string, opts: VfsOpts?) -> boolean
Drop the in-memory bytes for path from the writable
MemFs layer without removing the asset. Use after processing
large binaries to reclaim RAM.
Parameters
pathstring— VFS path whose bytes should be evicted.optsVfsOpts(optional) —{ root = "/source/" }.
Returns boolean — True if MemFs bytes were dropped; false otherwise.
vfs.evict("/zero/source/textures/imported_big.png")
globals/vfs/exists
vfs.exists(path: string, opts: VfsOpts?) -> boolean
Is the path known to the VFS? Checks the Stage-1 metadata
(.meta sidecar / ManifestView) — NOT "are the bytes locally
cached?". Use vfs.read(path) ~= nil to confirm bytes are
reachable.
Parameters
pathstring— VFS path to check.optsVfsOpts(optional) —{ root = "/source/" }.
Returns boolean — True if the path is known regardless of byte cache state.
assert(vfs.exists("@builtin/models/Cube"))
globals/vfs/isDirectory
vfs.isDirectory(path: string, opts: VfsOpts?) -> boolean
Is ONE path a directory? Answers from reality — the writable
layer's children, a resolver-served folder listing, an explicit
empty-directory marker — so a loose file whose extension collides
with an assetType name (notes.json) reads as the file it is while
a real <name>.<type>/ folder reads as a folder. Costs the same
whatever the containing folder holds; use vfs.list when you want
every entry's kind, this when you hold one path.
Parameters
pathstring— VFS path to classify.optsVfsOpts(optional) —{ root = "/source/" }.
Returns boolean — True if the path is a directory.
if vfs.isDirectory("/zero/source/Goblin.dynamicAsset") then print("folder asset") end
globals/vfs/isSaveExcluded
vfs.isSaveExcluded(path: string, opts: VfsOpts?) -> boolean
Does this path hold content the machine keeps to itself?
/source/tmp/ is session scratch and /source/local/ is this
machine's own durable content — each directory itself included,
and everything under it. Both are writable, hot-reloadable and
enumerable like the rest of /source/; what separates them is
where they stop. The engine filters them out of every world save
and every peer broadcast, so they reach no world, carry no
manifest row there, and a staging verb handed one refuses it by
name. The match reads a whole path segment, so
/source/tmpfoo/ is ordinary content. Ask here whenever your
code has to agree with what a world can hold.
Parameters
pathstring— VFS path to classify.optsVfsOpts(optional) —{ root = "/source/" }.
Returns boolean — True when the path is held back from world saves and sync.
if not vfs.isSaveExcluded(p) then table.insert(publishable, p) end
globals/vfs/list
vfs.list(path: string?) -> { VfsListEntry }
List entries in a VFS directory.
Parameters
pathstring(optional) — Directory path (defaults to/zero).
Returns { VfsListEntry } — Array of { name, isDirectory } tables.
for _, e in ipairs(vfs.list("/zero/source")) do print(e.name) end
globals/vfs/memResident
vfs.memResident() -> { { path: string, bytes: number, kind: string } }
List the MemFs entries that are NOT resident-by-default — the writable
in-memory layer's binary blobs and its large text files (text at or above
the inline-text size threshold). These are the bytes vfs.evict can
reclaim: the ones kept in RAM rather than left to fall through to the
on-disk BlobStore cache. Small text (resident by default) is omitted. The
audit counterpart to vfs.evict and to reading with { keep = true } —
use it to see what encoded bytes are held in RAM, and why.
Returns { { path: string, bytes: number, kind: string } } — Array of { path, bytes, kind }; kind is "binary" for non-text content and "large-text" for oversized text. Empty when the VFS isn't up.
for _, e in ipairs(vfs.memResident()) do print(e.path, e.bytes, e.kind) end
globals/vfs/mkdir
vfs.mkdir(path: string, opts: VfsOpts?) -> boolean
Create a directory. mkdir -p semantics — idempotent.
Errors if a file already exists at the same path. While play is
running an authored /source directory waits for the lock to
lift and the refusal RAISES with the reason; writing a file under
the path creates it as part of that write, and scratch under
/source/tmp/ creates as in edit mode.
Parameters
pathstring— Directory path.optsVfsOpts(optional) —{ root = "/source/" }.
Returns boolean — True if the directory exists after the call. A creation the running play session refuses raises with the whole reason.
vfs.mkdir("/zero/source/scenes/")
globals/vfs/move
vfs.move(src: string, dst: string, opts: { quiet: boolean? }?) -> (boolean, string?)
Move a file from src to dst. By default fires the destination's
write side effects; pass opts.quiet = true to suppress them.
While play is running a move takes the source away, so authored
/source content that predates play is refused and the refusal
RAISES with the reason; content this play session created moves and
stays tracked on the play shadow.
Parameters
srcstring— Source absolute VFS path.dststring— Destination absolute VFS path.opts{ quiet: boolean? }(optional) — Optional{ quiet: boolean? }.
Returns (boolean, string?) — True on success; (false, errmsg) when the paths themselves refuse it. A move the running play session refuses raises with the whole reason instead.
vfs.move("/zero/source/a.luau", "/zero/source/b.luau")
globals/vfs/mutationSeq
vfs.mutationSeq() -> number
Lifetime count of VFS mutations the engine has APPLIED — the drain's
clock. A write queues its side effects (an asset's content reload, the
assetType's onChange, a component or scene registration) and a later
frame runs them; this number advances as each one completes. Read it,
write, then poll for a larger value to learn the queue has moved past the
point you wrote at — instead of waiting a guessed number of frames. It
counts every mutation kind, so it answers about the pipeline rather than
about one file; asset.reloadSeq(ref) is the per-asset reading.
Returns number — Count of applied VFS mutations this session. Monotonic.
local at = vfs.mutationSeq()
vfs.write("/zero/source/tmp/note.txt", "hi")
repeat task.wait() until vfs.mutationSeq() > at
globals/vfs/pendingWrites
vfs.pendingWrites() -> { string }
List the /source paths with an in-flight local write the synced
manifest has not reflected yet — the read-your-writes frontier. A
just-written file appears here until its upload round-trips and the
synced dirty state catches up; world.vcsStatus unions these so a
fresh edit reads back as dirty immediately. Empty when fully synced.
Returns { string } — Array of VFS paths with pending (unconfirmed) local writes.
for _, p in ipairs(vfs.pendingWrites()) do print(p) end
globals/vfs/playShadowAuthors
vfs.playShadowAuthors() -> { [string]: { id: string, name: string? } }
The agent behind each currently-shadowed /source path: the ZeroMind
user id the write was attributed to, and the username to show for it.
Several agents drive one engine at once and every one of their in-play
source edits sits in the same shadow set, so this is how a review, a
refusal or a verdict tells one agent's pending work from another's. A
path written with no actor identity carries no entry — it belongs to no
agent in particular, and stays settleable by any of them.
Returns { [string]: { id: string, name: string? } } — Map of shadowed path to { id, name }.
local mine = vfs.currentAuthor()
for path, who in pairs(vfs.playShadowAuthors()) do
if mine == nil or who.id ~= mine.id then print(path, "belongs to", who.name) end
end
globals/vfs/playShadowPaths
vfs.playShadowPaths() -> { string }
List the /source paths edited during running play that are currently
held as copy-on-write SHADOWS (MemFs-only, on-disk original untouched) —
the universal play shadow-copy set. These are the in-play edits persist
promotes over the originals on confirm, or drops on a guarded discard.
Empty outside play or when nothing was edited.
Returns { string } — Array of normalized VFS paths currently shadowed.
for _, p in ipairs(vfs.playShadowPaths()) do print(p) end
globals/vfs/promotePlayShadow
vfs.promotePlayShadow(path: string) -> string
Promote a single play-shadow edit into a canonical write. Re-asserts the live overlay bytes through the full write pipeline with the play write lock released, then unmarks the path. The bytes stay in the engine end to end, so binary content promotes exactly. Takes ONE file path, and needs the write lock released, so run it inside a pause you take and hand back. A promotion that cannot happen raises with the reason: the path is not shadowed, the path is a folder covering shadowed edits, play is running, the workspace is read-only, or the write-through failed. A shadow entry whose bytes are gone is dropped as promoted, so the path comes back with nothing written for it.
Parameters
pathstring— Shadowed VFS path to promote (one of vfs.playShadowPaths()).
Returns string — The path the call settled — it is no longer shadowed.
engine.paused = true
local promoted = vfs.promotePlayShadow("/zero/source/cover.jpg")
engine.paused = false
globals/vfs/read
vfs.read(path: string, opts: VfsOpts?) -> string?
Read a file from the virtual filesystem. Binary-safe.
Returns file contents as a string, or nil if the file is not
known. Relative paths resolve under opts.root (default
/source/). When called from a coroutine and the bytes
aren't locally cached, transparently yields the coroutine
while the lazy fetch runs.
Parameters
pathstring— VFS path.optsVfsOpts(optional) —{ root = "/source/" }.
Returns string? — File contents (binary-safe), or nil.
local src = vfs.read("@builtin/components/Camera.luau")
globals/vfs/readAsync
vfs.readAsync(path: string, opts: VfsOpts?) -> string
Asynchronous binary-safe read. Returns a promise ID that resolves to the file contents. Useful for reading render textures from the main thread without blocking.
Parameters
pathstring— VFS path.optsVfsOpts(optional) —{ root = "/source/" }.
Returns string — Promise ID — pass to task.await().
local data = task.await(vfs.readAsync("/zero/runtime/screenshots/last.png"))
globals/vfs/reload
vfs.reload(modulePath: string?) -> boolean
Clear entries from the require() cache so the next
require(name) re-runs the module's source. Pass a single
module identity to drop only that entry; call with no
arguments to drop every cached module.
Parameters
modulePathstring(optional) — Module identity to reload (omit to reload all).
Returns boolean — For a single identity, whether a module was cached under that name and has now been dropped — false says the name matched nothing. The no-arg form returns true.
vfs.reload("@mylib/utils.helpers")
globals/vfs/remove
vfs.remove(path: string, opts: VfsOpts?) -> (boolean, string?)
Remove a file. Refuses to remove directories unless
opts.recursive = true. Refuses protected system roots. While
play is running, authored /source content that predates play is
refused and the refusal RAISES with the reason; content this play
session created is removable, a folder included.
Parameters
pathstring— VFS path to remove.optsVfsOpts(optional) —{ root = "/source/", recursive = false }.
Returns (boolean, string?) — True on success; (false, errmsg) when the path itself refuses the removal. A removal the running play session refuses raises with the whole reason instead, so a pcall around the call reads it — and the lock answers ahead of whether the path is there, so a locked /source path raises whether or not it holds anything.
vfs.remove("/zero/source/scratch.luau")
globals/vfs/revertPlayShadow
vfs.revertPlayShadow(path: string) -> string
Revert a single play-shadow edit: restore the pre-play copy captured at the first play-mode write (the last edit-mode state, unstaged edits included) into the live slot — or remove the file when it did not exist at that moment — then unmark the path. Hot-reload picks the original back up, so the running session actually reverts. Takes ONE file path, and needs the write lock released, so run it inside a pause you take and hand back. A revert that cannot happen raises with the reason, on the same terms as vfs.promotePlayShadow.
Parameters
pathstring— Shadowed VFS path to revert (one of vfs.playShadowPaths()).
Returns string — The path that is now reverted and no longer shadowed.
engine.paused = true
local reverted = vfs.revertPlayShadow("/zero/source/Foo.component/init.luau")
engine.paused = false
globals/vfs/unmarkPlayShadow
vfs.unmarkPlayShadow(path: string) -> boolean
Forget a single play-shadow path after it has been promoted (saved over source) or discarded. Tracking only — never touches the bytes.
Parameters
pathstring— VFS path to unmark.
Returns boolean — Always true.
vfs.unmarkPlayShadow("/zero/source/Foo.component/init.luau")
globals/vfs/unwatch
vfs.unwatch(watcherId: number) -> boolean
Remove a previously registered VFS watcher.
Parameters
watcherIdnumber— Watcher id returned byvfs.watch.
Returns boolean — True if the watcher was found and removed.
vfs.unwatch(id)
globals/vfs/watch
vfs.watch(path: string, callback: (string, string) -> ()) -> number
Register a callback that fires when a VFS path is written or
removed. Two match modes: exact, or folder/prefix (key ends with
/, and fires for any descendant). The callback runs in the VM
that registered it. Returns a watcher id for vfs.unwatch.
Parameters
pathstring— Exact path, or folder path ending in/.callback(string, string) -> ()—(mutated_path, kind) -> (), kind"write"or"remove".
Returns number — Watcher id.
local id = vfs.watch("/zero/source/", function(path, kind) print(kind, path) end)
globals/vfs/write
vfs.write(path: string, content: string, opts: VfsOpts?) -> (boolean, string?)
Write content to a file. Binary-safe. Overwrites existing
files by default — pass opts.overwrite = false to refuse to
clobber. While play is running a /source write lands on the play
shadow: it succeeds and reads back, live in the session with disk
source untouched, and is discarded on a guarded play-exit unless
accepted. Scratch under /source/tmp/ writes through untouched.
Pass opts.durable = true to say these bytes ARE the source: the
write reaches canonical /source with play still running and the
session still in play, hot-reloading the modules and components that
read it, so the edit is observed running in the same play session
with nothing left to promote. A durable write RAISES with the reason
when the bytes cannot become canonical source.
Parameters
pathstring— VFS path to write to.contentstring— File content (binary-safe).optsVfsOpts(optional) —{ root = "/source/", overwrite = true, quiet = false, durable = false }.
Returns (boolean, string?) — True on success; on failure returns false + error message. A durable write raises instead of returning false.
vfs.write("/zero/source/notes.md", body)
vfs.write("/zero/source/game/Vent.component/init.luau", src, { durable = true })
globals/video/create
video.create(url: string, options: VideoOptions?) -> string
Create a video player. Returns a texture handle (e.g.
"video_0") usable directly in material.setTexture() — its
frames sample like any other texture.
Parameters
urlstring— URL or asset path to an MP4 video file.optionsVideoOptions(optional) — Playback options:loop(default false),autoplay(default false),rate(default 1.0).
Returns string — Texture handle.
local tex = video.create("http://example.com/clip.mp4", { autoplay = true })
globals/video/destroy
video.destroy(handle: string) -> boolean
Destroy a video player and free the render target and all resources.
Parameters
handlestring— Video handle fromvideo.create.
Returns boolean — True if the player was found and destroyed.
video.destroy(rt)
globals/video/getInfo
video.getInfo(handle: string) -> VideoInfo?
Get video information and current playback state.
Parameters
handlestring— Video handle.
Returns VideoInfo? — { width, height, duration, currentTime, state, rate, loop } or nil if the handle is invalid.
local i = video.getInfo(rt); print(i.currentTime, "/", i.duration)
globals/video/pause
video.pause(handle: string) -> boolean
Pause video playback. Can be resumed with video.play.
Parameters
handlestring— Video handle.
Returns boolean — True if the video was playing and is now paused.
video.pause(rt)
globals/video/play
video.play(handle: string) -> boolean
Start or resume video playback.
Parameters
handlestring— Video handle fromvideo.create.
Returns boolean — True if the command was accepted.
video.play(rt)
globals/video/seek
video.seek(handle: string, time: number) -> boolean
Seek to a specific time (seconds) in the video.
Parameters
handlestring— Video handle.timenumber— Target time in seconds.
Returns boolean — True if the seek was performed.
video.seek(rt, 30.5)
globals/video/setLoop
video.setLoop(handle: string, loop: boolean) -> boolean
Enable or disable looping.
Parameters
handlestring— Video handle.loopboolean— Whether to loop playback.
Returns boolean — True if the setting was applied.
video.setLoop(rt, true)
globals/video/setRate
video.setRate(handle: string, rate: number) -> boolean
Set the playback speed multiplier. 1.0 = normal, 2.0 = double speed, 0.5 = half speed.
Parameters
handlestring— Video handle.ratenumber— Playback rate.
Returns boolean — True if the rate was set.
video.setRate(rt, 2.0)
globals/video/stop
video.stop(handle: string) -> boolean
Stop video playback and reset to the beginning.
Parameters
handlestring— Video handle.
Returns boolean — True if the command was accepted.
video.stop(rt)
globals/warn
warn(...)
Emit a warning message at the warn log level. Roblox/Luau-style counterpart to print.
Returns nil
globals/world/add
world.add(path: string, opts: AddOpts?)
Stage one path's manifest row, expanding to the full asset
family if the path lives inside a composite asset. Idempotent
at (stage, manifest_row). Pass { force = true } to bypass
the .zmignore / .gitignore gate — same intent as
git add -f. Without force, attempts to stage an ignored
path (or a path whose .refs points at an ignored dep)
error.
{ stage = "<name>" } stages into one of the caller's own staging
areas instead of the shared default one, so a commit naming that area
freezes these paths and leaves every other caller's staged.
Parameters
pathstring— The path to stage. Must be a non-empty string.optsAddOpts(optional) — Optional{ force: boolean?, stage: string? }. Defaults to{ force = false }on the default staging area.
world.add("/source/foo.luau")
world.add("/source/scene_dirty/entities/42.json", { force = true })
world.add("/source/fauna.module", { stage = "fauna" })
globals/world/add_all
world.add_all(opts: StageOpts?) -> { string }
Stage the dirty manifest rows this caller can claim, skipping
any path that matches .zmignore / .gitignore. Paths that match
an ignore pattern are silently skipped — world.add(path, { force = true }) is the explicit way to override the gate
for an individual path. Rows still flagged conflicted by
world.pullAsset are held back too — resolve them (edit +
world.add(path), or world.resolvePullConflict) and re-run.
A path another staging area holds is held back as well: the
working tree is one per branch and staging areas are not, so a
path some other caller has already selected for a commit of its
own belongs to that caller until it commits or hands it over.
world.add(path) names a path deliberately and takes it either
way, which is how a claim is handed over — and a path this area
already holds stays staged here, whoever else holds it too.
Parameters
optsStageOpts(optional) — Optional{ stage: string? }naming the staging area to stage into. Omitted, the call stages into the shared default area.
Returns { string } — The paths that were held back — those another staging area holds, then the conflicted ones. Empty when none were.
world.add_all()
world.add_all({ stage = "fauna" })
globals/world/affirm
world.affirm(token: string)
Consume an XXX-XXX-XXX-style affirmation token returned by
a destructive op surface (e.g. vfs.remove). The destruction
commits atomically with the pending-row delete; the token is
one-shot. Errors verbatim on expiry / wrong-user.
Parameters
tokenstring— The affirmation token.
world.affirm("ABC-DEF-GHI")
globals/world/args
world.args -> any
Returns any
globals/world/assetInstallable
world.assetInstallable(opts: AssetInstallableOpts) -> AssetInstallableReport
Report whether one published asset can be installed on its own,
without raising. world.previewInstall plans a real install and
raises when the closure will not resolve; this answers the prior
question — does this guid name something ZeroMind will hand over by
itself — as a verdict a caller can branch on. Content that ships
inside a larger library (a module inside a package, a material
inside a system) carries its own published identity, so the answer
is per asset rather than per library. Reads only.
Parameters
optsAssetInstallableOpts—{ guid }names the asset;refpins a commit-id instead of the latest.
Returns AssetInstallableReport — An AssetInstallableReport — installable, a verdict, a one-line detail, and the closure's nodes / tree_children / deps shape when one resolved.
world.assetInstallable({ guid = asset.guid("@builtin::materials.neon") })
globals/world/avatar_default_edit
world.avatar_default_edit -> any
Returns any
globals/world/avatar_default_play
world.avatar_default_play -> any
Returns any
globals/world/awaitOutgoingSync
world.awaitOutgoingSync()
Wait until every /source write this session made has reached
the branch it was written against. Raises naming the paths that did
not land. add / commit / push / checkout / merge / pull
already wait on their own; call this before rebinding after a burst
of writes, which world.checkout and world.swap refuse over.
Returns Nothing. Raises when a write did not land.
world.awaitOutgoingSync() ; world.checkout("main")
globals/world/branches
world.branches() -> { { branch: string, commit_id: string, current: boolean } }
Every branch this world has, with the commit each one's head
names and which one this session is on — git branch --list. Sorted
by name. A branch exists for everyone in the world; which one you are
on is yours alone, so current is true for at most one row here and
says nothing about where anybody else is.
Returns { { branch: string, commit_id: string, current: boolean } } — Array of { branch, commit_id, current }.
for _, b in ipairs(world.branches()) do print(b.branch, b.commit_id) end
globals/world/camera_default_edit
world.camera_default_edit -> any
Returns any
globals/world/camera_default_play
world.camera_default_play -> any
Returns any
globals/world/checkUpdates
world.checkUpdates() -> { UpdateReport }
Discover upstream changes for every asset this world has
pulled. Read-only — makes no local mutation and no VCS write.
Each locally-pulled row identifies its own origin asset via
origin_asset_guid (recorded as that entry's own asset guid at
pull time — see world.installAsset). For every distinct
origin asset among the pulled rows, this re-resolves that
asset's latest transitive closure and compares each returned
entry's checksum against the matching local row's recorded
origin_checksum.
Returns { UpdateReport } — An array of UpdateReport, one per re-resolved origin asset whose closure produced at least one changed entry. Empty when every pulled row is already current. A root whose closure can't be re-resolved (e.g. the origin world is unreachable) is silently skipped rather than aborting the whole scan.
local reports = world.checkUpdates()
globals/world/checkout
world.checkout(branch: string) -> string
Switch this session to another branch — git checkout <branch>.
The branch must already exist (create one with world.createBranch).
The tree is replaced by the branch's own content. Which branch this
session is on is this session's alone; the branch itself is shared,
so others may be on the one you move onto.
Uncommitted work is not at risk: it already has its row on the
branch it was written against and is in the tree again when you
check that branch out.
Returns only once the branch's content has landed, so world.head,
world.log, world.commit and the VFS all target the new branch
immediately afterwards.
Parameters
branchstring— The branch to switch to.
Returns string — The branch now checked out.
world.checkout("feature")
globals/world/commit
world.commit(message: string, opts: CommitOpts?) -> string
Open-or-resume a staging area, set the message, and materialise
the commit. Commits ONLY what's already staged via world.add /
world.add_all — git semantics, not git commit -a. The reducer
auto-deletes the stage row on success so a subsequent
world.commit opens a fresh one.
{ stage = "<name>" } materialises one of the caller's own staging
areas, so the commit carries the paths staged under that name and
leaves every other caller's staged. When a commit from another
caller has landed since the area was opened, this brings the area
onto the branch head there is now and commits it there.
Pre-flight .zmignore refs gate: every staged source's
aggregated deps (via asset.deps, which recurses composite
asset folders) are checked against the live ignore set. If
any dep target's path is currently ignored AND the dep target
is not itself in the stage, the commit is refused. This
mirrors the closure invariant — a commit whose deps can't
resolve cleanly shouldn't land. Force-staging the dep
alongside (world.add(dep, { force = true })) makes the
ignored dep satisfy the gate.
Parameters
messagestring— The commit message.optsCommitOpts(optional) — Optional{ stage: string? }naming the staging area to materialise. Omitted, the commit materialises the shared default area.
Returns string — The newly-allocated commit id (ULID string).
local id = world.commit("feat: ship widget")
local id = world.commit("fauna: the swallow colony", { stage = "fauna" })
globals/world/conflicts
world.conflicts() -> { ConflictEntry }
List every locally-pulled row currently flagged conflicted —
the findable surface world.pullAsset leaves behind on an
unresolved merge. Read-only.
Returns { ConflictEntry } — An array of ConflictEntry, one per conflicted row. Empty when nothing is conflicted.
local list = world.conflicts()
globals/world/connectedUsers
world.connectedUsers -> any
Returns any
globals/world/contentRequirements
world.contentRequirements(scope: { string }?) -> { { asset: string, typeName: string, detail: string } }
List the world's unmet content requirements: user-authored assets
whose type-declared content constraints are not yet satisfied (an
empty README, a .metadata with no description or tags — the
empty-skeleton state a fresh create emits for the author to fill).
The same walk world.push gates on: push refuses while this list is
non-empty, and world.publishBlockers reports it as the content
class beside the other two. Empty list = every checked asset meets
its type's contract.
Parameters
scope{ string }(optional) — Asset paths to restrict the check to — pass a status read's dirty + staged paths to check only content that would actually publish (a per-file path matches its containing asset; each path resolves directly, with no world enumeration). Omit for the full-world walk the push gate performs.
Returns { { asset: string, typeName: string, detail: string } } — Array of { asset, typeName, detail } requirement rows.
for _, r in ipairs(world.contentRequirements()) do print(r.asset, r.detail) end
globals/world/contribute
world.contribute(opts: ContributeOpts?) -> { ContributeOutcome }
Send improvements to installed content back upstream — git subtree push ending in a pull request. For each targeted origin
world: the diverging subtree is remapped to the origin's canonical
paths, three-way merged against the origin's CURRENT content
(regions the origin also changed become local conflicts to
resolve first), pushed as a contrib/<id> branch in the origin
world, and opened as a pull request there. With merge (the
default) the pull request is merged immediately when authorized —
a refusal leaves it open and reported, never a failure. After a
merge, the local fork re-pulls so its origin pins advance and the
asset no longer reads as ahead.
Parameters
optsContributeOpts(optional) — OptionalContributeOpts—targets(origin world guids; default all ahead),merge(default true),title,description,dryRun.
Returns { ContributeOutcome } — Array of ContributeOutcome, one per targeted origin.
local r = world.contribute({})
globals/world/createBranch
world.createBranch(name: string, fromCommit: string?)
Create a branch — git branch <name> [<start>]. The branch
starts at fromCommit (defaults to the session branch's HEAD)
and gets its own working tree, materialized from that commit.
The session stays on its current branch; move onto it with
world.checkout("<branch>") (git checkout).
Parameters
namestring— The new branch name.fromCommitstring(optional) — Commit id to start at. Defaults toworld.head().
world.createBranch("feature")
globals/world/deleteBranch
world.deleteBranch(branch: string)
Delete a branch — git branch -D <name>. Drops the branch and
the working tree it owns; its commits are left alone, since deleting
a branch is dropping the name and the tree under it, not rewriting
history. Uncommitted work on that branch goes with it and is NOT
recoverable from trash, so the call refuses the first time and
returns the affirmation needed to go through with it — affirm with
world.affirm(<token>). Refuses the branch this session is on
(check out another first) and the world's last branch.
Parameters
branchstring— The branch to delete.
world.deleteBranch("feature")
globals/world/diff
world.diff(...: string) -> any
Mirror git diff's CLI arg shape. Returns per-file diffs
by default; pass --stat for summary stats, --name-only for
just paths. Positional commit ids drive the two sources;
--staged pivots to staged-vs-HEAD. ---separated args
scope the diff to specific paths. --stage=<name> reads one of the
caller's own staging areas in place of the shared default one.
Parameters
...string— Variadic string args: flags, commit ids,--, path filters.
Returns any — Array of DiffFile tables (or string-list for --name-only).
local files = world.diff()
local files = world.diff("--staged")
local files = world.diff("abc", "def")
local names = world.diff("--name-only")
local files = world.diff("--staged", "--stage=fauna")
globals/world/discard
world.discard(opts: StageOpts?)
Drop a staging area without committing. Live manifest dirty flags are preserved so the user can re-stage later. No-op if the area doesn't exist.
Parameters
optsStageOpts(optional) — Optional{ stage: string? }naming the staging area to drop. Omitted, the call drops the shared default area.
world.discard()
world.discard({ stage = "fauna" })
globals/world/discardFile
world.discardFile(path: string)
Discard one file's unstaged working edits, taking its content
back to what it was staged or committed as — the
git restore <path> shape. The stage is the baseline when the
path is staged, the last commit when it is not, and where it is
neither there is nothing to come back to, so the path goes away.
Staging is left exactly as it was; world.unstage is the verb
that changes it. One shot: a path that reverts to a committed
version snapshots the discarded bytes to trash first, so that
case is recoverable via world.restore(<handle>). Errors when
the path is not dirty (nothing to discard).
Parameters
pathstring— The VFS path whose unstaged edits to discard.
world.discardFile("/source/foo.luau")
globals/world/fetch
world.fetch(branch: string?) -> FetchResult
Update the origin/<branch> remote-tracking ref — git fetch.
Mirrors the world's ZeroMind branch head into the local commit
history (no working-tree change) and reports how the session
branch relates to it: behind origin commits to pull, ahead
local commits to push, diverged when both. A stale installed
pin or out-of-band ZeroMind change shows up here as behind —
reconcile with world.pull().
Parameters
branchstring(optional) — Remote branch to fetch. Defaults to the session branch.
Returns FetchResult — A FetchResult table.
local f = world.fetch()
globals/world/forkLive
world.forkLive(opts: { source: string, sourceBranch: string?, maxBatches: number? }) -> number
Seed THIS (empty) world's live content from another world by
copying its whole manifest as clean Pulled rows — the
in-engine half of "fork a world". Provenance is preserved: each
row points at the content's ORIGINAL owner (a fork of a
fork-of-A still points at A), so the fork never claims to have
authored what it pulled. The copy runs server-side in bounded
batches (idempotent + resumable), looping until the source is
fully mirrored. Pair with world.add_all() + world.commit() +
world.push() to publish the fork.
Parameters
opts{ source: string, sourceBranch: string?, maxBatches: number? }—{ source, sourceBranch?, maxBatches? }—sourceis the source world GUID; branches default to"main";maxBatchescaps the batch loop (default 60 ⇒ up to ~120k entries).
Returns number — The number of pulled (dirty) rows now staged-pending on the fork.
world.forkLive({ source = "e89aa92e-4c1f-460e-acd8-73859dd3a346" })
globals/world/forkStatus
world.forkStatus() -> { ForkStatus }
Per-asset "ahead of origin" report — the fork analogue of git
status against an upstream. Every installed (pulled) row whose
content diverges from its pinned origin is listed, partitioned by
the TRUE origin world it was pulled from (nested dependencies
carry the world that authored them, not the intermediary they
arrived through). This is information for judgment: decide
whether a change belongs upstream, then world.contribute.
Returns { ForkStatus } — Array of ForkStatus partitions.
for _, f in ipairs(world.forkStatus()) do print(f.origin_world, #f.entries) end
globals/world/head
world.head() -> string?
Return the current branch HEAD commit id, or nil if the branch has no commits yet.
Returns string? — The commit id string, or nil.
local id = world.head()
globals/world/installAsset
world.installAsset(opts: InstallAssetOpts) -> InstallAssetResult
Install a published asset into this world, pulling the asset and every dependency it closes over and writing them into the source tree. Reports what it wrote so a caller can tell a fresh install from a no-op.
Parameters
optsInstallAssetOpts—{ guid }names the root asset to install;refpins a specific commit-id instead of the latest.
Returns InstallAssetResult — { assets_written, blobs_downloaded, root_guid, root_path, root_version, deps }.
local r = world.installAsset({ guid = assetGuid })
globals/world/installLibrary
world.installLibrary(opts: InstallLibraryOpts) -> InstallLibraryResult
Declarative cross-world dependency. Writes a single marker
file at /source/libs/@<name> whose body is the
zero/world-import/v1 JSON. The next commit ships it as one
regular manifest entry; ZM's import-derivation pass at
finalize-time decodes the marker and stamps the new commit's
imports[]. Unmodified library content never ships in the
importing world's tree — it's fetched from the source world
on demand by the engine's library resolver.
Parameters
optsInstallLibraryOpts— SeeInstallLibraryOpts.opts.worldis the upstream world's guid (required).opts.commitis the upstream commit_id to pin (optional; resolvesopts.reformainif omitted).opts.asis the local library name (defaults to the upstream world's slug).opts.refis the human-meaningful ref recorded in the marker.
Returns InstallLibraryResult summarising the install.
world.installLibrary({ world = "guid", as = "combat" })
globals/world/installedAssets
world.installedAssets() -> { InstalledAsset }
Every asset this world carries from ZeroMind, keyed by the published guid it was pulled from. The read that answers "what is actually in this world" by identity rather than by path — an installed asset's local name can be chosen by the installer, so a path is not the thing to check a pull against.
Returns { InstalledAsset } — An array of InstalledAsset sorted by local path, one per pulled row carrying a published guid.
for _, a in ipairs(world.installedAssets()) do print(a.asset_guid, a.path) end
globals/world/list
world.list() -> { WorldEntry }
List every world the authenticated user has access to.
Calls the spacetime list_my_worlds procedure which wraps
ZeroMind's GET /v1/me/worlds. Flattens each entry to one
record per world with the role promoted to a top-level field.
Returns { WorldEntry } — Array of WorldEntry records.
local worlds = world.list()
globals/world/log
world.log(opts: LogOpts?) -> { CommitRow }
Return the commit log for the current branch, newest first.
Pass opts.path to get the per-path history (git log -- <path>):
only the commits that touched that file, newest-first.
Parameters
optsLogOpts(optional) — Optional.opts.limitcaps the number of commits (default 50, 0 = all).opts.pathscopes the log to one file.
Returns { CommitRow } — Array of CommitRow tables.
local commits = world.log({ limit = 20 })
local touched = world.log({ path = "/source/foo.luau" })
globals/world/merge
world.merge(sourceBranch: string) -> MergeResult
Merge another branch into the session branch — git merge <source>. The merge runs locally in the world's SpacetimeDB clone
and is abortable with world.mergeAbort; nothing reaches ZeroMind
until the result is pushed. Requires a clean working tree (commit
or stash first — that is also what makes abort exact). Clean →
a two-parent merge commit lands on the session branch and the
merged content appears in the working tree. Conflicts → git-style
markers are projected into each conflicting text file, the
cleanly-merged remainder is applied as working-tree changes, and
world.vcsStatus().unmerged lists what needs attention: resolve
each path (edit out the markers / rewrite / remove the file),
then world.add + world.commit — that commit records the merge
(second parent = the source head) and clears the unmerged set.
Parameters
sourceBranchstring— The branch to merge in.
Returns MergeResult — A MergeResult — status is clean (with commit), conflicts (with conflicts), or up_to_date.
local r = world.merge("feature")
globals/world/mergeAbort
world.mergeAbort()
Abort the in-progress merge — git merge --abort. Clears the
unmerged set and restores the working tree to the pre-merge state
(the target head's committed content; the branch head never moved
during a conflicted merge). Errors when no merge is in progress.
world.mergeAbort()
globals/world/offLoaded
world.offLoaded(handle: number) -> boolean
Stop a callback registered with world.onLoaded from running.
Parameters
handlenumber— The handleworld.onLoadedreturned.
Returns boolean — true when the handle matched a registered callback.
world.offLoaded(h)
globals/world/offSaved
world.offSaved(handle: number) -> boolean
Stop a callback registered with world.onSaved from running.
Parameters
handlenumber— The handleworld.onSavedreturned.
Returns boolean — true when the handle matched a registered callback.
world.offSaved(h)
globals/world/offUnloaded
world.offUnloaded(handle: number) -> boolean
Stop a callback registered with world.onUnloaded from running.
Parameters
handlenumber— The handleworld.onUnloadedreturned.
Returns boolean — true when the handle matched a registered callback.
world.offUnloaded(h)
globals/world/onLoaded
world.onLoaded(cb: (...any) -> ()) -> number
Register a callback to run after a world finishes loading.
Parameters
cb(...any) -> ()— Called when the event fires, with whatever the event supplies.
Returns number — Handle for world.offLoaded.
local h = world.onLoaded(function() log.info("loaded") end)
globals/world/onSaved
world.onSaved(cb: (...any) -> ()) -> number
Register a callback to run after a world is saved.
Parameters
cb(...any) -> ()— Called when the event fires, with whatever the event supplies.
Returns number — Handle for world.offSaved.
local h = world.onSaved(function() log.info("saved") end)
globals/world/onUnloaded
world.onUnloaded(cb: (...any) -> ()) -> number
Register a callback to run after a world is unloaded.
Parameters
cb(...any) -> ()— Called when the event fires, with whatever the event supplies.
Returns number — Handle for world.offUnloaded.
local h = world.onUnloaded(function() log.info("unloaded") end)
globals/world/prConflicts
world.prConflicts(worldGuid: string?, number: number) -> any
Read a pull request's conflicts — what stands between it and a
merge. Returns the mergeability verdict, the merge base, both
heads, and one entry per conflicting path. A conflicting TEXT path
carries marked_text: the same <<<<<<< / ======= / >>>>>>>
rendering a merge leaves in the working tree, with the source and
target sides laid against their common ancestor. Resolve a path by
writing the settled bytes back to it and committing on the source
branch; the pull request re-analyses on the next read. A binary
path carries the two sides' hashes and no text — pick a side.
A mergeable pull request returns an empty conflict list.
Parameters
worldGuidstring(optional) — The world the pull request lives in. Defaults to the bound world.numbernumber— The pull request number.
Returns any — Decoded ZeroMind conflicts response.
local c = world.prConflicts(nil, 3)
for _, m in ipairs(world.prConflicts(originGuid, 3).markers) do print(m.path, m.marked_text) end
globals/world/prList
world.prList(worldGuid: string?, number: number?) -> any
Parameters
worldGuidstring(optional)numbernumber(optional)
Returns any
globals/world/prMerge
world.prMerge(worldGuid: string, number: number, strategy: string?) -> any
Merge a pull request — the agent-side merge button.
Parameters
worldGuidstring— The world the pull request lives in.numbernumber— The pull request number.strategystring(optional) —merge(default),squash, orfast_forward.
Returns any — Decoded ZeroMind merge response.
world.prMerge(originGuid, 3)
globals/world/prOpen
world.prOpen(opts: PrOpenOpts) -> any
List a world's pull requests, or fetch one.
Open a pull request — gh pr create. Proposes the work on one
(world, branch) pair to another. Defaults make the common cases one
argument: from a fork, the target is the world it was forked from, so
world.prOpen({ title = "..." }) proposes your work upstream. In an
ordinary world the target is the same world, so you get a
branch → main pull request.
The PR lives in — and is numbered by — the world it targets, exactly
as a forge numbers pull requests on the upstream repository. That is
also where world.prList finds it.
Parameters
optsPrOpenOpts—title(required), plusdescription,sourceWorld,sourceBranch,targetWorld,targetBranchto address any leg explicitly.
Returns any — Decoded ZeroMind response. The decoded ZeroMind pull request.
local prs = world.prList()
world.prOpen({ title = "fix the door hinge" })
world.prOpen({ title = "port the fix", targetWorld = otherGuid })
globals/world/prView
world.prView(worldGuid: string?, number: number) -> any
Read one pull request in full — gh pr view. Returns the record
plus a LIVE re-analysis against the current branch heads: mergeability
(clean / conflicts / fast_forwardable / up_to_date /
unrelated), conflict_count, and diff — every path the request
adds, modifies or deletes with its checksums. Read this before merging:
it is what tells you WHAT the request changes.
Parameters
worldGuidstring(optional) — The world the pull request lives in (its target world). Defaults to the session world.numbernumber— The pull request number.
Returns any — The decoded pull request view.
world.prView(nil, 1)
globals/world/previewInstall
world.previewInstall(opts: InstallAssetOpts) -> PreviewResult
Preview what installing an asset WOULD write, without writing
anything. Fetches + decodes the closure and plans placement (the
same helpers world.installAsset uses), returning a flat node
list plus rollup totals. A truncated closure is reported (not
raised) so a caller can surface it and block import.
Parameters
optsInstallAssetOpts—{ guid, at?, ref? }— same shape as installAsset.
Returns PreviewResult — nodes + totals + a truncated flag.
world.previewInstall({ guid = "..." })
globals/world/publishBlockers
world.publishBlockers() -> { PublishBlockerClass }
List every reason world.push would refuse to publish this
world, as one entry per blocker class: script errors in user
content, assets that don't meet their type's content requirements,
and asset references that can't be statically pinned. Each class
carries the same title the refusal prints, one items entry per
offending subject (an asset identity, or a <path>:<line> site),
and the single remedy covering that class. This is the account
world.push composes its refusal from, so it names the same
blockers with no push attempted — and in full, where a refusal
bounds how many of a class it prints. Empty list = the world
publishes. zm status prints this list.
Returns { PublishBlockerClass } — Array of PublishBlockerClass entries, empty when nothing blocks.
for _, c in ipairs(world.publishBlockers()) do
for _, i in ipairs(c.items) do print(c.kind, i.subject, i.detail) end
end
globals/world/pull
world.pull(branch: string?) -> PullResult
Fetch and reconcile with origin — git pull. Strictly behind →
fast-forward (the branch head moves to the origin mirror, no merge
commit). Diverged → three-way merge of the origin mirror, with the
same conflict/marker/resolve flow as world.merge (resolve the
unmerged paths, then world.add + world.commit; abortable with
world.mergeAbort). Requires a clean working tree.
Parameters
branchstring(optional) — Remote branch to pull. Defaults to the session branch.
Returns PullResult — A PullResult table.
local r = world.pull()
globals/world/pullAsset
world.pullAsset(opts: PullAssetOpts?) -> PullAssetResult
Pull upstream changes into a previously-installed asset,
three-way reconciling each entry against local edits. Re-resolves
the root's transitive closure at opts.ref (default latest),
then for every entry decides fast_forward / noop / converged
/ merge from (row.origin_checksum, localChecksum, entry.checksum) (M.__reconcileDecision):
noop— upstream hasn't moved; skipped.fast_forward/converged— the entry's latest text is written to the local path and the row's origin pointer advances. Binary and composite entries can't be content-synced through this call's only cross-world read primitive (text only), so a non-text entry with a real upstream change is surfaced as a conflict instead of silently going stale.merge(text entries) — a three-wayvcs.merge3runs over (base, local, theirs); a clean result is written and the origin pointer advances, a conflicted result is written WITH markers and the row is flagged conflicted (base + theirs checksums recorded forworld.resolvePullConflict).merge(binary / composite entries) — no text merge is possible; flagged conflicted with the structured base/theirs checksums (no marker write).
Closure drift: an entry the original install never landed is pulled fresh (added). A locally-pulled row nested under the root's own directory whose origin entry disappeared from the closure is removed when clean (pruned), or flagged conflicted when it carries local edits.
Parameters
optsPullAssetOpts(optional) — SeePullAssetOpts.opts.guidoropts.pathis required;opts.refpins the re-resolve to a specific upstream commit (defaults to latest finalized).
Returns PullAssetResult summarising what merged, conflicted, was pruned, and was newly added.
world.pullAsset({ guid = "..." })
globals/world/push
world.push(commitId: string?) -> (string?, string)
Publish the current branch to ZeroMind. The no-argument form
is a git merge --squash push: EVERY unpushed commit on the
branch collapses into a SINGLE ZeroMind commit (latest content
per path, parented on the branch's current remote HEAD). Because
only the merged final state's bytes are uploaded, a superseded or
lost intermediate-commit blob can never break the push — this is
what makes a churn-heavy world publishable. The local commit
history is preserved as the editing journal; on success every
squashed commit shares the one remote commit id. The explicit
commitId form still pushes that single commit verbatim via
publish_commit (advanced / chain-replay use; its parent must
already be on the remote).
Parameters
commitIdstring(optional) — Optional. A single commit to push verbatim. Omit for the squash push of the whole unpushed stack (the normal path).
Returns (string?, string) — Two values: the ZeroMind-allocated commit id, and the verdict. "published" with the new commit id when this call published; "already-published" when ZeroMind already carries what this call would have published — the state a push asks for, so it returns rather than raising. That verdict carries the commit's existing ZeroMind id when the publish names one (the single-commit form), and a nil id when it names none (the whole-stack form, which reports a chain). A squash whose merged final state carries dep.unresolved problems takes the slow path inside the same call: the engine re-resolves each pending literal against its live asset index and submits the resolutions to the publish procedure, which completes the push. A reference literal that still resolves to nothing is published with the asset holding it and stays a problem recorded on that asset, while a dep pin the squash severed raises instead; either way the engine names each one with its path, line, reference and reason. A publish ZeroMind refuses raises naming the condition and the command that clears it — a branch that moved under this push names world.pull().
local zmId = world.push()
local zmId, verdict = world.push()
world.push("01HABC...")
globals/world/reset
world.reset(targetCommitId: string) -> string?
Rewind HEAD to targetCommitId in one shot. Non-destructive
— orphaned commits stay in storage and each becomes a trash
entry the user can world.restore (in chain order) to re-attach
the branch. Errors when targetCommitId is not an ancestor of
HEAD. Returns a summary of what was rewound.
Parameters
targetCommitIdstring— The commit id to rewind to.
Returns string? — A summary message: the target plus the list of commits rewound past.
world.reset("01HABC...")
globals/world/resolveConflict
world.resolveConflict(path: string, mode: string, content: string?) -> ResolveResult
Resolve one conflicted /source record, one path at a time.
mode is merge | apply | take-local | take-backend |
discard. merge returns { merged, clean } and mutates nothing
— a clean merge can be finalized with apply, and a conflicted one
carries <<<<<<< / ======= / >>>>>>> markers to edit first. apply
writes the finalized content to /source; take-local writes the
retained local bytes; take-backend / discard keep the backend
head. Errors when the record is absent, a merge has no common
ancestor or hits binary content, or a write fails.
Parameters
pathstring— Canonical/sourcepath of the conflicted record.modestring— One of merge | apply | take-local | take-backend | discard.contentstring(optional) — Finalized bytes forapplymode.
Returns ResolveResult
world.resolveConflict("/zero/source/foo.luau", "take-local")
local r = world.resolveConflict(p, "merge"); if r.clean then world.resolveConflict(p, "apply", r.merged) end
globals/world/resolvePullConflict
world.resolvePullConflict(path: string, choice: string)
Resolve a conflicted row. A TEXT conflict is one where the file
currently contains conflict markers (world.pullAsset writes
markers only for a text three-way merge that didn't resolve
cleanly); a BINARY/composite conflict has no markers — the local
bytes were left untouched.
choice = "theirs" fetches clean upstream text by content hash
(conflict_theirs_blob_sha256 — path/rename-independent, since
blobs are content-addressed) and overwrites the local file with it
before clearing the flag. It errors, refusing to guess, when the
row records no theirs blob (a binary/composite conflict — a text
blob read can't address theirs for those; keep "ours" or
re-install the asset instead).
choice = "ours" keeps the local side. When the file carries
conflict markers, the local side is reconstructed from them (the
marker writers put ours first, so dropping each block's base and
theirs sections restores your bytes exactly) and written back;
a marker-free file is kept as-is. Either way the flag clears.
Either way, staging the resolved file (world.add) is the normal
git-add path once this returns — this call only clears the
manifest-level flag and (for "theirs") the file content.
Parameters
pathstring— The conflicted row's local VFS path.choicestring—"ours"or"theirs".
world.resolvePullConflict("/source/combat/rules.luau", "theirs")
globals/world/restore
world.restore(handle: any?)
Restore one trash entry by row_id. Errors verbatim on handler-not-yet-implemented / world-mismatch.
Parameters
handleany(optional) — The trash row id. May be a number or a numeric string.
world.restore(42)
globals/world/show
world.show(...: string) -> any
Mirror git show's CLI arg shape. Default returns commit
metadata + full diff vs parent. world.show("commit:/path")
returns just the bytes. Flags: --stat, --name-only.
Parameters
...string— Variadic string args: commit id, optional path, optional flags.
Returns any — Either a ShowResult table or a string (for commit:/path).
local r = world.show("abc123")
local r = world.show("--stat", "abc123")
local bytes = world.show("abc123:/foo.luau")
globals/world/startup_scene
world.startup_scene -> any
Returns any
globals/world/stash
world.stash(label: string?)
Save the caller's current pending dirty + staged state on
the active (world, branch) into a stash row. label is
optional free-form text. Non-destructive — dirty + staged
state is preserved on disk.
Parameters
labelstring(optional) — Optional. Free-form text label for the stash.
world.stash("wip widget refactor")
globals/world/stashDrop
world.stashDrop(handle: any?)
Request an affirmation token to drop a stash. Always
errors — successful mint surfaces the token as affirmation required: zm affirm <token>. The agent runs zm affirm <token> to actually drop; restoration via world.restore()
reappears the stash under a new row_id.
Parameters
handleany(optional) — The stash row id. May be a number or numeric string.
world.stashDrop(7)
globals/world/stashPop
world.stashPop(handle: any?) -> StashSnapshot
Author-only. Pop the stash row (deletes it server-side) and return the decoded snapshot. The caller is responsible for re-applying the snapshot to disk via the normal write paths (so ACL gates fire on every restored path).
Parameters
handleany(optional) — The stash row id. May be a number or numeric string.
Returns StashSnapshot containing dirty + staged entries.
local snap = world.stashPop(7)
globals/world/stashes
world.stashes() -> { StashRow }
List every stash row in the world. Anyone with read access
sees every stash; the per-row author_hex makes it clear
which entries the caller can pop / drop themselves.
Returns { StashRow } — Array of StashRow tables.
local rows = world.stashes()
globals/world/status
world.status -> any
Returns any
globals/world/status_text
world.status_text -> any
Returns any
globals/world/syncStatus
world.syncStatus() -> SyncStatus
Read the durable-sync status: { subscribed, content_synced, progress, pending_writes, unsaved_writes, uploads_abandoned, conflicts, binding }. conflicts maps each conflicted /source
path to { base_sha, local_sha, backend_sha, isBinary }.
unsaved_writes lists the /source paths this session wrote that
the server does not hold, and uploads_abandoned counts the uploads
the queue stopped carrying — read those two to tell a queue working
through a backlog from one that gave content up, which
pending_writes alone reads the same for. binding names which
world holds /source — bound, unbound, binding,
session_only, or unclassified before the boot has decided — and,
when an authorization attempt is on record,
which attempt is running and what the last one answered. Read
binding to tell a world that is still coming from one that was
never asked for: subscribed answers false for both. Synchronous.
Returns SyncStatus
local s = world.syncStatus(); print(s.pending_writes)
local s = world.syncStatus(); for _, p in ipairs(s.unsaved_writes) do print(p) end
local s = world.syncStatus(); if s.binding.awaiting_world then print(s.binding.state, s.binding.attempt) end
globals/world/trash
world.trash() -> { TrashRow }
List trash entries for the world. Anyone with read access
to the world can list trash — recovery is a shared safety net,
not a privacy boundary. The handle (row_id) feeds back into
world.restore.
Returns { TrashRow } — Array of TrashRow tables.
local rows = world.trash()
globals/world/uninstallLibrary
world.uninstallLibrary(name: string) -> string
Delete the library marker file. name accepts "@combat"
or "combat" (the leading @ is the convention carried by
the on-disk path).
Parameters
namestring— The library name, with or without the leading@.
Returns string — The marker path that was removed.
world.uninstallLibrary("@combat")
globals/world/unstage
world.unstage(path: string, opts: StageOpts?)
Remove a path from the staging area. Live manifest dirty state is untouched.
Parameters
pathstring— The path to unstage.optsStageOpts(optional) — Optional{ stage: string? }naming the staging area the path was staged into. Omitted, the call acts on the shared default area.
world.unstage("/source/foo.luau")
world.unstage("/source/foo.luau", { stage = "fauna" })
globals/world/vcsStatus
world.vcsStatus(opts: StageOpts?) -> StatusResult
Return the working-tree VCS status: dirty paths, staged
entries, ignored paths, untracked paths, and any unmerged ones.
An untracked path is one with no committed version behind it, and
it appears in dirty as well — git add . picks up new files too.
Named vcsStatus (not status) because world.status() is the
runtime-snapshot accessor owned by world_status.module; the
source-control surface keeps its own VCS-specific name so the
two never shadow each other.
Each dirty[i].dirtied_by is the identity of the most recent
writer; dirty_since_micros is the microsecond timestamp of the
first write of the current dirty run. local_identity is this
session's own writer identity in that same namespace — compare
the two to tell your own writes from another account's.
claimed_by_other_stages names the paths some other staging area
holds and which area holds each — the grain that tells two callers
apart when they share one writer identity, and the set
world.add_all holds back.
Parameters
optsStageOpts(optional) — Optional{ stage: string? }naming the staging area whose staged set to report. The dirty, ignored and untracked sets are the world's working tree and read the same whichever area is named.
Returns StatusResult — A StatusResult table.
local s = world.vcsStatus()
local s = world.vcsStatus({ stage = "fauna" })
globals/worldToScreen
worldToScreen(x, y, z) -> { x, y, visible }
Project a world-space point to screen-space coordinates. Returns one table, not two numbers. visible reports whether the point is IN FRONT OF the camera, which is what makes x/y meaningful — a point in front but outside the viewport is still visible = true, so test the coordinates against getViewportSize() to decide whether it is on screen.
Parameters
xnumber— World Xynumber— World Yznumber— World Z
Returns table — { x = number, y = number, visible = boolean } — x/y in pixels
globals/xpcall
xpcall(fn, handler, ...) -> ok, result
Protected call with custom error handler.