Log inGet started

modules

Updated 7 September 2026

The modules namespace — 2302 functions.

modules/AgentSkillAssetTypeRef/README

AgentSkillAssetTypeRef

Per-instance methods exposed on every AssetRef<agentSkill>. Loaded lazily by asset_ref.module.

modules/AgentSkillAssetTypeRef/getInstructions

getInstructions(self): string?

Read the skill's instructions.md body — the prose an agent receives when it invokes the skill.

Parameters

  • self any (optional)
local body = skillRef:getInstructions()

modules/AgentSkillAssetTypeRef/getManifest

getManifest(self): { [string]: any }

Read and parse the skill's skill.yaml manifest — its description, declared dependencies, and subskill ordering.

Parameters

  • self any (optional)
local m = skillRef:getManifest()

modules/AgentSkillAssetTypeRef/getReadme

getReadme(self): string?

Read the skill's README.md body.

Parameters

  • self any (optional)
print(skillRef:getReadme())

modules/AgentSkillAssetTypeRef/inspect

inspect(self): any

asset.inspect type-specific detail — the skill's whole structured surface: { description, when, checks, stages, verdicts, instructions, dependencies, subskills, manifestError }. checks carries each acceptance criterion as { check, observe } — what must hold, and where it is seen. stages carries the ordered passes as { name, detail, checks }, each holding the criteria that only mean anything once that pass has run. verdicts carries the outcomes a reader reports as { verdict, means } — the word, and what reporting it asserts. dependencies carries each declared identity with what it resolved to and whether it resolves here; a toolbox additionally carries the tools it holds, a module the exports it carries, and a tool its own signature, all read live rather than restated. subskills carries each nested skill's parent/sub invoke address. This is the dump skills.invoke renders — a skill with no readable instructions.md still describes, reporting the gap rather than erroring.

Parameters

  • self any (optional)
local subskills = asset.inspect(skillRef).detail.subskills

modules/AgentSkillAssetTypeRef/onChange

onChange(self, change)

Lifecycle hook: re-publish this skill when anything inside its folder is written, so a skill just authored is listed and an edited description is the one agents see. Reads the manifest and writes nothing back.

Parameters

  • self any (optional) — The changed skill's AssetRef.
  • change any (optional) — The write record the dispatcher passes through.

modules/AgentSkillAssetTypeRef/onRegister

onRegister(self)

Lifecycle hook: publish this skill to the roster agents read when the skill first registers. The scope it is listed under is derived from the asset's own identity.

Parameters

  • self any (optional) — The registering skill's AssetRef.

modules/AnimationAssetTypeBehavior/README

AnimationAssetTypeBehavior

Behaviour for the animation asset type — a clip's disk shape. The payload is data.zanim (the engine-native binary AnimAsset, parallel to .mesh's data.zmsh). onCreate writes that payload, and — when the clip's source rig guid is supplied — records it as source_rig.json (a .rig asset reference) so the clip knows the skeleton it was authored on and retargets onto any humanoid. Converters call asset.create("animation", name, { bytes, rig }) to mint a clip.

modules/AnimationAssetTypeBehavior/onChange

onChange(ref: any, change: { [string]: any })

React to a write inside a .animation/ instance: when the clip's source rig link (source_rig.json) changes, record the rig's humanoid classification into .metadata for search — the clip inherits it from the rig it targets. Originator-only; .metadata syncs as ordinary content.

Parameters

  • ref any (optional) — The AssetRef for the changed clip.
  • change { [string]: any }{ path, asset, type, kind, origin }.

modules/AnimationAssetTypeBehavior/reindexSearchMeta

reindexSearchMeta(self): ()

Re-derive this clip's search .metadata (its humanoid classification, resolved from the source rig). onChange calls it when source_rig.json changes; the search-metadata backfill calls it per clip to populate clips minted before the deriver existed.

Parameters

  • self any (optional)
animationRef:reindexSearchMeta()

modules/AssetChangeDispatch/README

require("@builtin/modules/asset_change_dispatch") -- AssetChangeDispatch

Routes a VFS source write to the enclosing typed-asset's onChange(ref, change) hook. Installs _G.__zero_dispatch_asset_change, which the engine calls (via ffi_callbacks::fire_asset_change_dispatch) once per source write.

This is the type-level analogue of the component-centric onAssetReload(field) fan-out. Components react to assets they reference; an asset type reacts to writes INSIDE its own instances. When any file under a <name>.<type>/ folder changes on the VFS, the engine hands the written path here; this module finds the enclosing typed-asset folder, loads its <type>.assetType/behavior.luau, and — if that module exports an onChange function — invokes onChange(ref, change). The engine half is deliberately thin (a generic "this path was written" signal); all the resolution + behaviour lives here in Luau, mirroring the __build_asset_ref_proxy split.

Usage: local AssetChangeDispatch = require("@builtin/modules/asset_change_dispatch")

modules/AssetChangeDispatch/dispatch

dispatch(path: string, kind: string?, origin: string?)

Dispatch the asset-type change hook for a VFS path. Called by the engine for two orthogonal axes:

  • kind: a per-file EDIT ("edited", path is the written file) vs an asset COMPLETION ("seeded", path is the typed-asset folder, now fully present from a world seed). The type inspects kind to decide whether to filter by which file changed or act on the whole asset.
  • origin: "local" for a write made on this client, "remote" for a peer-synced write — forwarded so hooks can gate on it (importers run on the originator only). Resolves the typed asset, loads its behavior.luau, and invokes onChange(ref, change) (change = { path, asset, type, kind, origin }). When the path has no enclosing typed asset, forwards to the loose-file seam (dispatch_loose) so the importer system can claim orphan writes.

Parameters

  • path string — The VFS path: the written file (edited) or the asset folder (seeded).
  • kind string? (optional) — "edited" (default) or "seeded".
  • origin string? (optional) — "local" (default) or "remote".
__zero_dispatch_asset_change("/zero/source/Goblin.dynamicAsset/prompt.json", "edited", "local")

modules/AssetChangeDispatch/dispatchDelete

dispatchDelete(path: string)

Dispatch the asset-type DELETE hook for a removed typed-asset FOLDER — the teardown counterpart of dispatch. path is the folder that was removed; this resolves its <name>.<type> identity, loads the type's behavior.luau, and invokes onDelete(ref) if defined (a .component unregisters its type). No-op when the path isn't a registered typed asset or the type defines no onDelete. The engine calls this via ffi_callbacks::fire_asset_delete_dispatch once per typed-asset folder removal.

Parameters

  • path string — The removed asset folder's VFS path.
__zero_dispatch_asset_delete("/zero/source/Spinner.component")

modules/AssetRef/README

require("@builtin/modules/asset_ref") -- AssetRef

AssetRef proxy builder. Attaches the default method metatable to every { __ref, type, name, guid, identity, path } envelope produced by asset.resolve / asset.ref, and dispatches type-specific methods from <typename>.assetType/behavior.luau so a .material ref carries material-only methods (and so on).

The Rust side (crates/zero_scripting/src/ffi/bindings/asset.rs) used to ship a C metatable with a fixed __index that knew about getSource / getBytes / getText / exists / inspect / meta. That shape couldn't grow without an FFI change, which meant the per-asset-type behaviour required to make refs useful (read a material's properties, instantiate a bundle, run a tool) had no hook. This module owns that responsibility now. It exposes one entry point — M.build(envelope) — that the Rust factory invokes via the _G.__build_asset_ref_proxy global the prelude installs. The build call attaches a shared metatable whose __index first serves the default method table, then falls through to the type's own ref table (loaded lazily from @builtin::assetTypes.<type>.behavior). The default method surface is fixed and engine-required — per-type ref tables CANNOT shadow getSource / getBytes / getText / exists or the lazy meta property, matching the contract in gh#1889. inspect is NOT a fixed default — it falls through to the type's own ref.inspect(self) (returning the type-specific detail for asset.inspect's dispatch), and is nil when the type defines none.

Usage: local AssetRef = require("@builtin/modules/asset_ref")

modules/AssetRef/build

build(envelope: any): any

Attach the AssetRef method metatable to an envelope table. Invoked by the Rust factory (push_asset_ref_handle_G.__build_asset_ref_proxy) immediately after the six envelope fields (__ref, type, name, guid, identity, path) have been set, so the metatable's __index only ever fires for method / property lookups, never for the literal envelope fields.

Parameters

  • envelope any (optional) — The freshly-built envelope table.
local r = require("modules.asset_ref").build({ type = "material", path = "/zero/source/Gold.material", ... })

modules/AssetRef/canInstantiate

canInstantiate(self): boolean

Whether this asset can be instantiated into a scene — true iff its asset type defines an instantiate method. Generic capability query (no type allowlist); consumers gate on it before offering a scene path (an Asset.source field, a viewport drop, a tool argument).

Parameters

  • self any (optional)
if asset.resolve("Golem","dynamicAsset"):canInstantiate() then ... end

modules/AssetRef/deps

deps(self): { deps: { any }, unresolved_deps: { any }, problems: { any } }

Return this asset's outbound reference table, aggregated across every file inside it (for composite asset folders) — same data asset.deps(ref) returns. deps holds the references that resolved ({ asset_guid, origin, literal, via, line?, checksum?, ... }), unresolved_deps the literals nothing answered ({ literal, via, reason, line? }), and problems the findings attached to the asset. An asset that references nothing returns all three empty.

Parameters

  • self any (optional)

modules/AssetRef/flushPendingPersists

flushPendingPersists()

Write out every asset whose edit-mode persistence is still coalesced, spending no allowance and waiting on no refill. The runtime-state wipe on a mode flip calls this first, so a change made in the last window before the flip reaches the asset instead of being cleared with the overlay it lives in. Call it before reading an asset's file for a value a runtime write may have just changed.

require("modules.asset_ref").flushPendingPersists()

modules/AssetRef/forgetRuntime

forgetRuntime(guid: string): boolean

Forget everything a type derived from ONE asset's content — the values it cached in ref.runtime off the bytes that asset used to hold. Called when an asset's content is REPLACED under a guid live consumers already hold: a type memoizes its parse, its GPU handle, its settings against the content it read, and each of those describes the previous bytes the moment the new ones land. Emptying the table in place rather than replacing it is what makes the clear reach every holder — the runtime table is shared by every resolver of the guid, and a type may be holding it directly.

Parameters

  • guid string — The asset's stable guid.
require("modules.asset_ref").forgetRuntime(ref.guid)

modules/AssetRef/loadTypeBehavior

loadTypeBehavior(asset_type: string): ({ [string]: any }?, string?)

Load an asset type's behavior.luau module table, reporting a behavior that raised while loading. The first return is the module (nil when the type ships no behavior.luau); the second is set when the type HAS a behavior.luau that raised, and carries the require key plus the error it raised. A caller that runs the type's hooks — asset.create runs onCreate — reads the second return to tell "this type declares no behavior" from "this type's behavior is broken", which are opposite situations for the asset it is about to write.

Parameters

  • asset_type string — The type name (e.g. "dynamicAsset", "material").
local mod, err = require("modules.asset_ref").loadTypeBehavior("dialogue")

modules/AssetRef/loadTypeModule

loadTypeModule(asset_type: string): { [string]: any }?

Load the full behavior.luau module table for an asset type ({ ref?, global?, onChange? }), or nil when the type ships no behavior.luau. Registry-driven resolution — same path the per-type ref dispatch uses. Exposed so the asset-change dispatcher (modules/asset_change_dispatch) can reach a type's onChange hook without duplicating the resolution logic.

Parameters

  • asset_type string — The type name (e.g. "dynamicAsset", "material").
local m = require("modules.asset_ref").loadTypeModule("dynamicAsset")

modules/AssetRef/persistInEditMode

persistInEditMode(self: any)

Generic edit-mode persistence hook an assetType calls when a change of its own is meant to reach the file. In EDIT mode, flush a ref's transient runtime overlay (ref.runtime) to its backing asset file by invoking the type's own saveDefinition(self), so the change syncs to peers and is saved. Works for any assetType that defines a saveDefinition; whether a given type's runtime writes route through here is that type's own contract. The write-through is rate-limited per asset: an asset carries an allowance of 8 writes that refills at one per 250ms. Changes made in one frame are coalesced onto a single re-emit, and a caller that changes a value and moves on has it on disk a frame or two later. A caller that keeps changing the same asset runs the allowance down to its refill rate, so over any span the asset costs at most that allowance plus one write per 250ms, whatever cadence the changes arrive at. In PLAY mode this is a deliberate no-op: runtime overlays stay transient (frame-fast) and are persisted back to the source asset on demand. An assetType opts in simply by exposing ref.saveDefinition; no per-type branching lives here.

Parameters

  • self any (optional) — Any AssetRef.
require("modules.asset_ref").persistInEditMode(matRef)

modules/AssetRefShapes/README

require("@builtin/modules/asset_ref_shapes") -- AssetRefShapes

Publishes what every AssetRef<category> answers to, so a member read on an asset-typed value is checked against the category's own surface.

An asset category's per-instance surface is authored: a <name>.assetType/behavior.luau declares M.ref = { ... }, and each category declares its own. So the members an AssetRef<inputMap> carries are knowable only to inputMap itself — no fixed set of types covers the ones a world defines, and the checker has no way to guess them. This module reads each registered category's M.ref table from its source (never executing it) and renders it as a Luau table type, then hands the whole set to the engine. From there a ref:method(...) on an asset-typed value resolves against the category's real surface: a name it does not carry is reported with the list of the ones it does. The sweep is complete and replaces what was published before, so a category whose type is removed stops being published. It runs at world load, and again whenever a behavior.luau is written — the assetType type's own onChange hook re-publishes, which is what makes an edited surface take effect without a restart.

Usage: local AssetRefShapes = require("@builtin/modules/asset_ref_shapes")

modules/AssetRefShapes/ensure

ensure()

Publish the surfaces if they are not known to be current, and do nothing when they are. This is what a checker calls before it reads them: it makes the published set complete at the moment of use rather than at some earlier moment that may not have arrived yet.

AssetRefShapes.ensure()

modules/AssetRefShapes/install

install()

Arm the world-load sweep. Idempotent.

AssetRefShapes.install()

modules/AssetRefShapes/instanceReturnsOf

instanceReturnsOf(typeRef: any, out: { { category: string, identity: string, method: string, definition: string, source: string } })

The instance-derived returns one category contributes: for each of its instances, the type each refShapes entry states for THAT asset. A category whose behavior declares no refShapes contributes none.

This is how a method whose result is shaped by the asset gets typed at all. inputMapRef:activate() answers one handle per binding the map declares — a set that is authored, differs per map, and changes when a control is added — so no fixed signature can state it and only the type itself can compute it.

Each entry is function(self) -> (typeExpression, source?): the type that call answers for THIS asset, and the module whose type vocabulary the expression is written in — the handles an inputMap:activate() answers are Handle, a name its own module declares. Omit the source when the expression names only types visible from anywhere.

Parameters

  • typeRef any (optional) — The AssetRef<assetType> for the category.
  • out { { category: string, identity: string, method: string, definition: string, source: string } } — Array the { category, identity, method, definition, source } records append to.
AssetRefShapes.instanceReturnsOf(asset.resolve("inputMap", "assetType"), {})

modules/AssetRefShapes/invalidate

invalidate()

Mark the published set stale, so the next ensure re-reads every category. Called when a type definition is written.

AssetRefShapes.invalidate()

modules/AssetRefShapes/publish

publish(): number

Sweep every registered asset type and publish its ref: surface, plus every instance-derived return its types compute. Complete each time: a category the sweep does not reach stops being published, so a removed type's surface never lingers.

AssetRefShapes.publish()

modules/AssetRefShapes/published

published(): { categories: { [string]: string }, returns: { [string]: { [string]: string } } }

What the checker currently believes, as { categories = { [category] = definition }, returns = { [identity] = { [method] = definition } } }. Publishes first, so it answers about the set a check would use rather than a stale one.

This is the answer to "is my type not published, or published and correct?" — from a call site the two look the same, because both are simply no diagnostic. Read it when a member access you expected to be reported was not.

AssetRefShapes.published().returns["@builtin::inputMaps.default"]

modules/AssetRefShapes/shapeOf

shapeOf(typeRef: any): string?

The Luau table type describing one category's ref: surface — the methods its behavior.luau declares, plus everything every AssetRef carries regardless of category. Returns nil when the type ships no behavior or declares no M.ref methods: a category that states nothing of its own is left opaque rather than described by the common members alone, so an access on it stays unchecked instead of being judged against a surface its author never wrote.

Parameters

  • typeRef any (optional) — The AssetRef<assetType> for the category.
local t = AssetRefShapes.shapeOf(asset.resolve("inputMap", "assetType"))

modules/AssetTypeAssetTypeRef/README

AssetTypeAssetTypeRef

Per-instance methods exposed on every AssetRef<assetType>. Loaded lazily by asset_ref.module.

An AssetRef<assetType> points at a <typename>.assetType/ definition folder. SHARED CODE that a type exposes to every instance is declared in that type's own behavior.luau as a modules map of tracked requires, e.g.: -- inside <typename>.assetType/behavior.luau M.modules = { shared = require(".shared") } and reached from any instance through the pinned instance->type link: -- inside any foo.<thatType>/init.luau local api = asset.containing(FILE).modules.shared Resolution follows the instance's pinned typeRef guid (its .refs via = "asset_type" dep), so identically-named modules in different types never collide and an imported instance reaches the exact type version it was authored against. This is the mechanism that makes a type fully self-contained: the behaviour every instance relies on lives in the type, not duplicated as a require("modules.x") in 100 instance files. See modules/asset_ref.module for the dispatch.

modules/AssetTypeAssetTypeRef/getReadme

getReadme(self): string?

Read the asset type's own README.md body (type-level docs — what THE TYPE is, surfaced by asset.inspect).

Parameters

  • self any (optional)
print(assetTypeRef:getReadme())

modules/AssetTypeAssetTypeRef/onChange

onChange(ref, change)

Re-publish every asset category's ref: surface after a write inside a type definition.

Parameters

  • ref any (optional) — The AssetRef<assetType> for the edited type.
  • change any (optional){ path, asset, type, kind, origin } for the write.
-- the engine calls this; a behavior.luau edit is picked up live

modules/AssetTypeAssetTypeRef/onCreate

onCreate(name: string): { [string]: string }

Generic-creation hook for asset.create("assetType", name). Scaffolds a new asset type by cloning this type definition's own template/ skeleton with the [name] placeholder rewritten to your type's name, so the new <name>.assetType/ is ready to edit (type.yaml, behavior.luau, and its own template/ instance body). Pure: returns the substituted file map.

Parameters

  • name string — The new type's name. Becomes its .<name> instance suffix.
asset.create("assetType", "Waypoint")

modules/AssetValidator/README

require("@builtin/systems/worldValidation.package/assetValidator") -- AssetValidator

Structure validator for asset folders. Delegates each asset to asset.validate — the engine primitive that checks the folder against its type's type.yaml (required files, one_of_groups, unexpected entries, content constraints), waives the describe-this-asset requirements for private subassets (assets nested inside a non-broadcasting container like a .bundle), and runs the type's own semantic validate hook — and maps the result into the report's Problem records.

asset.validate is the same primitive the world.push gate runs, so a world that validates clean here also passes the structural half of the publish gate. Each problem is attributed to the asset path (missing files anchor at the asset root; existing files — unexpected entries, content violations — anchor at the file). An asset whose type has no registered type.yaml gets a schema.unknown_type warning; an asset the registry cannot resolve (e.g. written this frame, registration still draining) gets a schema.unresolved_asset warning.

Usage: local AssetValidator = require("@builtin/systems/worldValidation.package/assetValidator")

modules/AssetValidator/dependencyProblemsFrom

dependencyProblemsFrom(path: string, deps: any, guidCache: { [string]: boolean })

Map an asset's reference table into publish-blocking Problem records. Split from the lookup so the mapping can be exercised on a reference table directly. A guid already present in guidCache is not looked up again.

Parameters

  • path string — Asset path the problems are attributed to.
  • deps any (optional) — Reference table in the shape asset.deps returns.
  • guidCache { [string]: boolean } — Memo of guid → whether the live asset index carries it.
local problems = AssetValidator.dependencyProblemsFrom(path, asset.deps(path), {})

modules/AssetValidator/validate

validate(entry, guidCache: { [string]: boolean }?)

Validate one asset folder via asset.validate and map the result into Problem records.

Parameters

  • entry any (optional) — Asset entry from vfsScanner{ path, name, type }.
  • guidCache { [string]: boolean }? (optional)
local problems = AssetValidator.validate({ path = "/source/Foo.component", name = "Foo.component", type = "component" })

modules/AssetValidator/validateBatch

validateBatch(assets)

Validate a batch of asset entries and flatten the per-asset problem lists into one array. Convenience wrapper over M.validate.

Parameters

  • assets any (optional) — Array of asset entries from the scanner.
local all = AssetValidator.validateBatch(bucket.assets)

modules/AvatarAssetTypeBehavior/README

AvatarAssetTypeBehavior

Behaviour for the avatar asset type — a playable character composed from three INDEPENDENT parts so each swaps on its own axis: - body — a .bundle: skinned mesh + bone entities (the look). - controller — the movement system (default: CharacterController + MovementState + Humanoid, the standard kinematic humanoid mover). - animation — the system that animates the body (default: the shared Locomotion blend space, which reads the controller's velocity and plays retargeted clips; or ClipPlayer; or your own component). Movement and animation are NOT tied together: attaching the controller never pulls in Locomotion, and choosing your own animation keeps the standard controller — so authoring your own locomotion is a one-field swap. The clip set retargets onto any humanoid rig, so the default locomotion drives any humanoid body with no per-body setup. :instantiate() composes the parts; :setCharacter(root, body) swaps the body. Point a scene's PlayerPrototype.body at a body carrying this avatar to make it the body every connecting player spawns with.

modules/AvatarAssetTypeBehavior/instantiate

instantiate(self, target: EntityRef?, opts: any): (EntityRef, { [string]: string })

Instantiate this avatar: spawn an avatar root, spawn the body bundle under it, then attach the movement controller and the animation system — independently. With the defaults the root gets the standard humanoid controller and the shared Locomotion that drives the body with retargeted clips. Pass a target entity REF to make that entity the avatar root (the player-avatar path passes the freshly-spawned avatar entity here).

Parameters

  • self any (optional)
  • target EntityRef? (optional) — Optional avatar root as an EntityRef. Omit to spawn one.
  • opts any (optional) — Optional { position, rotation, scale, name, temporary, idMap, diff, sourceTag } — the base placement opts land on the root; idMap pins the composed body root, label, and every body-bundle child to stable ids across re-instantiations; diff replays the user's sparse edits to the composed body. sourceTag marks the call as owned — the Asset component sets it when it drives the composition itself.
local av = avatarRef:instantiate()

modules/AvatarAssetTypeBehavior/onCreate

onCreate(name: string, opts: CreateOpts?): { [string]: string }

Generic-creation hook for asset.create("avatar", name, opts). Composes the avatar from a body plus an independent movement controller and animation system, written as avatar.json. The body is a .bundle (skinned mesh + bones), a plain .mesh (a simple visual), or omitted (a body-less avatar — a controller / first-person camera with no mesh). Defaults: the standard humanoid controller and, for a humanoid body, the shared Locomotion (clips from the locomotion preset, default "synty"). Override animation to author your own locomotion without touching movement; pass controller = false for a body the engine doesn't move; pass clip for a single-clip ClipPlayer. Humanoid-ness drives the default animation only and is auto-derived from a rigged body; pass humanoid to set it explicitly (e.g. a body-less first-person avatar that still carries Humanoid).

Parameters

  • name string
  • opts CreateOpts? (optional)
asset.create("avatar", "my_hero", { body = heroBundle })  -- standard controller + locomotion
asset.create("avatar", "fp_player", { humanoid = true })  -- body-less first-person player

modules/AvatarAssetTypeBehavior/setCharacter

setCharacter(self, root: any, body: any): EntityRef

Swap the body on a live avatar. Despawns the current body and spawns body (a .bundle ref) in its place. The controller + animation systems live on the avatar root and re-resolve the new body's rig, so the look swaps while movement and animation are kept. A single-clip ClipPlayer rides the skinned body, so it is re-attached to the new body.

Parameters

  • self any (optional)
  • root any (optional) — The live avatar root (an EntityRef from :instantiate()).
  • body any (optional) — The new body — a .bundle ref (guid / identity / AssetRef).
avatarRef:setCharacter(avatarRoot, asset.resolve("knight", "bundle"))

modules/BlankAppLayout/README

require("@builtin/_templates.blank_app.blank_app_layout") -- BlankAppLayout

Minimum-viable app — one screen built from the raw CSS-parity widget tree. Clone-and-edit starting point; replace build with your own tree.

Usage: local BlankAppLayout = require("@builtin/_templates.blank_app.blank_app_layout")

modules/BundleAssetTypeRef/README

BundleAssetTypeRef

Per-instance methods exposed on every AssetRef<bundle>. Loaded lazily by asset_ref.module.

modules/BundleAssetTypeRef/getTemplate

getTemplate(self): any

Parse the bundle's entity_template into a Lua value. The engine format is a flat array of entity records ({ name, original_id, parent_id, components, ... }), so the result is an array-table; the return type is any because the payload is decoded JSON. Returns nil when parsing fails or the bundle has no template. Parsed results are cached per-guid — repeat :instantiate on the same bundle reuses the parsed table instead of re-decoding the JSON. Cleared when the template is written — by :setTemplate or straight to the bundle's entity_template — so authoring-time edits land on the next instantiate.

Parameters

  • self any (optional)
local t = bundleRef:getTemplate()

modules/BundleAssetTypeRef/getTemplateRaw

getTemplateRaw(self): string?

Read the raw entity_template file (JSON) as a string. Use :getTemplate() for a parsed table.

Parameters

  • self any (optional)
local raw = bundleRef:getTemplateRaw()

modules/BundleAssetTypeRef/inspectDetail

inspectDetail(self): any

asset.inspect type-specific detail: { nodeCount, attachedComponents, rootName }, parsed from the bundle's own entity_template JSON — never instantiates the hierarchy. A bundle with no readable template returns an empty detail rather than erroring.

Parameters

  • self any (optional)
local attached = asset.inspect(bundleRef).detail.attachedComponents

modules/BundleAssetTypeRef/instantiate

instantiate(self, target: EntityRef?, opts: { [string]: any }?): (EntityRef, { [string]: string })

Instantiate this bundle. The DEFAULT — bundle:instantiate() with no target entity — spawns ONE root entity carrying an Asset component that references this bundle: the component spawns the bundle's hierarchy as TEMPORARY runtime children (never written to the saved scene) and owns save / reload / diff, so the scene stays clean (one entity per instance, not the whole exploded permanent tree). This is the one-call path to put a bundle in the world.

Passing a target entityId (instantiate ONTO that entity, which becomes the bundle root) OR opts.raw = true (build a permanent fresh hierarchy) selects the RAW explode: it spawns every child in the entity_template, remaps cross-entity references, applies any saved sparse diff, and writes the bundleProvenance live-link attribute, returning the root entity id and the originalId → runtimeId map. The Asset component, Player avatars, and template tooling use this raw path.

Spawn mode is selected by opts.deferred. Default (deferred = false) uses the legacy batch() flush — every component is visible on the SAME tick so spawn-then-query patterns (tests, scene-load critical path, spawn-then-read flow) keep working. Pass opts.deferred = true for hundreds-to-thousands-of-entities bundles (large levels, density spawners, gibbed-prop debris bundles): the spawn+component work wraps in queue() and the engine drainer spreads it across multiple frames at the QUEUE_DRAIN_BUDGET_PER_FRAME cap. The root entity id is minted same-tick either way (entity.spawn always returns immediately), but in deferred mode children + components arrive in the ECS over the next few frames — don't read them back synchronously.

Parameters

  • self any (optional)
  • target EntityRef? (optional) — Optional target entity — an entity REF (proxy) to explode ONTO (that entity becomes the bundle root; this selects the RAW path). Omit for the default (fresh root + Asset component).
  • opts { [string]: any }? (optional) — Optional { position, rotation, scale, name, temporary, raw, idMap, diff, sourceTag, deferred }. The base placement opts land on the returned root; raw = true forces the raw explode into a fresh permanent hierarchy (no Asset component); idMap reuses saved child ids (stable ids across reload); diff reapplies sparse child overrides keyed by template original_id; sourceTag labels the provenance record (defaults to "bundle"); deferred=true switches the spawn pump from same-tick batch() to cross-frame queue() — required for thousand-entity bundles to avoid one huge frame stall; temporary=true spawns every child born temporary (the Asset component's re-created scaffolding — kept out of the saved scene so a reload doesn't respawn them alongside a re-instantiate). idMap / diff / deferred / temporary apply to the raw path only.
local root = bundleRef:instantiate()                          -- scene-clean instance (Asset component); root is an EntityRef
local root, map = bundleRef:instantiate(entity.spawn("mount"))     -- raw, ONTO an entity ref
local root, map = bundleRef:instantiate(nil, { raw = true })  -- raw permanent fresh hierarchy

modules/BundleAssetTypeRef/listChildren

listChildren(self, rootsOnly: boolean?): { string }

List the names of every entity captured in THIS bundle instance's entity_template. Pass rootsOnly = true to restrict to entities with no parent_id. Best-effort: empty list when the template can't parse.

Parameters

  • self any (optional)
  • rootsOnly boolean? (optional) — When true, only entities with no parent_id are returned.
for _, name in ipairs(bundleRef:listChildren()) do print(name) end

modules/BundleAssetTypeRef/listContents

listContents(self): { { name: string, isDirectory: boolean } }

List the entries directly under the bundle root folder (meshes, textures, materials, sub-bundles) for tooling (inspectors, dependency graphs).

Parameters

  • self any (optional)
for _, e in ipairs(bundleRef:listContents()) do print(e.name) end

modules/BundleAssetTypeRef/meshBindings

meshBindings(self): { any }

List each renderable node's mesh→material binding from THIS bundle's entity_template — the static data a spawned Model / SkinnedModel resolves — WITHOUT instantiating anything. Answers "which material does this mesh bind" as a direct read, with no spawn / settle / tree-walk / despawn cycle. Each entry is { node, nodeId, component, mesh, material }, where mesh and material are ref descriptors { guid, identity, name, path } (nil when the component omits that field). One entry per renderable component, in template order. Best-effort: empty list when the template can't parse.

Parameters

  • self any (optional)
for _, b in ipairs(bundleRef:meshBindings()) do print(b.node, b.material and b.material.name) end

modules/BundleAssetTypeRef/onChange

onChange(ref: any, change: { [string]: any })

React to a write inside a .bundle/ instance: an entity_template change means the assembled model changed — regenerate the bundle's preview.png, its persisted visual description (syncs, publishes, feeds search and browser thumbnails), and record the component set + humanoid classification into .metadata for search facets. Originator-only; both artifacts sync to peers as ordinary content.

Parameters

  • ref any (optional) — The AssetRef for the changed container.
  • change { [string]: any }{ path, asset, type, kind, origin }.

modules/BundleAssetTypeRef/onCreate

onCreate(name: string, opts: CreateOpts?): { [string]: string }

Generic-creation hook for asset.create("bundle", name, opts). With opts.entity, composes that LIVE entity's hierarchy into the new bundle's entity_template in the same call — one step, capturing live component state (serialized component snapshots) + transforms of the root and every non-temporary descendant. With no opts, the bundle starts from the template skeleton's entity_template.

Parameters

  • name string
  • opts CreateOpts? (optional)
asset.create("bundle", "tree_prefab", { entity = rootRef })

modules/BundleAssetTypeRef/preview

preview(self, opts: { [string]: any }?)

Render a preview of this bundle — instantiate its entity hierarchy, auto-frame an offscreen camera over it, render one still, tear down.

Parameters

  • self any (optional)
  • opts { [string]: any }? (optional){ size? = { width, height }, angle? = { yaw, pitch } }.
local p = bundleRef:preview()

modules/BundleAssetTypeRef/reindexSearchMeta

reindexSearchMeta(self): ()

Re-derive this bundle's search .metadata (component set + humanoid classification) from its current entity_template, without touching the preview. onChange calls it on every template edit; the search-metadata backfill calls it per bundle to populate assets minted before the deriver existed. Reads the template fresh — the parsed cache is only invalidated by setTemplate.

Parameters

  • self any (optional)
bundleRef:reindexSearchMeta()

modules/BundleAssetTypeRef/setTemplate

setTemplate(self, template: { any }): boolean

Overwrite the bundle's entity_template with a Lua table, JSON-encoded. The VFS write triggers the engine's asset hot-reload pipeline, so every component subscribed to this bundle's guid (via a declared asset field) reconciles automatically. Returns true on success.

Parameters

  • self any (optional)
  • template { any } — The full entity-template table (array of records) to write.
bundleRef:setTemplate(bundleRef:getTemplate())

modules/BundleAssetTypeRef/update

update(self, entityId: string): boolean

Re-compose THIS bundle from entityId's current hierarchy and write the new template back to the bundle's on-disk path. Walks the whole hierarchy under entityId (recursive), capturing live component state + transforms of every non-temporary descendant. The VFS write fires the engine's asset hot-reload pipeline, so every component subscribed to this bundle's guid reconciles automatically.

Parameters

  • self any (optional)
  • entityId string — The entity whose live hierarchy is captured into the bundle.
bundleRef:update(playerId)   -- "save" the live edits back into the bundle

modules/BundleUpdate/README

require("@builtin/modules/bundle_update") -- BundleUpdate (also available as global 'bundle')

Adds bundle.update to the existing bundle namespace. Pure Luau — thin ergonomic wrapper around bundle.compose. The prelude grafts this onto the engine's bundle table at boot.

bundle.update(entityId, bundleRef?) re-composes the bundle from the entity's current hierarchy state, writing the new template into the bundle's existing on-disk path. The reconcile is fully engine-driven: the VFS write fires the generic asset hot-reload pipeline, which dispatches onAssetReload(field) on every component instance whose declared asset field references the bundle's guid. Components decide what reload means for them (Asset.component → despawn + re-instantiate template; user components → whatever they implement in onAssetReload). With one arg, the bundle ref is inferred from entity(entityId).component.get("Asset").source. With two args the explicit ref wins. There is no opt-in subscription surface — declaring an asset field IS the subscription.

Usage: local BundleUpdate = require("@builtin/modules/bundle_update") Also available as global: bundle

modules/BundleUpdate/installInto

installInto(bundle: BundleNamespace)

Install update onto the supplied bundle-shaped namespace. The prelude calls this once at boot with the engine's bundle global; users reach the result as bundle.update.

Parameters

  • bundle BundleNamespace — The target namespace table. No-op when given a non-table value.
require("modules.bundle_update").installInto(bundle)

modules/BundleUpdate/update

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

  • entityId string — The entity whose hierarchy is captured into the bundle.
  • bundleRef BundleRef? (optional) — Optional. When omitted, the ref is inferred from the entity's Asset.source field. When given, the explicit ref wins.
bundle.update(entityId)                     -- infer from Asset
bundle.update(entityId, { guid = "..." })   -- explicit ref

modules/CanvasAppLayout/README

require("@builtin/_templates.canvas_app.canvas_app_layout") -- CanvasAppLayout

Node-graph canvas built from the raw CSS-parity widget tree — absolutely positioned node cards over an SVG bezier connector; click a node to select it.

Usage: local CanvasAppLayout = require("@builtin/_templates.canvas_app.canvas_app_layout")

modules/CombatGlow/README

CombatGlow

The lit half of the combat effect family: the one additive material all five effects draw with, the four forms it carries, and the instance-data lanes that tell one entity's copy of it apart from another's. An effect leases a card through here and writes its state onto it every frame.

modules/CombatGlow/along

along(a: { number }, b: { number }, t: number): { number }

A point t of the way from a to b.

Parameters

  • a { number }{ x, y, z }.
  • b { number }{ x, y, z }.
  • t number — How far along, 0..1.
local head = CombatGlow.along(from, to, 0.4)

modules/CombatGlow/axis

axis(a: { number }, b: { number }): { number }

The unit vector running from a to b — what a rod form is told so it can leave its two end caps undrawn.

Parameters

  • a { number } — Where the rod starts, { x, y, z }.
  • b { number } — Where the rod ends, { x, y, z }.
CombatGlow.write(rod, { axis = CombatGlow.axis(from, to), … })

modules/CombatGlow/ball

ball(ctx: any, name: string, position: { number }, diameter: number): any

Lease a sphere carrying the family's material — what a bloom is drawn on. diameter is the sphere's world size in metres.

Parameters

  • ctx any (optional) — The play context.
  • name string — The entity name the card carries.
  • position { number } — World position { x, y, z }.
  • diameter number — Sphere diameter in metres.
local card = CombatGlow.ball(ctx, "muzzle_bloom", pos, 0.7)

modules/CombatGlow/disc

disc(ctx: any, name: string, position: { number }, normal: { number },

Lease a plane lying across a surface normal — what a ring is drawn on. width is the card's world size in metres, which is twice the ring's widest radius.

local card = CombatGlow.disc(ctx, "shock_ring", pos, { 0, 1, 0 }, 12)

modules/CombatGlow/distance

distance(a: { number }, b: { number }): number

The distance between two world points.

Parameters

  • a { number }{ x, y, z }.
  • b { number }{ x, y, z }.
local span = CombatGlow.distance(p.from, p.to)

modules/CombatGlow/lifted

lifted(c: { number }, amount: number): { number }

A colour lifted toward white by amount, held to 0..1 — the shade a light or a leading edge carries above the colour the caller asked for.

Parameters

  • c { number } — The colour { r, g, b }.
  • amount number — How far to lift each channel.
local hot = CombatGlow.lifted(p.color, 0.2)

modules/CombatGlow/rod

rod(ctx: any, name: string, from: { number }, to: { number }, width: number): any

Lease a rod stretched between two world points — what a streak or a bolt is drawn on. The rod covers the whole flight path and the shader lights the part of it the round has reached, so nothing moves per frame.

Parameters

  • ctx any (optional) — The play context.
  • name string — The entity name the rod carries.
  • from { number } — Where the path starts, { x, y, z }.
  • to { number } — Where the path ends, { x, y, z }.
  • width number — How wide the rod is, in metres.
local rod = CombatGlow.rod(ctx, "tracer_rod", from, to, 0.06)

modules/CombatGlow/unit

unit(v: any, fallback: { number }): { number }

A vector as a unit vector, or fallback when it has no length. Every effect in the family takes a direction or a normal from the caller, and a zero one is a value the caller may write.

Parameters

  • v any (optional) — The vector { x, y, z }.
  • fallback { number } — The unit vector to use when v has no length.
local n = CombatGlow.unit(p.normal, { 0, 1, 0 })

modules/CombatGlow/write

write(card: any, state: { [string]: any })

Write a card's whole state onto its instance-data lanes: which form it draws, how far through it is, its colour and gain, and the four scalars that form reads. A card whose entity has not been committed yet is skipped, and the next frame writes it.

Parameters

  • card any (optional) — The geometry instance a lease holds.
  • state { [string]: any }{ form, age, energy, seed, color, gain, a, b, c, d }.
CombatGlow.write(card, { form = CombatGlow.BLOOM, age = 0.3, color = c })

modules/CombatGlow/writeTo

writeTo(e: any, state: { [string]: any })

Write a frame's state onto an entity that is already in hand — the spelling a catalogue still uses, where the entity was just spawned and the proxy is the thing being written.

Parameters

  • e any (optional) — The entity proxy.
  • state { [string]: any }{ form, age, energy, seed, color, gain, a, b, c, d }.
CombatGlow.writeTo(e, { form = CombatGlow.RING, a = 0.7, b = 0.1 })

modules/ComponentAssetTypeRef/README

ComponentAssetTypeRef

Per-instance methods exposed on every AssetRef<component>. Loaded lazily by asset_ref.module.

modules/ComponentAssetTypeRef/attachTo

attachTo(self, entityId: string, fields: { [string]: any }?): any

Attach this component to an entity, equivalent to entity(entityId).component.add(compRef.name, fields). The component's type name comes from the ref's name (the leaf folder stem with .component stripped) — never guessed.

Parameters

  • self any (optional)
  • entityId string — Target entity ID.
  • fields { [string]: any }? (optional) — Optional public field overrides.
compRef:attachTo(playerId, { speed = 5 })

modules/ComponentAssetTypeRef/getAssetFields

getAssetFields(self): any

The component type's asset-field declarations — a map of field name to asset category (e.g. { material = "material" }), or nil when the component declares no Field.assetRef fields.

Parameters

  • self any (optional)
for field, cat in pairs(compRef:getAssetFields() or {}) do end

modules/ComponentAssetTypeRef/getInfo

getInfo(self): any

Metadata for the component type: { name, builtin, executionOrder, hooks }, where hooks is a { <hookName> = true } map scanned from the source.

Parameters

  • self any (optional)
local info = compRef:getInfo() print(info.executionOrder)

modules/ComponentAssetTypeRef/getInitScript

getInitScript(self): string?

Read the component's entry script (init.luau / init.lua) as raw text.

Parameters

  • self any (optional)
local src = compRef:getInitScript()

modules/ComponentAssetTypeRef/getReadme

getReadme(self): string?

Read the component's README.md body.

Parameters

  • self any (optional)
print(compRef:getReadme())

modules/ComponentAssetTypeRef/getSource

getSource(self): string?

The component type's registered source text — the live definition in the runtime registry. getInitScript reads the on-disk entry file; this reads what the engine actually registered (and resolves builtin components by identity).

Parameters

  • self any (optional)
local src = compRef:getSource()

modules/ComponentAssetTypeRef/inspect

inspect(self): any

asset.inspect type-specific detail: { fields, methods, events, hooks, assetFields, executionOrder }, parsed from the component's own entry script (getInitScript) via luau_introspect. Cached on the asset's content checksum, so re-inspecting unchanged source is free. A component with no readable entry script returns an empty detail rather than erroring.

Parameters

  • self any (optional)
local events = asset.inspect(compRef).detail.events

modules/ComponentAssetTypeRef/isRegistered

isRegistered(self): boolean

Whether the component type is registered with the ECS, so that component.add(name) takes it. The source is stored the moment the asset is written; the type registers when that registration drains, on a later frame, and this reads the registration rather than the source.

Parameters

  • self any (optional)
Test.waitUntil(function() return compRef:isRegistered() end, 120)

modules/ComponentAssetTypeRef/listInstances

listInstances(self): { string }

List every entity in the live scene that currently has a component of this type. Useful for inspector tooling.

Parameters

  • self any (optional)
for _, id in ipairs(compRef:listInstances()) do print(id) end

modules/ComponentAssetTypeRef/listPublicFields

listPublicFields(self): { string }

Best-effort list of the component's public field names by parsing the entry script. Recognises public.<name> = ... assignments at module scope; doesn't expand metatable declarations. Comments and string literals are not read. Use as a discovery hint, not a strict schema.

Parameters

  • self any (optional)
for _, f in ipairs(compRef:listPublicFields()) do print(f) end

modules/ComponentAssetTypeRef/onChange

onChange(ref, change)

Asset-type change callback: (re)register + hot-reload this component type whenever its .component is seeded (a world seed) or its entry script (init.luau / init.lua) is edited. This is what registers and live-reloads USER components — library components register through the VFS write hook (author-immutable content does not dispatch onChange). Mirrors the .material / .shader assetTypes owning their own registration. Convergent

  • idempotent: registration never writes back into the asset folder, and the underlying registry upsert is a no-op when the source is unchanged.

Parameters

  • ref any (optional)
  • change any (optional)

modules/ComponentAssetTypeRef/onDelete

onDelete(ref)

Asset-type delete callback: unregister this component type when its .component folder is removed. The teardown counterpart of onChange's registration — the type owns both halves of its runtime lifecycle, the way the .material / .shader types do. __components.unregister drops the type from the runtime registry, the FFI type-info / schema mirrors, and the /registered/components/ projection; instances already attached to live entities keep running (their closures are already bound).

Parameters

  • ref any (optional)

modules/ComponentAssetTypeRef/onRegister

onRegister(self)

Initial-registration callback: register this component's type from its entry script the moment the instance first registers. Fired by the world-ready onRegister sweep BEFORE the world entrypoint (and its scene load) runs, so component.add-by-name resolves for entities the same load materialized. Idempotent: the registry upsert is a no-op when the source is unchanged, and a later onChange re-registration converges on the same definition.

Parameters

  • self any (optional)

modules/ComputeShaderAssetTypeRef/README

ComputeShaderAssetTypeRef

Per-instance methods exposed on every AssetRef<computeShader>. Loaded lazily by asset_ref.module.

modules/ComputeShaderAssetTypeRef/buffer

buffer(self, name: string): any?

The buffer this shader last made under name. Two systems driving one shader over the same data reach for this rather than each creating their own; a system that wants its own calls createBuffer again.

Parameters

  • self any (optional)
  • name string — The name the buffer was created under.
local params = shaderRef:buffer("params") or shaderRef:createBuffer("params", { type = "vec4", len = 3 })

modules/ComputeShaderAssetTypeRef/compile

compile(self)

Compile this shader now, rather than on its first dispatch. Idempotent. NORMALLY UNNECESSARY — a dispatch compiles on first use. Reach for this only to avoid the one-frame first-dispatch warm-up in a latency-critical spot.

Parameters

  • self any (optional)
shaderRef:compile()

modules/ComputeShaderAssetTypeRef/compileByName

compileByName(ref: string)

Compile a compute shader by reference from its .computeShader VFS source — the lazy compile-on-first-use entry. A system that must guarantee the shader is registered before its first dispatch calls this (via compute.compileByName) to bring it online through the same generic compileCompute path an edit runs. Resolution goes through the universal asset system — a reference that doesn't resolve is a bad reference in the content that owns it, not something to special-case here.

Parameters

  • ref string — A compute-shader asset reference (identity / guid) resolvable by asset.resolve.
require("modules.asset_ref").loadTypeModule("computeShader").compileByName("@builtin::shaders.compute_double")

modules/ComputeShaderAssetTypeRef/copyBufferToTexture

copyBufferToTexture(self, source: any, name: string, width: number, height: number, format: string?): TextureHandle

Copy one of this shader's buffers into a cached GPU texture, staying on the GPU — the path for an image a compute pass produced.

Parameters

  • self any (optional)
  • source any (optional) — The buffer holding tightly-packed rows in the format's texel layout.
  • name string — Texture name, unique within this shader.
  • width number — Texture width in texels.
  • height number — Texture height in texels.
  • format string? (optional) — Texel format: "rgba16f" (default), "rgba32f", "rgba8".
local tex = shaderRef:copyBufferToTexture(packed, "foam", 256, 256, "rgba16f")

modules/ComputeShaderAssetTypeRef/createBuffer

createBuffer(self, name: string, opts: { [string]: any }): any

Create a GPU buffer this compute shader owns. It is a substrate buffer — the same one every other part of the engine deals in — filed under this shader's guid, name, and a serial, so two callers asking this shader for a params each get their own. Pass the handle to any shader's dispatch, including another shader's, to bind it there. The shader remembers what it made: shaderRef:buffer(name) returns the last buffer created under that name, which is how two callers share one instead.

Parameters

  • self any (optional)
  • name string — What this buffer is for, e.g. "params" or "verts".
  • opts { [string]: any }{ type, len, usage? }type is the element ("f32", "vec3", "vec4", "quat", "mat4"), len is how many of them, and usage adds what the buffer is used for beyond the storage it always has: "readback" to read it on the CPU, "vertex" to draw it as geometry, "index" to draw it as an index run, "indirect" for a draw to read its arguments out of. A buffer can carry several.

There is no integer element: a buffer is a block of 32-bit words, so a binding the shader declares as u32 or atomic<u32> in bindings.yaml is created as "f32" here and written with buf:writeU32. The element type sets the STRIDE; what the words mean is the shader's to say.

local verts = shaderRef:createBuffer("verts", { type = "vec3", len = 1024, usage = { "readback" } })
verts:write(packed)  -- an array of numbers, or a `buffer` already holding the words
local values = verts:read():result()
local counts = shaderRef:createBuffer("counts", { type = "f32", len = 64 })  -- bindings.yaml: element: u32
counts:writeU32({ 0, 0, 0, 0 })
local geo = shaderRef:createBuffer("geo", { type = "vec3", len = 4096, usage = { "vertex", "readback" } })
local args = shaderRef:createBuffer("args", { type = "f32", len = 5, usage = { "indirect" } })  -- DrawIndexedIndirectArgs

modules/ComputeShaderAssetTypeRef/createSampler

createSampler(self, name: string, opts: { [string]: any }?): TextureHandle

Create a sampler owned by this compute shader, for its texture bindings.

Parameters

  • self any (optional)
  • name string — Sampler name, unique within this shader.
  • opts { [string]: any }? (optional) — Sampler options — filtering and addressing.
local smp = shaderRef:createSampler("linear", { filter = true, clamp = true })

modules/ComputeShaderAssetTypeRef/createStorageTexture2D

createStorageTexture2D(self, name: string, opts: { [string]: any }): TextureHandle

Create a write-only 2D storage texture owned by this compute shader — the target a raymarch or image pass writes.

Parameters

  • self any (optional)
  • name string — Texture name, unique within this shader.
  • opts { [string]: any }{ width, height, format? }.
local out = shaderRef:createStorageTexture2D("out", { width = 1920, height = 1080, format = "rgba16f" })

modules/ComputeShaderAssetTypeRef/createTexture3D

createTexture3D(self, name: string, opts: { [string]: any }): TextureHandle

Create a 3D texture owned by this compute shader — a density volume, an occupancy grid, a signed-distance field. Keyed by the shader's guid plus name, so it cannot collide with another asset's.

Parameters

  • self any (optional)
  • name string — Texture name, unique within this shader.
  • opts { [string]: any }{ width, height, depth, format?, storage? }.
local density = shaderRef:createTexture3D("density", { width = 64, height = 64, depth = 64, format = "r16f", storage = true })

modules/ComputeShaderAssetTypeRef/createTextureHistory

createTextureHistory(self, name: string, opts: { [string]: any }): TextureHandle

Create a temporal history pair owned by this compute shader — the previous frame's result to read while writing this frame's.

Parameters

  • self any (optional)
  • name string — History name, unique within this shader.
  • opts { [string]: any }{ width, height, format? }.
local history = shaderRef:createTextureHistory("taa", { width = 1920, height = 1080, format = "rgba16f" })

modules/ComputeShaderAssetTypeRef/destroy

destroy(self): boolean

Destroy this texture or sampler and free its GPU memory.

Parameters

  • self any (optional)

modules/ComputeShaderAssetTypeRef/dispatch

dispatch(self, opts: { [string]: any }): boolean

Dispatch this compute shader with named buffers bound to its declared storage bindings, in order. Compiles on first use. A zero in any workgroup dimension is refused and recorded as a dispatch failure, so a count derived from how much data there is passes through math.max(1, ...) first.

Parameters

  • self any (optional)
  • opts { [string]: any }{ buffers, workgroups }buffers names one compute buffer per declared storage binding; workgroups is { x, y, z }, { x }, or x.
shaderRef:dispatch({ buffers = { "positions" }, workgroups = { 64 } })

modules/ComputeShaderAssetTypeRef/dispatchEx

dispatchEx(self, opts: { [string]: any }): boolean

Dispatch this compute shader with explicit texture / storage-texture / sampler resources, one per declared binding in order. A params: block's uniform is engine-owned and takes no entry here. A zero in any workgroup dimension is refused and recorded as a dispatch failure, so a count derived from how much data there is passes through math.max(1, ...) first.

Parameters

  • self any (optional)
  • opts { [string]: any }{ resources, workgroups } — each resource is { kind, name }.
shaderRef:dispatchEx({ resources = { { kind = "storage_2d", name = "target" } }, workgroups = { 8, 8 } })

modules/ComputeShaderAssetTypeRef/dispatchOnVertices

dispatchOnVertices(self, opts: { [string]: any }): boolean

Dispatch this compute shader with a model's vertex buffer bound at the first storage binding, and opts.buffers filling the rest. Use to mutate vertex positions directly. A zero in any workgroup dimension is refused and recorded as a dispatch failure, so a count derived from how many vertices there are passes through math.max(1, ...) first.

Parameters

  • self any (optional)
  • opts { [string]: any }{ model, buffers?, workgroups }model is the mesh guid whose vertices the shader writes.
shaderRef:dispatchOnVertices({ model = meshHandle.guid, workgroups = { 64 } })

modules/ComputeShaderAssetTypeRef/getBindings

getBindings(self): { [string]: any }

List this compute shader's declared bindings + params (parsed from bindings.yaml). Returns { bindings = { {name, kind, access?, element?, format?}, ... }, params = { {name, type, default}, ... } }. This is the editor-discovery surface — the SAME parse the compile uses.

Parameters

  • self any (optional)
for _, b in ipairs(computeRef:getBindings().bindings) do print(b.name, b.kind) end

modules/ComputeShaderAssetTypeRef/getSource

getSource(self): string?

Read the compute WGSL body (shader.wgsl) as raw text.

Parameters

  • self any (optional)
local src = computeRef:getSource()

modules/ComputeShaderAssetTypeRef/onChange

onChange(ref, change)

Asset-type change callback: (re)compile the compute shader whenever its WGSL body or bindings.yaml is written. This is the ONLY thing that compiles a .computeShader — so it fires on the initial create (the template write) AND on every later edit, with no world reload. Convergent: see compileCompute.

Parameters

  • ref any (optional)
  • change any (optional)

modules/ComputeShaderAssetTypeRef/read

read(self): Readback

Start a GPU→CPU read-back of this 3D texture's voxels. The read takes frames to arrive — ask the returned Readback whether it is :ready(), then drain it.

Parameters

  • self any (optional)
local pending = density:read()
if pending:ready() then local voxels = pending:result() end

modules/ComputeShaderAssetTypeRef/setParam

setParam(self, prop: string, value: number): boolean

Set one scalar parameter declared in this shader's bindings.yaml params: block. The next dispatch sees the new value; a value set before the shader's first compile is the value it starts with.

Parameters

  • self any (optional)
  • prop string — Parameter name as declared in bindings.yaml.
  • value number — New scalar value.
shaderRef:setParam("scale", 4.0)

modules/ComputeShaderAssetTypeRef/setSource

setSource(self, src: string): boolean

Overwrite the compute WGSL body on disk. Hot-reload recompiles the shader on the next frame. Returns true on success.

Parameters

  • self any (optional)
  • src string — New WGSL source (only @compute fn main + helpers).
computeRef:setSource(myWgsl)

modules/ComputeShaderAssetTypeRef/status

status(self): { { [string]: any } }

What the engine did with this shader's dispatches, one record per target. A dispatch is recorded into a command encoder frames after the call that asked for it returned, so this is where its outcome lands: ok is the most recent outcome, dispatches counts what reached the encoder, failures how many of those could not be recorded, and lastError says why the last failure failed (kept after a recovery). target is the mesh guid for a dispatchOnVertices, empty for a dispatch that writes only its bound buffers. This answers "is this pass running?" — an empty result means nothing has dispatched this shader.

Parameters

  • self any (optional)
for _, d in ipairs(shaderRef:status()) do print(d.target, d.ok, d.lastError) end

modules/ComputeShaderAssetTypeRef/write

write(self, data: buffer | string | { number }): boolean

Upload voxels into this 3D texture: a buffer or a binary string carrying the texture's byte layout verbatim, or one number per channel in the texture's format.

Parameters

  • self any (optional)
  • data buffer | string | { number } — Voxel bytes, or voxel values in texel order.

modules/ComputeShaderAssetTypeRef/writeFloats

writeFloats(self, floats: { number }, formatOrOpts: (string | { [string]: any })?): boolean

Upload float voxels into this 3D texture, converting to the texture's format.

Parameters

  • self any (optional)
  • floats { number } — Voxel values in texel order.
  • formatOrOpts (string | { [string]: any })? (optional) — Source format name, or an options table.

modules/ContentVersion/README

require("@builtin/modules/content_version") -- ContentVersion

Per-path content-version counters — a cheap, synchronous "has this file changed?" token that lets an assetType behavior memoize a parse in its ref's runtime and serve repeated reads as a pure table lookup instead of re-reading + re-parsing the file every call.

A source write bumps the written path's counter. A memoized reader keyed by the value get(path) returned when it parsed can then check, on every later call, whether the counter still matches — an integer compare, no vfs.read, no parse. It rebuilds only when the counter moved. Two write surfaces feed it, so the token reflects a change no matter where it came from:

  • vfs.write / vfs.move / vfs.remove bump SYNCHRONOUSLY, in the same call — so a script that writes a file and reads it back in the SAME tick sees the new content immediately (the asset-change onChange dispatch fires a frame LATER, too late for a same-tick read).
  • the generic asset-change dispatcher bumps on every source write it routes, including peer-synced and engine-originated writes that never pass through the Luau vfs.* surface — with the engine's normalized path, which is the canonical form a reader keys on. Counters are per PATH (not one global epoch): the per-frame dirty-entity writer churns scene-dirty paths every frame during play, and a global epoch would let that churn invalidate every unrelated cache. Per-path isolation means only a change to THE file a reader depends on rebuilds it. Per-VM. The map holds one small integer per distinct written source path.

Usage: local ContentVersion = require("@builtin/modules/content_version")

modules/ContentVersion/bump

bump(path: string)

Bump path's version counter, invalidating every reader memoized against its previous value. Called by the vfs.* write surface and the asset-change dispatcher; content code rarely calls it directly.

Parameters

  • path string — VFS path whose content changed.
require("modules.content_version").bump(p)

modules/ContentVersion/get

get(path: string): number

The current version counter for path (0 if never written this VM). A memoized reader stores the value it saw when it parsed, and treats a later call as a cache hit exactly while get(path) still returns it.

Parameters

  • path string — Normalized VFS path.
local v = require("modules.content_version").get(p)

modules/DataAssetTypeRef/README

DataAssetTypeRef

Per-instance methods on every AssetRef<data> — a configured value instance of a dataType contract. Instances carry values only; structure and behavior live on the contract.

modules/DataAssetTypeRef/contract

contract(self)

The dataType contract this instance is bound to.

Parameters

  • self any (optional)
local weaponType = smg:contract()

modules/DataAssetTypeRef/data

data(self, wanted: string?)

Resolve this instance into a read-only typed value object: this instance's values.yaml with the contract's schema defaults applied, ref fields materialized (assetRef -> AssetRef, dataRef -> nested value object), and the bound contract chain's behavior methods reachable on it — the bound contract's own methods win over an ancestor's when both define the same name, and any explicit instance value wins over both. Errors loudly when the instance violates its contract, when wanted is given and the bound contract's chain doesn't include it, and when dataRef fields form a circular chain.

Parameters

  • self any (optional)
  • wanted string? (optional) — Optional contract the caller requires — an assertion, not a filter: the returned object still carries the FULL bound schema and method chain.
local w = smg:data("tdWeapon"); print(w.damage, w:effectiveRating())

modules/DataAssetTypeRef/inspect

inspect(self): any

asset.inspect type-specific detail: { contract, valueKeys }, parsed from this instance's own values.yaml — the bound contract identity plus the sorted top-level keys of its values map. Never resolves the contract or materializes ref fields (that's :data()).

Parameters

  • self any (optional)
local valueKeys = asset.inspect(dataRef).detail.valueKeys

modules/DataAssetTypeRef/onCreate

onCreate(name: string, opts: { contract: string })

Create a data instance bound to a contract. Seeds values.yaml with the contract's declared defaults so a fresh instance is immediately valid wherever every required field has a default.

Parameters

  • name string
  • opts { contract: string }

modules/DataAssetTypeRef/satisfies

satisfies(self, wanted: string): (boolean, string?)

Whether this instance's contract chain includes wanted — true for the bound contract itself and for any contract it extends.

Parameters

  • self any (optional)
  • wanted string — Contract identity (bare leaf or full identity).
if smg:satisfies("weapon") then ... end

modules/DataAssetTypeRef/satisfiesDetail

satisfiesDetail(self, wanted: string): { ok: boolean, code: string, reason: string? }

Chain-membership check that also reports WHY, as a stable code a caller can branch on without matching reason text.

Parameters

  • self any (optional)
  • wanted string — Contract identity (bare leaf or full identity).
local d = smg:satisfiesDetail("weapon")
if not d.ok and d.code ~= "contract-mismatch" then warn(d.reason) end

modules/DataAssetTypeRef/validateInstance

validateInstance(self): (boolean, { DS.Violation })

Validate this instance's values against its contract's merged schema (missing required fields, constraint breaches, unknown fields, ref-field existence + contract compatibility).

Parameters

  • self any (optional)
local ok, v = smg:validate()

modules/DataSchema/README

DataSchema

Pure schema engine for the typed-data system. A contract's schema.yaml declares fields + constraints; this module parses the decoded declaration, merges extends chains, applies defaults, and validates value tables. Pure Luau — no FFI, no VFS reads — callers decode the YAML themselves and inject asset/contract resolvers.

modules/DataSchema/applyDefaults

applyDefaults(merged: { [string]: FieldSpec }, values: { [string]: any }): { [string]: any }

Produce a NEW value table with every schema default filled in where values has no explicit entry — recursively: struct values gain their subfield defaults and array elements gain their item defaults, at every depth. Neither input is mutated.

Parameters

  • merged { [string]: FieldSpec } — Merged field map from mergeChain.
  • values { [string]: any } — The instance's raw value table.
local filled = DS.applyDefaults(merged, rawValues)

modules/DataSchema/mergeChain

mergeChain(chain: { Schema? }): ({ [string]: FieldSpec }?, { string })

Merge an extends chain of parsed schemas into one field map. The chain is ordered ROOT PARENT FIRST, derived contract LAST. A child redeclaring a parent field is a problem — shared shape comes from the parent, per-child shape from new fields.

Parameters

  • chain { Schema? } — Array of Schema, root parent first. A nil hole (e.g. a failed parseSchema result passed straight in) is a problem entry.
local merged, problems = DS.mergeChain({ itemSchema, weaponSchema })

modules/DataSchema/parseSchema

parseSchema(raw: any): (Schema?, { string })

Parse a decoded schema.yaml table into a Schema. Returns (schema, problems) — schema is nil when any problem was found. fields may be written as a map (name -> spec) or as a sequence of specs each carrying its own name:; both key the resulting fields by name.

Parameters

  • raw any (optional) — The decoded document ({ extends?, fields }).
local schema, problems = DS.parseSchema(Yaml.decode(bytes))

modules/DataSchema/validateValues

validateValues(

Validate a raw value table against a merged field map. Checks missing required fields (a field with a default is never missing), per-field constraints, unknown top-level fields, and ref fields via the injected resolvers.

local violations = DS.validateValues(merged, rawValues, resolvers)

modules/DataTypeAssetTypeRef/README

DataTypeAssetTypeRef

Per-instance methods exposed on every AssetRef<dataType> — the contract surface of the typed-data system: schema (extends chain merged), validation, defaults, chain navigation, instance listing.

modules/DataTypeAssetTypeRef/chain

chain(self)

The full extends chain as refs, root parent first, this contract last.

Parameters

  • self any (optional)
for _, link in ipairs(t:chain()) do print(link.identity) end

modules/DataTypeAssetTypeRef/defaults

defaults(self)

The default value table the merged schema declares.

Parameters

  • self any (optional)
local d = weaponType:defaults()

modules/DataTypeAssetTypeRef/inspect

inspect(self): any

asset.inspect type-specific detail: { fields, extends }, the contract's OWN declared field schema (not the merged extends chain) — fields is { {name, type, required}, ... } sorted by name, parsed from this contract's own schema.yaml. extends is the parent contract identity, or nil for a root contract.

Parameters

  • self any (optional)
local fields = asset.inspect(dataTypeRef).detail.fields

modules/DataTypeAssetTypeRef/instances

instances(self)

Every data instance bound to this contract (or to a contract that extends it). Scans the whole data-asset registry — a discovery surface for authoring and tooling, not a per-frame call. A broken instance (unreadable values.yaml, dangling contract binding) warns and is skipped, so one bad instance never aborts enumeration of the rest.

Parameters

  • self any (optional)
for _, ref in ipairs(weaponType:instances()) do print(ref.identity) end

modules/DataTypeAssetTypeRef/parent

parent(self)

The parent contract ref (extends), or nil for a root contract.

Parameters

  • self any (optional)
local base = rangedType:parent()

modules/DataTypeAssetTypeRef/schema

schema(self)

The contract's merged field schema — its own fields plus every field inherited through the extends chain.

Parameters

  • self any (optional)
local schema = weaponType:schema()

modules/DataTypeAssetTypeRef/validateValues

validateValues(self, values: { [string]: any })

Validate a value table against this contract's merged schema.

Parameters

  • self any (optional)
  • values { [string]: any }
local ok, v = weaponType:validate({ damage = 12 })

modules/Debris/README

Debris

Timed entity despawn — Roblox Debris:AddItem(instance, lifetime) analog. Wraps the internal __debris.* FFI with a typed Luau surface. Installed as the global debris via the prelude.

Collapses the common task.delay + entity.despawn pattern into one call. Use for transient entities — bullet tracers, hit FX, dropped pickups, tween-completion cleanups, ragdoll cleanup. Semantics (match Roblox):

  • debris.add(id, lifetime) queues a despawn after lifetime seconds. Default lifetime is 10 seconds.
  • Calling add twice on the same entity REPLACES the prior deadline.
  • If the entity is despawned through any other path the pending record is dropped silently — no error.
  • Pending entries survive world.save / world.load (saved lifetime is seconds-remaining, so timers resume from where they paused).

modules/Debris/add

add(id: any, lifetime: number?): DebrisHandle

Schedule the entity for despawn after lifetime seconds (default 10). Calling again on the same entity replaces the prior deadline. Negative or zero lifetime despawns immediately. Returns a handle for cancel(), or 0 if the entity id couldn't be resolved.

Parameters

  • id any (optional) — Entity id, name, or proxy table.
  • lifetime number? (optional) — Seconds before despawn — defaults to 10 when nil.
local bullet = entity.spawn("Bullet"); debris.add(bullet.id, 2.0)
local h = debris.add(target.id, 5); debris.cancel(h)

modules/Debris/cancel

cancel(handleOrId: any): boolean

Cancel a pending despawn. Accepts either a handle from debris.add or an entity id / proxy. Returns true if a pending record was actually removed.

Parameters

  • handleOrId any (optional) — Cancel handle, or entity id / name / proxy.
debris.cancel(handle); debris.cancel(target.id)

modules/Debris/clear

clear(): boolean

Drop every pending entry. Used by the test suite to isolate cases — not part of the user-facing surface.

modules/Debris/count

count(): number

Number of currently pending debris entries — handy for diagnostics overlays.

print(debris.count(), "pending despawns")

modules/Debris/list

list(): { DebrisEntry }

Snapshot every pending entry as a flat array of {id, remainingSecs, handle} records. Order is not stable — don't rely on it.

for _, e in debris.list() do print(e.id, e.remainingSecs) end

modules/Debris/pending

pending(id: any): number?

Return the number of seconds remaining before the entity is despawned, or nil if it isn't scheduled.

Parameters

  • id any (optional) — Entity id, name, or proxy.
local s = debris.pending(bullet.id); if s then print("dies in", s) end

modules/DockedAppLayout/README

require("@builtin/_templates.docked_app.docked_app_layout") -- DockedAppLayout

Docked-shell UI template — a top toolbar, a scrolling body, and a bottom status bar built as a raw CSS-parity widget tree. Clone-and-edit starting point for editor-style tool apps.

Usage: local DockedAppLayout = require("@builtin/_templates.docked_app.docked_app_layout")

modules/DynamicAssetTypeRef/README

DynamicAssetTypeRef

Behaviour for the dynamicAsset asset type — a prompt-driven, self-regenerating 3D asset. Reference example for the asset-type onChange change-callback. Loaded lazily by modules/asset_ref.

modules/DynamicAssetTypeRef/onChange

onChange(ref, change)

Regenerate this dynamic asset when its own prompt.json is written with a prompt other than the one already generated. A write anywhere else in the instance is ignored, and a prompt arriving while a generation is in flight is held for the poll loop to pick up when that one settles.

Parameters

  • ref any (optional) — The changed .dynamicAsset's reference.
  • change any (optional) — The change record the asset dispatcher raised for the write.

modules/EcsAudioListenerSpec/README

EcsAudioListenerSpec

modules/EcsAudioSourceSpec/README

EcsAudioSourceSpec

modules/EcsCameraSpec/README

EcsCameraSpec

modules/EcsColliderSpec/README

EcsColliderSpec

modules/EcsCollisionGroupsSpec/README

EcsCollisionGroupsSpec

modules/EcsComponentSpecs/README

EcsComponentSpecs

modules/EcsHandle/README

EcsHandle

modules/EcsLightSpec/README

EcsLightSpec

modules/EcsMaterialSpec/README

EcsMaterialSpec

modules/EcsMeshSpec/README

EcsMeshSpec

modules/EcsMorphWeightsSpec/README

EcsMorphWeightsSpec

modules/EcsPhysicsJointSpec/README

EcsPhysicsJointSpec

modules/EcsPhysicsSpec/README

EcsPhysicsSpec

modules/EcsPlan/README

EcsPlan

modules/EcsPlayerOwnedSpec/README

EcsPlayerOwnedSpec

modules/EcsRetargetProfileSpec/README

EcsRetargetProfileSpec

modules/EcsSkeletonSpec/README

EcsSkeletonSpec

modules/EcsSkySpec/README

EcsSkySpec

modules/EcsTessellationSpec/README

EcsTessellationSpec

modules/EcsTransformConstraintsSpec/README

EcsTransformConstraintsSpec

modules/EcsTransformSpec/README

EcsTransformSpec

modules/EcsVisibilityRangeSpec/README

EcsVisibilityRangeSpec

modules/EcsWheelColliderSpec/README

EcsWheelColliderSpec

modules/EditorPanelAssetTypeRef/README

EditorPanelAssetTypeRef

Per-instance methods + the onRegister lifecycle hooks for every AssetRef<editorPanel>. Loaded lazily by asset_ref.module.

modules/EditorPanelAssetTypeRef/getInitScript

getInitScript(self): string?

Read the instance's init.luau source as raw text (inspectors).

Parameters

  • self any (optional)

modules/EditorPanelAssetTypeRef/loadSpecMethod

loadSpecMethod(self): any

Read this instance's panel spec (the table its init.luau returns).

Parameters

  • self any (optional)
local spec = panelRef:loadSpec()

modules/EditorPanelAssetTypeRef/onChange

onChange(self, change)

Parameters

  • self any (optional)
  • change any (optional)

modules/EditorPanelAssetTypeRef/onRegister

onRegister(self)

Parameters

  • self any (optional)

modules/EffectAssetTypeRef/README

EffectAssetTypeRef

Per-instance methods exposed on every AssetRef<effect>. Loaded lazily by asset_ref.module the first time an effect ref is touched in a VM. An effect is a <name>.effect/ folder holding effect.yaml (the family, the declared parameters and the measured cost) and init.luau (the definition that builds the effect on screen). :describe() reads the declaration, :play(opts) runs the definition.

modules/EffectAssetTypeRef/cost

cost(self): { [string]: any }

The effect's declared cost — what one unpooled play of it was measured to draw. { gpuMs, vramBytes, measuredOn }. The asset is the one machine-readable home of these numbers; a README states them by quoting this declaration.

Parameters

  • self any (optional)
print(fx:cost().gpuMs)

modules/EffectAssetTypeRef/describe

describe(self): { [string]: any }

Everything the effect declares about itself: its canonical identity, its family, a one-line summary, the measured cost and the full parameter list. The single call an author makes before playing an unfamiliar effect.

Parameters

  • self any (optional)
local d = fx:describe(); print(d.family, #d.params, d.cost.gpuMs)

modules/EffectAssetTypeRef/getReadme

getReadme(self): string?

Read the effect's README.md body.

Parameters

  • self any (optional)
print(fx:getReadme())

modules/EffectAssetTypeRef/inspect

inspect(self): any

asset.inspect type-specific detail: the effect's family, its declared cost and its parameter names, read from its own effect.yaml.

Parameters

  • self any (optional)
local detail = asset.inspect(fx).detail

modules/EffectAssetTypeRef/onChange

onChange(ref: any, change: any)

Drop the effect declaration cached on this ref after a write inside the instance, so the next read re-parses effect.yaml from the file on disk.

Parameters

  • ref any (optional) — The changed .effect asset's reference.
  • change any (optional) — The change record the asset dispatcher raised for the write.

modules/EffectAssetTypeRef/params

params(self): { { [string]: any } }

The effect's declared parameters, in declaration order. Each entry is { name, type, default, min, max, options, desc }min/max are present only on the numeric ones and options only on the enums, where it is the closed list of names that parameter accepts. This is the list :play validates a caller's overrides against.

Parameters

  • self any (optional)
for _, p in ipairs(fx:params()) do print(p.name, p.default) end

modules/EffectAssetTypeRef/play

play(self, opts: { [string]: any }?): { [string]: any }

Play the effect once at a position. This is the unpooled reference path: it allocates the effect's emitters and entities on the call and releases them when the effect finishes, and every entity it spawns is temporary, so nothing it draws enters a saved scene or replicates.

Parameters

  • self any (optional)
  • opts { [string]: any }? (optional){ position? = { x, y, z }, rotation? = quat, direction? = { x, y, z }, params? = { … }, held? = boolean }. params are overrides on the declaration; anything omitted takes its declared default. held starts the effect stopped at time zero so the caller drives it with handle:seek(t) — what a preview or a pixel probe needs to read the same frame twice. effects.play is the same call with the pool in front of it, for gameplay code firing the same effect over and over.
local h = fx:play { position = { 0, 2, 0 }, params = { scale = 6 } }

modules/EffectAssetTypeRef/preview

preview(self, opts: { [string]: any }?): { [string]: any }

Render this effect to a still through the shared preview rig — the image preview.writePreview persists as the asset's preview.png. The effect is played on the rig and advanced to the moment its definition reports as most representative before the frame is taken.

Parameters

  • self any (optional)
  • opts { [string]: any }? (optional){ size? = { width, height }, angle? = { yaw, pitch } }.
local shot = fx:preview({ size = { width = 256, height = 256 } })

modules/EffectAssetTypeRef/resolveParams

resolveParams(self, overrides: { [string]: any }?): { [string]: any }

Resolve a caller's overrides against the declaration: every declared parameter gets a value, each one coerced to its declared type and held inside its documented range. A name the effect does not declare raises, naming the ones it does — a misspelling that silently did nothing would be indistinguishable from a parameter that has no effect.

Parameters

  • self any (optional)
  • overrides { [string]: any }? (optional) — Table of { [paramName] = value }, or nil for the defaults.
local p = fx:resolveParams({ scale = 6 })

modules/EffectBackends/README

EffectBackends

The five kinds of thing an effect is made of, each behind one contract the runtime leases, re-seats and re-fires. An effect family that needs a new way of drawing registers a backend here rather than widening the runtime: a particle or mesh emitter, a material applied to a mesh the caller already has, generated geometry the effect owns, a decal projector, and a render feature.

modules/EffectBackends/builtin

builtin(): { [string]: Backend }

The backend kinds this module ships, keyed by name, for the runtime to register at startup.

for kind, impl in pairs(EffectBackends.builtin()) do … end

modules/EffectBackends/featureParams

featureParams(identity: string): { [string]: any }

The parameter table a render feature backend writes for one feature identity. A render feature reads its own entry each frame to find what the effect playing through it is asking for; active says whether any play currently holds it.

Parameters

  • identity string — The render feature's asset identity.
local p = require("@builtin::systems.effects.backends").featureParams(IDENTITY)

modules/EffectBackends/setDecalOpacity

setDecalOpacity(inst: any, opacity: number)

Fade a live decal projector, so a scorch mark or an impact ring dies on the effect's own clock rather than waiting for a script to fade it by hand.

Parameters

  • inst any (optional) — The decal instance a lease holds.
  • opacity number — Master fade, 0..1.
EffectBackends.setDecalOpacity(lease.instance, 1 - t)

modules/EffectParams/README

EffectParams

The parameter vocabulary an effect declares in effect.yaml and a caller overrides at play time. One declaration of what each type accepts, read by the effect asset's resolveParams and by the runtime's handle:setParam, so a value means the same thing wherever it is written.

modules/EffectParams/coerce

coerce(where: string, decl: { [string]: any }, value: any): any

Coerce one authored value onto the type its declaration names, and hold a number inside its documented range. Raises when the value cannot be read as the declared type, naming what the type takes.

Parameters

  • where string — The effect identity the message names.
  • decl { [string]: any } — The parameter declaration — { name, type, min, max }.
  • value any (optional) — The caller's value.
local v = EffectParams.coerce(identity, decl, { 0, 3, 0 })

modules/EffectParams/find

find(declared: { { [string]: any } }, name: string): { [string]: any }?

The declaration of one named parameter out of a list, or nil.

Parameters

  • declared { { [string]: any } } — Array of parameter declarations.
  • name string — The parameter name to find.
local d = EffectParams.find(decls, "scale")

modules/EffectParams/resolve

resolve(where: string, declared: { { [string]: any } },

Resolve a caller's overrides against a declaration list: every declared parameter gets a value, each coerced to its declared type and held inside its documented range. A name the effect does not declare raises, naming the ones it does.

local p = EffectParams.resolve(id, decls, { scale = 6 })

modules/EffectParams/typeNames

typeNames(): { string }

The declared type names as an ordered list, for an error that has to name what is accepted.

error("takes one of " .. table.concat(EffectParams.typeNames(), ", "))

modules/EffectsRuntime/README

EffectsRuntime

The machinery behind effects.play — the backend registry, the pool every play leases its backends from, the frame driver that advances every live play, and the handle a caller stops, retargets and re-tunes. The effects global is the documented surface over this; an effect asset's own :play is the same call with pooling turned off.

modules/EffectsRuntime/backendKinds

backendKinds(): { string }

The backend kinds registered right now, in name order.

print(table.concat(EffectsRuntime.backendKinds(), ", "))

modules/EffectsRuntime/definition

definition(identity: string): { [string]: any }

Load an effect's own definition module — the init.luau beside its effect.yaml.

Parameters

  • identity string — The effect's canonical identity.
local def = EffectsRuntime.definition(identity)

modules/EffectsRuntime/drain

drain(): { [string]: number }

Free every backend the pool is holding idle. This is the whole of the retention policy: the pool keeps what it has leased for as long as the engine runs, and releases it only here. Nothing a play still holds is touched — a drain during a live play frees what is idle and leaves the rest to its own end.

local r = effects.drain(); print(r.freed, r.kept)

modules/EffectsRuntime/observe

observe(): { [string]: any }

What the effects 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.

local o = effects.observe(); print(o.live, o.leased, o.pooled, o.bytes)

modules/EffectsRuntime/play

play(opts: { [string]: any }): { [string]: any }

Play an effect. The definition builds itself through a context that leases its backends from the pool, and the runtime's driver advances it until it ends — so a caller runs no update loop and a play that finishes gives back everything it held.

Parameters

  • opts { [string]: any }{ identity, path, declared, params, position, direction, target, held, pooled, definition }.
EffectsRuntime.play({ identity = id, definition = def, pooled = true })

modules/EffectsRuntime/registerBackend

registerBackend(kind: string, impl: Backends.Backend)

Register a way of drawing under a kind name. An effect family that needs one the runtime does not ship registers it here, and every effect reaches it through ctx.lease(kind, …) with no change to the runtime.

Parameters

  • kind string — The kind name a spec asks for.
  • impl Backends.Backend — The backend — key, acquire, seat, start, stop, quiet, place, bytes, active, silence and free.
EffectsRuntime.registerBackend("ribbonTrail", myBackend)

modules/EffectsRuntime/resolve

resolve(spelling: string): any

Find the one effect a spelling names. A canonical identity resolves directly; a short name resolves when exactly one effect carries it, and names every candidate when more than one does rather than picking a winner.

Parameters

  • spelling string — A canonical identity or a short name.
local ref = EffectsRuntime.resolve("explosion")

modules/EffectsRuntime/silenceReasons

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 observe() reports is one of these.

for _, r in ipairs(effects.silenceReasons()) do print(r.reason, r.means) end

modules/Engine/README

require("@builtin/modules/engine") -- Engine (also available as global 'engine')

Process-level engine state surface. Read-only boot profile and read-write engine mode. Both exposed as computed properties via metatable __index / __newindex — never call them as methods. Usage: if engine.profile == "editor" then ... end engine.mode = "play" engine.setMode("play", { strict = false }) Implemented as a thin Luau wrapper over internal FFI: engine.profile → __engineSettings.get("profile") engine.mode → __mode.get() / __mode.set(value) engine.setMode → __mode.set(value, strict) engine.paused → __pause.get() / __pause.set(value) engine.timeScale → __timescale.get() / __timescale.set(value) engine.gameplayReady → __gameplay.ready() engine.mode = X is load-bearing — the setter owns the mode-flip side effects so any path that changes mode behaves identically: edit → play: drain pending edit-mode marks into the dirty overlay (so wld.edit() can reassemble the user's authored state) BEFORE flipping __mode. Never writes canonical scene.json. play → edit: flip __mode first, then reload the active non-additive layer so the dirty overlay reapplies + play-mode runtime mutations are discarded. Toolbox wrappers (wld.play() / wld.edit()) are thin pass-throughs — they MUST NOT carry side-effect logic. If a future caller writes engine.mode = "play" directly (or via a different toolbox tool), the same side effects fire. Putting drain/reload on the toolbox alone would let a direct assignment leave the scene in a half-applied state. See man engine for the full surface and side effects.

Usage: local Engine = require("@builtin/modules/engine") Also available as global: engine

modules/Engine/_fireWorldLoaded

_fireWorldLoaded()

INTERNAL. Mark the world fully loaded and fan out to every onWorldLoaded subscriber. Called by the builtin world-entrypoint loader once onWorldLoad has returned. Idempotent per load — re-fires on a genuine reload (mirrors a scene's onReady), so the flag is set true and subscribers run on each call.

modules/Engine/_resetWorldLoaded

_resetWorldLoaded()

INTERNAL. Clear the world-loaded latch on unbind/unload so a subsequent bind re-fires onWorldLoaded for the new world.

modules/Engine/markScriptingBaseline

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.

engine.markScriptingBaseline()
world.on("player_join", function() end)
engine.resetScriptingState() -- the subscription above is gone

modules/Engine/offDeviceRebuilt

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

  • id number — Watcher id returned by engine.onDeviceRebuilt.
local id = engine.onDeviceRebuilt(function() end)
engine.offDeviceRebuilt(id)

modules/Engine/offModeChange

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

  • id number — Watcher id returned by engine.onModeChange.
local id = engine.onModeChange(function() end)
engine.offModeChange(id)

modules/Engine/offPauseChange

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

  • id number — Watcher id returned by engine.onPauseChange.

modules/Engine/offWorldLoaded

offWorldLoaded(id: number): boolean

Remove an onWorldLoaded subscriber by its watcher id.

Parameters

  • id number

modules/Engine/offWorldReady

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

  • id number — Watcher id returned by engine.onWorldReady.

modules/Engine/offWorldUnloading

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

  • id number — Watcher id returned by engine.onWorldUnloading.

modules/Engine/onDeviceRebuilt

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).
engine.onDeviceRebuilt(function(generation)
-- the noise field lived only on the GPU, so it is computed again
regenerateNoiseTexture()
end)

modules/Engine/onModeChange

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).
local id = engine.onModeChange(function(new, old)
print("flipped " .. old .. " -> " .. new)
end)

modules/Engine/onPauseChange

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).
local id = engine.onPauseChange(function(paused)
print(paused and "frozen" or "running")
end)

modules/Engine/onWorldLoaded

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.

modules/Engine/onWorldReady

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.

modules/Engine/onWorldUnloading

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.

modules/Engine/resetScriptingState

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.

modules/Engine/scriptingRegistryCounts

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

modules/Engine/setMode

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

  • mode string"edit" or "play".
  • options { strict: boolean? }? (optional){ strict: boolean? }. strict = false waives the play gate for this call; true or omitted honours the world's lsp.strict_mode.
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

modules/EntityRecords/README

EntityRecords

Walks a live entity hierarchy into the flat record array the engine's entity templates use. Shared by bundle (its entity_template) and by scene builds (their baked output), so both speak one record shape.

modules/EntityRecords/authoredComponentData

authoredComponentData(

A component instance's snapshot with the fields the instance wrote about its own runtime removed, leaving what states how it was CONFIGURED. A capture reads it to build the record, and whoever diffs a live entity against that record reads it too, so both sides speak the same fields.

local d = EntityRecords.authoredComponentData(id, ty, nil, snapshot)

modules/EntityRecords/captureRecord

captureRecord(rid: string, parentOriginalId: string?): any

Capture ONE entity into a template record. Reads LIVE component public state (serialized component snapshots), not init data, so the record matches what is on screen. Each component INSTANCE gets its own entry, carrying instance_name when the instance has one, so a type the entity carries several of comes back as the same several. The record also carries the entity's own active flag, every attribute it holds, and its lifecycle mode and replication scope when either is other than the default. The entity's runtime id IS its record original_id, so cross-entity component references — which already point at runtime ids — round-trip and get remapped on the next instantiate. Each component entry names the fields holding such a reference in entity_fields, taken from the component's declared field kinds, so a rebuild resolves exactly those.

Parameters

  • rid string — Runtime entity id to capture.
  • parentOriginalId string? (optional) — Parent's original_id, or nil for a root record.
local rec = EntityRecords.captureRecord(id, nil)

modules/EntityRecords/componentIsCodeAttached

componentIsCodeAttached(rid: string, componentType: string): boolean

Whether another component's lifecycle attached this component instance, rather than an author putting it there. A composed asset brings its own machinery with it — a humanoid avatar attaches a character controller to the body it expands into — and that machinery comes back on its own wherever the composition does. A record that named it would put a second one beside the one the expansion just produced, and a rebuild that removed every component its records leave unnamed would tear the expansion off the entity it belongs to. Both sides of a rebuild ask this.

Reached through the _G singleton the origin module publishes, which is the same answer the scene serializer takes for the same question; a load order that has not published it yet reads every component as authored.

Parameters

  • rid string — Runtime entity id carrying the instance.
  • componentType string — Component type name as the entity reports it.
if EntityRecords.componentIsCodeAttached(id, "Humanoid") then continue end

modules/EntityRecords/compose

compose(

Build the flat record array by walking rootId's hierarchy. Skips temporary entities and their descendants — scaffolding and editor-only tooling stay out of a baked result. The explicit root is always captured: the caller named THAT entity as the thing to serialize, so temporary pruning applies to descendants.

local records = EntityRecords.compose(rootId)

modules/EntityRecords/composeMany

composeMany(

Compose several roots into one flat record array. A build captures a SET of roots (a builder may spawn several unparented entities), not the single root a bundle composes from. The roots are ordered the same way siblings are — by rank, then name, then id.

local records = EntityRecords.composeMany({ idA, idB })

modules/ExplosionEffect/README

ExplosionEffect

The definition behind explosion.effect — a fireball shell, a rising smoke column, thrown debris and a flash of light, all proportioned off the blast radius the caller asks for.

modules/FontAssetTypeRef/README

FontAssetTypeRef

Hooks for .font assets. onCreate is the type's contribution to the generic asset.create("font", name, opts) flow (mirroring texture.assetType): it parses the font file ONCE into the baked, vectorized glyph format (data.zfnt) — the asset payload. onRegister loads that baked format via the engine font.register(name, zfnt) primitive — a font is a general CPU resource, so one registration makes fontFamily = "<name>" resolve on the egui UI text surface AND on text.* 2D/3D rendering, and feeds font.glyph / font.textMesh for true 3D text.

modules/FontAssetTypeRef/onCreate

onCreate(name: string, opts: CreateOpts): { [string]: string }

The .font type's contribution to asset.create("font", name, opts). Parses the font file ONCE into the baked vectorized glyph format and stores it as data.zfnt. The original font bytes are embedded inside the baked payload, so no separate font file is kept. The heavy parse happens here, at author time — onRegister then loads the result cheaply.

Parameters

  • name string — The font asset name (also the registered fontFamily).
  • opts CreateOpts
asset.create("font", "Inter", { bytes = fontBytes, ext = "ttf" })

modules/FontAssetTypeRef/onRegister

onRegister(self)

Install this .font instance into the engine's text systems. Reads the instance's baked data.zfnt and calls font.register(name, zfnt), which loads the vectorized glyph data into the runtime store (for font.glyph / font.textMesh) and feeds the embedded font bytes to the 2D/3D text and egui UI systems. Falls back to a raw font file for instances authored before the baked layout (font.register reparses, with a slow-path warn). Fired once per instance by the asset system (live on asset.create, and in the world-load sweep). Guarded so a single bad font never errors the sweep.

Parameters

  • self any (optional)

modules/FxFrameProbe/README

FxFrameProbe

The reader half of the frame probe — the grid fx_frame_probe.renderFeature writes each frame, and the measurements taken off it. A suite requires this module and enables the feature; the feature records the target it writes here, so both halves address one target. They are separate assets because renderer.feature.create compiles a feature's init.luau as its own chunk rather than requiring it, so a feature cannot publish state to a caller through its own module table. What it is for: a returned screenshot is not evidence. A fully transparent emitter reports live particles, reports that it drew, and changes the image hash. A reader needs a magnitude, taken from the frame a viewer actually sees, that a known control is shown to move.

modules/FxFrameProbe/channelSums

channelSums(grid: { [string]: any }?): { [string]: number }

Sum each colour channel over the grid — how much red, green and blue the frame carries. What a colour parameter has to move.

Parameters

  • grid { [string]: any }? (optional) — A grid from read().
local c = probe.channelSums(probe.read())

modules/FxFrameProbe/litRegion

litRegion(grid: { [string]: any }?, baseline: { [string]: any }?, threshold: number): { [string]: number }

The size of the lit region: how many tiles rise threshold luminance above baseline's same tile, and the radius of a disc with that area, in tiles. This is the spatial reading — how WIDE something drew, which no whole-frame number can answer.

Parameters

  • grid { [string]: any }? (optional) — The grid to measure.
  • baseline { [string]: any }? (optional) — The grid of the same view with the subject absent.
  • threshold number — How far above the baseline tile a tile must rise to count.
local r = probe.litRegion(after, before, 6).radius

modules/FxFrameProbe/movedTiles

movedTiles(grid: { [string]: any }?, other: { [string]: any }?, threshold: number): number

How many tiles differ between two grids by at least threshold luminance, in either direction. Zero says the two readings are the same picture — which is what a reading that has not caught up with a change yet also answers, so this is how a caller tells a still frame from one that has not arrived.

Parameters

  • grid { [string]: any }? (optional) — The grid to measure.
  • other { [string]: any }? (optional) — The grid to measure it against.
  • threshold number — How far a tile must differ to count.
if probe.movedTiles(now, before, 4) > 0 then --[[ the change landed ]] end

modules/FxFrameProbe/read

read(): { [string]: any }?

Read the last grid the probe wrote. Returns { width, height, tiles }, where tiles[y * width + x + 1] is that tile's mean colour as { r, g, b } in 0..255, plus lum — the tile's luminance on the same scale.

local grid = probe.read(); print(grid.tiles[1].lum)

modules/FxFrameProbe/totalLuminance

totalLuminance(grid: { [string]: any }?): number

Sum the grid's luminance — the whole frame's brightness as one number.

Parameters

  • grid { [string]: any }? (optional) — A grid from read().
local before = probe.totalLuminance(probe.read())

modules/FxKindsEffect/README

FxKindsEffect

The fixture the effects runtime's non-emitter backends are read through. Each parameter stages exactly one backend, and a parameter at zero stages none of it — so one effect covers a play that draws through four kinds and a play that stages nothing at all.

modules/GI.Scenarios/README

require("@builtin/systems/globalIllumination.package/scenarios") -- GI.Scenarios

Several baked lighting states per probe volume, blended at runtime.

A volume holds one bake, so a scene lit for noon cannot become a scene lit for dusk without baking again — which takes far longer than a transition is allowed to. Capturing each bake under a name and blending the captures turns that into an interpolation the frame can afford. Irradiance adds, so blending the spherical-harmonic coefficients is the same as blending the light that produced them. Each coefficient's fourth lane is not light, though: it carries the probe's per-axis visibility reach and the markers the sampler reads. Those describe the geometry the volume sits in, which every scenario shares, so they are carried from the heaviest-weighted scenario rather than averaged — the mean of two distances describes no wall that exists. Carrying them from the heaviest contributor also makes the ends of a transition exact: a blend that names one scenario at full weight reproduces that scenario's field lane for lane.

Usage: local GI.Scenarios = require("@builtin/systems/globalIllumination.package/scenarios")

modules/GI.Scenarios/blend

blend(volumeEntityId: string, weights: { [string]: number }) -> boolean

Publish the weighted mix of named scenarios to the renderer.

Parameters

  • volumeEntityId string
  • weights { [string]: number }

Returns boolean

modules/GI.Scenarios/capture

capture(volumeEntityId: string, name: string) -> number

Store the volume's currently published field under a name. Returns how many probes it holds.

Parameters

  • volumeEntityId string
  • name string

Returns number

modules/GI.Scenarios/clear

clear(volumeEntityId: string)

Parameters

  • volumeEntityId string

modules/GI.Scenarios/field

field(volumeEntityId: string, name: string) -> { number }?

Parameters

  • volumeEntityId string
  • name string

Returns { number }?

modules/GI.Scenarios/forget

forget(volumeEntityId: string, name: string)

Parameters

  • volumeEntityId string
  • name string

modules/GI.Scenarios/list

list(volumeEntityId: string) -> { string }

Parameters

  • volumeEntityId string

Returns { string }

modules/GI.Scenarios/mix

mix(fields: { { number } }, weights: { number }) -> { number }

Blend baked SH fields by weight. Pure — no volume, no GPU.

Parameters

  • fields { { number } }
  • weights { number }

Returns { number }

modules/GI.Scenarios/stats

stats() -> table

Returns table

modules/GI.SkyResponse/README

require("@builtin/systems/globalIllumination.package/skyResponse") -- GI.SkyResponse

A probe volume's sky term, evaluated at publish time instead of baked in.

A probe bake follows each path until it escapes, and what escapes carries the sky's colour. Baking that in means the sky is fixed the moment the bake finishes: a scene cannot dim its sky for dusk, or clear an overcast, without paying for the bake again. The sky enters the path integral as throughput * sky at the point a ray escapes, and throughput — the product of the albedos the path already passed through — does not depend on the sky at all. So the baked field splits exactly into a term that has nothing to do with the sky and a term that is linear in it, per channel: field(sky) = lights + sky * aperture lights is a bake under no sky. aperture is what a unit sky adds: how much of it each probe can see, tinted by whatever the light passed through on the way, so sky arriving through a red wall stays red. Both come from real bakes at the same settings, and the bake seeds its paths from the probe and sample index alone — so the two runs trace the same paths and the subtraction that isolates the aperture carries no sampling residual.

Usage: local GI.SkyResponse = require("@builtin/systems/globalIllumination.package/skyResponse")

modules/GI.SkyResponse/apply

apply(volumeEntityId: string, sky: { number }) -> boolean

Publish the volume's field for a sky colour. No bake.

Parameters

  • volumeEntityId string
  • sky { number }

Returns boolean

modules/GI.SkyResponse/calibrate

calibrate(volumeEntityId: string) -> table

Bake the volume twice — once with no sky, once with a unit sky — and keep the two terms the split needs.

Parameters

  • volumeEntityId string

Returns table

modules/GI.SkyResponse/calibrated

calibrated(volumeEntityId: string) -> boolean

Parameters

  • volumeEntityId string

Returns boolean

modules/GI.SkyResponse/clear

clear(volumeEntityId: string)

Parameters

  • volumeEntityId string

modules/GI.SkyResponse/combine

combine(lights: { number }, aperture: { number }, sky: { number }) -> { number }

The field for a sky colour, from the two calibrated terms. Pure.

Parameters

  • lights { number }
  • aperture { number }
  • sky { number }

Returns { number }

modules/GI.SkyResponse/terms

terms(volumeEntityId: string) -> table?

Parameters

  • volumeEntityId string

Returns table?

modules/GaussianSplatAssetTypeBehavior/README

GaussianSplatAssetTypeBehavior

Behaviour for the gaussianSplat asset type — how a capture becomes something standing in a scene. A cloud draws through a GaussianSplat component pointed at the container, so :instantiate() — the uniform contract every consumer reaches — spawns exactly that, in the axis convention the container recorded.

modules/GaussianSplatAssetTypeBehavior/instantiate

instantiate(self, target: EntityRef?, opts: { [string]: any }?): (EntityRef, { [string]: string })

Instantiate this capture — spawn an entity drawing the cloud, in the axis convention the container recorded. The same name a bundle and an avatar answer to, so an asset goes into a scene the same way whatever kind it is.

Passing a target entityRef spawns the cloud as that entity's CHILD, so an owner that tears down its children takes the cloud with it. With no target the cloud is a fresh root.

Parameters

  • self any (optional)
  • target EntityRef? (optional) — Optional owning EntityRef — the cloud spawns as its child.
  • opts { [string]: any }? (optional){ position?, rotation?, scale?, name?, temporary? } — the base placement opts; temporary keeps the spawn out of the saved scene.
local root = asset.resolve("room", "gaussianSplat"):instantiate()
local root = captureRef:instantiate(entity.spawn("mount"))

modules/GuideAssetTypeRef/README

GuideAssetTypeRef

Per-instance methods exposed on every AssetRef<guide>. Loaded lazily by asset_ref.module.

modules/GuideAssetTypeRef/getGuide

getGuide(self): string?

Read the guide's guide.md body.

Parameters

  • self any (optional)
local md = guideRef:getGuide()

modules/GuideAssetTypeRef/getReadme

getReadme(self): string?

Read the guide's README.md body.

Parameters

  • self any (optional)
print(guideRef:getReadme())

modules/GuideAssetTypeRef/inspect

inspect(self): any

asset.inspect type-specific detail: { headings, wordCount }, parsed from this guide's own guide.md. A guide with no readable guide.md returns an empty headings list and zero wordCount rather than erroring.

Parameters

  • self any (optional)
local headings = asset.inspect(guideRef).detail.headings

modules/ImpactEffect/README

ImpactEffect

The definition behind impact.effect — a flash at the point of impact and a burst thrown back along the surface normal, whose character is the named surface that was hit.

modules/ImpactEffect/surfaces

surfaces(): { string }

The names the surface parameter accepts, in declaration order — the same list the parameter's own declaration carries.

for _, s in ipairs(require(IMPACT).surfaces()) do print(s) end

modules/ImporterAssetTypeRef/README

ImporterAssetTypeRef

Per-instance methods exposed on every AssetRef<importer>. Loaded lazily by asset_ref.module.

modules/ImporterAssetTypeRef/getInitScript

getInitScript(self): string?

Read the importer's entry script as raw text.

Parameters

  • self any (optional)
local src = importerRef:getInitScript()

modules/ImporterAssetTypeRef/getReadme

getReadme(self): string?

Read the importer's README.

Parameters

  • self any (optional)
print(importerRef:getReadme())

modules/ImporterAssetTypeRef/run

run(self, payload: any): any

Invoke the importer with a payload (e.g. a path to the file to import). Loads the importer module on demand and calls its primary exported function. Raises a Luau error tagged with the importer's identity on load failure / missing entry.

Parameters

  • self any (optional)
  • payload any (optional) — Argument forwarded verbatim to the importer entry.
importerRef:run("/zero/source/foo.glb")

modules/ImporterShared/README

require("@builtin/@builtin::assetTypes.importer.shared") -- ImporterShared

The importer SYSTEM — shared by every .importer/ instance. Owns dispatch, origin-gating, content-gating, the asset.containing bundle skip, re-entrancy, output marking, and bundle relocation. The engine emits one generic "a source path was written" signal per write (onLooseWrite); this module decides whether any registered importer should claim it and runs the winner. Individual importers implement ONLY canImport(path, bytes) + import(ctx) — never trigger logic.

Usage: local ImporterShared = require("@builtin/@builtin::assetTypes.importer.shared")

modules/ImporterShared/awaitJob

awaitJob(path: string, timeoutSecs: number?): ImportJob?

Wait until the latest job for path reaches a terminal state (imported / failed / unclaimed / skipped — NOT the in-flight "queued" or "running"), or the timeout elapses. Returns the job (nil when no job exists for the path at all).

Parameters

  • path string — The source VFS path.
  • timeoutSecs number? (optional) — Max seconds to wait (default 30).

modules/ImporterShared/cancelAllQueued

cancelAllQueued(): number

Cancel EVERY queued source that hasn't started importing yet (in-flight imports are left to finish). Use it to abandon a large accidental drop.

modules/ImporterShared/cancelQueued

cancelQueued(path: string): boolean

Cancel a QUEUED source before it runs: drop it from the pump's queue and settle its job as "cancelled". A source already importing is mid-parse and can't be unwound cleanly, so only queued items cancel — returns false for a path that is already running, terminal, or was never queued.

Parameters

  • path string — The queued source VFS path.

modules/ImporterShared/explain

explain(path: string): { [string]: any }

Dry-run the dispatch gates for a path WITHOUT importing: does the file exist, which importers claim it, and which gate (if any) would stop an import right now. The answer to "I wrote this file and nothing happened".

Parameters

  • path string — The source VFS path.

modules/ImporterShared/importedAssets

importedAssets(): { { [string]: any } }

Every asset in the world carrying import provenance, newest at first. Each row: { asset (path), importer (guid), source ({guid,path}), iteration, at }. Backed by the asset.list presence filter — one cross-type query.

modules/ImporterShared/job

job(path: string): ImportJob?

The latest import job recorded for an exact source path, or nil when no dispatch has reached a claimant for it.

Parameters

  • path string — The source VFS path.

modules/ImporterShared/jobs

jobs(filter: { state: string?, path: string?, limit: number? }?): { ImportJob }

Recent import jobs, newest first. Pass a filter to narrow: state keeps one state, path substring-matches the source path, limit caps the count (default 25).

Parameters

  • filter { state: string?, path: string?, limit: number? }? (optional) — Optional { state?: string, path?: string, limit?: number }.

modules/ImporterShared/listImporters

listImporters(): { { name: string, identity: string } }

Every registered importer: { name, identity } per .importer asset.

modules/ImporterShared/onLooseWrite

onLooseWrite(path: string, origin: string?)

Engine-driven: a source path with no enclosing typed-asset folder was written. Gate on origin + re-entrancy, then ENQUEUE it on the bounded import queue (default concurrency 1) — the pump reads the bytes, decides whether a registered importer claims it, applies the content + containment gates, and runs the winner, one import at a time.

Parameters

  • path string — The written VFS path.
  • origin string? (optional) — "local" for a write made on this client, "remote" for a peer-synced write. Only "local" writes trigger — peers receive the originator's derived bundle as ordinary synced content.

modules/ImporterShared/queue

queue(): { ImportJob }

The live import backlog: queued sources (still waiting behind the concurrency cap, in FIFO order) followed by the ones importing right now. The focused "what is the importer doing this instant" view, distinct from jobs (the full recent-dispatch log including terminal states).

modules/ImporterShared/reimportAsset

reimportAsset(assetPath: string): string?

Reimport a produced asset in place: resolve its source and re-run the importers on it (the containment gate regenerates the container's derived assets from the retained source). Returns the produced path, or nil when no source could be resolved.

Parameters

  • assetPath string — The produced asset's path.

modules/ImporterShared/resolveImportSource

resolveImportSource(assetPath: string): string?

Resolve the source file a produced asset was imported from. Prefers the provenance source guid (survives rename/move), falls back to the recorded path, then to scanning the container for an importer-claimed file.

Parameters

  • assetPath string — The produced asset's path (e.g. a .bundle).

modules/ImporterShared/runImporters

runImporters(path: string, forcedContainer: string?): string?

Run the importers on path NOW and return the produced asset path (a .bundle / .texture / .audio / …), or nil if no importer claims it. The manual, deterministic counterpart to the engine's loose-write dispatch: forced (no origin, content, or containment gate) and synchronous — it runs in the calling task and returns only when the import is complete, so a caller can write a raw source quietly (vfs.write(path, bytes, { quiet = true })) and then import it deterministically instead of racing the watcher. A source that already lives inside its own output container re-imports in place, regenerating the container's derived assets from the retained source.

Parameters

  • path string — The raw source VFS path to import.
  • forcedContainer string? (optional) — Optional output container to regenerate in place — used by reimportAsset so a renamed/relocated source still reimports the original asset instead of minting a differently-named sibling.

modules/ImporterShared/runTargets

runTargets(target: any, opts: { recursive: boolean?, mode: string? }?): { [string]: any }

Import/reimport one target, an array of targets, or a folder. A produced asset (with provenance) reimports in place; a loose source imports; a folder is scanned (recursive by default) and its imported assets reimported + loose sources imported, filtered by opts.mode ("all" | "new" | "existing").

Parameters

  • target any (optional) — A VFS path string or an array of path strings.
  • opts { recursive: boolean?, mode: string? }? (optional){ recursive?: boolean (default true), mode?: "all"|"new"|"existing" }.

modules/ImporterShared/stats

stats(): { queued: number, running: number, states: { [string]: number } }

Live queue counters: queued (sources waiting on the pump — the authoritative backlog depth), running (imports in flight), and states (every recorded job tallied by state). queued and running are exact; states tallies the job registry, which keeps every in-flight job plus the most recent settled records.

modules/InputBindingAssetTypeRef/README

InputBindingAssetTypeRef

modules/InputBindingAssetTypeRef/onCreate

onCreate(name: string, opts: CreateOpts): { [string]: string }

Generic-creation hook for asset.create("inputBinding", name, opts). Writes a record carrying every device class, so a binding is complete the moment it exists.

Parameters

  • name string
  • opts CreateOpts
asset.create("inputBinding", "boost", { label = "Boost", kind = "button", kbm = '{ B.key("ShiftLeft") }', gamepad = '{ B.padButton("left_shoulder") }', touch = '{ B.touchButton({ zone = "right-lower" }) }' })

modules/InputBindingAssetTypeRef/problemsWith

problemsWith(record: any, name: string): { string }

Check a binding record for the things that make it unusable, and return the problems rather than raising, so a tool can report every fault in a map at once instead of stopping at the first.

Parameters

  • record any (optional) — The binding record to check.
  • name string — The binding's name, for the messages.

modules/InputMacroAssetTypeRef/README

InputMacroAssetTypeRef

Per-instance methods exposed on every AssetRef<inputMacro>. Loaded lazily by asset_ref.module.

modules/InputMacroAssetTypeRef/getEvents

getEvents(self): { any }?

Parse events.json into a Lua table.

Parameters

  • self any (optional)
local events = macroRef:getEvents()

modules/InputMacroAssetTypeRef/getEventsRaw

getEventsRaw(self): string?

Read the macro's events.json body as raw JSON text.

Parameters

  • self any (optional)
local raw = macroRef:getEventsRaw()

modules/InputMacroAssetTypeRef/length

length(self): number

Number of recorded events.

Parameters

  • self any (optional)
print(macroRef:length())

modules/InputMacroAssetTypeRef/replay

replay(self): number

Replay the macro by handing its recorded events to the sim toolbox's macro tool, which dispatches each timed input event through the engine input surface. Returns the number of events queued for replay.

Parameters

  • self any (optional)
local n = macroRef:replay()

modules/InputMapAssetTypeRef/README

InputMapAssetTypeRef

modules/InputMapAssetTypeRef/activateShape

activateShape(self): (string, string)

The type activate() answers for one map — a field per control it declares, each a Handle.

Parameters

  • self any (optional) — The map.
local text = M.refShapes.activate(mapRef)

modules/InputMapAssetTypeRef/onChange

onChange(self, change)

Re-activate this map after a write inside the instance, so an edit to its bindings takes hold in the running session. Only the map that is currently active is re-activated; a removal is ignored.

Parameters

  • self any (optional) — The changed .inputMap asset's reference.
  • change any (optional) — The change record the asset dispatcher raised for the write.

modules/InputMapAssetTypeRef/onCreate

onCreate(name: string, opts: CreateOpts): { [string]: string }

Generic-creation hook for asset.create("inputMap", name, opts). With opts.record, bakes it into both shapes a map is read in — the "play it, bake it, tweak it" on-ramp Zin.map.bake drives.

init.luau returns the record's flat actions / axes tables, which is what Zin.map.materialize reads. Beside it, one <name>.inputBinding/ child per entry, which is what a controller subscribes to through activate(). Each child carries all three device classes, a class the baked map had nothing for written as false with the reason beside it.

A binding that fails to serialize (an unknown kind, or an axis/vector composite whose nested arm fails — the whole composite drops) is dropped from init.luau with an -- UNSERIALIZED kind: <kind> (<name>) comment line; a function-valued axis gate / curve is dropped with a -- <name>.gate omitted / -- <name>.curve omitted comment line. An entry whose name no asset folder can take, or that lost every device class, stays in init.luau alone and is named in the header comment as a control the bake could not make.

With no opts.record, contributes nothing on top of the type's template/ skeleton.

Parameters

  • name string
  • opts CreateOpts
Zin.map.bake("my_scheme")

modules/InspectorAppLayout/README

require("@builtin/_templates.inspector_app.inspector_app_layout") -- InspectorAppLayout

Property-inspector shell on the raw widget tree — a section header and labelled field rows (text input, slider, checkbox) whose edits update the displayed state.

Usage: local InspectorAppLayout = require("@builtin/_templates.inspector_app.inspector_app_layout")

modules/LightmapDataAssetTypeRef/README

LightmapDataAssetTypeRef

Per-instance methods on every AssetRef<lightmapData> — the baked-lighting container the bake flow writes and component awakes read. Entries are keyed (lightmaps by entity id, probe fields by field id); each entry pairs a manifest record with a raw f32 payload file inside the container.

modules/LightmapDataAssetTypeRef/beginBatch

beginBatch(self)

Hold the manifest open across a run of writes. Each setLightmap / setProbeField still writes its payload file as it goes, but the manifest is read once here and written once by commitBatch, instead of being decoded and re-encoded per entry. A bake storing many entries into one container is the case this exists for. Re-entrant calls are ignored, and a batch left open by an error is closed by the next commitBatch.

Parameters

  • self any (optional)
container:beginBatch()
for _, s in surfaces do container:setLightmap(s.id, s.meta, s.texels) end
container:commitBatch()

modules/LightmapDataAssetTypeRef/clearEntries

clearEntries(self)

Remove every entry and payload, leaving an empty manifest — the container-wide teardown baking.clear uses for a full-scene clear.

Parameters

  • self any (optional)
container:clearEntries()

modules/LightmapDataAssetTypeRef/commitBatch

commitBatch(self)

Write the manifest a beginBatch has been holding and close the batch. No-op when no batch is open.

Parameters

  • self any (optional)
container:commitBatch()

modules/LightmapDataAssetTypeRef/lightmap

lightmap(self, key: string): (LightmapEntry?, { number }?)

Read one entity's baked lightmap.

Parameters

  • self any (optional)
  • key string — The receiver entity id.
local entry, texels = container:lightmap(entityId)

modules/LightmapDataAssetTypeRef/manifest

manifest(self)

The decoded manifest: { version, lightmaps = { [entityId] = entry }, probeFields = { [fieldId] = entry } }.

Parameters

  • self any (optional)
local m = container:manifest()

modules/LightmapDataAssetTypeRef/onCreate

onCreate(name: string, opts: { scene: string? }?)

Create an empty baked-lighting container. The bake flow (baking.all / Lightmap.bake / VolumeProbe.bake) fills it.

Parameters

  • name string
  • opts { scene: string? }? (optional)

modules/LightmapDataAssetTypeRef/probeField

probeField(self, key: string): (ProbeFieldEntry?, { number }?)

Read one probe volume's baked field.

Parameters

  • self any (optional)
  • key string — The volume's stable field id.
local entry, sh = container:probeField(fieldId)

modules/LightmapDataAssetTypeRef/removeLightmap

removeLightmap(self, key: string)

Remove one entity's lightmap entry and its payload file. No-op when the entry is absent.

Parameters

  • self any (optional)
  • key string — The receiver entity id.
container:removeLightmap(entityId)

modules/LightmapDataAssetTypeRef/removeProbeField

removeProbeField(self, key: string)

Remove one probe volume's field entry and its payload file. No-op when the entry is absent.

Parameters

  • self any (optional)
  • key string — The volume's stable field id.
container:removeProbeField(fieldId)

modules/LightmapDataAssetTypeRef/setLightmap

setLightmap(self, key: string, meta: { [string]: any }, texels: { number })

Store one entity's baked lightmap: writes the texel payload file and its manifest entry, replacing any prior entry under the same key.

Parameters

  • self any (optional)
  • key string — The receiver entity id.
  • meta { [string]: any }{ resolution, intensity } — the parameters the runtime needs to rebuild the lightmap and place it in the atlas.
  • texels { number } — Flat f32 RGBA texel array (resolution² × 4 floats, dilated, alpha = coverage).
container:setLightmap(entityId, { resolution = 256, intensity = 1 }, data)

modules/LightmapDataAssetTypeRef/setProbeField

setProbeField(self, key: string, meta: { [string]: any }, sh: { number })

Store one probe volume's baked field: writes the SH payload file and its manifest entry, replacing any prior entry under the same key.

Parameters

  • self any (optional)
  • key string — The volume's stable field id.
  • meta { [string]: any }{ boundsMin = {x,y,z}, boundsMax = {x,y,z}, res = {x,y,z}, count }.
  • sh { number } — Flat f32 SH L2 array — 36 floats per probe, X-fastest.
container:setProbeField(fieldId, { boundsMin = mn, boundsMax = mx, res = r, count = n }, sh)

modules/MaterialAssetTypeRef/README

MaterialAssetTypeRef

Per-instance methods exposed on every AssetRef<material>. Loaded lazily by asset_ref.module via require("@builtin::assetTypes.material.behavior") the first time a material ref is touched in a VM.

modules/MaterialAssetTypeRef/applyToEntity

applyToEntity(self, entityId: string): boolean

Apply this material to an entity by setting the material field on its Model / SkinnedModel component (where the renderer reads it). A material only renders where there is a mesh.

Parameters

  • self any (optional)
  • entityId string — Target entity ID.
matRef:applyToEntity(playerId)

modules/MaterialAssetTypeRef/getDefinition

getDefinition(self): string?

Read the on-disk mat.yaml body as raw text. Use vfs.write(self.path .. "/mat.yaml", ...) to write the file directly, :setProperty / :setTexture followed by :saveDefinition to write the current values into it, or :setShader to rewrite it for a new shader.

Parameters

  • self any (optional)
local yaml = matRef:getDefinition()

modules/MaterialAssetTypeRef/getShader

getShader(self): string?

Read the shader identity currently bound to this material — parses mat.yaml and returns the shader: field as a string. The result is whatever the YAML names (a built-in like "pbr", a guid, or a full identity like "@builtin::shaders.pbr"); pass it to asset.resolve(..., "shader") to get a ref.

Parameters

  • self any (optional)
local s = matRef:getShader()

modules/MaterialAssetTypeRef/inspector

inspector(self): { any }

The material's editing surface for a host UI: titled sections of field rows, each carrying its editing kind and the closure that writes the change back. Kinds come from the SHADER's declared vocabulary (its properties.yaml descriptors), values from the live cache (the shader's defaults overlaid by this material's overrides). The host renders the rows in its own field language; this never builds widgets. Writes go through setProperty / setTexture / setShader, so every edit takes the same GPU-push + persistence path a scripted write takes.

Parameters

  • self any (optional)
for _, section in ipairs(matRef:inspector()) do print(section.title) end

modules/MaterialAssetTypeRef/isRegistered

isRegistered(self): boolean

Whether this material exists as an asset — its mat.yaml is present. Writing the asset is what registers the material, so file presence IS the registration check.

Parameters

  • self any (optional)
if matRef:isRegistered() then ... end

modules/MaterialAssetTypeRef/onChange

onChange(ref, change)

Asset-type change callback: (re)register this material into the renderer's MaterialRegistry whenever its .material is seeded (a baked builtin at boot, or a world seed) or its mat.yaml is edited. This is the ONLY thing that populates the registry for a material — the legacy boot-time library loader (which registered every material by name) is gone; materials register through their own assetType exactly as shaders do (__shader.compile). Keyed by identity, guid as alias, so a MaterialRef.id (name / identity / guid) resolves to the right entry. Convergent + idempotent: see registerToGpu.

Parameters

  • ref any (optional)
  • change any (optional)

modules/MaterialAssetTypeRef/onCreate

onCreate(name: string, opts: CreateOpts): { [string]: string }

Generic-creation hook for asset.create("material", name, opts). Pure: returns the mat.yaml content for the caller to persist to the authored destination; the write registers the material definition (CPU-side) — GPU resources are built later, only on actual use. Properties resolve from the shader's properties.yaml overlaid by these overrides; no shader compile happens at create. Flat non-reserved top-level keys on opts are shader-property overrides (e.g. base_color = {1,0,0,1}, roughness = 0.3), admitted by the open schema and resolved against the shader's properties.yaml inside the hook.

Parameters

  • name string — Material identity (the instance name).
  • opts CreateOpts
asset.create("material", "Gold", { shader = "pbr", base_color = {1, 0.84, 0, 1}, metallic = 1, roughness = 0.2 })
asset.create("material", "Glass", { shader = "pbr", base_color = {0.2, 0.9, 0.4, 0.45}, render = { blend = "alphaBlend", depth_write = false } })

modules/MaterialAssetTypeRef/preview

preview(self, opts: { [string]: any }?)

Render a preview of this material on a unit sphere.

Parameters

  • self any (optional)
  • opts { [string]: any }? (optional){ size? = { width, height } }.
local p = matRef:preview()

modules/MaterialAssetTypeRef/saveDefinition

saveDefinition(self): boolean

Persist the material's live runtime overrides into its mat.yaml (the lazy/explicit flush). Property/texture sets are frame-fast and transient by default (they live on the ref's runtime, wiped on mode change); call this to bake the current values into the asset so they survive a restart. Routes through the mat.yaml write → RegisterMaterial path. The changed values are written where they already stand in the authored file, so its comments, the order of its keys and blocks, and the spelling of the literals it was written with all survive the save.

Parameters

  • self any (optional)
matRef:saveDefinition()

modules/MaterialAssetTypeRef/setProperties

setProperties(self, patch: { [string]: any }): number

Set many properties at once, resolving each key against the shader's vocabulary the way setProperty does. Entries the material's shader doesn't expose are skipped (a generic patch table won't error on shader differences).

Parameters

  • self any (optional)
  • patch { [string]: any } — Table of { [propertyName] = value } pairs.
matRef:setProperties({ roughness = 0.2, metallic = 0.9 })

modules/MaterialAssetTypeRef/setShader

setShader(self, shaderRef: any): (boolean, any)

Switch this material to a different shader by rewriting the material asset itself (mat.yaml), then letting the asset-reload pipeline mark every instance dirty so the renderer repacks. The full mat.yaml is regenerated for the new shader's property vocabulary: current property values and texture links are carried across by canonical ROLE (via modules.material_remap), so MAIN_TEXalbedobase_color_texture and friends survive the swap to our best ability. Properties the new shader does not expose are dropped; the name, builtin, parent, description, and render: block are preserved verbatim. This modifies the on-disk asset (or, in play mode, its runtime copy) — not a throwaway registry entry — so the change persists and propagates to all instances.

Parameters

  • self any (optional)
  • shaderRef any (optional) — An AssetRef<shader> (preferred) or a shader-identity string ("pbr", "@builtin::shaders.unlit").
matRef:setShader(asset.resolve("@builtin::shaders.unlit", "shader"))
matRef:setShader("pbr")

modules/MaterialRemap/README

MaterialRemap

Canonical property/texture-role dictionary for cross-shader value preservation. When a material swaps shaders the new shader exposes a DIFFERENT vocabulary (one shader's MAIN_TEX is another's albedo is a third's base_color_texture). This module maps any known alias onto a canonical role so values carry across the swap "to our best ability".

modules/MaterialRemap/canonicalizeTextures

canonicalizeTextures(

Canonicalize texture-slot names so links survive a shader swap. Each known slot is rewritten to its canonical on-disk slot name; unknown slots pass through unchanged. (The engine does not yet expose a target shader's reflected texture-slot list, so canonicalizing to the builtin convention is the best-effort path — see material_remap module header.)

modules/MaterialRemap/propertyRole

propertyRole(name: string): string?

Canonical role for a scalar/color property name, or nil when unknown.

Parameters

  • name string — Property name as it appears in a shader / mat.yaml.
material_remap.propertyRole("MAIN_COLOR") -- "base_color"

modules/MaterialRemap/remapProperties

remapProperties(

Remap a table of old property values onto a target shader's accepted property names. Direct name matches win; otherwise the old key's canonical role is matched against the role of each target name. Values whose role the target shader does not expose are dropped (best-effort preservation).

modules/MaterialRemap/roleShapePair

roleShapePair(role: string): ({ colour: string, scalar: string })?

The colour+scalar role pair a role belongs to, as { colour, scalar }, or nil when the role stands alone. Both halves answer with the same pair, so a caller holding either one can ask which value shape belongs where.

Parameters

  • role string — A canonical role from propertyRole.
material_remap.roleShapePair("emissive") -- { colour = "emissive", scalar = "emissive_intensity" }

modules/MaterialRemap/textureRole

textureRole(slot: string): string?

Canonical texture-slot role (and on-disk slot name) for a texture slot name, or nil when unknown.

Parameters

  • slot string — Texture slot name as it appears in a shader / mat.yaml.
material_remap.textureRole("MAIN_TEX") -- "base_color_texture"

modules/MaterialSchema/README

MaterialSchema

A shader's declared property vocabulary, and the routing of an authored key onto it. Every path that accepts material properties — the .material assetType and renderer.material.create — resolves the backing shader's properties.yaml through here, so the two agree on which keys are real, which are texture slots, and what to say about a key that is neither.

modules/MaterialSchema/declaredNames

declaredNames(schema: Schema): { string }

Every name a schema declares, sorted — properties and texture slots.

Parameters

  • schema Schema — A Schema from forShader.
local names = MaterialSchema.declaredNames(schema)

modules/MaterialSchema/declaredNamesList

declaredNamesList(schema: Schema): string

Comma-joined declared names, for the "not declared" diagnostic so the author sees exactly which keys the shader accepts.

Parameters

  • schema Schema — A Schema from forShader.
warn("declared: " .. MaterialSchema.declaredNamesList(schema))

modules/MaterialSchema/displacedMessage

displacedMessage(name: string, key: string, declared: string): string

The diagnostic for an authored key whose value another spelling of the same declared property took precedence over.

Parameters

  • name string — The material's name or registry key.
  • key string — The authored key whose value did not apply.
  • declared string — The declared property that took its value from another spelling.
warn(MaterialSchema.displacedMessage(name, "emissive_color", "emissive"))

modules/MaterialSchema/forShader

forShader(shader: string): Schema?

The declared vocabulary of the shader backing a material, split into uniform properties and texture slots. Read from the shader assetType's getProperties() (its properties.yaml).

Parameters

  • shader string — Shader identity, path, or short name ("pbr").
local s = MaterialSchema.forShader("pbr")

modules/MaterialSchema/isTextureValue

isTextureValue(value: any): boolean

Whether a value is a texture binding rather than a scalar/vector. A renderer.texture handle (kind == "TextureHandle") or any AssetRef<texture> envelope. Scalar/vector values are bare numbers or numeric arrays, so this never misfires on a colour like {1, 0, 0, 1}.

Parameters

  • value any (optional) — The authored value.
MaterialSchema.isTextureValue(tex) -- true

modules/MaterialSchema/roleIndex

roleIndex(props: { [string]: boolean }): { [string]: string }

The role index for a set of declared property names: canonical role -> the single declared name claiming it. A role two declared names both claim is left out, so an authored spelling never resolves to an arbitrary one of them. This is what turns emissive_color into the emissive a shader declares, and ao_strength into its occlusion_strength.

Parameters

  • props { [string]: boolean } — Declared property names, as a { [name]: true } set.
local byRole = MaterialSchema.roleIndex({ emissive = true })

modules/MaterialSchema/route

route(schema: Schema, key: string, value: any): (Route, string)

Resolve an authored key against a schema: which surface it belongs to and under what name. Routes by the schema's texture slots and by the value's own shape (a texture value is a texture binding whatever the key is called), then by canonical role — so a spelling from another engine's convention reaches the property this shader declares for that role, under the name the shader declares it by.

Parameters

  • schema Schema — A Schema from forShader.
  • key string — The authored key.
  • value any (optional) — The authored value.
local route, name = MaterialSchema.route(schema, "color", {1,0,0,1})

modules/MaterialSchema/routeAll

routeAll(schema: Schema, authored: { [string]: any }): (

Route a whole authored property table at once, so two spellings that resolve to one declared property are settled the same way everywhere.

Parameters

  • schema Schema — A Schema from forShader.
  • authored { [string]: any }{ [key] = value } as written by the author.
local props, tex, unknown = MaterialSchema.routeAll(schema, parsed)

modules/MaterialSchema/settle

settle(schema: Schema, claimed: { Claim }): (

Settle the authored values that resolved onto declared properties, so every path that accepts material properties answers the same way. One declared name takes one value — the spelling the shader declares verbatim, else the first in key order, so the outcome is the authored table's rather than the order pairs walked it in. Two spellings meeting on one half of a colour+scalar pair the shader declares both halves of are not competing: a number is the scalar and a vector is the colour, the reading the uniform buffer already makes of them, so a material naming its glow colour under one convention and its brightness under another keeps both values.

Parameters

  • schema Schema — A Schema from forShader.
  • claimed { Claim } — Every Claim that routed to a property.
local props, displaced = MaterialSchema.settle(schema, claims)

modules/MaterialSchema/undeclaredMessage

undeclaredMessage(name: string, key: string, shader: string, schema: Schema): string

The diagnostic for a key the shader declares under neither surface, naming the material, the key, the shader, and the accepted vocabulary. One wording for every path that accepts material properties.

Parameters

  • name string — The material's name or registry key.
  • key string — The key that is not declared.
  • shader string — The backing shader's name.
  • schema Schema — A Schema from forShader.
warn(MaterialSchema.undeclaredMessage(name, key, shader, schema))

modules/MaterialSchema/valueShape

valueShape(value: any): "scalar" | "vector" | "either"

What shape an authored value has, for the one question a colour+scalar pair asks of it. A number is the scalar; a numeric array or an {r, g, b} colour is the vector. Anything else answers "either" and stays with the half its key claimed.

Parameters

  • value any (optional) — The authored value.
MaterialSchema.valueShape(5) -- "scalar"

modules/MeshAssetTypeBehavior/README

MeshAssetTypeBehavior

Behaviour for the mesh asset type — the disk + CPU side of the disk-asset ↔ GPU-mesh split. onCreate writes the engine-native ZMSH geometry payload as the container's data.zmsh primary; :load() decodes it into the CPU store and returns a CPU handle. Both go through the public renderer.mesh.* API — this behaviour calls no __ FFI directly.

modules/MeshAssetTypeBehavior/getVertices

getVertices(self)

Read this mesh's vertices — one entry per vertex, { pos = {x,y,z}, normal = {x,y,z}, uv = {u,v} }. Requires the CPU copy to be resident: turn on keepCpu (meshRef:setSettings({ keepCpu = true })) for an in-use mesh. Errors loudly — naming the exact reason — rather than silently loading a transient copy.

Parameters

  • self any (optional)
local verts = meshRef:getVertices()

modules/MeshAssetTypeBehavior/handle

handle(self)

Materialize this mesh asset's live GPU resource (Disk→CPU→GPU) and return its MeshHandle. The handle is cached on the interned ref's shared runtime table — its presence IS "loaded to the GPU", so once materialised every later call (and every consumer of the same asset) gets the SAME handle back directly → ONE GPU entry, no re-work. (The mode-flip runtime wipe clears the cache so a mode change re-materialises.) A component that renders a mesh holds the mesh resource and calls this internally — you rarely call it by hand.

CPU lifecycle: the keepCpu setting (:settings/:setSettings) governs whether the CPU copy survives the upload. DEFAULT (keepCpu = false): the CPU copy is dropped right after the GPU upload (the GPU handle holds no data → no double memory). keepCpu = true retains the CPU store for geometry reads/edits.

Cluster LOD: the clusterLod setting governs whether this materialisation queues a cluster-LOD bake. DEFAULT (clusterLod = true): the bake is queued and the DAG attaches on the frame it finishes. clusterLod = false skips it, so the mesh carries no hierarchy and costs nothing to virtualize.

Parameters

  • self any (optional)
local h = meshRef:handle()

modules/MeshAssetTypeBehavior/instantiate

instantiate(self, target: EntityRef?, opts: { [string]: any }?): (EntityRef, { [string]: string })

Instantiate this mesh — the uniform instantiate(target?, opts?) contract every scene-instantiable asset answers to. A mesh becomes an entity carrying a Model that renders it. With target the entity spawns as a child of that owner (so an owning Asset component tears it down with its other children); with no target it is a fresh root. The base opts — position, rotation, scale, name, temporary — place the root.

Parameters

  • self any (optional)
  • target EntityRef? (optional) — Optional owning entity ref.
  • opts { [string]: any }? (optional){ position?, rotation?, scale?, name?, temporary? }.
meshRef:instantiate(owner)

modules/MeshAssetTypeBehavior/load

load(self)

Load this .mesh asset's geometry into the guid-keyed CPU store and return a CPU handle (the Disk→CPU step). The handle carries the guid, vertex / index counts, and per-handle geometry ops (getTriangles / getVertices / getBounds / encode / unload) that read the Rust-side store — it holds no geometry itself. Upload to the GPU with renderer.mesh.create(handle); the DEFAULT is to handle:unload() right after. Delegates to renderer.mesh.loadCpu (the sole __ caller).

Parameters

  • self any (optional)
local cpu = meshRef:load(); local gpu = renderer.mesh.create(cpu); cpu:unload()

modules/MeshAssetTypeBehavior/morphTargets

morphTargets(self): { 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 keeps the names its source file gave its blend shapes, so a face is driven by the shape it means rather than by the ordinal that shape imported at. A target the source never named reads as an empty string, and a mesh with no morph targets returns an empty array. Requires the mesh to be materialised (meshRef:handle(), or anything rendering it) — it errors loudly naming that, rather than answering as though the mesh carried no shapes.

Parameters

  • self any (optional)
for i, name in meshRef:morphTargets() do print(i, name) end

modules/MeshAssetTypeBehavior/onChange

onChange(ref: any, change: { [string]: any })

React to a write inside this mesh asset. A write to the stored geometry re-uploads it into the GPU mesh registered under this asset's guid, so everything already rendering it draws the new shape. A .metadata write reconciles the second (lightmap) UV set to the lightmapUvs setting: "generate" (re)creates a non-overlapping unwrap into the second UV set, "none" strips it, "keep" leaves the stored geometry untouched. The re-cook round-trips the geometry through the codec, so tangents, skinning, and the skeleton are preserved. In play mode the re-cook returns immediately: a setting flip never rewrites the persisted geometry while the world is running (the play-lock); a resident mesh re-materialises on its next fetch.

Parameters

  • ref any (optional) — The AssetRef for the changed container.
  • change { [string]: any }{ path, asset, kind, origin }path the written file, asset the container folder, kind "edited"/"seeded", origin "local"/"remote".

modules/MeshAssetTypeBehavior/onCreate

onCreate(name: string, opts: CreateOpts): { [string]: string }

Generic-creation hook for asset.create("mesh", name, opts). Pure: returns the content-file map; asset.create writes it to the authored destination, registering the .mesh asset under its minted guid. Disk-only — nothing is uploaded to the GPU here (the GPU mesh is a separate, explicit renderer.mesh.create step keyed by this asset's guid).

opts is raw geometry { positions, indices, normals?, uvs?, colors? } (flat float / u32 arrays, encoded to data.zmsh via renderer.mesh.encode), or a pre-encoded { bytes } payload (stored verbatim).

Parameters

  • name string — Mesh identity (the instance name).
  • opts CreateOpts
asset.create("mesh", "tree", { positions = {...}, indices = {...} })

modules/MeshAssetTypeBehavior/preview

preview(self, opts: { [string]: any }?)

Render a preview of this mesh, instantiated and framed. Drives the mesh's own model-instantiation path in an isolated preview scope.

Parameters

  • self any (optional)
  • opts { [string]: any }? (optional){ size? = { width, height } }.
local p = meshRef:preview()

modules/MeshAssetTypeBehavior/setSettings

setSettings(self, patch: { [string]: any })

Write a partial settings patch to this mesh asset's .metadata. Pass any subset of the settings schema; only those keys change, the rest keep their stored value (asset.set_field deep-merges). Unknown keys error loudly. Settings are serializable and persist across reloads.

Parameters

  • self any (optional)
  • patch { [string]: any }{ keepCpu: boolean?, lightmapUvs: string?, clusterLod: boolean? } — any subset of the settings schema.
meshRef:setSettings({ keepCpu = true })

modules/MeshAssetTypeBehavior/setVertices

setVertices(self, positions)

Replace this mesh's vertex positions IN PLACE — indices, normals/uvs, and skinning are preserved, the AABB recomputes, and the edit shows on screen (the GPU re-fetches the changed CPU copy). Requires the CPU copy resident: turn on keepCpu (meshRef:setSettings({ keepCpu = true })) for an in-use mesh. Errors loudly — naming the reason — when the CPU copy isn't resident or the vertex count doesn't match.

Parameters

  • self any (optional)
  • positions any (optional) — One position per vertex: an array of {x,y,z} (or [x,y,z]), or a flat {x,y,z, ...} array. The count must match the mesh's vertex count.
local v = meshRef:getVertices()
local p = {}; for i, vert in ipairs(v) do p[i] = { vert.pos.x*0.01, vert.pos.y*0.01, vert.pos.z*0.01 } end
meshRef:setVertices(p)   -- scale the mesh to 1/100

modules/MeshAssetTypeBehavior/settings

settings(self): { [string]: any }

Read this mesh asset's settings, with every schema default filled in. The returned table always carries the full settings schema.

Parameters

  • self any (optional)
if meshRef:settings().keepCpu then ... end

modules/ModuleAssetTypeRef/README

ModuleAssetTypeRef

Per-instance methods exposed on every AssetRef<module>. Loaded lazily by asset_ref.module.

modules/ModuleAssetTypeRef/getExports

getExports(self): { { name: string, type: string } }?

The module's exported names and their value types — requires the module and reflects over the table it returns. Available for every module (world-authored or library), since a module is just an asset.

Parameters

  • self any (optional)
for _, e in ipairs(modRef:getExports() or {}) do print(e.name, e.type) end

modules/ModuleAssetTypeRef/getInitScript

getInitScript(self): string?

Read the module's entry script as raw text.

Parameters

  • self any (optional)
local src = modRef:getInitScript()

modules/ModuleAssetTypeRef/getReadme

getReadme(self): string?

Read the module's README body.

Parameters

  • self any (optional)
print(modRef:getReadme())

modules/ModuleAssetTypeRef/inspect

inspect(self): any

asset.inspect type-specific detail: { exports }, where exports is parsed from the module's own entry script (getInitScript) via luau_introspect.moduleExports — name/kind/signature/desc of every top-level export, read from the source text rather than a require() reflection. Cached on the asset's content checksum, so re-inspecting unchanged source is free. A module with no readable entry script returns an empty exports list rather than erroring.

Parameters

  • self any (optional)
local exports = asset.inspect(modRef).detail.exports

modules/ModuleAssetTypeRef/loadModule

loadModule(self): any

Require the module by its canonical identity — same as require(self.identity), but pcall-wrapped so a load failure raises a Luau error tagged with the module identity rather than propagating the raw error.

Parameters

  • self any (optional)
local mod = modRef:loadModule()

modules/ModuleAssetTypeRef/onChange

onChange(ref, change)

Asset-type change callback: hot-reload this module whenever a .luau / .lua file inside it is edited (or the module is seeded). This is what live-reloads USER modules — library modules reload through the VFS write hook (author-immutable content does not dispatch onChange). Mirrors the .material / .shader assetTypes owning their own reload. Convergent: the reload only invalidates the require() cache + fires watchers and never writes back into the asset folder.

Parameters

  • ref any (optional)
  • change any (optional)

modules/ModuleAssetTypeRef/onRegister

onRegister(self)

Initial-registration callback: register every .luau / .lua file this module owns — its entry AND its plain-file submodules — into the require() layer, so require("<mod>.<sub>") resolves the instant it first registers (fired before the world entrypoint runs). A .luau inside a NESTED typed-asset folder (a nested .module / .component / …) belongs to that asset and registers through its own onRegister, so it is skipped here.

Parameters

  • self any (optional) — The per-instance AssetRef<module>.
-- driven by the assetType lifecycle; not called directly

modules/MuzzleFlashEffect/README

MuzzleFlashEffect

The definition behind muzzleFlash.effect — a ragged bloom at the muzzle, a lick of flame down the bore line, a cone of sparks and a flash of light, all proportioned off the flash width the caller asks for.

modules/PackageAssetTypeRef/README

PackageAssetTypeRef

Per-instance methods exposed on every AssetRef<package>. Loaded lazily by asset_ref.module.

modules/PackageAssetTypeRef/getDefinition

getDefinition(self): string?

Read the package's package.yaml body as raw text.

Parameters

  • self any (optional)
local raw = packageRef:getDefinition()

modules/PackageAssetTypeRef/getReadme

getReadme(self): string?

Read the package's README body.

Parameters

  • self any (optional)
print(packageRef:getReadme())

modules/PackageAssetTypeRef/inspect

inspect(self): any

asset.inspect type-specific detail: { contents }, where contents is { {name, type}, ... } sorted by name — the package's own direct children (components, modules, tools, docs), enumerated via vfs.list against the package's own path. Never reads/executes any child's content.

Parameters

  • self any (optional)
local contents = asset.inspect(packageRef).detail.contents

modules/PackageAssetTypeRef/listContents

listContents(self): { { name: string, isDirectory: boolean } }

List the package's child entries — every VFS entry one level below the package root. Use this to enumerate components, modules, tools, etc. shipped by the package.

Parameters

  • self any (optional)
for _, e in ipairs(packageRef:listContents()) do print(e.name) end

modules/Particles.Curves/ColorSequence.new

ColorSequence.new(a, b?) -> ColorSequence

Three shapes: constant ({r,g,b}), two-point lerp (startColor, endColor), or keypoint table ({ {time=, value={r,g,b,a?}, envelope={r,g,b}?}, ... }).

Parameters

  • a any (optional)
  • b any (optional)

Returns ColorSequence

modules/Particles.Curves/ColorSequence:evaluate

ColorSequence:evaluate(t: number) -> (r, g, b, a)

Deterministic per-channel sample.

Parameters

  • t number

Returns (r, g, b, a)

modules/Particles.Curves/ColorSequence:getKeypoints

ColorSequence:getKeypoints() -> {table}

Returns a fresh array of { time, value = {r,g,b,a}, envelope = {r,g,b} }.

Returns {table}

modules/Particles.Curves/ColorSequence:pack

ColorSequence:pack(buffer: {number}, offset: number) -> number

Pack the color curve into a flat float array; returns the next-write offset.

Parameters

  • buffer {number}
  • offset number

Returns number

modules/Particles.Curves/ColorSequence:sample

ColorSequence:sample(t: number, rng?: number) -> (r, g, b, a)

Sample with per-channel RGB envelope randomness (alpha is deterministic).

Parameters

  • t number
  • rng number (optional)

Returns (r, g, b, a)

modules/Particles.Curves/NumberSequence.new

NumberSequence.new(a, b?) -> NumberSequence

Three shapes: constant (value), two-point lerp (start, end), or keypoint list ({ {time=, value=, envelope=}, ... }), whose entries also read positionally as ({ {time, value[, envelope]}, ... }). A field that is not a number is refused at the call.

Parameters

  • a any (optional)
  • b any (optional)

Returns NumberSequence

modules/Particles.Curves/NumberSequence:evaluate

NumberSequence:evaluate(t: number) -> number

Deterministic interpolation at life-fraction t in [0, 1].

Parameters

  • t number

Returns number

modules/Particles.Curves/NumberSequence:getKeypoints

NumberSequence:getKeypoints() -> {table}

Returns a fresh array of { time, value, envelope } entries.

Returns {table}

modules/Particles.Curves/NumberSequence:pack

NumberSequence:pack(buffer: {number}, offset: number) -> number

Pack the curve into a flat float array; returns the next-write offset.

Parameters

  • buffer {number}
  • offset number

Returns number

modules/Particles.Curves/NumberSequence:sample

NumberSequence:sample(t: number, rng?: number) -> number

Envelope-randomized sample; rng (in [0, 1]) seeds the jitter.

Parameters

  • t number
  • rng number (optional)

Returns number

modules/Particles.Curves/README

require("@builtin/systems/particles.package/curves") -- Particles.Curves

NumberSequence and ColorSequence — keyframe curves for VFX properties.

Animatable keyframe curves used by the particle system. Modelled on Roblox's NumberSequence / ColorSequence so the muscle memory carries over, with two differences:

  1. Up to 16 keypoints (Roblox caps at 20; we cap a bit lower so the GPU pack fits in a fixed-size param block).
  2. ColorSequence supports per-channel envelope (RGB) — Roblox does not. Curves are immutable plain tables with a metatable. Build once, sample many times. The shader-side pack format is a flat float array — see NumberSequence.pack / ColorSequence.pack for the contract. Usage: local size = NumberSequence.new({ { time = 0.0, value = 0.1 }, { time = 0.3, value = 1.0, envelope = 0.2 }, { time = 1.0, value = 0.0 }, }) print(size:evaluate(0.5)) -- deterministic interpolation print(size:sample(0.5)) -- envelope randomness applied local color = ColorSequence.new( { 1, 0.8, 0.2 }, -- yellow { 1, 0.1, 0.0 } -- red )

Usage: local Particles.Curves = require("@builtin/systems/particles.package/curves")

modules/Particles.Curves/isColorSequence

isColorSequence(x: any) -> boolean

True if x is a ColorSequence answering the calls one takes. A sequence that crossed a boundary preserving only its fields has its methods put back, so the value a caller tests is one it can go on to sample.

Parameters

  • x any (optional)

Returns boolean

modules/Particles.Curves/isNumberSequence

isNumberSequence(x: any) -> boolean

True if x is a NumberSequence answering the calls one takes. A sequence that crossed a boundary preserving only its fields has its methods put back, so the value a caller tests is one it can go on to sample.

Parameters

  • x any (optional)

Returns boolean

modules/Particles.Curves/new

new(a: any, b: any?): any

Parameters

  • a any (optional)
  • b any? (optional)
NumberSequence.new(1.0)
NumberSequence.new(0.0, 1.0)
NumberSequence.new({ {time=0,value=0}, {time=1,value=1,envelope=0.1} })
NumberSequence.new({ {0, 0}, {1, 1, 0.1} })

modules/Particles.Meshes/README

require("@builtin/systems/particles.package/meshes") -- Particles.Meshes

Built-in source-mesh templates for mesh particles.

Source-mesh geometries for the mesh-particle path of the particle system. Each built-in returns a table with positions (flat xyz array), normals (flat xyz array), uvs (flat uv array), and indices (flat u32 array) — the shape particles.create expects under its mesh field. Built-in kinds: "cube" — 24 verts × 36 indices (face-normalled box) "octahedron" — 24 verts × 24 indices (face-normalled octahedron) "tetrahedron" — 12 verts × 12 indices (face-normalled tetrahedron) "plane" — 4 verts × 6 indices (axis-aligned XY quad) Custom geometry: Pass mesh = { positions = {...}, normals = {...}, uvs = {...}, indices = {...} } directly to particles.create. The module also accepts an asset ref (assetRef("identity", "mesh")) but resolution happens through the engine's asset system — the fast path is built-in kinds.

Usage: local Particles.Meshes = require("@builtin/systems/particles.package/meshes")

modules/Particles.Meshes/list

list() -> {string}

Returns the built-in mesh kind names.

Returns {string}

modules/Particles.Meshes/resolve

resolve(spec: string | table) -> table

Resolve a mesh spec to { positions, normals, uvs, indices }. Spec is one of "cube" | "octahedron" | "tetrahedron" | "plane", or a custom { positions, normals, uvs?, indices } table.

Parameters

  • spec string | table

Returns table

modules/Particles.Shapes/README

require("@builtin/systems/particles.package/shapes") -- Particles.Shapes

Emission-shape samplers — CPU-side, called at spawn time.

Spawn positions and initial velocity directions for the standard emission shapes. CPU sampling is fine here because it runs at most N times per frame where N = emission rate, not N = active particle count — and the math is trivial. Shapes (matching Roblox naming): point — emit from origin box — Volume / Surface sphere — Volume / Surface, partial = hemisphere cap cylinder — Volume / Surface, axis = Y disc — Surface (XZ plane), partial = annulus inner radius cone — Surface, partial = half-angle Styles: "volume" — uniform inside the shape "surface" — uniform on the shape's boundary inOut (determines initial velocity direction): "outward" — surface-normal outward "inward" — surface-normal inward "inandout" — random sign per particle

Usage: local Particles.Shapes = require("@builtin/systems/particles.package/shapes")

modules/Particles.Shapes/frame

frame(ax: number, ay: number, az: number): (number, number, number, number, number, number, number, number, number)

Build an orthonormal frame whose middle axis is the given direction. Shape samplers emit around a local +Y axis; this frame maps those local samples into world space so emission can be aimed along any vector. Returns 9 numbers — right, axis, forward — for allocation-free use in per-particle loops: world = local.x * right + local.y * axis + local.z * forward. The roll about the axis is unspecified but stable for a given direction. A zero-length or +Y direction returns the exact identity frame.

Parameters

  • ax number — Direction X (any length; normalized internally).
  • ay number — Direction Y.
  • az number — Direction Z.
local rx, ry, rz, ax2, ay2, az2, fx, fy, fz = Shapes.frame(0, 0, -1)

modules/Particles.Shapes/inOuts

inOuts() -> {string}

Returns the directions a sample aims in — outward, inward, inandout.

Returns {string}

modules/Particles.Shapes/sample

sample(shape: string, opts: table) -> (px, py, pz, dx, dy, dz)

Sample one spawn position + unit direction from the named shape. opts: { size = {x,y,z}, style = "volume"|"surface", inOut = "outward"|"inward"|"inandout", partial = number, spreadAngle = degrees }.

Parameters

  • shape string
  • opts table

Returns (px, py, pz, dx, dy, dz)

modules/Particles.Shapes/shapes

shapes() -> {string}

Returns the supported shape names — point, box, sphere, cylinder, disc, cone.

Returns {string}

modules/Particles.Shapes/styles

styles() -> {string}

Returns the fill styles a shape is sampled with — volume, surface.

Returns {string}

modules/Particles/M.list

M.list() -> {ParticleSystem}

Every live particle system this VM has created, newest last.

Returns {ParticleSystem}

modules/Particles/M.observe

M.observe(system: ParticleSystem?) -> table

One emitter's document, or — with no argument — the whole world's: { emitters, count, alive, capacity, spawned, silent, bytes, frame }.

Parameters

  • system ParticleSystem? (optional)

Returns table

modules/Particles/M.silenceReasons

M.silenceReasons() -> table

The closed set of reasons an emitter can be producing nothing, in the order they are resolved, each with what it means.

Returns table

modules/Particles/M.whySilent

M.whySilent(system: ParticleSystem) -> (string?, string?)

The reason one emitter is producing nothing and the detail line naming what it is about, or nil when it is producing.

Parameters

  • system ParticleSystem

Returns (string?, string?)

modules/Particles/ParticleSystem:billboard

ParticleSystem:billboard(): { [string]: any }

The camera frame this emitter's geometry was last built against: which camera supplied it, the world point it was read at, the right/up pair, and the direction that pair faces.

A sprite is a flat card placed in the compute pass against ONE camera frame, so a view looking along normal sees it face-on and a view looking across normal sees its edge. A mesh emitter spins each copy about the axis running from it to position. When a frame drawn from somewhere else shows an emitter that every count reports as producing, this is the reading that says where its geometry is turned, and means names which part of the frame this emitter's kind builds from.

local b = sys:billboard(); print(b.source, b.normal[1], b.normal[2], b.normal[3])

modules/Particles/ParticleSystem:clear

ParticleSystem:clear()

Kill every live particle by re-zeroing the state buffer.

modules/Particles/ParticleSystem:destroy

ParticleSystem:destroy()

Release every GPU buffer and the render entity. Idempotent.

modules/Particles/ParticleSystem:emit

ParticleSystem:emit(count: number)

One-shot burst of N particles, independent of rate / enabled.

Parameters

  • count number

modules/Particles/ParticleSystem:getActiveCount

ParticleSystem:getActiveCount() -> number

How many particles are alive right now: the slots whose lifetime has not elapsed under the simulated time this emitter's dispatches have advanced. Falls to 0 when the last particle ages out.

Returns number

modules/Particles/ParticleSystem:getCastsShadows

ParticleSystem:getCastsShadows(): boolean

Whether this emitter's particles block light.

print(debris:getCastsShadows())

modules/Particles/ParticleSystem:getColliders

ParticleSystem:getColliders(): { any }

The colliders currently in force, in the shape setColliders takes — so what comes out of one goes back into the other.

local n = #sys:getColliders()

modules/Particles/ParticleSystem:getCreator

ParticleSystem:getCreator(): { [string]: any }

Whose this emitter is. owner and name are the keys its creator stated on the spec, and source and line are the code that made the call, read off the stack — so an emitter nobody tagged still names the module it came from. actor and actorName are the account this engine session runs under, and createdAt is when the emitter was made, which orders two emitters one creator built across separate loads.

local who = sys:getCreator(); print(who.owner, who.source, who.line)

modules/Particles/ParticleSystem:getDepthFade

ParticleSystem:getDepthFade() -> number

That distance.

Returns number

modules/Particles/ParticleSystem:getGpuBytes

ParticleSystem:getGpuBytes() -> table

Every GPU buffer this emitter holds, by name, plus their total.

Returns table

modules/Particles/ParticleSystem:getLightEmission

ParticleSystem:getLightEmission() -> number

That share.

Returns number

modules/Particles/ParticleSystem:getLightInfluence

ParticleSystem:getLightInfluence() -> number

That share.

Returns number

modules/Particles/ParticleSystem:getMaxCount

ParticleSystem:getMaxCount() -> number

Buffer cap.

Returns number

modules/Particles/ParticleSystem:getRenderEntity

ParticleSystem:getRenderEntity() -> string?

Entity id of the renderable a sprite emitter spawns at world origin; nil for a mesh emitter, which draws as a GPU population instead.

Returns string?

modules/Particles/ParticleSystem:getRenderLayer

ParticleSystem:getRenderLayer(): string

The render layers this emitter's particles draw on, as the space-separated name string every render-layer surface speaks.

print(smoke:getRenderLayer())

modules/Particles/ParticleSystem:getSimulatedTime

ParticleSystem:getSimulatedTime() -> number

Seconds of simulation this emitter's dispatches have advanced. Every particle's age is measured against this clock.

Returns number

modules/Particles/ParticleSystem:getSpawnedCount

ParticleSystem:getSpawnedCount() -> number

How many slots the emitter has filled since the last clear(), saturating at maxCount. The emission schedule's own total, and the span the draw covers.

Returns number

modules/Particles/ParticleSystem:isPlaying

ParticleSystem:isPlaying() -> boolean

True while the timeline emits: enabled, past delay, and inside duration (or looping / untimed).

Returns boolean

modules/Particles/ParticleSystem:isSorted

ParticleSystem:isSorted() -> boolean

Whether this emitter reorders its own particles back to front every frame.

Returns boolean

modules/Particles/ParticleSystem:isVisible

ParticleSystem:isVisible() -> boolean

Whether the emitter's population reaches the frame.

Returns boolean

modules/Particles/ParticleSystem:observe

ParticleSystem:observe() -> table

Everything the engine holds for this emitter in one document: population, capacity, timeline, GPU bytes, render-side liveness, the GPU's own confirmation of the population, and — when it is producing nothing — the reason from a closed set.

Returns table

modules/Particles/ParticleSystem:play

ParticleSystem:play(restart?: boolean)

Start (or re-arm) the emission timeline: resets the clock to -delay, re-arms scheduled bursts, and enables emission. restart = true also clears live particles.

Parameters

  • restart boolean (optional)

modules/Particles/ParticleSystem:requestCensus

ParticleSystem:requestCensus()

Ask the GPU to count its own live slots on the next dispatch. The answer arrives on a later frame and reads back through observe().confirmation.

modules/Particles/ParticleSystem:setAcceleration

ParticleSystem:setAcceleration(x, y, z: number)

Set the constant acceleration vector (combined with gravity + wind).

Parameters

  • x any (optional)
  • y any (optional)
  • z number

modules/Particles/ParticleSystem:setBlendMode

ParticleSystem:setBlendMode(mode: string)

"alpha" (default) or "additive".

Parameters

  • mode string

modules/Particles/ParticleSystem:setBursts

ParticleSystem:setBursts(bursts: table)

Replace the scheduled-burst list ({ {time, count}, ... }); nil/empty removes the schedule.

Parameters

  • bursts table

modules/Particles/ParticleSystem:setCastsShadows

ParticleSystem:setCastsShadows(on: boolean?)

State whether this emitter's particles block light. A caster is drawn into the shadow map as the geometry it is — a sprite emitter's quads as quads, a mesh emitter's copies as copies — and the surfaces behind it are shaded in shadow. The declaration reaches both emitter kinds, so it means the same thing whichever one carries it.

Parameters

  • on boolean? (optional)true to block light, false to let it through. Omitted, the emitter goes back to the default, which is letting it through.
debris:setCastsShadows(true)

modules/Particles/ParticleSystem:setColliders

ParticleSystem:setColliders(colliders: { any }?): number

Replace the shapes this emitter's particles collide against. Each entry is { kind = "plane" | "sphere" | "box" | "world", position = {x,y,z}, ... } — a plane also takes normal, a sphere radius, a box halfExtents. A world entry is the scene's own geometry, read from the distance field sceneProxy builds: it takes only radius, how far from a surface a particle counts as touching it. At most 8 are carried; the rest are dropped and reported.

Parameters

  • colliders { any }? (optional) — Array of collider descriptions.
sys:setColliders({ { kind = "world" } })

modules/Particles/ParticleSystem:setCollision

ParticleSystem:setCollision(mode: string?, opts: { [string]: any }?)

Choose what a particle does when it meets a collider.

Parameters

  • mode string? (optional)"off", "bounce", "stop" or "kill".
  • opts { [string]: any }? (optional) — Optional { restitution, friction }. restitution is how much normal speed survives a bounce, friction how much tangential speed is lost on contact — both in [0, 1].
sys:setCollision("bounce", { restitution = 0.5, friction = 0.3 })

modules/Particles/ParticleSystem:setColor

ParticleSystem:setColor(value: ColorSequence | {number})

Per-particle color curve. A {number} array {r, g, b} is read as a constant color, and a list of such arrays as a ramp through them.

Parameters

  • value ColorSequence | {number}

modules/Particles/ParticleSystem:setDepthFade

ParticleSystem:setDepthFade(distance: number)

Set the distance, in world units, over which this emitter's particles dissolve into the surface drawn behind them. 0 turns the fade off.

Parameters

  • distance number

modules/Particles/ParticleSystem:setDirection

ParticleSystem:setDirection(x, y, z: number)

Aim emission: the shape's local +Y axis maps onto this world-space vector (positions and initial velocities both rotate). Pass nil to reset to the native +Y frame.

Parameters

  • x any (optional)
  • y any (optional)
  • z number

modules/Particles/ParticleSystem:setDrag

ParticleSystem:setDrag(d: number)

Set the velocity damping factor (per-second exponential decay).

Parameters

  • d number

modules/Particles/ParticleSystem:setEnabled

ParticleSystem:setEnabled(b: boolean)

Pause / resume continuous emission. Does not kill live particles.

Parameters

  • b boolean

modules/Particles/ParticleSystem:setGravity

ParticleSystem:setGravity(x, y, z: number)

Set the gravity vector (m/s^2). Default {0, -9.81, 0}.

Parameters

  • x any (optional)
  • y any (optional)
  • z number

modules/Particles/ParticleSystem:setLifetime

ParticleSystem:setLifetime(min: number, max?: number)

Per-particle lifetime range in seconds.

Parameters

  • min number
  • max number (optional)

modules/Particles/ParticleSystem:setLightEmission

ParticleSystem:setLightEmission(share: number)

How much of itself each particle emits whatever the scene's light is, from 0 to 1. At 1 a particle is self-illuminated.

Parameters

  • share number

modules/Particles/ParticleSystem:setLightInfluence

ParticleSystem:setLightInfluence(share: number)

How much of the scene's light these particles take, from 0 to 1.

Parameters

  • share number

modules/Particles/ParticleSystem:setMaterial

ParticleSystem:setMaterial(materialName: string)

Replace the material applied to the render entity entirely.

Parameters

  • materialName string

modules/Particles/ParticleSystem:setOrientation

ParticleSystem:setOrientation(mode: string)

"FacingCamera" | "FacingCameraWorldUp" | "VelocityParallel" | "VelocityPerpendicular".

Parameters

  • mode string

modules/Particles/ParticleSystem:setOrigin

ParticleSystem:setOrigin(x, y, z: number)

Move the emitter to a new world-space origin.

Parameters

  • x any (optional)
  • y any (optional)
  • z number

modules/Particles/ParticleSystem:setParam

ParticleSystem:setParam(name: string, value: number)

Generic setter — matches PARAM_LAYOUT keys or "user" for the user block.

Parameters

  • name string
  • value number

modules/Particles/ParticleSystem:setRate

ParticleSystem:setRate(r: number)

Set continuous emission rate (particles/sec).

Parameters

  • r number

modules/Particles/ParticleSystem:setRenderLayer

ParticleSystem:setRenderLayer(layers: any)

Put this emitter's particles on named render layers. A camera or a capture including the layer draws them and one excluding it does not, so a reflection probe, a portal camera, a minimap or a clean screenshot can take the scene with the emitter's effect in it or without. The layers reach both emitter kinds.

Parameters

  • layers any (optional) — A layer name, an array of names, or a space-separated string. Omitted, the emitter goes back to the default layer.
smoke:setRenderLayer("vfx")

modules/Particles/ParticleSystem:setRotSpeed

ParticleSystem:setRotSpeed(min: number, max?: number)

Per-particle rotation-speed range (degrees/sec).

Parameters

  • min number
  • max number (optional)

modules/Particles/ParticleSystem:setRotSpeedCurve

ParticleSystem:setRotSpeedCurve(value: NumberSequence | number | {start, finish} | {table})

Rotation-speed curve — multiplies each particle's own rotSpeed across its life. Written the same four ways as the transparency curve.

Parameters

  • value NumberSequence | number | {start, finish} | {table}

modules/Particles/ParticleSystem:setRotation

ParticleSystem:setRotation(min: number, max?: number)

Per-particle initial rotation range (degrees).

Parameters

  • min number
  • max number (optional)

modules/Particles/ParticleSystem:setShape

ParticleSystem:setShape(opts: table)

Replace the emission shape — { kind, size, style, inOut, partial, spreadAngle } (any subset). Existing values are kept for omitted fields.

Parameters

  • opts table

modules/Particles/ParticleSystem:setSize

ParticleSystem:setSize(value: NumberSequence | number | {min, max} | {table})

Size, read the way create reads its size: a curve — a NumberSequence or the keypoint list {{time, value, envelope?}, ...} one is built from — is sampled per particle every frame on the GPU over base 1, and a number or {min, max} pair is the base each spawn samples under a constant curve of 1. Either way a particle draws at the size that was written.

Parameters

  • value NumberSequence | number | {min, max} | {table}

modules/Particles/ParticleSystem:setSorted

ParticleSystem:setSorted(on: boolean?)

State whether this emitter reorders its own particles back to front every frame. nil hands the choice back to the blend mode.

Parameters

  • on boolean? (optional)

modules/Particles/ParticleSystem:setSpeed

ParticleSystem:setSpeed(min: number, max?: number)

Per-particle initial-speed range.

Parameters

  • min number
  • max number (optional)

modules/Particles/ParticleSystem:setSquash

ParticleSystem:setSquash(value: NumberSequence | number | {start, finish} | {table})

X-dimension scale curve (>1 stretches, <1 squashes). Written the same four ways as the transparency curve.

Parameters

  • value NumberSequence | number | {start, finish} | {table}

modules/Particles/ParticleSystem:setTexture

ParticleSystem:setTexture(texture: string)

Set the sprite texture (asset id or VFS path).

Parameters

  • texture string

modules/Particles/ParticleSystem:setTransparency

ParticleSystem:setTransparency(value: NumberSequence | number | {start, finish} | {table})

Transparency curve (0 = opaque, 1 = invisible). A {start, finish} pair of numbers is read as a two-stop ramp — a table holding one number as that value across the whole life — and a keypoint list as the curve through its stops.

Parameters

  • value NumberSequence | number | {start, finish} | {table}

modules/Particles/ParticleSystem:setUserParam

ParticleSystem:setUserParam(index: number, value: number)

Write one slot (1..32) of the 32-float user param block. Custom WGSL update shaders read these at params[31 + index].

Parameters

  • index number
  • value number

modules/Particles/ParticleSystem:setVisible

ParticleSystem:setVisible(on: boolean)

Whether the population the emitter holds reaches the frame. Emission is setEnabled; the draw is this, and a hidden emitter keeps its particles.

Parameters

  • on boolean

modules/Particles/ParticleSystem:setWind

ParticleSystem:setWind(x, y, z: number)

Set the wind vector (m/s).

Parameters

  • x any (optional)
  • y any (optional)
  • z number

modules/Particles/ParticleSystem:simulate

ParticleSystem:simulate(t: number, stepDt?: number)

Deterministically advance the system by t seconds in fixed steps (default 1/60). Drives the full update — timeline, bursts, compute passes — so a system can be prewarmed or scrubbed to a known state.

Parameters

  • t number
  • stepDt number (optional)

modules/Particles/ParticleSystem:stop

ParticleSystem:stop(clearParticles?: boolean)

Stop emitting. Live particles finish their lifetime unless clearParticles = true.

Parameters

  • clearParticles boolean (optional)

modules/Particles/ParticleSystem:update

ParticleSystem:update(dt: number)

Step the simulation. Drives continuous emission, dispatches the compute update pass, and re-packs per-frame uniforms. Call once per frame.

Parameters

  • dt number

modules/Particles/README

require("@builtin/systems/particles.package/engine") -- Particles

Generic GPU-driven particle system — compute-shader simulation, zero-copy rendering.

A generic GPU particle substrate for VFX. Every active particle lives in a GPU storage buffer; the simulation runs entirely as compute dispatches; the per-frame vertex output is consumed directly by the renderer via renderer.mesh.create (no readback, no CPU re-upload). Spawning happens on the CPU (the bookkeeping cost is bounded by emission rate, not particle count) and writes new particle slots straight into the GPU state buffer. The module is one-stop: the high-level ParticleEmitter component declared elsewhere in src/lua/lib/components/ is a thin wrapper around this API. Usage: local particles = require("@builtin::systems.particles.engine") local fire = particles.create({ maxCount = 2000, rate = 100, -- particles/sec lifetime = { 0.8, 1.6 }, speed = { 2.5, 4.0 }, shape = { kind = "cone", size = {1, 1, 1}, partial = 0.3 }, gravity = { 0, 1.5, 0 }, -- buoyant drag = 0.6, size = NumberSequence.new({ { time = 0, value = 0.4 }, { time = 0.3, value = 1.0, envelope = 0.2 }, { time = 1, value = 0.0 }, }), color = ColorSequence.new({ 1, 0.9, 0.3 }, { 0.8, 0.1, 0 }), transparency = NumberSequence.new(0, 1), texture = "@builtin::textures.fire_sprite", blendMode = "additive", }) -- Each frame: fire:setOrigin(torch.position.x, torch.position.y, torch.position.z) fire:update(dt) See references/luau-cookbook.md#particles (zero-engine skill) for more recipes — explosion bursts, magic swirls, custom WGSL update injection.

Usage: local Particles = require("@builtin/systems/particles.package/engine")

modules/Particles/create

create(spec: table) -> ParticleSystem

Allocate a new particle system: GPU buffers, registered compute shader, renderer.mesh.create-wrapped renderable. See the about block for the full spec field list. Returns a handle whose methods are summarised below.

Parameters

  • spec table

Returns ParticleSystem

modules/Particles/list

list(filter: any?): { any }

Every particle system this VM has created and not destroyed, in creation order — or, given a filter, the ones whose creator matches it. An emitter is something this module made, so the list is answered from its own registry rather than by walking entities.

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 sweeps what it left behind on a previous load without touching anybody else's.

Parameters

  • filter any? (optional) — Optional. A string matches the owner key a creator stated; a table matches every one of owner, name and source that it names.
for _, sys in ipairs(Particles.list()) do print(sys:getActiveCount()) end
for _, sys in ipairs(Particles.list("starfield")) do sys:destroy() end

modules/Particles/observe

observe(system: any?): { [string]: any }

One emitter's reading, or — called with no argument — the whole world's: every live emitter's document plus the totals they sum to. An engine with no emitters answers count = 0 with an empty list, which reads differently from an engine whose emitters are all silent.

Parameters

  • system any? (optional) — Optional ParticleSystem to read; omitted, every live one.
local world = Particles.observe(); print(world.alive, world.silent)

modules/Particles/silenceReasons

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.

for _, r in ipairs(Particles.silenceReasons()) do print(r.reason, r.means) end

modules/Particles/whySilent

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

  • system any (optional) — The particle system to ask about.
local why, detail = Particles.whySilent(fire)

modules/PlasmaBoltEffect/README

PlasmaBoltEffect

The definition behind plasmaBolt.effect — a round pulsing core crossing a span, a short trail behind it, motes falling off it and a light it carries as it goes.

modules/PopulationAssetTypeBehavior/README

PopulationAssetTypeBehavior

Behaviour for the population asset type — a hardware-instanced population at rest. onCreate writes the recipe (population.json) plus the instance matrices as bytes (transforms.bin); :draw() turns that recipe back into live GPU buffers and draw registrations, and instantiate puts one entity in the scene that owns them for as long as it lives.

modules/PopulationAssetTypeBehavior/bounds

bounds(self): any

The world-space box the drawn instances occupy — each variant's mesh AABB carried through every one of its matrices. The matrices are world-space, so this is where the population stands, whatever entity owns it.

Parameters

  • self any (optional)
local box = live:bounds()

modules/PopulationAssetTypeBehavior/count

count(self): number

Instances the engine reports drawing across every registration this holds. Read back from the renderer rather than from the recipe, so a registration that went away, or that the renderer turned away, counts as gone.

Parameters

  • self any (optional)
print(live:count())

modules/PopulationAssetTypeBehavior/destroy

destroy(self)

Release every registration and its transform buffer. The draw is dropped BEFORE its buffer is destroyed: a registration reserves slots against the buffer it was given, so a buffer that goes away takes its registration with it. The meshes belong to their .mesh assets and stay. A second call finds an empty list and returns.

Parameters

  • self any (optional)
live:destroy()

modules/PopulationAssetTypeBehavior/draw

draw(self, opts: { [string]: any }?): any

Rebuild this population's live draws: allocate a GPU transform buffer per variant, upload that variant's run of transforms.bin into it, resolve the mesh and material, and register one instanced draw. The returned value OWNS those resources — hold it and call :destroy() to release them; a registration nobody holds can be neither enumerated nor dropped afterwards. The matrices are world-space, so the draws stand where the recipe placed them.

Parameters

  • self any (optional)
  • opts { [string]: any }? (optional){ castsShadows? = true, renderLayer? }.
local live = populationRef:draw(); … ; live:destroy()

modules/PopulationAssetTypeBehavior/drawCalls

drawCalls(self): number

Draw calls this population costs — one per variant the renderer is drawing, at any instance count. A variant the renderer turned away costs nothing and is counted nowhere; :errors() says why.

Parameters

  • self any (optional)
print(live:drawCalls())

modules/PopulationAssetTypeBehavior/errors

errors(self): { any }

Why this population is drawing less than its recipe asks for: one entry per registration the renderer turned away, carrying the variant it belongs to, the mesh it names and the renderer's own reason. A population drawing everything it holds answers with an empty list, so this and :drawCalls() agree with the frame.

Parameters

  • self any (optional)
for _, e in ipairs(live:errors()) do warn(e.variant, e.error) end

modules/PopulationAssetTypeBehavior/inspectDetail

inspectDetail(self): any

asset.inspect type-specific detail: { total, variantCount, variants }, read straight from population.json — never allocates a buffer or registers a draw. A population whose recipe can't be read returns an empty detail rather than erroring.

Parameters

  • self any (optional)
local n = asset.inspect(populationRef).detail.total

modules/PopulationAssetTypeBehavior/instantiate

instantiate(self, target: EntityRef?, opts: { [string]: any }?): (EntityRef, { [string]: string })

Instantiate this population — the uniform instantiate(target?, opts?) contract every scene-instantiable asset answers to. A population becomes ONE entity carrying a Population component that references this asset: the component rebuilds the live draws on awake and releases them on destroy, so the scene records one entity for a population of any size and a reload replaces the draws rather than stacking a second set. With target the entity spawns as a child of that owner (so an owning Asset component tears it down with its other children); with no target it is a fresh root. The base opts — position, rotation, scale, name, temporary — place the root.

Parameters

  • self any (optional)
  • target EntityRef? (optional) — Optional owning entity ref.
  • opts { [string]: any }? (optional){ position?, rotation?, scale?, name?, temporary? }.
populationRef:instantiate(owner)

modules/PopulationAssetTypeBehavior/onCreate

onCreate(name: string, opts: CreateOpts?): { [string]: string }

Generic-creation hook for asset.create("population", name, opts). Pure: returns the content-file map; asset.create writes it to the mode-aware destination. Each variant contributes its mesh + material to population.json and its matrices to transforms.bin, in variant order.

Parameters

  • name string — Population identity (the instance name).
  • opts CreateOpts? (optional)
asset.create("population", "forest", { variants = { { mesh = meshGuid, material = matGuid, transforms = flat } } })

modules/PopulationAssetTypeBehavior/setRenderLayer

setRenderLayer(self, renderLayer: number)

Put every registration this holds on the render layers renderLayer names. Constant time per registration — the transforms are not re-uploaded and nothing is re-registered — so a population can follow a membership that moves, and the next frame drawn tests its copies against the new one.

Parameters

  • self any (optional)
  • renderLayer number — The membership bitmask, the same value :draw({ renderLayer }) takes. At least one bit must be set.
live:setRenderLayer(mask)

modules/PopulationAssetTypeBehavior/settled

settled(self): boolean

Whether the renderer has answered for every registration this holds. A registration is made a stage before the renderer sees it, so the frame it is made in is one where nothing yet says whether the copies are drawn; :errors() is complete from the frame this turns true.

Parameters

  • self any (optional)
if live:settled() then check(live:errors()) end

modules/PopulationAssetTypeBehavior/spec

spec(self): any

The recipe this population draws from: { version, total, variants }, where each variant carries { mesh, material?, materialKey?, count, offset } — the .mesh it draws, the material it draws with, how many instances it holds, and where its matrices start in transforms.bin (counted in instances).

Parameters

  • self any (optional)
local total = populationRef:spec().total

modules/PopulationAssetTypeBehavior/transforms

transforms(self): string

The instance matrices as raw bytes — total * 64, little-endian f32, column-major mat4 per instance, in variant order.

Parameters

  • self any (optional)
local blob = populationRef:transforms()

modules/Prelude/README

require("@builtin/modules/prelude") -- Prelude

Auto-require globals available in every script without require(). Injects Entity, Physics, Transform, and Material as global Luau wrappers.

This script runs once after FFI registration. It requires builtin library modules and exposes them as globals. Users never need to write: local Entity = require(".entity_reflect") Instead they just use Entity directly. Only high-frequency, universally-useful APIs belong here. Specialized modules (e.g. agent tools, editor UI) still use require(). Convention: __name = raw FFI binding (Rust native call, never used directly by scripts) Name = Luau wrapper (user-facing, auto-injected via this prelude)

Usage: local Prelude = require("@builtin/modules/prelude")

modules/PresetAssetTypeRef/README

PresetAssetTypeRef

Per-instance methods exposed on every AssetRef<preset>. Loaded lazily by asset_ref.module.

modules/PresetAssetTypeRef/applyTo

applyTo(self, entityId: string, componentType: string, overrides: { [string]: any }?): boolean

Apply this preset to a component on an entity. Looks up the component by componentType on entityId, then writes each preset property onto the component's public table. Errors cleanly when the entity has no component of that type. Returns true on success.

Parameters

  • self any (optional)
  • entityId string — Target entity ID.
  • componentType string — Component type name (e.g. "CharacterController").
  • overrides { [string]: any }? (optional) — Optional shallow overrides applied on top of the preset.
presetRef:applyTo(playerId, "CharacterController")

modules/PresetAssetTypeRef/getDefinition

getDefinition(self): string?

Read the preset's preset.yaml body as raw text.

Parameters

  • self any (optional)
local raw = presetRef:getDefinition()

modules/PresetAssetTypeRef/inspect

inspect(self): any

asset.inspect type-specific detail: { componentType, fieldValues }, parsed from the preset's own preset.yamlcomponentType is the component: field, fieldValues is the decoded properties table (preset.load with no overrides). Cached on the asset's content checksum, so re-inspecting unchanged source is free. A preset with no readable preset.yaml returns an empty detail rather than erroring.

Parameters

  • self any (optional)
local fieldValues = asset.inspect(presetRef).detail.fieldValues

modules/PresetAssetTypeRef/load_preset

load_preset(self, overrides: { [string]: any }?): { [string]: any }?

Load the preset's properties as a plain Lua table, optionally merging caller-supplied overrides on top. Equivalent to preset.load(self.identity, overrides).

Parameters

  • self any (optional)
  • overrides { [string]: any }? (optional) — Optional table merged shallowly over the loaded properties.
local data = presetRef:load({ speed = 12 })

modules/Preview/README

Preview

Compose an asset's own instantiate path into a rendered still preview. Each previewable assetType's M.ref.preview calls one of these primitives; they spawn temporary entities, auto-frame an offscreen camera over them, render one frame, read it back as a PNG, and tear down. Scheduled previews drain through one serial work queue whose drainer renders in shared batches — subjects settle together and capture concurrently — so a world-scale backfill completes in seconds per hundred assets while the engine stays responsive.

modules/Preview/fromEntities

fromEntities(makeFn: () -> any, opts: { [string]: any }?): PreviewResult

Instantiate entities via makeFn, auto-frame an offscreen camera to their bounds, render one frame, and return the still PNG (base64) + stats. Spawned entities are marked temporary and despawned after the render. Renders are serialised on the shared preview rig; renders requested while the preview queue is draining a batch join its shared pass, so each still shows only its own subject at single-render wall-clock.

Parameters

  • makeFn () -> any — Function returning an entity id/proxy or array of them to preview.
  • opts { [string]: any }? (optional){ size? = { width, height }, angle? = { yaw, pitch } }.
preview.fromEntities(function() local e = entity.spawn("x"); e.component.add("Model", { model = meshRef:handle() }); return e end)

modules/Preview/liveFromEntities

liveFromEntities(makeFn: () -> any, opts: { [string]: any }?): PreviewResult

Instantiate entities via makeFn and hold them under a live orbiting camera instead of capturing a still: the returned session's rtHandle names a render target that camera writes EVERY frame (a UI image node with src = rtHandle shows it live), setOrbit(yaw, pitch, dist?) moves the camera around the subject's measured centre, and dispose() despawns the subject, light and camera and destroys the target. The session holds a key light over the subject the way a still render does. fromEntities routes here when opts.live is true, so every type's ref:preview({ live = true }) returns one of these with no wiring.

Parameters

  • makeFn () -> any — Function returning an entity id/proxy or array of them to preview.
  • opts { [string]: any }? (optional){ live = true, size? = { width, height }, angle? = { yaw, pitch } }.
local p = matRef:preview({ live = true })

modules/Preview/materialOnSphere

materialOnSphere(matRef: any, opts: { [string]: any }?): PreviewResult

Render a material on a unit sphere — the canonical material preview.

Parameters

  • matRef any (optional) — An AssetRef, or a material identity/name string.
  • opts { [string]: any }? (optional){ size? = { width, height }, angle? = { yaw, pitch } }.
preview.materialOnSphere(matRef)

modules/Preview/queueStatus

queueStatus(): { [string]: any }

Report the preview work queue: how many renders are pending, the batch currently rendering, session totals, and the most recent failures.

local s = preview.queueStatus(); print(s.pending, s.active)

modules/Preview/schedulePreview

schedulePreview(ref: any, opts: { [string]: any }?)

Queue a writePreview for ref on the preview work queue. The per-type generation hooks (material / texture / bundle onChange) call this on every content write; a burst of writes renders once, after the content settles, so the render sees the final bytes. Renders drain in shared batches in schedule order; poll queueStatus for progress.

Parameters

  • ref any (optional) — The asset's AssetRef.
  • opts { [string]: any }? (optional){ debounce?: number } — seconds the entry waits before its render becomes eligible (default 1; a backfill pass over settled content passes 0).
preview.schedulePreview(matRef)

modules/Preview/swatch

swatch(texRef: any, opts: { [string]: any }?): PreviewResult

Render a texture as a flat plane facing the camera — its swatch.

Parameters

  • texRef any (optional) — An AssetRef, or a texture guid/identity string.
  • opts { [string]: any }? (optional){ size? = { width, height }, angle? = { yaw, pitch } }.
preview.swatch(texRef)

modules/Preview/writePreview

writePreview(ref: any, opts: { [string]: any }?): (boolean, string?)

Render an asset's preview via its type's ref:preview() and persist it as preview.png inside the asset folder. The preview is the asset's visual description — it syncs and publishes like any other file, feeds image-based search, and gives browsers a thumbnail. Skips scratch content (/source/tmp/). Writes only when the rendered bytes differ from the existing file, so re-rendering an unchanged asset produces no sync traffic.

Parameters

  • ref any (optional) — The asset's AssetRef — its type must expose preview (material, texture, bundle).
  • opts { [string]: any }? (optional) — Forwarded to ref:preview(); defaults to a 256×256 render.
preview.writePreview(matRef)

modules/ProcGraphAssetTypeBehavior/README

ProcGraphAssetTypeBehavior

Per-instance methods exposed on every AssetRef<procGraph> — a .procGraph asset is a serialized procedural graph (proc.graph.v1). The methods below let callers use the graph THROUGH its ref (ref:eval("output:mesh"), ref:inputs(), ref:asOp()) instead of round-tripping through the proc module + a path string. asOp is also how the op registry resolves a .procGraph referenced as a graph node: the type owns "how a graph becomes an op", the evaluator's registry delegates to it. Loaded lazily by asset_ref.module via require("@builtin::systems.procgen.procGraph.behavior") the first time a procGraph ref is touched in a VM.

modules/ProcGraphAssetTypeBehavior/asOp

asOp(self): any

Wrap this graph as an op def so it can be used as a NODE in another graph: its params mirror this graph's declared inputs, its outputs mirror this graph's declared outputs, and its version folds this graph's structure with every content op it references (so an edit anywhere beneath re-keys the wrapping node). The op registry resolves a graph node whose op is a .procGraph ref through this method — the type owns the graph-as-op mapping; the evaluator's wrapGraphAsOp is the library that does the heavy folding.

Parameters

  • self any (optional)
local opDef = procRef:asOp()

modules/ProcGraphAssetTypeBehavior/compile

compile(self): { [string]: any }

Compile this asset's init.luau into its graph.json — the serialized form the evaluator loads when another graph composites this one, and the file whose write makes every live Generator bound to this asset re-cook. This is the SOURCE step, not the evaluation step: it produces the graph, it does not run it (that is a cook, which the Generator drives). The type compiles on its own whenever the source is written, so calling this is only for forcing it — a rebuild after restoring a file, or a script that wants the node count back.

Parameters

  • self any (optional)
local r = procRef:compile()

modules/ProcGraphAssetTypeBehavior/eval

eval(self, target: string, opts: any?): any

Evaluate one output of this graph and return the produced value (Geometry / InstanceSet / Texture / Material / Bundle / …). target is an output name ("mesh"), or an explicit "output:<name>" / "node:<id>[:<out>]" selector. opts forwards to proc.eval ({ inputs = { … }, seed = N }) so the graph's exposed inputs can be overridden per call.

Parameters

  • self any (optional)
  • target string — Output name or selector.
  • opts any? (optional) — Optional { inputs, seed, … } (see proc.eval).
local mesh = procRef:eval("mesh", { inputs = { radius = 3 } })

modules/ProcGraphAssetTypeBehavior/graph

graph(self): any

This asset's procedural graph, built from its init.luau source. The source is the graph's definition, so this answers with what the asset currently describes rather than with whatever was last compiled.

Parameters

  • self any (optional)
local g = procRef:graph()

modules/ProcGraphAssetTypeBehavior/inputs

inputs(self): { [string]: any }

The graph's declared inputs — the parameters that drive it, each with its procgen type and default. This is the vocabulary a Generator exposes as editable overrides.

Parameters

  • self any (optional)
for name, decl in pairs(procRef:inputs()) do print(name, decl.type) end

modules/ProcGraphAssetTypeBehavior/instantiate

instantiate(self, target: EntityRef?, opts: { [string]: any }?): (EntityRef, { [string]: string })

Put this graph in the scene — the uniform instantiate(target?, opts?) contract every scene-instantiable asset answers to. An entity carrying a Generator bound to this asset: the Generator cooks the graph and keeps the result as that entity's managed children, re-cooking whenever the graph or its params change — so the scene holds a live instance of the description, not a frozen copy of one evaluation. With target the generator spawns as a child of that owner (so an owning Asset component tears it down with its other children); with no target it is a fresh root. The base opts — position, rotation, scale, name, temporary — place the root.

Parameters

  • self any (optional)
  • target EntityRef? (optional) — Optional owning entity ref.
  • opts { [string]: any }? (optional){ position?, rotation?, scale?, name?, temporary?, output?, params?, autoBake? }output names which declared output to realize, params seeds the graph's exposed inputs.
local gen = procRef:instantiate()
local gen = procRef:instantiate(nil, { params = { rockCount = 800 } })

modules/ProcGraphAssetTypeBehavior/onChange

onChange(ref, change)

Change callback: recompile whenever the entry source is written. The graph.json this produces lands inside the same folder, which is what a live Generator bound to this asset watches — so editing the source is the whole update path, with no build step between writing and seeing it. Writes to any other file in the folder (that graph.json, the README, the sidecars) are not the definition and do not recompile — by the path filter here, and by the compiled-source guard for a change that names no path.

Parameters

  • ref any (optional)
  • change any (optional)

modules/ProcGraphAssetTypeBehavior/onRegister

onRegister(self)

Initial-registration callback: compile the graph the moment the instance registers, so an asset restored without a graph.json — or one whose source changed while the engine was down — has its compiled form before anything composites or cooks it.

Parameters

  • self any (optional)

modules/ProcGraphAssetTypeBehavior/outputs

outputs(self): { string }

The graph's declared output names (sorted). Each is a value the graph produces and a valid eval target.

Parameters

  • self any (optional)
local outs = procRef:outputs()

modules/ProcGraphAssetTypeBehavior/promote

promote(self, opts: { [string]: any }?): { [string]: any }

Publish this graph as an OPERATION — a .procNode whose def is this graph. A graph referenced as a node is a private subgraph of whatever names it; an op is part of the vocabulary every graph searches, so procgen ops and proc.search_ops find it, and any graph can reach for it by name. The op's params are this graph's declared inputs and its outputs are this graph's declared outputs, and the node stays bound to this asset — editing the graph changes the op, and every graph using it re-cooks. The .procNode is written as source (init.luau) like any other, so it is a node to keep editing: give it its own eval and it becomes an op in its own right rather than this graph under another name.

Parameters

  • self any (optional)
  • opts { [string]: any }? (optional) — Optional { name, out, description, tags } — the op's name (default: this graph's), the .procNode folder to write (default: inside this graph, which is where the op form of it belongs), and the description + tags its .metadata carries (default: this graph's own).
local node = procRef:promote()
local node = procRef:promote({ name = "StuddedTop", tags = { "mesh", "lego" } })

modules/ProcGraphAssetTypeBehavior/validate

validate(self): { any }

Type-check this graph's connections. Returns proc.validate's diagnostic list (empty when the graph is well-typed) — each entry is { level, code, node, input, expected, actual, suggestions }.

Parameters

  • self any (optional)
for _, d in ipairs(procRef:validate()) do warn(d.code, d.node, d.input) end

modules/ProcNodeAssetTypeBehavior/README

ProcNodeAssetTypeBehavior

Registration lifecycle for .procNode assets — custom procedural operations authored as content. An instance's init.luau returns either a GRAPH (return proc.define(...) — the node carrying its own pipeline, the self-contained form) or a def table (op-style { inputs, params, outputs, eval } for a hand-written eval); this behaviour push-registers it into the proc op registry, keyed by the asset's stable GUID. Graphs reference these nodes by a typed GUID ref (never by name), so resolution is push-model with no dynamic name probing.

modules/ProcNodeAssetTypeBehavior/onChange

onChange(ref, _change)

Change callback: re-register + hot-reload this node whenever its .procNode is seeded or its entry script is edited. The node is re-registered from its current entry source; the version bump propagates the edit to every graph that reaches the node.

Parameters

  • ref any (optional)
  • _change any (optional)

modules/ProcNodeAssetTypeBehavior/onDelete

onDelete(ref)

Delete callback: unregister this node's op when its .procNode folder is removed. The delete ref carries no guid (the folder is already gone), so the guid is resolved from the removed folder path via the registry's path->guid index. A graph referencing it afterward errors loudly at eval — resolution is push-model, never a silent fallback.

Parameters

  • ref any (optional)

modules/ProcNodeAssetTypeBehavior/onRegister

onRegister(self)

Initial-registration callback: register this node's op the moment the instance first registers, so a graph loaded in the same world-ready sweep can resolve it. Idempotent with onChange.

Parameters

  • self any (optional)

modules/ProcPackAssetTypeBehavior/README

ProcPackAssetTypeBehavior

Registration lifecycle for .procPack assets — a FAMILY of procedural operations authored as content. The instance's init.luau returns a pack table whose ops map op id to def, and this behaviour registers the whole map through registry.registerPack, so every op is validated exactly as a builtin is and tagged with the pack it came from. Ops registered this way carry the STABLE STRING IDS the pack names them with, which is what separates a pack from a .procNode — a single op keyed by its asset guid.

modules/ProcPackAssetTypeBehavior/onChange

onChange(ref, _change)

Change callback: re-register the family whenever the .procPack is seeded or its entry script is edited. Every op is re-registered from the current source and the version bump propagates the edit to every graph that reaches any of them; an op the edit removed is unregistered.

Parameters

  • ref any (optional)
  • _change any (optional)

modules/ProcPackAssetTypeBehavior/onDelete

onDelete(ref)

Delete callback: unregister every op this pack registered when its .procPack folder is removed. A graph naming one afterwards errors loudly at eval rather than cooking against a definition that no longer exists.

Parameters

  • ref any (optional)

modules/ProcPackAssetTypeBehavior/onRegister

onRegister(self)

Initial-registration callback: register this pack's whole family the moment the instance first registers, so a graph loaded in the same world-ready sweep can resolve every op in it. Idempotent with onChange.

Parameters

  • self any (optional)

modules/Queue/README

require("@builtin/modules/queue") -- Queue (also available as global 'queue')

Defer FFI mutations across frames — queue(fn) -> (ok, err, drainPromise).

Inside the body, write FFI calls (entity.spawn, component.add, position.set, etc.) enqueue rather than execute synchronously. The engine's per-frame drainer applies them in FIFO order with a per-frame budget. Returns (true, nil, drainPromise) on success, (false, err, nil) if the body throws — in which case the partial batch is cleared so it does not later apply. drainPromise is a promise handle that resolves once every mutation queued inside fn has been applied by the engine drainer. await(drainPromise) blocks the calling coroutine until full drain — use this to gate post-queue work (scene/layer load fires onLoad after the drain promise resolves). If nothing was queued (e.g. an empty fn), the promise resolves before queue() returns. Nested queue() is a no-op: the inner call just runs in the already-active queue scope. The depth counter is re-entrant. See docs/plans/2026-05-06-luau-queue-deferred-mutations.md for the full design (drain budget, error semantics, future per-key fences for reads).

Usage: local Queue = require("@builtin/modules/queue") Also available as global: queue

modules/RenderError/README

RenderError

Shared "this render is broken" marker. visibleError(entityId) swaps an entity to the builtin ERROR text model + error material, so a failed render reads as an unmistakable 3D "ERROR" sign on screen instead of empty pixels — empty silently reads as "fine" and a broken object gets mistaken for working. Every render component (built-in Model / SkinnedModel, or a user-authored one) calls this, so the error model is defined in ONE place: change it here and every render component updates.

modules/RenderError/visibleError

visibleError(entityId: string, reason: string?): boolean

Swap entityId to the builtin ERROR marker so a broken render is unmistakable on screen. Un-skins the entity first (a bad skeleton can't collapse the marker), binds the ERROR mesh + error material via the public ecs.* API, and logs the reason at error level. Returns true if the marker was applied.

Parameters

  • entityId string — The entity whose renderable becomes the ERROR marker.
  • reason string? (optional) — Short human string describing what failed (logged).
renderError.visibleError(self.entityId, "mesh never became GPU-resident")

modules/RenderFeatureAssetTypeRef/README

RenderFeatureAssetTypeRef

Per-instance methods exposed on every AssetRef<renderFeature>. Loaded lazily by asset_ref.module the first time a renderFeature ref is touched in a VM. A render feature is an init.luau exporting { setup?(ctx), render(ctx), teardown?(ctx) }; :enable() instantiates it (the engine begins calling its render(ctx) hook every frame) and returns a live handle, also visible under /zero/runtime/renderFeatures/.

modules/RenderFeatureShared/README

RenderFeatureShared

Shared helpers + the authoring contract for renderFeature modules.

modules/ReportFormatter/README

require("@builtin/systems/worldValidation.package/reportFormatter") -- ReportFormatter

Filtering + formatting helpers for the world validation report. Pure functions: every call returns a new value, never mutates the input report.

Three exports: filter (re-filter an existing Report without re-scanning the VFS), format (render to a string in one of four shapes), and summary (one-line health line). All three are pure — they never mutate the input report.

Usage: local ReportFormatter = require("@builtin/systems/worldValidation.package/reportFormatter")

modules/ReportFormatter/filter

filter(report, opts)

Return a new Report containing only problems that match the filter options. Counts are recomputed from the filtered set so the caller can trust counts against the visible problems list. Pure — the input report is not mutated.

Parameters

  • report any (optional) — Report produced by the main validator.
  • opts any (optional) — Filter options — { severity, category, source, code, path (Lua pattern), includePlaceholders (default true), limit }.
local errs = ReportFormatter.filter(r, { severity = "error" })
local libsOnly = ReportFormatter.filter(r, { source = "library:@builtin" })

modules/ReportFormatter/format

format(report, format)

Render a Report into a string in the chosen format.

Parameters

  • report any (optional) — Report produced by the main validator.
  • format any (optional) — One of "human" (default), "markdown", "json", "summary". Unknown values fall back to "human".
print(ReportFormatter.format(r, "human"))
local md = ReportFormatter.format(r, "markdown")

modules/ReportFormatter/summary

summary(report)

Compact one-line health summary string — world: NE/NW libraries: NE/NW total: OK|FAIL.

Parameters

  • report any (optional) — Report produced by the main validator.
print(ReportFormatter.summary(r))

modules/ResourceHandle/README

ResourceHandle

Recognises a live GPU resource handle — the { kind = "<Category>Handle", category, guid, name } table renderer.mesh.create, renderer.material.create and renderer.texture.create return. Every boundary that carries a component field value out of the session that produced it consults this to tell a handle from an AssetRef.

modules/ResourceHandle/isLive

isLive(v: any): boolean

Whether a value is a live GPU resource handle.

Parameters

  • v any (optional) — Any component field value.
ResourceHandle.isLive(renderer.mesh.create(geometry)) -- true

modules/ResourceHandle/label

label(v: any): string

A short phrase naming a handle, for a message about the value.

Parameters

  • v any (optional) — A handle table.
ResourceHandle.label(meshHandle) -- "mesh handle 'msh_grass_1'"

modules/ResourceHandle/park

park(owner: string, componentType: string, dropped: { [string]: any }?)

Hold, for the rest of this session, the live handles one component's durable record left out — keyed by the entity and component they came from. Each call replaces what that pair had parked before.

Parameters

  • owner string — The entity id the component sits on.
  • componentType string — The component's type name.
  • dropped { [string]: any }? (optional) — The { field = handle } map left out of the record, or nil when the component's record carries every field it holds.
ResourceHandle.park(id, "@builtin::components.Model", { model = mesh })

modules/ResourceHandle/withParked

withParked(data: any, owner: string, componentType: string): (any, number)

The component field map to apply, with every field this session parked for owner's componentType and the record does not carry put back.

Parameters

  • data any (optional) — A component's { field = value } map from a record.
  • owner string — The entity id the component is being applied to.
  • componentType string — The component's type name.
ResourceHandle.withParked(record.data, id, "@builtin::components.Model")

modules/ResourceHandle/withoutLive

withoutLive(data: any): (any, { [string]: string }?)

The component field map to apply, with any live-handle value left out — the form a value read from durable content takes in a session other than the one that minted it. The field falls back to the component's declared default, and the caller reports what it stood for.

Parameters

  • data any (optional) — A component's { field = value } map.
ResourceHandle.withoutLive(record.data) -- data, nil

modules/RigAssetTypeBehavior/README

RigAssetTypeBehavior

Behaviour for the rig asset type — a skeleton's disk shape. The payload is rig.json (a readable JSON document: ordered bones with names, parent hierarchy, the node index each maps to, rest local transform, and inverse-bind matrix, plus the retarget profile (role -> bone) and the humanoid classification). A rig is its own primitive: a skinned mesh references a rig, an animation references its source rig, and retargeting maps one rig onto another. The format is text, not binary, so an agent can open it and fix a mis-derived profile or a bad bone parent with an edit. Importers call asset.create("rig", name, { json }) to mint a rig.

modules/RigAssetTypeBehavior/onChange

onChange(self, _change)

Drop the cached decode when the rig's content changes — a hot-reload or a re-import — so the next :doc() re-parses the new rig.json.

Parameters

  • self any (optional) — The rig AssetRef that changed.
  • _change any (optional) — What happened to it; the cache is dropped whatever it was.

modules/RigAssetTypeBehavior/onCreate

onCreate(name: string, opts: CreateOpts): { [string]: string }

Generic-creation hook for asset.create("rig", name, opts). Pure: returns the content-file map; asset.create writes it to the authored destination, registering the .rig asset under its minted guid. The JSON document carries the skeleton, its retarget profile (the role -> bone driver, present for a humanoid), and its humanoid classification — a non-humanoid rig (a prop, a plant, a quadruped) simply has no profile in the same document.

Parameters

  • name string — Rig identity (the instance name).
  • opts CreateOpts
asset.create("rig", "PolygonSyntyCharacter", { json = rigJson })

modules/SceneAssetTypeRef/README

SceneAssetTypeRef

Per-instance methods exposed on every AssetRef<scene>. Loaded lazily by asset_ref.module.

modules/SceneAssetTypeRef/build_scene

build_scene(self, opts: { save: boolean?, layer: any?, trigger: string? }?): any

Run this scene's build.luau against what it resolves right now and land what it declares in the scene — for the case where something the builder reads changed and the file did not. Writing build.luau already runs the build, so a call after a write is unnecessary. content() writes the scene's entities and editorOnly() writes the ones that are present while authoring and absent in play; each is reconciled against the entities the last build placed, so an entity keeps its id across every rebuild and anything else in the scene is left alone. Runs in edit mode on a LOADED scene — the entities it lands on are the live ones. opts.save = false leaves the result unsaved; by default the scene is saved, which is what puts the build in scene.json. A call made while a build for this scene is running returns that build right away instead of starting a second one — a build the scheduler is still advancing. One whose task was cancelled, or that stopped for any other reason without returning, hands the scene back to this call, which runs the build. A build that a newer write to build.luau replaces stops where it stands and leaves the scene to the rebuild that write asked for.

Parameters

  • self any (optional)
  • opts { save: boolean?, layer: any?, trigger: string? }? (optional) — Optional { save = false }.
layers.active.asset:build()

modules/SceneAssetTypeRef/discard_scene

discard_scene(self, opts: any): boolean

Discard this scene's UNSAVED (dirty) edits and restore its saved scene.json. When the scene is LOADED, the overlay is deleted and the live layer is respawned from canonical (with the scene-load gate held so the respawn is not re-marked dirty); when it is NOT loaded, the on-disk overlay is simply deleted. Edit-mode overlay only — not the play-mode baseline.

Parameters

  • self any (optional)
  • opts any (optional) — Optional { to = <name|path> }, or a bare name/path string, to target a scene other than this asset's own path.
sceneRef:discard()

modules/SceneAssetTypeRef/getBuildScript

getBuildScript(self): string?

Read the scene's build.luau body as raw text.

Parameters

  • self any (optional)
local src = sceneRef:getBuildScript()

modules/SceneAssetTypeRef/getEntrypoint

getEntrypoint(self): string?

Read the scene's entrypoint.luau body as raw text.

Parameters

  • self any (optional)
local code = sceneRef:getEntrypoint()

modules/SceneAssetTypeRef/getSceneJson

getSceneJson(self): { [string]: any }?

Parse the scene's scene.json into a Lua table.

Parameters

  • self any (optional)
local s = sceneRef:getSceneJson()

modules/SceneAssetTypeRef/getSceneJsonRaw

getSceneJsonRaw(self): string?

Read the scene's scene.json body as raw JSON text.

Parameters

  • self any (optional)
local raw = sceneRef:getSceneJsonRaw()

modules/SceneAssetTypeRef/inspect

inspect(self): any

asset.inspect type-specific detail: { entityCount, entityNames, player }, parsed from the scene's own scene.json — never executes entrypoint.luau. A scene whose scene.json can't be read/parsed returns an empty detail rather than erroring.

Parameters

  • self any (optional)
local entityNames = asset.inspect(sceneRef).detail.entityNames

modules/SceneAssetTypeRef/listEntities

listEntities(self): { string }

List the entity names declared at the top level of the scene's JSON body. Best-effort; nested children are not flattened.

Parameters

  • self any (optional)
for _, n in ipairs(sceneRef:listEntities()) do print(n) end

modules/SceneAssetTypeRef/load_scene

load_scene(self, opts: { [string]: any }?): any

Load this scene into the root ("main") slot. Equivalent to layers.load(self) — the AssetRef envelope is passed straight through, so the loader threads scene identity by guid. Pass opts for additive overlays / persistence / origin offset (same shape as layers.load's second argument).

Parameters

  • self any (optional)
  • opts { [string]: any }? (optional) — Optional load options forwarded to layers.load.
sceneRef:load()
sceneRef:load({ additive = true, name = "hud_overlay" })

modules/SceneAssetTypeRef/onChange

onChange(ref, change)

A write inside a .scene folder. A write to build.luau re-runs the build on the loaded scene, which is what makes an edit to the code show up in the scene without reloading it. The rebuild finishes after the write has returned, and posts a notice naming the scene and how many entities each surface placed — the write's own answer is that the file was written. A write that arrives while a rebuild is running gets a rebuild of its own once that one finishes, and several such writes get the single run that reads them all.

Parameters

  • ref any (optional) — The scene whose folder was written.
  • change any (optional){ path, asset, type, kind, origin }.
__zero_dispatch_asset_change("/zero/source/scenes/main.scene/build.luau")

modules/SceneAssetTypeRef/onCreate

onCreate(_name: string): { [string]: string }

Generic-creation hook for asset.create("scene", name). A new scene is the type's template shape verbatim.

Parameters

  • _name string
asset.create("scene", "my_level")

modules/SceneAssetTypeRef/save_scene

save_scene(self, opts: any): any

Publish this scene's current state to its canonical scene.json. When the scene is LOADED, its live scenegraph is captured into canonical; when it is NOT loaded, its pending dirty overlay is promoted into canonical. Either way the durable scene.json ends up reflecting the authored state. opts.to (a scene name or path) saves to a different scene ("save as"). opts.layer is the layer holding this scene, for a caller that already has it: a scene is findable through layers.list only once its load has registered it, so a save made DURING that load names its layer itself and captures the live scenegraph rather than reading it as unloaded.

Parameters

  • self any (optional)
  • opts any (optional) — Optional { to = <name|path>, layer = <SceneProxy> }, or a bare name/path string.
sceneRef:save()
layers.active.asset:save({ to = "level_2" })

modules/SceneAssetTypeRef/validate

validate(self): { { code: string, severity: string, message: string } }

Semantic content validation for a scene: the player-setup rule set run over this scene's scene.json, surfaced through asset.validate, plus a warning when the scene has no explicit sky entity.

Parameters

  • self any (optional)

modules/SceneBuild/README

SceneBuild

Runs a builder function in an entity capture scope and composes what it created into records, then reconciles records into the scene with ids held stable across rebuilds. Also holds the build surface a build script reads, whose build.asset authors the assets a build produces and hands back the same one on every run the code has not changed.

modules/SceneBuild/attribute

attribute(refusals: { Refusal }): { Refusal }

Read a refusal's traceback for the one frame that belongs to the code being built. A build composed from several contributors reports this so an author reads which contributor was refused rather than which build ran.

Parameters

  • refusals { Refusal } — The refusal array entity.capture hands back.
local named = SceneBuild.attribute(select(4, entity.capture(fn)))

modules/SceneBuild/buildSurface

buildSurface(folder: string, sourceDigest: string): BuildSurface

The build surface a build script reads: the operations that belong to the build itself rather than to the scene it states. build.asset(kind, name, produce) is the asset a build makes — produce runs when the build script changed and its result is authored at <folder>/<name>.<kind>, and every other run hands back that same asset, guid and all, without running produce at all. The returned AssetRef is what a component field names, so the reference survives the save and the reload.

Parameters

  • folder string — The build's own folder, which the assets it produces are authored inside.
  • sourceDigest string — The digest of the build script running now, as M.digest reports it — what decides whether an asset it produced is still the asset the code states.
local surface = SceneBuild.buildSurface(dir, SceneBuild.digest(src))

modules/SceneBuild/digest

digest(source: string): string

A short, stable digest of a script's source. Two different scripts give different digests, and the same script gives the same one on every machine and every run — which is what makes it the answer to "did the code that produced this change?".

Parameters

  • source string — The script body to digest.
local key = SceneBuild.digest(vfs.read(path))

modules/SceneBuild/drift

drift(owner: string?): { Drift }

Where the live scene disagrees with the build that states it. Every entity a build placed records what that build last said about each of its properties, so anything an author has changed since reads back differently — and this is that list: the entity, the property, what the build said, and what the scene holds now.

These are the values a rebuild KEEPS. A build repeating itself leaves them alone, and only a build that states something DIFFERENT about that property takes it back. So this is what to read to know that a scene and its build.luau disagree, and where, before deciding which should win.

Property names are the ones the build records: n name, i internal, p position, r rotation, s scale, and a:<name> for an attribute.

Parameters

  • owner string? (optional) — Optional build name, as M.ownerOf reports it, to read just that build. Omitted, every build-owned entity in the scene is read.
for _, d in ipairs(SceneBuild.drift()) do print(d.name, d.property) end

modules/SceneBuild/notePreview

notePreview(entityId: string, values: { [string]: any }, componentType: string?): nil

Record the values an author left on entityId, an entity a build owns. The build states that entity from its own source, so the values hold until it runs again — and M.takePreview is what the next run reads to say which of them it replaced and with what. Each record REPLACES the one before it: what it states is everything the entity carries now, so a name dropped between two records is dropped here too.

Parameters

  • entityId string — Runtime entity id of the owned entity.
  • values { [string]: any } — The values the entity carries now, by name.
  • componentType string? (optional) — The component that states them, so a rebuild knows to state that type again instead of leaving it to the scene.
SceneBuild.notePreview(id, { count = 9 }, "SceneModule")

modules/SceneBuild/ownerOf

ownerOf(entityId: string): string?

The build that placed entityId, or nil when no build placed it. A reconcile writes the name of the build onto every entity it places, as an attribute the scene records beside the entity's name and transform, so the answer holds across a reload — and an entity an author spawned carries no owner at all.

Parameters

  • entityId string — Runtime entity id.
if SceneBuild.ownerOf(id) ~= nil then print("a build states this") end

modules/SceneBuild/previewedComponentType

previewedComponentType(entityId: string): string?

The component type that recorded a preview for entityId, or nil when none is waiting. A component that records one is SAYING that a build states its fields and that it announces the replacement itself — so a rebuild states that type again rather than leaving it to the scene, which is what lets the announcement happen. Every other component is merged.

Parameters

  • entityId string — Runtime entity id of the owned entity.
if SceneBuild.previewedComponentType(id) == "SceneModule" then end

modules/SceneBuild/reconcile

reconcile(

Apply records to the scene under target, reusing the entities a previous reconcile left behind. A record that maps to a live entity updates THAT entity — same runtime id, so every reference to it survives the rebuild — and only a record with no live entity spawns one. Entities the previous build held that this one no longer emits are despawned. Name, transform, hidden, active, attributes, lifecycle mode, network scope, whether the entity's live state replicates, and components are all made to match the record, so a rebuild that drops a component or an attribute drops it from the scene. Each of them is a diff: what already matches the record is left exactly as it is, so a rebuild that changed nothing changes nothing — a running component keeps running and the scene stays clean. Only entities the build owns are touched: anything else under target is left exactly as it was. A component field holding an entity reference is resolved as the records are applied: a reference to an entity of the SAME build points at the entity this reconcile landed it on, and a reference to any other entity keeps pointing where it did. owner names the build. Every entity it places carries that name and the record's identity as attributes of its own, which is what lets a rebuild find the entities the last one placed without anything being remembered between them — the pair is in the scene, and a reload brings it back with the entity. Two builds sharing a target stay out of each other's way by using different owners. A record's identity is its place in the hierarchy — the chain of names from the build root down to it — so dropping, inserting or reordering a sibling leaves every other entity where it was. Several children of one parent sharing a name are told apart by their rank among those, counted in the order the builder created them.

local ids = SceneBuild.reconcile(records, root, "chairs")
local ids, created = SceneBuild.reconcile(records, layer, "build")
SceneBuild.reconcile(SceneBuild.run(build), layers.active, "build")

modules/SceneBuild/run

run(builder: () -> ()): ({ any }, { Refusal })

Run builder inside an entity capture scope and return the records for every entity it created. The builder writes ordinary spawn code — real entity.spawn, real component.add, real loops — and the entities it creates are real for the duration of the call. They are composed into records and then despawned, so run leaves the scene untouched and hands back data. Reconciling that data into a scene is M.reconcile. The builder's entities are despawned even when it raises, so a failed build never leaks a half-built hierarchy into the scene. What a component the builder attached created while running its own lifecycle belongs to that component: the record names the COMPONENT, and the same lifecycle runs again wherever the record is put back, so the entities come from there rather than from records of their own. That covers a nested build — a placement the builder makes runs its own module and owns what it lands — and every other component that expands into entities. An operation the scope refused is refused BEFORE it lands, so the records describe the live world exactly as the builder left it, and a builder that ran to its end around a refusal somebody caught for it composes what it did make. Every such refusal comes back as the second return, naming the contributor it stopped, for the caller to report alongside what it baked.

Parameters

  • builder () -> () — Function taking no arguments; spawns whatever it wants.
local records, refused = SceneBuild.run(function() entity.spawn("chair") end)

modules/SceneBuild/sourceOf

sourceOf(owner: string): string?

The file that states the build named owner — what the reconcile running that build passed as opts.source. Nil for a build that has not run in this session and for one that named no source.

Parameters

  • owner string — Build name, as M.ownerOf reports it.
local file = SceneBuild.sourceOf(SceneBuild.ownerOf(id))

modules/SceneBuild/takePreview

takePreview(entityId: string): { [string]: any }?

Take the values M.notePreview recorded for entityId and clear them. Each set of values is read once — by whichever run of the build states that entity next.

Parameters

  • entityId string — Runtime entity id of the owned entity.
local set = SceneBuild.takePreview(id)

modules/SceneModuleAssetTypeRef/README

SceneModuleAssetTypeRef

Per-instance methods exposed on every AssetRef<sceneModule>. Loaded lazily by asset_ref.module.

modules/SceneModuleAssetTypeRef/build

build(self, params: { [string]: any }?): ({ any }, { SceneBuild.Refusal })

Run this module's builder with params and return the entity records it built. The builder runs inside the entity capture scope, so it writes ordinary spawn code and what it created comes back as data — the scene is untouched by the call. SceneBuild.reconcile is what lands the records.

Parameters

  • self any (optional)
  • params { [string]: any }? (optional) — Per-placement input overrides, keyed by declared input name. An input the table omits resolves to its declared default. A builder that ran to its end around an operation the scope refused — one it caught itself, or one something between it and the refused call caught — hands back the records for what it did make, and every such refusal beside them: the operation was refused before it landed, so the records describe the scene as the builder left it.
local records, refused = moduleRef:build({ seed = 3 })

modules/SceneModuleAssetTypeRef/getInitScript

getInitScript(self): string?

Read the module's entry script (init.luau / init.lua) as raw text.

Parameters

  • self any (optional)
local src = moduleRef:getInitScript()

modules/SceneModuleAssetTypeRef/inputs

inputs(self): { InputSpec }

The inputs this module declares, sorted by name — each one's name, its Field kind, its declared default, and, for a closed-set input, the members it accepts. This is what an inspector renders as widgets and what a caller reads to learn which params a placement can set.

Parameters

  • self any (optional)
for _, i in ipairs(moduleRef:inputs()) do print(i.name, i.kind) end

modules/SceneModuleAssetTypeRef/inspectDetail

inspectDetail(self): any

asset.inspect type-specific detail: { inputs } — the declared inputs, as :inputs() reports them. A module whose entry script cannot be loaded reports an empty input list rather than erroring.

Parameters

  • self any (optional)
local declared = asset.inspect(moduleRef).detail.inputs

modules/SceneModuleAssetTypeRef/instantiate

instantiate(self, target: EntityRef?, opts: { [string]: any }?): (EntityRef, { [string]: string })

Instantiate this module — the uniform instantiate(target?, opts?) contract every scene-instantiable asset answers to, and what makes a sceneModule placeable wherever a scene-instantiable asset is accepted.

With target it builds with opts.params and reconciles the records under that entity, owned by that entity: the entities a previous build placed there name it as their owner and are UPDATED in place, so ids hold across every rebuild and across a reload.

With no target it places the module the way an author places any asset: one entity carrying a SceneModule component bound to this module. That component is what runs the build. The base opts — position, rotation, scale, name, temporary — place the root either way.

Parameters

  • self any (optional)
  • target EntityRef? (optional) — Optional owning entity ref.
  • opts { [string]: any }? (optional){ position?, rotation?, scale?, name?, temporary?, params? }params seeds the module's declared inputs.
moduleRef:instantiate(owner, { params = { seed = 3 } })

modules/SceneModuleInspector/README

SceneModuleInspector

The SceneModule component's custom entity-inspector view — the bound module's DECLARED inputs as editable field rows.

modules/SceneModuleInspector/buildFieldSpecs

buildFieldSpecs(declared: { any }, params: { [string]: any }?): { FieldSpec }

Build the editable field descriptors for a module's declared inputs: one spec per input, sorted by name, carrying the input's kind, its current value (the param override when the placement set one, else the declared default), and the members a closed-set input accepts. Pure — no UI, no entity access — so the field mapping is testable headless.

Parameters

  • declared { any } — The module's declared inputs, as moduleRef:inputs() reports.
  • params { [string]: any }? (optional) — The placement's current param-override table (may be nil).

modules/SceneModuleInspector/sections

sections(entityId: string, proxy: any): any?

The SceneModule's inspector sections: the bound module's identity, the file that states the placement, one editable row per declared input, and a Rebuild action. Returns nil when the proxy is unreadable (the inspector shows the generic fields alone).

Parameters

  • entityId string — The owning entity's id.
  • proxy any (optional) — The live SceneModule component proxy.

modules/SceneModuleInspector/statedIn

statedIn(entityId: string): string?

The file the build that placed entityId is written in, for a placement a build made. That build states the placement's params every time it runs, so the rows below read as a preview of what it would state rather than as the placement's own settings. Nil for a placement an author made, whose params are the author's.

Parameters

  • entityId string — The inspected entity's id.
local file = SceneModuleInspector.statedIn(id)

modules/ScriptValidator/README

require("@builtin/systems/worldValidation.package/scriptValidator") -- ScriptValidator

Per-file Luau / Lua script validator. Wraps lsp.check to return real parser + type-check diagnostics for a single script, on top of cheap textual sanity checks (read failure, empty file).

Each script gets the full LSP pass — lsp.check(path, opts) from the engine's embedded Luau LSP. Severity, line, column, code, and message are taken straight from the LSP and re-wrapped in the validator's Problem shape. The LSP diagnostics carry stable codes (unknown-global, type-error, parse-error, …) — we forward them verbatim so callers can severity = "error" or code = "parse-error" and get exactly the rows they expect. Two cheap textual checks run BEFORE the LSP is invoked:

  1. script.read_failed (error) — vfs.read returned nil (file deleted between scan and read, or unreadable).
  2. script.empty (warning) — zero non-whitespace content. These don't duplicate anything the LSP produces — the LSP is skipped when read fails (no source to feed it) and the empty check is informational about the file rather than the code.

Usage: local ScriptValidator = require("@builtin/systems/worldValidation.package/scriptValidator")

modules/ScriptValidator/validate

validate(script, opts)

Validate a single script. Combines two cheap textual checks (script.read_failed, script.empty) with the embedded Luau LSP's full diagnostic pass via lsp.check(path).

Parameters

  • script any (optional){ path, name } script entry from vfsScanner.
  • opts any (optional) — Reserved for future use; currently ignored.
local problems = ScriptValidator.validate({ path = "/source/foo.luau", name = "foo.luau" })

modules/ScriptValidator/validateBatch

validateBatch(scripts, opts)

Validate a batch of scripts and flatten the per-script problem lists into one array.

Parameters

  • scripts any (optional) — Array of script entries from vfsScanner.
  • opts any (optional) — Reserved for future use.
local all = ScriptValidator.validateBatch(bucket.scripts)

modules/ServiceAssetTypeRef/README

ServiceAssetTypeRef

Per-instance methods exposed on every AssetRef<service>. Loaded lazily by asset_ref.module. These act on the SPECIFIC service the reference points to — asset.resolve("mesh_gen","service"):invoke{...} runs THAT service.

modules/ServiceAssetTypeRef/balance

balance(self): (number?, string?)

Read the caller's current credit balance — the balance every service draws from. Returns (nil, errMsg) when not signed in.

Parameters

  • self any (optional)
local credits = serviceRef:balance()

modules/ServiceAssetTypeRef/cost

cost(self, operation: string?): number?

The declared credit cost of an operation (the provider's up-front estimate; the exact charge is returned on each generation). Defaults to the primary / only operation.

Parameters

  • self any (optional)
  • operation string? (optional) — Optional operation name.
local c = serviceRef:cost()

modules/ServiceAssetTypeRef/frameworkInputs

frameworkInputs(self, operation: string?): { { [string]: any } }

The inputs a call on this service takes on top of what the operation declares — operation, naming which of the service's operations to run, and asset_path, naming where the run writes what it generates. Each row reads as a declared input does: { name, type, required, desc, framework = true }.

Parameters

  • self any (optional)
  • operation string? (optional) — The operation these apply to. Omit for the service's default.
local extra = serviceRef:frameworkInputs("sfx")

modules/ServiceAssetTypeRef/getDefinition

getDefinition(self): string?

Read this service's declaration source (init.luau).

Parameters

  • self any (optional)
local src = serviceRef:getDefinition()

modules/ServiceAssetTypeRef/getReadme

getReadme(self): string?

Read this service's README.md body.

Parameters

  • self any (optional)
print(serviceRef:getReadme())

modules/ServiceAssetTypeRef/info

info(self): { [string]: any }

A one-line summary of this service: its offering, what it produces, the operations it offers, and which of them a call runs when it names none.

Parameters

  • self any (optional)
local i = serviceRef:info()

modules/ServiceAssetTypeRef/inspect

inspect(self): any

asset.inspect type-specific detail: { methods, description }, where methods is this service's declared operations — the top-level keys of its operations = { ... } table, parsed from the declaration's own source text — and description is the declaration's top-level summary, also parsed from source. A service with no readable declaration returns an empty methods list rather than erroring.

Parameters

  • self any (optional)
local methods = asset.inspect(serviceRef).detail.methods

modules/ServiceAssetTypeRef/invoke

invoke(self, input: { [string]: any }?): { [string]: any }

Run a generation for this service. Picks the operation from input.operation (or the service's only / primary operation), runs its declared pipeline as a background task, and returns immediately with a generation handle. An input the operation does not declare is refused here, before anything is charged. Watch the run with the services toolbox status tool (or the watch tool) until it reads completed; that row's asset is the finished, spawnable asset. Consumes credits — check :cost() and :balance() first.

Parameters

  • self any (optional)
  • input { [string]: any }? (optional){ prompt, ..., operation?, asset_path? } — the operation's inputs.
local g = asset.resolve("mesh_gen","service"):invoke({ prompt = "a treasure chest" })

modules/ServiceAssetTypeRef/onCreate

onCreate(name: string, opts: { [string]: any }): { [string]: string }

Generic-creation hook for asset.create("service", name, opts). Returns the scaffold content files for a new service declaration.

Parameters

  • name string — Service identity (the instance name).
  • opts { [string]: any }
asset.create("service", "mesh_gen", { offering = "origozero/mesh_gen" })

modules/ServiceAssetTypeRef/operations

operations(self): { { [string]: any } }

List this service's callable operations and their declared inputs, output, and credit cost — the per-service surface to read before invoking. framework carries the inputs the CALL takes on top of what the operation declares (:frameworkInputs()).

Parameters

  • self any (optional)
for _, op in ipairs(serviceRef:operations()) do print(op.name, op.cost) end

modules/ServiceAssetTypeRef/resume

resume(self, record: { [string]: any }): { [string]: any }

Pick a recorded run of this service up where an earlier engine left it. Runs on world load for every run that did not finish; the record names the operation, the step in flight and the job it submitted, and the run continues from there without paying for the same work again.

Parameters

  • self any (optional)
  • record { [string]: any } — A run record as the framework's ServiceTask.durable returns it.
asset.resolve("mesh_gen","service"):resume(record)

modules/ServiceFramework/README

ServiceFramework

The runtime every .service instance declares against — reached via asset.containing(__FILE__).modules.shared. The TYPE owns all the machinery (metered invoke, the submit→poll→download→write pipeline, the async generation handle, error classification); an instance only declares its surface with Service.define{...}. A normal generation service is pure data — no per-instance logic.

modules/ServiceFramework/balance

balance(_self: any): ({ [string]: any }?, string?)

Read what the caller can spend on this service: spendable (the number an operation's cost must fit inside), with pool (the account balance) and agentRemaining (what is left of the caller's own allocation, when it works under one) behind it. Returns (nil, errMsg) when not signed in.

Parameters

  • _self any (optional)
local credits = asset.resolve("mesh_gen","service"):balance().spendable

modules/ServiceFramework/cost

cost(self: any, operation: string?): number?

The declared credit cost of an operation (the provider's estimate). The exact amount charged is returned on each generation; this is the up-front figure to budget against. Defaults to the primary/only operation.

Parameters

  • self any (optional)
  • operation string? (optional) — Optional operation name.
local c = asset.resolve("mesh_gen","service"):cost()

modules/ServiceFramework/define

define(spec: { [string]: any }): any

Declare a metered service. An instance's init.luau calls this with its surface — the offering identity and one or more operations — and returns the result. The TYPE runs the pipeline; the instance writes no machinery.

Parameters

  • spec { [string]: any }{ name, description, offering, output?, primary?, retry?, operations }. An operation's result is either a single file (bytes / url) or a SET (files), and a set assembles one asset: files = { { as, url } } fetches each map into the asset's own folder (an entry states bytes instead of url when a step already bound its content), pack = { { as, channels = { r, g, b, a } } } gathers named maps' channels into one raster where a slot samples several properties from one image (a channel is a constant 0-255, a file name for its red, or "file.g" for a stated channel), and bind = { slot = name } binds fetched and packed maps to the asset's slots. A map that reaches no slot is named on the job row and in the log. retry is { attempts?, delay? } — how many times a call refused for a reason that describes the moment (a rate limit, a busy gateway, a write conflict) is sent again, and the first wait in seconds between attempts, which doubles each time. Defaults to 4 attempts starting at 1 second. An operation may declare its own retry to override the service's.
return Service.define({ offering = "origozero/mesh_gen", operations = { generate = { ... } } })

modules/ServiceFramework/frameworkInputs

frameworkInputs(self: any, operation: string?): { { [string]: any } }

The inputs a call on this service takes on top of what the operation declares — operation, naming which of the service's operations to run, and asset_path, naming where the run writes what it generates. Each row is { name, type, required, desc, framework = true }, the same shape a declared input reads as. operation is required of a service that declares no default; asset_path is offered by an operation that writes a single file, and left out by one that authors a folder of them under a name of its own. The operation row's text names the operation THIS service runs when a call omits it, or the ones a call chooses between.

Parameters

  • self any (optional)
  • operation string? (optional) — The operation these apply to. Omit for the service's default.
local extra = asset.resolve("audio_gen","service"):frameworkInputs("sfx")

modules/ServiceFramework/info

info(self: any): { [string]: any }

A one-line summary of this service for discovery: its offering, what it produces, and the operations it offers.

Parameters

  • self any (optional)
local i = asset.resolve("mesh_gen","service"):info()

modules/ServiceFramework/invoke

invoke(self: any, input: { [string]: any }?): { [string]: any }

Run a generation for this service. Picks the operation from input.operation (or the service's primary / only operation), checks the operation's declared cost against the caller's balance/budget up front — raising with a legible reason if it can't be afforded — then runs the pipeline as a background task. An input the operation does not declare is refused here, before anything is charged. Returns immediately with a generation handle. Watch the task — the services toolbox status tool (or the watch tool) — until status == "completed", then the finished asset is that row's asset. The engine auto-imports the raw output into a spawnable asset (a mesh becomes a .bundle, an image a .texture, a sound a .audio); that imported asset — not the raw file — is what the completed result points to, ready to spawn. Consumes credits; check :cost() and :balance() first.

Parameters

  • self any (optional)
  • input { [string]: any }? (optional){ prompt, ..., operation?, asset_path? } — the operation's inputs.
local g = asset.resolve("mesh_gen","service"):invoke({ prompt = "a treasure chest" })

modules/ServiceFramework/operations

operations(self: any): { { [string]: any } }

List this service's callable operations and their declared inputs, output, and credit cost — the per-service surface an agent reads before invoking. framework carries the inputs the call takes on top of what the operation declares (:frameworkInputs()).

Parameters

  • self any (optional)
for _, op in ipairs(asset.resolve("mesh_gen","service"):operations()) do print(op.name, op.cost) end

modules/ServiceFramework/resume

resume(self: any, record: { [string]: any }): { [string]: any }

Pick a run of this service up where an earlier engine left it. The record names the operation, its inputs, the step in flight, the bindings the steps before it produced, and the provider or gateway job that step already submitted; the run continues from there, waiting on that job rather than paying for the same work again, and lands its asset the way an uninterrupted run does. Runs on world load for every recorded run that did not finish.

Parameters

  • self any (optional)
  • record { [string]: any } — A run record as ServiceTask.durable returns it.
asset.resolve("mesh_gen", "service"):resume(record)

modules/ServiceFramework/resumeAll

resumeAll(): number

Pick up every recorded run that did not finish: the ones an earlier engine submitted and did not live to collect. Runs when the world has loaded. A run's service is waited for rather than assumed present, because a service can be authored in the world and register a moment after the library's own; each run resumes on its own task, so one that has to wait holds up none of the others.

require("@builtin::assetTypes.service.shared").resumeAll()

modules/ServiceRunAssetTypeRef/README

ServiceRunAssetTypeRef

Per-instance methods on every AssetRef<serviceRun> — one generation's durable record: what it was asked to make, the gateway job doing the work, and the state that job reached.

modules/ServiceRunAssetTypeRef/finished

finished(self): boolean

Whether this run reached a terminal state. A run that is neither completed nor failed was still working when it was last heard from.

Parameters

  • self any (optional)
if not run:finished() then print("still out there") end

modules/ServiceRunAssetTypeRef/jobId

jobId(self): string?

The gateway job this run's work is running under. The job outlives the engine that submitted it, so this is what reaches an already-paid result when the run did not finish here.

Parameters

  • self any (optional)
local id = asset.resolve("gen_4f2a_0", "serviceRun"):jobId()

modules/ServiceRunAssetTypeRef/record

record(self): { [string]: any }

This run's record: { id, service, operation, prompt, jobId, status, progress, assetPath, error }.

Parameters

  • self any (optional)
local rec = asset.resolve("gen_4f2a_0", "serviceRun"):record()

modules/ShaderAssetTypeRef/README

ShaderAssetTypeRef

Per-instance methods exposed on every AssetRef<shader>. Loaded lazily by asset_ref.module.

modules/ShaderAssetTypeRef/compileByName

compileByName(ref: string)

Compile a shader by reference from its .shader VFS source — the lazy compile-on-first-use entry. A material that references an as-yet-uncompiled shader makes the renderer record the want; the engine calls this (via __zero_request_shader_compile) to bring the shader online through the same generic compile path an edit runs. Resolution goes through the universal asset system — a reference that doesn't resolve is a bad reference in the content that owns it, not something to special-case here.

Parameters

  • ref string — A shader asset reference (identity / guid) resolvable by asset.resolve.
require("modules.asset_ref").loadTypeModule("shader").compileByName("@builtin::shaders.pbr")

modules/ShaderAssetTypeRef/compileStatus

compileStatus(self): { status: string, error: string?, key: string? }

This shader's latest compile outcome — WITHOUT reading the engine log. Returns { status, error?, key? } where status is "compiled" (registered clean under every key), "failed" (with error = the real compiler message), or "pending" (not compiled yet / unknown). A shader registers under each of its keys — identity AND guid — and a material may look it up by either, so this checks them all and reports the WORST one (key names it): a registration that landed under the identity but not the guid reads as "pending" instead of hiding behind the healthy key. Compilation is async — a write queues it — so a "pending" right after editing means check again next frame. This is the authoritative "did my shader compile?" signal: a write succeeding and getProperties returning a schema do NOT mean the WGSL compiled.

Parameters

  • self any (optional)
local s = shaderRef:compileStatus(); if s.status == "failed" then print(s.error) end

modules/ShaderAssetTypeRef/compiledWgsl

compiledWgsl(self): string?

The WGSL the shader compiler received for this shader, exactly as it received it. What getWgsl returns is the body as written; this is what that body became — the generated group(1) material interface above it, the domain's framework and entry points around it, every #include expanded and every #ifdef resolved. A compile error's line numbers, and the handle index naga prints where it has no name, are positions in THIS text, so a compileStatus() of "failed" is read against it. It answers for a failed compile as well as a clean one, and needs no material, entity or draw.

Parameters

  • self any (optional)
local s = shaderRef:compileStatus()
if s.status == "failed" then print(s.error, shaderRef:compiledWgsl()) end

modules/ShaderAssetTypeRef/getGlsl

getGlsl(self): string?

Read the GLSL body, when present. Returns nil for WGSL-only shaders.

Parameters

  • self any (optional)
local glsl = shaderRef:getGlsl()

modules/ShaderAssetTypeRef/getProperties

getProperties(self): { { [string]: any } }

List this shader's declared material properties (parsed from properties.yaml). Each entry is { name, type, default, min?, max? }. This is the editor-discovery surface — the SAME parse the compile uses.

Parameters

  • self any (optional)
for _, p in ipairs(shaderRef:getProperties()) do print(p.name, p.type) end

modules/ShaderAssetTypeRef/getSourceCode

getSourceCode(self): string?

Read the primary shader body (shader.wgsl) as raw text.

Parameters

  • self any (optional)
local src = shaderRef:getSourceCode()

modules/ShaderAssetTypeRef/getWgsl

getWgsl(self): string?

Read the WGSL body directly, ignoring the GLSL fallback. Use when you need to detect "is this shader WGSL-native?" vs the generic getSourceCode lookup that auto-falls-back.

Parameters

  • self any (optional)
local wgsl = shaderRef:getWgsl()

modules/ShaderAssetTypeRef/listBindings

listBindings(self): { string }

List which top-level uniform / storage-buffer block names appear in the shader source — best-effort regex parse. Useful to cross-check against material:getPropertyNames() when debugging a "property doesn't exist" gap. Not a full WGSL parser; complex shaders with macros may report incomplete results.

Parameters

  • self any (optional)
for _, name in ipairs(shaderRef:listBindings()) do print(name) end

modules/ShaderAssetTypeRef/listMaterialsUsing

listMaterialsUsing(self): { string }

List the material identities currently bound to this shader, by scanning the registered material catalogue. O(n) over the material list; cache the result if you call it on a hot path.

Parameters

  • self any (optional)
for _, m in ipairs(shaderRef:listMaterialsUsing()) do print(m) end

modules/ShaderAssetTypeRef/onChange

onChange(ref, change)

Asset-type change callback: (re)compile the shader whenever its WGSL body or properties.yaml is written, and bring materials already bound to it onto the declared property list when that list changed shape. This is the ONLY thing that compiles a .shader — the implicit "WGSL written → recompile" path is gone — so it fires on the initial create (the template write) AND on every later edit, with no world reload. Convergent: see compileShader.

Parameters

  • ref any (optional)
  • change any (optional)

modules/ShaderAssetTypeRef/schemaParseCount

schemaParseCount(): number

How many times a shader's properties.yaml has been parsed in this session, across every shader and every surface that reads one — the editor-discovery method, the compile, the alias registration and the alias lookup. A read whose bytes match the parse already held answers from it and leaves this unchanged, so the count rises once per distinct revision of a file rather than once per read.

local m = require("modules.asset_ref").loadTypeModule("shader"); local before = m.schemaParseCount(); ref:getProperties(); print(m.schemaParseCount() - before)

modules/ShaderAssetTypeRef/setWgsl

setWgsl(self, source: string): boolean

Overwrite the WGSL body on disk. Hot-reload picks the new body up on the next frame so any material using this shader recompiles. Returns true on success.

Parameters

  • self any (optional)
  • source string — New WGSL source.
shaderRef:setWgsl(myWgsl)

modules/ShaderAssetTypeRef/shadingModel

shadingModel(self): string

Which shading model this shader's body uses, and with it what the engine can take the shader apart into.

"engine-lit" — the body exposes fn surface(...) -> PbrSurface. It hands the engine a surface (albedo, roughness, metallic, emissive, normal, occlusion) and the engine lights it, so the capture tool's PBR debug passes read real material channels.

"self-shading" — the body exposes fn fragment(...) -> vec4<f32> and returns the finished pixel. There is no separate albedo to read, so those same passes render what the body returns.

"unknown" — no readable body, or one that exposes neither entry point.

The test is the literal one the compiler applies to the same source, so this reports the model the shader was actually built as.

Parameters

  • self any (optional)
if shaderRef:shadingModel() == "self-shading" then print("albedo pass shows final colour") end

modules/ShaderModuleAssetTypeRef/README

ShaderModuleAssetTypeRef

Per-instance methods exposed on every AssetRef<shaderModule>. Loaded lazily by asset_ref.module.

modules/ShaderModuleAssetTypeRef/getSource

getSource(self): string?

Read this module's WGSL (module.wgsl) as raw text.

Parameters

  • self any (optional)
local src = moduleRef:getSource()

modules/ShaderModuleAssetTypeRef/onRegister

onRegister(self)

Initial-registration callback: file this module's WGSL under every name it answers to the moment the instance first registers, so a shader that #includes it resolves on its first compile rather than only after the module has been written once this session. Idempotent — the guard below skips a re-register of identical source.

Parameters

  • self any (optional)

modules/ShaderModuleAssetTypeRef/register

register(self): string?

Read this module's WGSL through the VFS and file what it holds now under every name it answers to, so a shader compiled next expands this text. Files unconditionally: the registry answers whether the text moved, and keeps the catalog generation still when the same bytes arrive again.

Parameters

  • self any (optional)
local wgsl = moduleRef:register()

modules/ShaderModuleAssetTypeRef/setSource

setSource(self, src: string): boolean

Overwrite this module's WGSL. Every shader that includes it recompiles on the next frame.

Parameters

  • self any (optional)
  • src string — New WGSL source.
moduleRef:setSource(myWgsl)

modules/ShockwaveRingEffect/README

ShockwaveRingEffect

The definition behind shockwaveRing.effect — an annulus expanding across a surface from a point, thinning and fading as it runs out to the radius it was given.

modules/SoundClipAssetTypeRef/README

SoundClipAssetTypeRef

Hooks for .soundClip assets. onCreate is the type's contribution to the generic asset.create("soundClip", name, opts) flow (mirroring texture.assetType). onChange keeps a managed container's data.zaud encoded payload in sync when its source.<ext> or .metadata settings are edited — the asset type reacting to writes inside its own instances (the type-level analogue of a component's onAssetReload). This hook owns every re-encode after the container exists.

modules/SoundClipAssetTypeRef/loopSeam

loopSeam(self): (any?, string?)

Measure what this clip's samples do where a whole-clip loop wraps — the reading that says whether the clip can be looped without a click. The wrap's own step is reported against the step the signal ordinarily makes between neighbouring samples, so the figure is in the units the signal moves in and a quiet ambience reads the same way as a loud drone. A clip whose partials wrap reads near 1; one carrying a strike at its head and silence at its tail reads in the tens, and seamless is ratio <= threshold. Taken on the decoded payload, so it answers for what the codec left behind — including for a clip that arrived already encoded.

Parameters

  • self any (optional)
local seam = clipRef:loopSeam(); if not seam.seamless then print(seam.ratio) end

modules/SoundClipAssetTypeRef/onChange

onChange(ref: any, change: { [string]: any })

React to a write inside a .soundClip/ instance, keeping the encoded data.zaud payload in sync. Editing source.<ext> re-encodes from the new audio. Editing the .metadata settings block re-encodes from the container's source: the managed source.<ext>, or — for a PCM-baked container — the decoded data.zaud itself. data.zaud / README.md writes are ignored. The dispatcher's same-asset guard suppresses the re-dispatch of our own synchronous data.zaud write, so no loop forms.

Parameters

  • ref any (optional) — The AssetRef for the changed container.
  • change { [string]: any }{ path, asset, type, kind, origin }path is the written file, asset the container folder, kind "edited"/"seeded", origin "local"/"remote".

modules/SoundClipAssetTypeRef/onCreate

onCreate(name: string, opts: CreateOpts): { [string]: string }

Generic-creation hook for asset.create("soundClip", name, opts). Pure: returns the content-file map; the caller (asset.create) writes it to the authored destination (/source/<name>.soundClip/) and that write registers the soundClip asset.

Two input shapes, mirroring how clips originate:

  • PCM — { pcm, sampleRate, channels }: generated / decoded interleaved f32 samples, in any container audio.encodePcm reads — a buffer, a binary string of little-endian f32, or a flat number array (no source file). Encoded to the canonical data.zaud payload (the engine's ZAUD format) via audio.encodePcm. data.zaud is the primary the runtime decodes — so a generated clip becomes a persistent, reload-stable asset addressed by guid.
  • ENCODED AUDIO — { bytes, ext }: OGG / MP3 / WAV / FLAC source bytes, kept verbatim as source.<ext>, with data.zaud encoded from them against the instance's settings.

Parameters

  • name string — SoundClip identity (the instance name).
  • opts CreateOpts
asset.create("soundClip", "beep", { pcm = buffer.create(960 * 4), sampleRate = 48000, channels = 1 })
asset.create("soundClip", "music", { bytes = oggBytes, ext = "ogg" })

modules/SoundClipAssetTypeRef/pcm

pcm(self): (string?, number?, number?)

Decode this soundClip asset's data.zaud payload into its samples, the rate they play at, and the number of channels they interleave.

Parameters

  • self any (optional)
local pcm, sr, ch = clipRef:pcm()

modules/SoundClipAssetTypeRef/setSettings

setSettings(self, patch: { [string]: any })

Write a partial settings patch to this soundClip asset's .metadata. Pass any subset of the settings schema; only those keys change, the rest keep their stored value (asset.set_field deep-merges). Unknown keys error loudly. Writing .metadata re-runs the type's onChange, which re-encodes data.zaud against the new settings — so changing bitrateKbps recompresses the clip.

Parameters

  • self any (optional)
  • patch { [string]: any }{ codec: string?, bitrateKbps: number?, vbr: boolean?, sampleRate: number?, forceMono: boolean?, loadType: string?, loopStart: number?, loopEnd: number? }
clipRef:setSettings({ bitrateKbps = 64, forceMono = true })

modules/SoundClipAssetTypeRef/settings

settings(self): { [string]: any }

Read this soundClip asset's settings, with every schema default filled in. The returned table always carries the full settings schema. The settings say how the clip is encoded and where its loop points sit; what its samples do where a whole-clip loop wraps is clipRef:loopSeam().

Parameters

  • self any (optional)
if clipRef:settings().forceMono then ... end

modules/StyleAssetTypeRef/README

StyleAssetTypeRef

Per-instance methods exposed on every AssetRef<style>. Loaded lazily by asset_ref.module.

modules/StyleAssetTypeRef/getInitScript

getInitScript(self): string?

Read the style's entry script as raw text.

Parameters

  • self any (optional)
local src = styleRef:getInitScript()

modules/StyleAssetTypeRef/getReadme

getReadme(self): string?

Read the style's README.

Parameters

  • self any (optional)
print(styleRef:getReadme())

modules/StyleAssetTypeRef/inspect

inspect(self): any

asset.inspect type-specific detail: { tokenCategories }, the top-level keys of the style's own M.tokens = { ... } table literal, parsed from the entry script's source text via luau_introspect.tableLiteralKeys — never requires the style. Cached on the asset's content checksum, so re-inspecting unchanged source is free. A style with no readable entry script returns an empty detail rather than erroring.

Parameters

  • self any (optional)
local tokenCategories = asset.inspect(styleRef).detail.tokenCategories

modules/StyleAssetTypeRef/loadTheme

loadTheme(self): any

Load the style module and return its exported theme table. Raises a Luau error tagged with the style identity if the require fails.

Parameters

  • self any (optional)
local theme = styleRef:loadTheme()

modules/TestSuiteAssetTypeRef/README

TestSuiteAssetTypeRef

Per-instance methods exposed on every AssetRef<testSuite>. Loaded lazily by asset_ref.module. These methods act on the SPECIFIC asset instance the reference points to — asset.resolve(id, "testSuite"):run() runs only THAT suite. Running every suite is the tests toolbox's job (it iterates asset.list("testSuite") and calls :run() on each); it is deliberately not offered here.

modules/TestSuiteAssetTypeRef/getReadme

getReadme(self): string?

Read this suite's README.md body.

Parameters

  • self any (optional)
print(suiteRef:getReadme())

modules/TestSuiteAssetTypeRef/inspect

inspect(self): any

asset.inspect type-specific detail: { testCount, testNames }, parsed from Test.it("<name>", ...) calls in this suite's own init.luau text. A string scan, not a suite run — inspecting a suite never registers or executes its tests. A suite with no readable init.luau returns an empty testNames list rather than erroring.

Parameters

  • self any (optional)
local testNames = asset.inspect(suiteRef).detail.testNames

modules/TestSuiteAssetTypeRef/run

run(self, opts: { quiet: boolean? }?): any

Run THIS test suite (only this one) and return its results. Refuses in play mode — tests are an edit-mode / authoring concern, not gameplay. The run is cooperative (the framework yields a frame between tests) so it never blocks the engine or trips the execute watchdog.

Parameters

  • self any (optional)
  • opts { quiet: boolean? }? (optional) — Optional { quiet }.
local r = asset.resolve("vfs", "testSuite"):run()

modules/TestSuiteAssetTypeRef/tests

tests(self): { { name: string, skip: boolean } }

List the tests this suite declares (loads THIS asset's init.luau to read its registered Test.it / Test.skip names; does not run them).

Parameters

  • self any (optional)
for _, t in ipairs(suiteRef:tests()) do print(t.name) end

modules/TextureAssetTypeRef/README

TextureAssetTypeRef

Hooks for .texture assets. onCreate is the type's contribution to the generic asset.create("texture", name, opts) flow (mirroring material.assetType). onChange keeps a MANAGED container's data.ztex encoded payload in sync when its source.<ext> or .metadata settings are edited — the asset type reacting to writes inside its own instances (the type-level analogue of a component's onAssetReload). The loose-image → container PROMOTE step is the separate texture.importer (loose-write seam); this hook owns every re-encode AFTER the container exists.

modules/TextureAssetTypeRef/handle

handle(self)

Put this texture asset on the GPU under its own guid and return its TextureHandle 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(guid) reports the arrival. The handle is cached on the interned ref's shared runtime table for as long as something holds the one it handed out, so every consumer and material slot naming the asset over that time gets the SAME handle back → ONE GPU entry. Once the last of them lets go, the next call asks for the asset again and is answered with a handle on the texture the device already holds under that guid — which is also what a mode flip's runtime wipe leads to. The material binds this handle's guid; it never creates the GPU texture itself.

CPU lifecycle: governed by the asset's SERIALIZABLE keepCpu setting in its .metadata settings block (asset.set_field(ref, "settings", { keepCpu = true })). By DEFAULT (unset) the decoded pixels are dropped right after the GPU upload (the GPU handle holds no data → no double-memory cost); with keepCpu = true the CPU store keeps them for later pixel reads / edits.

Parameters

  • self any (optional)
local h = texRef:handle() -- bind h.guid on a material; it draws once resident

modules/TextureAssetTypeRef/load

load(self)

Load this .texture asset's pixels into the guid-keyed CPU store and return a CPU handle for per-pixel access (no GPU readback). The handle holds no pixels — only the guid, dims, texel format, and the read/write/encode/ unload ops. The pixels stay at the precision they were authored with: handle.format is "rgba8", "rgba16" or "rgba32f", and :readPixel reports channels in that format's own units. Upload to the GPU with renderer.texture.create(handle); the DEFAULT is to handle:unload() after. The handle's :encode() re-encodes the (possibly edited) pixels into a fresh ZTEX blob for asset.create("texture", …), at the same format.

Parameters

  • self any (optional)
local cpu = texRef:load(); local r,g,b,a = cpu:readPixel(3, 4); cpu:unload()

modules/TextureAssetTypeRef/onChange

onChange(ref: any, change: { [string]: any })

React to a write inside a .texture/ instance, keeping the encoded data.ztex payload in sync. Editing source.<ext> re-encodes from the new image. Editing the .metadata settings block re-encodes from the container's source: the managed source.<ext>, a plain container's image primary, or — for a raw-pixel container — the decoded data.ztex itself. Every pixel change also regenerates the container's preview.png. README.md writes are ignored. The dispatcher's same-asset guard suppresses the re-dispatch of our own synchronous data.ztex write, so no loop forms.

Parameters

  • ref any (optional) — The AssetRef for the changed container.
  • change { [string]: any }{ path, asset, type, kind, origin }path is the written file, asset the container folder, kind "edited"/"seeded", origin "local"/"remote".

modules/TextureAssetTypeRef/onCreate

onCreate(name: string, opts: CreateOpts): { [string]: string }

Generic-creation hook for asset.create("texture", name, opts). Pure: returns the content-file map; the caller (asset.create) writes it to the authored destination (/source/<name>.texture/) and that write registers the texture asset. Disk-only — nothing is uploaded to the GPU here.

Two input shapes, mirroring how textures originate:

  • RAW PIXELS — { rgba, width, height, format? }: a generated / decoded pixel buffer (no source image). Encoded to the canonical data.ztex payload (the engine's ZTEX format; zero_texture::blob) via __texture.encode. data.ztex is the primary the renderer uploads — so a generated texture becomes a persistent, reload-stable asset addressed by guid, never an ephemeral GPU handle. format picks the on-disk precision: 8 bits per channel by default, or "rgba16" / "rgba32f" for a data raster (height / displacement field, baked lightmap, imported elevation) whose values 8 bits would quantize.
  • ENCODED IMAGE — { bytes, ext }: PNG / JPEG / WebP / … source bytes, stored verbatim as the <name>.<ext> primary (the renderer image-decodes on upload; the same shape asset.create produced before).

Parameters

  • name string — Texture identity (the instance name).
  • opts CreateOpts
asset.create("texture", "skyGradient", { rgba = pixels, width = 256, height = 256 })
asset.create("texture", "terrain_height", { rgba = heights, width = 512, height = 512, format = "rgba16" })
asset.create("texture", "brick_albedo", { rgba = pixels, width = 256, height = 256, format = "bc7_srgb" })
asset.create("texture", "bricks", { bytes = imageBytes, ext = "jpg" })

modules/TextureAssetTypeRef/preview

preview(self, opts: { [string]: any }?)

Render a preview of this texture as a flat swatch.

Parameters

  • self any (optional)
  • opts { [string]: any }? (optional){ size? = { width, height } }.
local p = texRef:preview()

modules/TextureAssetTypeRef/setSettings

setSettings(self, patch: { [string]: any })

Write a partial settings patch to this texture asset's .metadata. Pass any subset of the settings schema; only those keys change, the rest keep their stored value (asset.set_field deep-merges). Unknown keys error loudly. Writing .metadata re-runs the type's onChange, which re-encodes data.ztex against the new settings — so changing filter recompiles the texture and the renderer rebinds it with the new sampler.

Parameters

  • self any (optional)
  • patch { [string]: any }{ format: string?, generateMipmaps: boolean?, maxDimension: number?, filter: string?, keepCpu: boolean? }
texRef:setSettings({ filter = "nearest" })

modules/TextureAssetTypeRef/settings

settings(self): { [string]: any }

Read this texture asset's settings, with every schema default filled in. The returned table always carries the full settings schema.

Parameters

  • self any (optional)
if texRef:settings().filter == "nearest" then ... end

modules/TextureRef/README

TextureRef

The GPU key a material's texture slot binds by, resolved from whatever form the author wrote it in. Every path that accepts a texture reference — the .material assetType, renderer.material.create, renderer.material.setTexture — resolves through here, so the same string binds the same texture wherever it is written.

modules/TextureRef/isProcedural

isProcedural(ref: string): boolean

Whether a reference is one the GPU texture cache resolves on its own (color: / default: / runtime:), so it must be passed through untouched rather than looked up as an asset.

Parameters

  • ref string — The reference string.
TextureRef.isProcedural("color:1,0,0,1") -- true

modules/TextureRef/resolve

resolve(ref: any, altIdentity: string?): (string, string)

Resolve a texture reference to the resident GPU key the renderer binds a slot by, materialising the texture on the way. Accepts a guid, an asset identity, a bare name, a .texture path, or the image path a texture was imported from. color: / default: / runtime: forms and live GPU handles (a video frame, a render target) pass through untouched.

Parameters

  • ref any (optional) — The authored reference.
  • altIdentity string? (optional) — Optional second candidate, tried when ref resolves to nothing — a slot's stable identity, so a binding whose guid was orphaned by a delete + recreate still finds the texture the author named.
TextureRef.resolve("wall.texture") -- "9f2c…", "asset"

modules/TextureRef/unresolvedMessage

unresolvedMessage(ref: string, what: string): string

The message describing a reference that names no texture. Bound anyway, the slot renders the shader's declared fallback, so the caller says this rather than letting the material render as though nothing was asked for.

Parameters

  • ref string — The unresolved reference.
  • what string — The call being made, for the message's subject.
TextureRef.unresolvedMessage("sky.jpg", "renderer.material.setTexture")

modules/ToolAssetTypeRef/README

ToolAssetTypeRef

Per-instance methods exposed on every AssetRef<tool>. Loaded lazily by asset_ref.module.

modules/ToolAssetTypeRef/getDefinition

getDefinition(self): string?

Read the tool's definition — its init.luau source, which carries the typed signature and the --!desc/--!arg/--!return/--!example docstring that together form the tool's schema.

Parameters

  • self any (optional)
local raw = toolRef:getDefinition()

modules/ToolAssetTypeRef/getInitScript

getInitScript(self): string?

Read the tool's entry script as raw text.

Parameters

  • self any (optional)
local src = toolRef:getInitScript()

modules/ToolAssetTypeRef/getReadme

getReadme(self): string?

Read the tool's README body.

Parameters

  • self any (optional)
print(toolRef:getReadme())

modules/ToolAssetTypeRef/inspect

inspect(self): any

asset.inspect type-specific detail: { desc, args, returns, examples, signature }, parsed from the tool's own entry script (getInitScript) via luau_introspect.docstrings — the same --!desc/--!arg/--!return/--!example docstring that documents the tool's schema. Cached on the asset's content checksum, so re-inspecting unchanged source is free. A tool with no readable entry script returns an empty detail rather than erroring.

Parameters

  • self any (optional)
local args = asset.inspect(toolRef).detail.args

modules/ToolAssetTypeRef/run

run(self, args: any): any

Invoke the tool's entry script. Requires the tool's identity and calls the first function it exports — the canonical function M.<name>(args) shape every tool ships. args is forwarded verbatim. Raises a Luau error (tagged with the tool's identity) on load or invocation failure rather than returning a swallowed nil — failing loudly matches the rest of the engine's tool dispatch surface.

Parameters

  • self any (optional)
  • args any (optional) — Optional table of arguments to pass to the tool's entry.
toolRef:run({ subject = "world" })

modules/ToolAssetTypeRef/wrap

wrap(fn: (...any) -> ...any, regionName: string): (...any) -> ...any

Standardize a single tool function into a ZmToolResult-returning call. THIS is where the tool-result contract lives — the engine's tool-bind paths (boot + runtime) apply wrap to every tool function so the result type is a system guarantee, never the individual tool's choice. The tool body returns its raw value, returns the Lua failure convention (nil, reason), or raises with error(msg); wrap produces the canonical envelope { ok, value | error, durationMs, tool } from all three — a (nil, reason) return becomes { ok = false, error = reason } instead of silently dropping the reason and reporting a bare success with no value. Timing is measured with the engine profiler: a region named for the tool is opened when the call is invoked and closed when it returns, so each invocation is both timed (region elapsed -> durationMs) and visible as a profiler region. Each envelope is also reported into the running task's tool-result buffer, which execute() auto-surfaces as the response's toolResults.

A tool that states MORE than one value on success — a path AND what that path holds — keeps them: the envelope carries the first as value, and every value past it follows the envelope out, so local a, b, c = tools.use(...) reads the tool the way the tool's own signature declares it. Keeping only the first turns every such declaration into a promise nobody can read.

Parameters

  • fn (...any) -> ...any
  • regionName string

Returns ...any

modules/ToolAssetTypeRef/wrapToolbox

wrapToolbox(box: { [string]: any }, namespace: string): { [string]: any }

Wrap every function on a toolbox table so each tool call returns a ZmToolResult (see wrap). Non-function fields pass through untouched.

Parameters

  • box { [string]: any }
  • namespace string

modules/ToolboxAssetTypeRef/README

ToolboxAssetTypeRef

Per-instance methods exposed on every AssetRef<toolbox>. Loaded lazily by asset_ref.module.

modules/ToolboxAssetTypeRef/getReadme

getReadme(self): string?

Read the toolbox's README.md body.

Parameters

  • self any (optional)
print(toolboxRef:getReadme())

modules/ToolboxAssetTypeRef/hasShared

hasShared(self): boolean

Check whether the toolbox ships a shared.module/.

Parameters

  • self any (optional)
if toolboxRef:hasShared() then ... end

modules/ToolboxAssetTypeRef/inspect

inspect(self): any

asset.inspect type-specific detail: { tools }, where tools is { name, desc } for every *.tool/ child the toolbox owns, walked directly from the toolbox's own folder — desc is the child tool's own --!desc docstring line, read from its entry script without requiring the tool module. A toolbox with no readable folder returns an empty tools list rather than erroring.

Parameters

  • self any (optional)
local boxTools = asset.inspect(toolboxRef).detail.tools

modules/ToolboxAssetTypeRef/listTools

listTools(self): { any }

List the tool refs the toolbox carries. Walks the toolbox folder, finds every *.tool/ child, and resolves each to an AssetRef<tool>.

Parameters

  • self any (optional)
for _, t in ipairs(toolboxRef:listTools()) do print(t.identity) end

modules/TracerEffect/README

TracerEffect

The definition behind tracer.effect — a hot head running a span at a muzzle velocity, with a tail drawn out behind it.

modules/Trails/README

require("@builtin/systems/trails.package/trails") -- Trails

Continuous ribbon geometry that follows a moving point.

A trail is a strip of quads threaded along the positions something occupied over the last few seconds. Each recorded point contributes two vertices, offset either side of the direction of travel, and consecutive points are joined into a continuous surface — so a spark, a wingtip vapour trail or a sword arc is one mesh rather than a queue of sprites that betrays its spacing when the subject moves fast. Width, colour and opacity run from the head (the newest point) to the tail (the oldest), indexed by each point's age rather than its position in the list, so the fade reads the same whether the subject is crawling or sprinting. ribbon is pure: the same points, camera and settings produce the same arrays, so a trail's geometry can be checked without a scene.

Usage: local Trails = require("@builtin/systems/trails.package/trails")

modules/Trails/expire

expire(path: table, now: number) -> number

Drop points older than lifetime. Returns how many went.

Parameters

  • path table
  • now number

Returns number

modules/Trails/newPath

newPath(opts: table?) -> table

A fresh path. lifetime seconds a point survives, minDistance metres before a new one is recorded, maxPoints the ring's bound.

Parameters

  • opts table? (optional)

Returns table

modules/Trails/pathOf

pathOf(id: string) -> table?

Parameters

  • id string

Returns table?

modules/Trails/push

push(path: table, x: number, y: number, z: number, now: number) -> boolean

Record the head position when it has travelled minDistance from the last point. Returns whether a point was recorded.

Parameters

  • path table
  • x number
  • y number
  • z number
  • now number

Returns boolean

modules/Trails/register

register(id: string, path: table)

Parameters

  • id string
  • path table

modules/Trails/ribbon

ribbon(path: table, opts: table) -> table?

Build the ribbon over a path's points — flat positions / normals / uvs / colors / indices, the shape renderer.mesh.create takes. nil under two points, which is no surface.

Parameters

  • path table
  • opts table

Returns table?

modules/Trails/stats

stats() -> table

Returns table

modules/Trails/unregister

unregister(id: string)

Parameters

  • id string

modules/Transform/README

Transform (global)

Math helpers for positions, rotations, and directions on transforms. Exposed as the global Transform table; entity-aware helpers accept an id string or an entity proxy. Also available as global: Transform

modules/Transform/direction

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

  • fromX number — From x.
  • fromY number — From y.
  • fromZ number — From z.
  • toX number — To x.
  • toY number — To y.
  • toZ number — To z.
local dx, dy, dz = Transform.direction(0, 0, 0, 1, 0, 0)

modules/Transform/directionBetween

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

  • entityA string | EntityRef — Source entity (id string or proxy).
  • entityB string | EntityRef — Target entity (id string or proxy).
local dx, dy, dz = Transform.directionBetween("cam", "target")

modules/Transform/distance

distance(x1: number, y1: number, z1: number, x2: number, y2: number, z2: number): number

Euclidean distance between two world-space positions.

Parameters

  • x1 number — First point x.
  • y1 number — First point y.
  • z1 number — First point z.
  • x2 number — Second point x.
  • y2 number — Second point y.
  • z2 number — Second point z.
local d = Transform.distance(0, 0, 0, 1, 1, 1)

modules/Transform/distanceBetween

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

  • entityA string | EntityRef — First entity (id string or proxy).
  • entityB string | EntityRef — Second entity (id string or proxy).
local d = Transform.distanceBetween("cam", "box")

modules/Transform/euler

euler(qx: number, qy: number, qz: number, qw: number): (number, number, number)

Convert quaternion to euler angles (yaw, pitch, roll) in radians.

Parameters

  • qx number — Quaternion x.
  • qy number — Quaternion y.
  • qz number — Quaternion z.
  • qw number — Quaternion w.
local yaw, pitch, roll = Transform.euler(0, 0, 0, 1)

modules/Transform/eulerToQuat

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

  • yaw number — Y-axis rotation in radians.
  • pitch number? (optional) — X-axis rotation in radians. Defaults to 0.
  • roll number? (optional) — Z-axis rotation in radians. Defaults to 0.
local qx, qy, qz, qw = Transform.eulerToQuat(math.pi / 2)

modules/Transform/lerp

lerp(ax: number, ay: number, az: number, bx: number, by: number, bz: number, t: number): (number, number, number)

Linearly interpolate between two positions.

Parameters

  • ax number — Start x.
  • ay number — Start y.
  • az number — Start z.
  • bx number — End x.
  • by number — End y.
  • bz number — End z.
  • t number — Interpolation factor [0, 1].
local x, y, z = Transform.lerp(0, 0, 0, 1, 1, 1, 0.5)

modules/Transform/lerp1

lerp1(a: number, b: number, t: number): number

Linearly interpolate two scalars.

Parameters

  • a number — Start value.
  • b number — End value.
  • t number — Interpolation factor [0, 1].
local v = Transform.lerp1(0, 10, 0.5)

modules/Transform/lerpAngle

lerpAngle(a: number, b: number, t: number): number

Lerp between two angles via the shortest arc; returns a value in [-pi, pi].

Parameters

  • a number — Start angle in radians.
  • b number — End angle in radians.
  • t number — Interpolation factor [0, 1].
local a = Transform.lerpAngle(0, math.pi, 0.5)

modules/Transform/localToWorld

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

  • px number — Parent position x.
  • py number — Parent position y.
  • pz number — Parent position z.
  • pqx number — Parent rotation x.
  • pqy number — Parent rotation y.
  • pqz number — Parent rotation z.
  • pqw number — Parent rotation w.
  • lx number — Local x.
  • ly number — Local y.
  • lz number — Local z.
local wx, wy, wz = Transform.localToWorld(px, py, pz, pqx, pqy, pqz, pqw, lx, ly, lz)

modules/Transform/lookAt

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

  • entityOrId string | EntityRef — Entity id, name, or proxy for the entity to rotate.
  • txOrTarget any (optional) — A number (world x), a point table, or an entity id / name / proxy whose world position is resolved as the look-at target.
  • ty any? (optional) — World y of the target. Omitted when txOrTarget is a point or an entity.
  • tz number? (optional) — World z of the target. Omitted when txOrTarget is a point or an entity.
  • up any? (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.
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

modules/Transform/lookAtQuat

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

  • fx number — Origin x.
  • fy number — Origin y.
  • fz number — Origin z.
  • tx number — Target x.
  • ty number — Target y.
  • tz number — Target z.
local qx, qy, qz, qw = Transform.lookAtQuat(0, 0, 0, 1, 0, 1)

modules/Transform/lookRotation

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

  • fx number — Eye x — where the entity stands.
  • fy number — Eye y.
  • fz number — Eye z.
  • tx number — Target x — the world point it faces.
  • ty number — Target y.
  • tz number — Target z.
  • ux number? (optional) — Up hint x. World +Y when the hint is omitted.
  • uy number? (optional) — Up hint y.
  • uz number? (optional) — Up hint z.
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) }

modules/Transform/normalizeAngle

normalizeAngle(a: number): number

Normalize an angle into [-pi, pi].

Parameters

  • a number — The angle in radians.
local a = Transform.normalizeAngle(3 * math.pi)

modules/Transform/orbit

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

  • centerX number — Center x.
  • centerY number — Center y.
  • centerZ number — Center z.
  • radius number — Horizontal distance from the center.
  • height number — Vertical offset from centerY.
  • angle number — Orbital angle in radians.
local x, y, z, qx, qy, qz, qw = Transform.orbit(0, 1, 0, 5, 2, t)

modules/Transform/quatFromAxisAngle

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

  • ax number — Axis x.
  • ay number — Axis y.
  • az number — Axis z.
  • angle number — Rotation angle in radians.
local qx, qy, qz, qw = Transform.quatFromAxisAngle(0, 1, 0, math.pi)

modules/Transform/quatFromBasis

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

  • rx number — Right axis x.
  • ry number — Right axis y.
  • rz number — Right axis z.
  • ux number — Up axis x.
  • uy number — Up axis y.
  • uz number — Up axis z.
  • fx number — Forward axis x.
  • fy number — Forward axis y.
  • fz number — Forward axis z.
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)

modules/Transform/quatFromYaw

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

  • yaw number — Rotation in radians around the Y axis.
local qx, qy, qz, qw = Transform.quatFromYaw(math.pi / 2)

modules/Transform/quatFromYawPitch

quatFromYawPitch(yaw: number, pitch: number): (number, number, number, number)

Create quaternion from yaw and pitch in radians.

Parameters

  • yaw number — Y-axis rotation in radians.
  • pitch number — X-axis rotation in radians.
local qx, qy, qz, qw = Transform.quatFromYawPitch(0, math.pi / 4)

modules/Transform/quatIdentity

quatIdentity(): (number, number, number, number)

Identity quaternion (0, 0, 0, 1).

local qx, qy, qz, qw = Transform.quatIdentity()

modules/Transform/quatInverse

quatInverse(qx: number, qy: number, qz: number, qw: number): (number, number, number, number)

Quaternion inverse. Equal to the conjugate for unit quaternions.

Parameters

  • qx number — Quaternion x.
  • qy number — Quaternion y.
  • qz number — Quaternion z.
  • qw number — Quaternion w.
local ix, iy, iz, iw = Transform.quatInverse(qx, qy, qz, qw)

modules/Transform/quatMul

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

  • ax number — Left quat x.
  • ay number — Left quat y.
  • az number — Left quat z.
  • aw number — Left quat w.
  • bx number — Right quat x.
  • by number — Right quat y.
  • bz number — Right quat z.
  • bw number — Right quat w.
local qx, qy, qz, qw = Transform.quatMul(ax, ay, az, aw, bx, by, bz, bw)

modules/Transform/quatRotateVec

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

  • qx number — Quaternion x.
  • qy number — Quaternion y.
  • qz number — Quaternion z.
  • qw number — Quaternion w.
  • vx number — Vector x.
  • vy number — Vector y.
  • vz number — Vector z.
local rx, ry, rz = Transform.quatRotateVec(qx, qy, qz, qw, 1, 0, 0)

modules/Transform/quatToEuler

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

  • qx number — Quaternion x.
  • qy number — Quaternion y.
  • qz number — Quaternion z.
  • qw number — Quaternion w.
local yaw, pitch, roll = Transform.quatToEuler(qx, qy, qz, qw)

modules/Transform/readVec3

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

  • value Vec3Input — The vector to normalize.
  • label string? (optional) — Name reported in the error when the value is not a vector. Defaults to "Transform".
local v = Transform.readVec3({ x = 1, y = 2, z = 3 })

modules/Transform/slerp

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

  • ax number — Start quaternion x.
  • ay number — Start quaternion y.
  • az number — Start quaternion z.
  • aw number — Start quaternion w.
  • bx number — End quaternion x.
  • by number — End quaternion y.
  • bz number — End quaternion z.
  • bw number — End quaternion w.
  • t number — Interpolation factor [0, 1].
local qx, qy, qz, qw = Transform.slerp(0, 0, 0, 1, 1, 0, 0, 0, 0.5)

modules/Transform/snapVec3

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 }.
  • step number | Vec3Input — Uniform step size, or a per-axis vector of step sizes.
local v = Transform.snapVec3({ 1.4, 2.6, -0.4 }, 1)

modules/Transform/toQuaternion

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

  • rotation any (optional) — The rotation to normalize, in any form of the RotationInput union.
  • label string? (optional) — Name reported in the error when the value is not a rotation. Defaults to "Transform".
local q = Transform.toQuaternion({ pitch = 0, yaw = 90, roll = 0 })

modules/Transform/tryQuaternion

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

  • rotation any (optional) — The value to read as a rotation.
  • label string? (optional) — Name reported in the message. Defaults to "Transform".
local q, why = Transform.tryQuaternion(value, "myTool")

modules/Transform/vec.add

vec.add(ax: number, ay: number, az: number, bx: number, by: number, bz: number): (number, number, number)

Component-wise vec3 addition.

Parameters

  • ax number — First vector x.
  • ay number — First vector y.
  • az number — First vector z.
  • bx number — Second vector x.
  • by number — Second vector y.
  • bz number — Second vector z.
local x, y, z = Transform.vec.add(1, 2, 3, 4, 5, 6)

modules/Transform/vec.cross

vec.cross(ax: number, ay: number, az: number, bx: number, by: number, bz: number): (number, number, number)

Cross product a x b.

Parameters

  • ax number — First vector x.
  • ay number — First vector y.
  • az number — First vector z.
  • bx number — Second vector x.
  • by number — Second vector y.
  • bz number — Second vector z.
local cx, cy, cz = Transform.vec.cross(1, 0, 0, 0, 1, 0)

modules/Transform/vec.dot

vec.dot(ax: number, ay: number, az: number, bx: number, by: number, bz: number): number

Dot product of two vec3s.

Parameters

  • ax number — First vector x.
  • ay number — First vector y.
  • az number — First vector z.
  • bx number — Second vector x.
  • by number — Second vector y.
  • bz number — Second vector z.
local d = Transform.vec.dot(1, 0, 0, 0, 1, 0)

modules/Transform/vec.length

vec.length(x: number, y: number, z: number): number

Euclidean length of a vec3.

Parameters

  • x number — Vector x.
  • y number — Vector y.
  • z number — Vector z.
local len = Transform.vec.length(1, 2, 3)

modules/Transform/vec.normalize

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

  • x number — Vector x.
  • y number — Vector y.
  • z number — Vector z.
local nx, ny, nz = Transform.vec.normalize(0, 5, 0)

modules/Transform/vec.scale

vec.scale(x: number, y: number, z: number, s: number): (number, number, number)

Component-wise scalar multiplication of a vec3.

Parameters

  • x number — Vector x.
  • y number — Vector y.
  • z number — Vector z.
  • s number — Scalar factor.
local x, y, z = Transform.vec.scale(1, 2, 3, 2)

modules/Transform/vec.sub

vec.sub(ax: number, ay: number, az: number, bx: number, by: number, bz: number): (number, number, number)

Component-wise vec3 subtraction (a - b).

Parameters

  • ax number — First vector x.
  • ay number — First vector y.
  • az number — First vector z.
  • bx number — Second vector x.
  • by number — Second vector y.
  • bz number — Second vector z.
local x, y, z = Transform.vec.sub(4, 5, 6, 1, 2, 3)

modules/Transform/worldToLocal

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

  • px number — Parent position x.
  • py number — Parent position y.
  • pz number — Parent position z.
  • pqx number — Parent rotation x.
  • pqy number — Parent rotation y.
  • pqz number — Parent rotation z.
  • pqw number — Parent rotation w.
  • wx number — World x.
  • wy number — World y.
  • wz number — World z.
local lx, ly, lz = Transform.worldToLocal(px, py, pz, pqx, pqy, pqz, pqw, wx, wy, wz)

modules/UiMotionCapture/README

UiMotionCapture

Publishes the UI-animation capture views — the UI analogues of the motion_vectors pass. capture pass=ui_motion shows WHERE the UI is animating (change-magnitude heatmap); capture pass=ui_motion_flow shows WHICH WAY it is moving (directional optical flow). Each view's drawing feature is created on demand the first time it is captured, so a session that never selects one pays nothing.

modules/UiMotionCapture/register

register()

Register the ui_motion (change magnitude) and ui_motion_flow (directional flow) capture views. Idempotent — safe to call at boot and again later; re-registering keeps each view's channel.

require("modules.ui_motion_capture").register()

modules/Validator/README

require("@builtin/systems/worldValidation.package/validator") -- Validator

Orchestrator for the world validator. Composes vfsScanner + scriptValidator + assetValidator + reportFormatter into one call that produces a Report with world and library buckets cleanly separated.

The validator does ONE pass over the VFS to discover every script and asset, then dispatches each entry to the appropriate per-type validator (scripts → scriptValidator → lsp.check; asset folders → assetValidator → asset.validate). It does not spawn entities or allocate GPU resources. Repeated calls are independent — there is no shared mutable state across runs. The four helper modules (vfsScanner, scriptValidator, assetValidator, reportFormatter) sit alongside this module at the package root and are pulled in via the ~.X package-relative form so the whole package is position-independent — moving it to any identity continues to resolve correctly without code edits.

Usage: local Validator = require("@builtin/systems/worldValidation.package/validator")

modules/Validator/check

check(opts)

Validate authored content. By DEFAULT scopes to YOUR world — everything under /source/ EXCEPT /source/libs/ — because imported libraries and engine builtins are not yours to validate (and scanning the whole builtin tree is slow and noisy). Pass opts.scope to widen: "libraries" for every imported library, "library:<name>" for one, "all" for world + libraries together. The cargo check equivalent for a Zero world.

Parameters

  • opts any (optional) — Optional table — scope?: "world"|"libraries"|"library:<name>"|"all" (default "world") plus filter fields forwarded to the formatter: { severity, category, code, source, path, includePlaceholders, limit }.
local r = WorldValidation.check()                         -- your world only
local r = WorldValidation.check({ severity = "error" })   -- your world, errors only
local r = WorldValidation.check({ scope = "all" })        -- world + imported libraries

modules/Validator/filter

filter(report, opts)

Re-filter an existing Report without re-scanning the VFS. Forwards to reportFormatter.filter; the resulting Report's counts are recomputed from the visible problems.

Parameters

  • report any (optional) — Report produced by any of the validate* functions.
  • opts any (optional) — FilterOpts — { severity, category, code, source, path, includePlaceholders, limit }.
local errs = WorldValidation.filter(report, { severity = "error" })

modules/Validator/format

format(report, format)

Render a Report into a string. format selects the renderer.

Parameters

  • report any (optional) — Report produced by any of the validate* functions.
  • format any (optional)"human" (default) / "markdown" / "json" / "summary".
print(WorldValidation.format(report, "human"))
local md = WorldValidation.format(report, "markdown")

modules/Validator/placeholders

placeholders()

Enumerate the registered placeholder checks. Each placeholder is a check the validator runs today but with a stub implementation — listing them tells callers which validations still need real primitives wired up.

for _, ph in ipairs(WorldValidation.placeholders()) do print(ph.code) end

modules/Validator/saveReport

saveReport(report, path, opts)

Render a Report and write it to path. Format is taken from opts.format if present, otherwise inferred from the destination extension (.md → markdown, .json → json, anything else → human). Returns (ok, message?).

Parameters

  • report any (optional) — Report produced by any of the validate* functions.
  • path any (optional) — Destination VFS path (e.g. /source/.validation/run.md).
  • opts any (optional) — Optional { format } override.
WorldValidation.saveReport(report, "/source/.validation/run.md")
WorldValidation.saveReport(report, "/source/audit.json", { format = "json" })

modules/Validator/summary

summary(report)

Compact one-line health summary — world: NE/NW libraries: NE/NW total: OK|FAIL.

Parameters

  • report any (optional) — Report produced by any of the validate* functions.
print(WorldValidation.summary(WorldValidation.check()))

modules/Validator/validateLibraries

validateLibraries(opts)

Validate every imported library under /source/libs/. The returned Report has world = nil and libraries populated for every library that exists on disk.

Parameters

  • opts any (optional) — Optional filter table — same shape as M.check.
local r = WorldValidation.validateLibraries()
local r = WorldValidation.validateLibraries({ source = "library:@builtin" })

modules/Validator/validateLibrary

validateLibrary(name, opts)

Validate ONE named library under /source/libs/<name>/. When the library does not exist, the Report carries a single library.missing error against that name so callers can tell a clean run from a missing-dependency run.

Parameters

  • name any (optional) — Library directory name (e.g. "@builtin", "@mylib").
  • opts any (optional) — Optional filter table — same shape as M.check.
local r = WorldValidation.validateLibrary("@builtin")
local r = WorldValidation.validateLibrary("@mylib", { severity = "error" })

modules/Validator/validateWorld

validateWorld(opts)

Validate ONLY the world's authored content (/source/ excluding /source/libs/). The returned Report has world populated and libraries = {}.

Parameters

  • opts any (optional) — Optional filter table — same shape as M.check.
local r = WorldValidation.validateWorld()
local r = WorldValidation.validateWorld({ severity = "error" })

modules/ValueType/README

ValueType

Converts a handle-backed value type — ColorSequence, NumberSequence — between the live object a session holds and the durable payload its serialize() produces. Every boundary that writes a component field to a record, or applies a record back onto a component, converts here.

modules/ValueType/bind

bind(v: any, kind: string): any

Put a value type's methods back on a value read out of a component field. Field.table keeps the table it is handed without its metatable and gives that stored table back on every read, so binding once makes the field answer :evaluate / :keypoints for the rest of the session.

Parameters

  • v any (optional) — A live value carrying a handle.
  • kind string — The kind to bind as when v names none itself.
ValueType.bind(component.color, "ColorSequence"):evaluate(0)

modules/ValueType/isLive

isLive(v: any): boolean

Whether a value is a live handle-backed value type — the form that holds a session-local curve handle.

Parameters

  • v any (optional) — Any component field value.
ValueType.isLive(NumberSequence.new(0, 1)) -- true

modules/ValueType/isPayload

isPayload(v: any): boolean

Whether a value is the durable payload of a handle-backed value type — the form a record carries.

Parameters

  • v any (optional) — Any component field value.
ValueType.isPayload(NumberSequence.new(0, 1):serialize()) -- true

modules/ValueType/keypoints

keypoints(v: any, kind: string): { any }?

The keypoint list a value holds, whichever of the two forms it is in. A caller that knows the field's type passes it as kind so a bare { __h = n } — a handle written by a session that named no kind — is still read as that type.

Parameters

  • v any (optional) — A live value, a durable payload, or a bare handle table.
  • kind string — The kind to read v as when v names none itself.
ValueType.keypoints(field, "ColorSequence")

modules/ValueType/kindOf

kindOf(v: any): string?

The kind name a value declares, when it is one this module converts.

Parameters

  • v any (optional) — Any component field value.
ValueType.kindOf(ColorSequence.new({1, 0, 0})) -- "ColorSequence"

modules/ValueType/revive

revive(v: any): any?

The live value a durable payload names, rebuilt in this session.

Parameters

  • v any (optional) — Any component field value.
ValueType.revive(record.color)

modules/ValueType/serialize

serialize(v: any): any?

The durable payload for a live value — what a record stores in place of the session-local handle.

Parameters

  • v any (optional) — Any component field value.
ValueType.serialize(field) -- { kind = "ColorSequence", keypoints = {...} }

modules/ValueType/withRevived

withRevived(data: any): (any, number)

The component field map to apply, with every durable payload rebuilt as the live value it names — the counterpart of the saver writing payloads in place of handles.

Parameters

  • data any (optional) — A component's { field = value } map from a record.
ValueType.withRevived(record.data)

modules/VfsScanner/README

require("@builtin/systems/worldValidation.package/vfsScanner") -- VfsScanner

Recursive /source/ walker for the world validator. Separates world content from imported libraries so downstream validators can attribute every problem to the right bucket.

An asset root is any folder whose name has a registered asset type suffix (Foo.component, bar.module, Baz.toolbox, …). When the scanner crosses one it records the root and does NOT recurse into it as raw files — the asset validator handles the interior. Scripts (.luau, .lua) discovered outside any asset root are also recorded (e.g. _shared.luau siblings inside a toolbox, or stray top-level scripts).

Usage: local VfsScanner = require("@builtin/systems/worldValidation.package/vfsScanner")

modules/VfsScanner/assetSuffixes

assetSuffixes(): { string }

Expose the registered asset-suffix list (read-only). External callers that want to recognise asset folders the same way the scanner does can iterate this list.

for _, s in ipairs(VfsScanner.assetSuffixes()) do print(s) end

modules/VfsScanner/listLibraryNames

listLibraryNames(): { string }

Enumerate the immediate children of /source/libs/. Each child is a library identity (e.g. @builtin, @mylib). Returns an empty array if /source/libs/ does not exist.

for _, name in ipairs(VfsScanner.listLibraryNames()) do print(name) end

modules/VfsScanner/scanAll

scanAll()

Convenience: scan world + every imported library in one call.

local scan = VfsScanner.scanAll(); print(#scan.world.assets)

modules/VfsScanner/scanLibraries

scanLibraries(): { [string]: any }

Walk every imported library and return a map keyed by name.

local libs = VfsScanner.scanLibraries(); for n, b in pairs(libs) do print(n, #b.assets) end

modules/VfsScanner/scanLibrary

scanLibrary(name: string)

Walk one named library under /source/libs/<name>/. The returned Bucket's rootPath is the library root so callers can derive relative paths cheaply.

Parameters

  • name string — Library directory name (e.g. "@builtin").
local bucket = VfsScanner.scanLibrary("@builtin")

modules/VfsScanner/scanWorld

scanWorld()

Walk only the world bucket — everything under /source/ except /source/libs/. The scanner stops recursing whenever it reaches an asset-suffixed folder; asset interiors are handled by the assetValidator.

local bucket = VfsScanner.scanWorld()

modules/WorkflowAssetTypeRef/README

WorkflowAssetTypeRef

Per-instance methods exposed on every AssetRef<workflow>. Loaded lazily by asset_ref.module.

modules/WorkflowAssetTypeRef/getManifest

getManifest(self): { [string]: any }

Read and parse workflow.yaml: what this workflow is, when it is reached for, and the phases it moves through.

Parameters

  • self any (optional)
local m = wf:getManifest()

modules/WorkflowAssetTypeRef/getSource

getSource(self): string?

Read the program itself — the JavaScript body that runs when the workflow is started.

Parameters

  • self any (optional)
local src = wf:getSource()

modules/WorkflowAssetTypeRef/inspect

inspect(self): { [string]: any }

What this workflow is, without running it: its description, when to reach for it, the phases it moves through, and what it expects in args.

Parameters

  • self any (optional)
local info = wf:inspect()

modules/WorkflowAssetTypeRef/start

start(self, args: { [string]: any }?): { [string]: any }

Start a run of this workflow. Returns immediately: the run is already executing, and it parks the moment it asks its first question. Answer what it asks through the workflow toolbox — the run decides what comes next.

Parameters

  • self any (optional)
  • args { [string]: any }? (optional) — Optional table handed to the program as its args.
local run = wf:start({ concept = "outrun a tornado" })

modules/World/README

require("@builtin/modules/world") -- World (also available as global 'world')

Public Luau API for the bound world. Composes internal __world FFI primitives with per-mode slot defaults (world_defaults), source-control toolbox (world_vcs), and the connected-users registry (connected_users) into a single namespace. Usage: world.guid() -- current world GUID, nil if none world.swap(guid) -- bind to another world (promise) world.avatar_default_edit = ref -- per-mode avatar default world.on("player_join", cb) -- world-level event hooks Implemented as a thin Luau wrapper. The world global is a plain table whose __index metatable falls through to the internal __world namespace for unknown reads. Other library modules (world_defaults, world_vcs, connected_users) attach their surfaces via installInto(world) from prelude.luau; their metatable wrappers chain through this base layer correctly.

Usage: local World = require("@builtin/modules/world") Also available as global: world

modules/Yaml/README

Yaml

YAML decode + encode for authored engine content. Supports the YAML subset engine configs use: block mappings + sequences, flow collections, typed plain scalars, quoted strings, comments, literal and folded block scalars, an optional leading ---. Unsupported constructs (anchors, aliases, tags, directives, multi-document streams, tab indentation) raise with the offending line number.

modules/Yaml/decode

decode(text: string): any

Decode a YAML document into a Luau value. Raises (with the line number) on malformed input or constructs outside the supported subset — never misparses silently.

Parameters

  • text string — The YAML document text.
local doc = Yaml.decode(vfs.read(path))

modules/Yaml/encode

encode(value: { [any]: any }): string

Encode a Luau table as a YAML document (block style, two-space indent, sorted keys). Raises on values YAML can't represent (functions, userdata, non-string mapping keys).

Parameters

  • value { [any]: any } — The table to encode.
vfs.write(path, Yaml.encode({ contract = "weapon", values = v }))

modules/ZJsAssetTypeRef/README

ZJsAssetTypeRef

Per-instance methods exposed on every AssetRef<zJs>. Loaded lazily by asset_ref.module the first time a zJs ref is touched in a VM. A zJs asset is a <name>.zJs/ folder whose main.js holds a JavaScript ES module. The methods here surface the static write-time diagnostics the type computes for that module and execute it: run / runAsync for a script or module, and exports to read a module's export namespace as a Luau table.

modules/ZJsAssetTypeRef/check

check(self): { ok: boolean, diagnostics: { Diagnostic } }

Static diagnostics for THIS module's main.js: parse errors plus imports that do not resolve to a zJs module. Reads the cache onChange maintains, running the parse+resolve pipeline once on a cold reference.

Parameters

  • self any (optional)
local r = asset.resolve("greeter", "zJs"):check(); if not r.ok then ... end

modules/ZJsAssetTypeRef/exports

exports(self, opts: { env: { [string]: any }? }?): any

Evaluate THIS module as an ES module on a fresh VM and return its export namespace marshaled to a Luau table: each named export plus default. Cross-asset imports resolve through the live asset index, so a JS library built from several .zJs assets is consumed as one table. Refuses when the module carries diagnostics, naming the first; a cycle, an unresolved import, or a thrown module body surfaces as a Luau error.

Parameters

  • self any (optional)
  • opts { env: { [string]: any }? }? (optional)
local lib = asset.resolve("mathlib", "zJs"):exports(); print(lib.add(1, 2))

modules/ZJsAssetTypeRef/onChange

onChange(ref, change)

Asset-type change callback: recompute this module's static diagnostics whenever its main.js is edited or the whole instance is seeded. Convergent: it reads the source and asset index and replaces the ref's cached pipeline result; it never writes to the VFS. Edits to sidecars (README.md, .metadata) leave the cache untouched.

Parameters

  • ref any (optional)
  • change any (optional)

modules/ZJsAssetTypeRef/onCreate

onCreate(name: string, opts: CreateOpts): { [string]: string }

Generic-creation hook for asset.create("zJs", name, opts). Pure: returns the instance's content file map for the caller to persist. opts.source, when given, becomes main.js verbatim; otherwise a hello-world module naming the instance is scaffolded from the template.

Parameters

  • name string — zJs module identity (the instance name).
  • opts CreateOpts
asset.create("zJs", "greeter", { source = "export const x = 1" })

modules/ZJsAssetTypeRef/run

run(self, opts: { env: { [string]: any }? }?): any

Execute THIS module on a fresh JavaScript VM and marshal the result to Luau. A plain script (no imports or exports) runs as a script and returns its completion value; a module evaluates through the module graph and returns nil, since module evaluation has no completion value. opts.env seeds host values as JavaScript globals. Refuses when the module carries diagnostics, naming the first, and directs a script that leaves asynchronous work pending to runAsync.

Parameters

  • self any (optional)
  • opts { env: { [string]: any }? }? (optional)
local sum = asset.resolve("calc", "zJs"):run({ env = { base = 10 } })

modules/ZJsAssetTypeRef/runAsync

runAsync(self, opts: { env: { [string]: any }?, timeoutMs: number? }?): any

Execute THIS module's program on a fresh VM under the asynchronous runner, driving armed timers and pending promises to completion. Returns the engine run handle: poll handle.done, then read handle.ok / handle.value / handle.error, or call handle:await() from a yieldable context. opts.timeoutMs bounds a never-settling run. Refuses when the module carries diagnostics, naming the first.

Parameters

  • self any (optional)
  • opts { env: { [string]: any }?, timeoutMs: number? }? (optional)
local h = asset.resolve("job", "zJs"):runAsync({ timeoutMs = 1000 }); h:await()

modules/ZinputActions/README

ZinputActions

DEPRECATED: A control asset carries what an action carried, plus the gamepad and touch an action never had. Author an .inputMap with .inputBinding children, activate it, and subscribe: local map = self.inputMap:activate() then map.jump:onPressed(fn). See man topics/input.

modules/ZinputActions/_clearHandlers

_clearHandlers()

Test-only: clear every handler (does NOT touch action definitions).

Zin.actions._clearHandlers()

modules/ZinputActions/_dispatchHandlers

_dispatchHandlers(firstTickThisFrame: boolean?)

Internal: dispatch action handlers. Called by Zin.tick after axes/chords advance. Fires Begin/End/Change/Held handlers based on each action's polling state this tick. firstTickThisFrame is false on a same-engine-frame re-tick (e.g. the autoTick worker and an explicit Zin.tick both land in one frame): edge fires (Begin/End/Change) are per-frame events and must not fire twice, so they are gated to the first tick of the frame. Held is a per-tick redeliver and still fires every call. A nil argument is treated as the first tick (edges fire).

Parameters

  • firstTickThisFrame boolean? (optional) — Whether this is the frame's first dispatch pass.
Zin.actions._dispatchHandlers()

modules/ZinputActions/_owns

_owns(handle: number): boolean

Internal: cross-API ownership probe. Used by Zin.disconnect to route handles to the right disconnect implementation, since Zin.input.on* and Zin.actions.bind share a handle namespace.

Parameters

  • handle number — The numeric handle to probe.
if Zin.actions._owns(h) then Zin.actions.disconnect(h) end

modules/ZinputActions/_resetUnknownWarnings

_resetUnknownWarnings()

Test-only: forget which names have already been reported as unregistered, so a fresh suite sees the warning again.

Zin.actions._resetUnknownWarnings()

modules/ZinputActions/_setAllocator

_setAllocator(fn: () -> number)

Internal: wire a shared id allocator, so handles from this module and from Zin.input.on* never collide. Called once at module-load time from zinput/init.luau.

Parameters

  • fn () -> number — The allocator function that returns the next handle id.
M._setAllocator(allocateZinHandle)

modules/ZinputActions/_setEnsureBindingsFn

_setEnsureBindingsFn(fn: () -> ())

Internal: wire the lazy-default-map hook. Called once at module- load time from zinput/init.luau.

Parameters

  • fn () -> () — The hook invoked on a cold read (name not in the registry).
M._setEnsureBindingsFn(ensureDefaultBindings)

modules/ZinputActions/_setEnsureLiveFn

_setEnsureLiveFn(fn: () -> ())

Internal: wire the liveness hook. Called once at module-load time from zinput/init.luau.

Parameters

  • fn () -> () — The hook invoked on every cold-checked read below.
M._setEnsureLiveFn(ensureInputLive)

modules/ZinputActions/_settled

_settled(): boolean

Internal: whether the most recent dispatch pass delivered nothing and saw nothing held. The tick's quiescence gate reads it.

if Zin.actions._settled() then ... end

modules/ZinputActions/active

active(name: string): boolean

True if an action is currently active in the input context stack. An action is "active" iff its declared context matches the top of the context stack (so push("ui") suppresses every non-"ui" action).

Parameters

  • name string — The action name.
if Zin.actions.active("jump") then ... end

modules/ZinputActions/bind

bind(name: string, fn, opts: BindOpts?): ActionHandle?

Register a handler. Returns a numeric handle (also accepted by Zin.input.disconnect). Returns nil if name is not a defined action.

Parameters

  • name string — The action name.
  • fn any (optional) — Handler fn(name, state, io, gpe) -> "sink" | any.
  • opts BindOpts? (optional) — Optional { priority, fire, once }.
local h = Zin.actions.bind("jump", function() jump() end)

modules/ZinputActions/clear

clear()

Wipe every defined action. Primarily for tests.

Zin.actions.clear()

modules/ZinputActions/define

define(spec: ActionSpec)

Define one or more actions. Each entry replaces any existing action under the same name; other actions are preserved.

Parameters

  • spec ActionSpec — Map of name -> binding | { binding... } | { context, binding(s) }.
Zin.actions.define({ jump = Zin.bindings.key("Space") })

modules/ZinputActions/disconnect

disconnect(handle: ActionHandle): boolean

Tear down a handler returned by bind. Idempotent.

Parameters

  • handle ActionHandle — The handle from bind (or onPressed/onReleased/etc).
Zin.actions.disconnect(h)

modules/ZinputActions/get

get(name: string): ActionEntry?

Internal: the registry record behind a name, for profile capture and conflict indexing. Returns nil if the action is not defined.

Parameters

  • name string — The action name.
local entry = Zin.actions.get("jump")

modules/ZinputActions/handlerCount

handlerCount(name: string): number

Number of registered handlers for an action (0 if none / unknown).

Parameters

  • name string — The action name.
assert(Zin.actions.handlerCount("jump") == 1)

modules/ZinputActions/has

has(name: string): boolean

True if an action with this name is defined.

Parameters

  • name string — The action name to test.
if Zin.actions.has("jump") then ... end

modules/ZinputActions/held

held(name: string): boolean

True if any binding on the action is currently delivering input. For boolean bindings: any held. For axis/vector bindings: non-zero magnitude. Suppressed by context gating.

Parameters

  • name string — The action name.
if Zin.actions.held("attack") then swing() end

modules/ZinputActions/heldTime

heldTime(name: string): number?

Seconds the action has been held, taken as the MAX held-time across the action's boolean bindings. Returns nil if no binding is held or if the action is gated off by the current input context. Vector / axis bindings are skipped — use a held-time threshold against Zin.axes.value for held-direction analogs.

Parameters

  • name string — The action name.
local t = Zin.actions.heldTime("interact")

modules/ZinputActions/names

names(): { string }

All defined action names, in arbitrary order.

for _, n in ipairs(Zin.actions.names()) do print(n) end

modules/ZinputActions/onChanged

onChanged(name: string, fn, opts: BindOpts?): ActionHandle?

Fire on axis/vector value delta (state = "Change"). For boolean actions Change fires on every press AND release transition — use onPressed / onReleased instead if you only want edges.

Parameters

  • name string — The action name.
  • fn any (optional) — Handler fn(name, state, io, gpe).
  • opts BindOpts? (optional) — Optional { priority, once }.
Zin.actions.onChanged("move", function(_, _, io) print(io.value) end)

modules/ZinputActions/onHeld

onHeld(name: string, fn, opts: BindOpts?): ActionHandle?

Fire every tick while held (state = "Held"). Fires whenever held() is true at dispatch time, regardless of value change. Inherits the action's context constraint (no per-handler context filter).

Parameters

  • name string — The action name.
  • fn any (optional) — Handler fn(name, state, io, gpe).
  • opts BindOpts? (optional) — Optional { priority, once }.
Zin.actions.onHeld("interact", function(_, _, io) charge(io.value) end)

modules/ZinputActions/onPressed

onPressed(name: string, fn, opts: BindOpts?): ActionHandle?

Fire on rising edge (state = "Begin"). Sugar for bind with fire = {"Begin"}.

Parameters

  • name string — The action name.
  • fn any (optional) — Handler fn(name, state, io, gpe).
  • opts BindOpts? (optional) — Optional { priority, once }.
Zin.actions.onPressed("jump", function() ... end)

modules/ZinputActions/onReleased

onReleased(name: string, fn, opts: BindOpts?): ActionHandle?

Fire on falling edge (state = "End"). Sugar for bind with fire = {"End"}.

Parameters

  • name string — The action name.
  • fn any (optional) — Handler fn(name, state, io, gpe).
  • opts BindOpts? (optional) — Optional { priority, once }.
Zin.actions.onReleased("attack", function() ... end)

modules/ZinputActions/pressed

pressed(name: string): boolean

True if any boolean binding on the action was just pressed this frame — the press EDGE, so it fires once per press no matter how long the input is held. Use it for one-shot input: fire, jump, pause, undo. For continuous input that should repeat every frame the input is down, read the level instead with Zin.state.keyDown(key). Axis/vector bindings do not contribute to press edges. Suppressed by context gating.

Parameters

  • name string — The action name.
if Zin.actions.pressed("jump") then ... end

modules/ZinputActions/reason

reason(name: string): string

Why a named action is not delivering, from the closed set this registry can distinguish: unknownControl (no action is registered under the name — the value readers answer their neutral value, which is not a reading), contextInactive (registered, but its context is not on top of the stack), atRest (live, and the devices it binds are not being driven), or delivering.

Parameters

  • name string — The action name.
if Zin.actions.reason("jump") == "unknownControl" then ... end

modules/ZinputActions/released

released(name: string): boolean

True if any boolean binding on the action was just released this frame. Suppressed by context gating.

Parameters

  • name string — The action name.
if Zin.actions.released("attack") then ... end

modules/ZinputActions/remove

remove(name: string)

Remove an action by name. No-op if not defined.

Parameters

  • name string — The action name to remove.
Zin.actions.remove("jump")

modules/ZinputActions/repeated

repeated(name: string, opts: { delay: number?, period: number? }?): boolean

Should a synthetic repeat fire this frame for the action? Returns true if any of the action's boolean key bindings reports State.keyRepeatFired. Mouse bindings are skipped (use a hold- time threshold for press-and-hold UX). Suppressed by context gating.

Parameters

  • name string — The action name.
  • opts { delay: number?, period: number? }? (optional) — Optional { delay, period } override of the global repeat defaults.
if Zin.actions.repeated("scrollLeft") then ... end

modules/ZinputActions/value

value(name: string): any

Read the action's current value.

  • Axis binding → number in [-1, 1]
  • Vector binding → { x, y } numbers in [-1, 1]
  • Boolean binding → 1 when held, 0 when not (consumers usually use held() instead; this exists so a single API works for any kind) When multiple bindings exist, the first one whose kind matches the caller's expectation wins (axis > vector > boolean in declaration order). When suppressed by context, returns the identity value for the first binding's kind: 0 for axis/boolean, { x = 0, y = 0 } for vector.

Parameters

  • name string — The action name.
local mv = Zin.actions.value("move")  -- { x, y }

modules/ZinputAxes/README

ZinputAxes

DEPRECATED: An axis1 / axis2 control carries the same deadzone, smoothing, curve and invert, and reads a stick and a dragging thumb as well as a key. Author an .inputMap with .inputBinding children, activate it, and subscribe: local map = self.inputMap:activate() then map.move:onInput(fn). See man topics/input.

modules/ZinputAxes/_resetGateWarnings

_resetGateWarnings()

Test-only: reset the once-per-axis gate-error warning state so a fresh suite can verify warning behavior again.

Zin.axes._resetGateWarnings()

modules/ZinputAxes/_resetUnknownWarnings

_resetUnknownWarnings()

Test-only: forget which names have already been reported as unregistered, so a fresh suite sees the warning again.

Zin.axes._resetUnknownWarnings()

modules/ZinputAxes/_setEnsureBindingsFn

_setEnsureBindingsFn(fn: () -> ())

Internal: wire the lazy-default-map hook. Called once at module- load time from zinput/init.luau.

Parameters

  • fn () -> () — The hook invoked on a cold read (name not in the registry).
M._setEnsureBindingsFn(ensureDefaultBindings)

modules/ZinputAxes/_setEnsureLiveFn

_setEnsureLiveFn(fn: () -> ())

Internal: wire the liveness hook. Called once at module-load time from zinput/init.luau.

Parameters

  • fn () -> () — The hook invoked on every cold-checked read below.
M._setEnsureLiveFn(ensureInputLive)

modules/ZinputAxes/_settled

_settled(): boolean

Internal: whether every axis sits at zero, current and target alike. The tick's quiescence gate reads it.

if Zin.axes._settled() then ... end

modules/ZinputAxes/advance

advance(dt: number)

Parameters

  • dt number

modules/ZinputAxes/clear

clear()

Wipe every axis and its state. Primarily for tests.

Zin.axes.clear()

modules/ZinputAxes/define

define(spec: AxisSpec)

Define one or more axes. Each entry replaces any existing axis of the same name; other axes are preserved.

Parameters

  • spec AxisSpec — Map of name -> { binding, deadzone?, smoothing?, curve?, invert?, context?, gate? }.
Zin.axes.define({ aim_x = { binding = ..., deadzone = 0.05 } })

modules/ZinputAxes/get

get(name: string): AxisDef?

Internal introspection: definition record (or nil).

Parameters

  • name string — The axis name.
local def = Zin.axes.get("aim_x")

modules/ZinputAxes/has

has(name: string): boolean

True if an axis with this name is defined.

Parameters

  • name string — The axis name.
if Zin.axes.has("aim_x") then ... end

modules/ZinputAxes/names

names(): { string }

All defined axis names, in arbitrary order.

for _, n in ipairs(Zin.axes.names()) do print(n) end

modules/ZinputAxes/raw

raw(name: string): any

Read the raw, unsmoothed, unshaped value of the underlying binding. For vector bindings returns {x,y}; for everything else returns a number in [-1, 1]. Useful for debugging or comparing pre/post processing.

Parameters

  • name string — The axis name.
print(Zin.axes.raw("aim_x"))

modules/ZinputAxes/reason

reason(name: string): string

Why a named axis is not delivering, from the closed set this registry can distinguish: unknownControl (no axis is registered under the name — the value readers answer zero, which is not a reading), contextInactive (registered, but its context is not on top of the stack), gateRefused (its own gate answered no), atRest (live, and the binding it reads is not being driven), or delivering.

Parameters

  • name string — The axis name.
if Zin.axes.reason("look") == "gateRefused" then ... end

modules/ZinputAxes/remove

remove(name: string)

Remove an axis. No-op if not defined.

Parameters

  • name string — The axis name.
Zin.axes.remove("aim_x")

modules/ZinputAxes/value

value(name: string): any

Read the smoothed, shaped, context-gated value. Scalar axes return a number in [-1, 1]. Vector axes return {x, y} numbers in [-1, 1]. Returns 0 / {x=0,y=0} if the axis isn't defined.

Parameters

  • name string — The axis name.
local mv = Zin.axes.value("move")  -- { x, y }

modules/ZinputMap/README

ZinputMap

DEPRECATED: An .inputMap asset with .inputBinding children is the map, and Zin.scheme is the live set: several can be live at once, each declaring the groups it suppresses. Zin.map.bake(name) writes the active map out as one, which is the migration. See man topics/input.

modules/ZinputMap/_reactivate

_reactivate(record: any): any

Re-apply the active map from a changed record — the edit-in-place path an .inputMap asset takes when its source is written while it is live. Who asked for the map is carried across: a write to its source is the same map with new bindings, not a caller taking it up.

Parameters

  • record any (optional) — The map record, freshly read from its source.
Zin.map._reactivate(loadRecord(self))

modules/ZinputMap/_reset

_reset()

Test-only: clear active-map state (the profile registry keeps whatever was applied).

Zin.map._reset()

modules/ZinputMap/activate

activate(record: any): any

Activate a map record: materialize it and apply the flattened result as the live binding set (through the profile registry, so persistence and conflict surfaces keep working). Per-class axis bindings beyond the primary register as <axis>@<class> sibling axes; consumers that combine device values read both (e.g. look + look@touch).

Parameters

  • record any (optional) — The map (or profile) record, or an inputMap asset ref.
Zin.map.activate(require("@builtin::inputMaps.default"))

modules/ZinputMap/activeName

activeName(): string?

The active map's name, or nil.

if Zin.map.activeName() == "default" then ... end

modules/ZinputMap/addTouchButton

addTouchButton(actionName: string, buttonOpts: { zone: string?, label: string?, icon: string? }, emitKey: string?): any

Add a touchButton binding to an action's touch class on the active effective map, then re-flatten and re-activate so it takes effect immediately. Creates the action entry if actionName doesn't exist yet (the overlay's synthetic emit:<code> buttons).

Parameters

  • actionName string — The action to attach the button to.
  • buttonOpts { zone: string?, label: string?, icon: string? }{ zone: string?, label: string?, icon: string? } — the touchButton binding's presentation (see Zin.bindings.touchButton).
  • emitKey string? (optional) — Optional key code — when set and the action has no kbm class yet, seeds it with B.key(emitKey) so a synthetic action is self-contained from the moment it's created.
Zin.map.addTouchButton("emit:KeyF", { label = "Cast" }, "KeyF")

modules/ZinputMap/bake

bake(name: string): any

Write the active effective map as a new inputMap asset — synthesis made explicit and editable. The snapshot includes every live entry, overlay-registered emit:<code> actions included. Returns the created ref.

Parameters

  • name string — The new asset's name.
Zin.map.bake("my_scheme")

modules/ZinputMap/bindingsFor

bindingsFor(eff: any, name: string, class: string): any

The effective bindings for one action or axis and device class.

Parameters

  • eff any (optional) — An effective map (from materialize/effective).
  • name string — The action or axis name.
  • class string — "kbm" | "gamepad" | "touch".
local touch = Zin.map.bindingsFor(eff, "jump", "touch")

modules/ZinputMap/effective

effective(): any

The active map's effective form, or nil before any activation.

local eff = Zin.map.effective()

modules/ZinputMap/ensureActive

ensureActive(): any

Ensure a map is active: keeps the current one, else activates the builtin default map. The bootstrap the on-screen controls and controllers call.

Zin.map.ensureActive()

modules/ZinputMap/isFallback

isFallback(): boolean

Whether the active map is the fallback ensureActive armed on its own, rather than one a caller activated. A reader that presents the map to a player — the on-screen controls — asks this to tell a scheme a world offered from the keyboard floor under a name that was read.

if not Zin.map.isFallback() then draw(Zin.map.effective()) end

modules/ZinputMap/materialize

materialize(record: any): any

Materialize a map record into its effective form: extends chain resolved (child wins per action/axis/class), then the touch class synthesized from the kbm shape wherever absent. Returns { name, description, actions = { [name] = { context, classes, synthesized = { touch = true? } } }, axes = { ... } }.

Parameters

  • record any (optional) — The map (or profile) record.
local eff = Zin.map.materialize(require("@builtin::inputMaps.default"))

modules/ZinputMap/removeTouchButton

removeTouchButton(actionName: string, binding: any): boolean

Remove a touchButton binding previously added via addTouchButton, then re-flatten and re-activate. Drops the action entry entirely once every class is empty — cleanup for synthetic emit:<code> actions the overlay created.

Parameters

  • actionName string — The action the binding was added to.
  • binding any (optional) — The binding table returned by addTouchButton.
Zin.map.removeTouchButton("emit:KeyF", binding)

modules/ZinputObserve/README

ZinputObserve

modules/ZinputObserve/_publish

_publish()

Internal: publish this layer's half of the engine's input observation for the current frame. Called once per frame from Zin.tick while Zin.observe.wanted() holds.

Zin.observe._publish()

modules/ZinputObserve/arm

arm(on: boolean?)

Hold the engine's input observation open, so /runtime/input and input.observe() carry this layer's half of the document every frame. A read of either arms it for a window of frames on its own; this is for a test or a tool that wants it building continuously.

Parameters

  • on boolean? (optional) — Arm (the default) or disarm.
Zin.observe.arm(true)

modules/ZinputObserve/armedFrames

armedFrames(): number

How many more frames the arming window has left. A read of Zin.observe.frame(), input.observe() or /runtime/input sets it back to the full window; every frame that passes takes one off it, and 0 means nothing is observing.

print(Zin.observe.armedFrames())

modules/ZinputObserve/control

control(name: string): any

Everything known about one named control in a single call: which maps contribute it, its bindings per device class, its subscriber count, the value it reported on the most recent tick, whether that reached a subscriber, and — when it is live and silent — why.

Parameters

  • name string — The control name.
local c = Zin.observe.control("move")

modules/ZinputObserve/frame

frame(): any

The mapping layer's account of the most recent tick: every live map, every live control with what it did and why, and what the tick cost.

The window is ONE tick — the most recent one. Reading consumes nothing, so any number of observers in the same frame all get the same answers.

local f = Zin.observe.frame()

modules/ZinputObserve/means

means(reason: string): string?

What one reason name means, or nil for a name outside the set.

Parameters

  • reason string — The reason name.
print(Zin.observe.means("gateRefused"))

modules/ZinputObserve/reasons

reasons(): { any }

The closed set of reasons a control resolves to, each with what it means and what to do about it. The resolver answers with exactly one of these names.

for _, r in ipairs(Zin.observe.reasons()) do print(r.name, r.means) end

modules/ZinputObserve/wanted

wanted(): boolean

Whether the engine wants this layer's half of the input observation built this frame — true while something has read input.observe() or /runtime/input recently enough.

if Zin.observe.wanted() then ... end

modules/ZinputObserve/whySilent

whySilent(name: string): any

Why a named control is not reaching the game right now, as one reason from the closed set Zin.observe.reasons() lists, with the particulars behind it.

Resolves against the devices as they are at the moment of the call, so it answers for a control the tick has never reached and for one that does not exist. The layer field says which of the three naming layers answered — the live maps' controls, the action registry, or the axis registry — since a name can be live in one and unknown in the others.

Parameters

  • name string — The control name.
local why = Zin.observe.whySilent("look")

modules/animGraph/README

require("@builtin/modules/api/engine/animGraph") -- animGraph (also available as global 'animGraph')

Per-entity engine animation graph over the __animGraph FFI: clips, mixers, 2D blend spaces, crossfade, weights, and graph state. Each call routes onto the entity's AnimGraphComponent (auto-resolved from script context or passed explicitly).

Usage: local animGraph = require("@builtin/modules/api/engine/animGraph") Also available as global: animGraph

modules/animation/README

require("@builtin/modules/api/engine/animation") -- animation (also available as global 'animation')

Observe what the engine is animating: every body it is posing, the clips driving each one and where their playheads are, how many of a body's bones a clip's retarget reached, and — for a body that is not moving — the one reason why, from a closed set.

Usage: local animation = require("@builtin/modules/api/engine/animation") Also available as global: animation

modules/animation/animating

animating(): { AnimationBody }

The bodies the engine measured a changing pose on — what is animating right now.

for _, b in animation.animating() do print(b.entity, b.clips[1] and b.clips[1].name) end

modules/animation/bodies

bodies(): { AnimationBody }

Every body the engine holds animation state for.

for _, b in animation.bodies() do print(b.entity, b.matched .. "/" .. b.total) end

modules/animation/body

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

  • entityId string | EntityRef — The entity's stable id, or an EntityRef.
local b = animation.body(hero.id); print(b and b.reason)

modules/animation/clips

clips(entityId: string | EntityRef): { AnimationClip }

The clips contributing to a body's pose right now, with their playheads and their retarget coverage.

Parameters

  • entityId string | EntityRef — The entity's stable id, or an EntityRef.
for _, c in animation.clips(hero.id) do print(c.name, c.time, c.matched) end

modules/animation/coverage

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

  • entityId string | EntityRef — The entity's stable id, or an EntityRef.
local m, t = animation.coverage(hero.id); print(m .. "/" .. t)

modules/animation/declare

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

  • entityId string — 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 = {} })

modules/animation/forget

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

  • entityId string — The body to drop.
animation.forget(body.id)

modules/animation/observe

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.

local a = animation.observe(); print(a.animatingCount, a.riggedBodyCount)
for _, b in animation.observe().bodies do print(b.entity, b.animating, b.reason) end

modules/animation/whyStill

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

  • entityId string | EntityRef — The entity's stable id, or an EntityRef.
local why, detail = animation.whyStill(hero.id); if why then print(why, detail) end

modules/antiAliasing/README

require("@builtin/systems/antiAliasing/antiAliasing") -- antiAliasing

Edge antialiasing from the finished frame — the stair-stepping along a high-contrast silhouette is replaced by a gradient, without softening the regions on either side of it.

Usage: local antiAliasing = require("@builtin/systems/antiAliasing/antiAliasing")

modules/antiAliasing/active

active(): boolean

Whether the antialiasing pass is running this frame.

if antiAliasing.active() then ... end

modules/antiAliasing/disable

disable()

Turn edge antialiasing off and release the pass. The settings are kept, so a later enable() brings back the same tuning.

antiAliasing.disable()

modules/antiAliasing/enable

enable(opts: AntiAliasingOpts?): AntiAliasingState

Turn edge antialiasing on and set it. Any omitted field keeps its current value.

Parameters

  • opts AntiAliasingOpts? (optional) — Antialiasing settings — see AntiAliasingOpts.
antiAliasing.enable({ edgeThreshold = 0.125 })

modules/antiAliasing/get

get(): AntiAliasingState

The antialiasing settings currently in force.

local t = antiAliasing.get().edgeThreshold

modules/antiAliasing/paramsBuffer

paramsBuffer(): any?

The buffer this system's passes read. The render feature binds what this hands it, so the pass has the values this module packed.

local p = antiAliasing.paramsBuffer()

modules/ao.shared/README

require("@builtin/systems/ambientOcclusion/aoShared") -- ao.shared

The parts every ambient-occlusion technique has in common: the WGSL prelude its kernel is written against, and the render targets, filter and passes that turn a per-pixel visibility term into shading.

A technique differs from its siblings in one thing only: how it estimates, for one pixel, what fraction of the hemisphere above the surface is visible. Everything either side of that estimate — reconstructing the surface from depth and normal, sizing the screen-space reach of a world-space radius, filtering the result across the noise the estimate carries, and multiplying it into the frame — is the same work whichever estimate produced it. So it lives here once. A technique file is its kernel and nothing else, which is what makes the three readable against each other.

Usage: local ao.shared = require("@builtin/systems/ambientOcclusion/aoShared")

modules/ao.shared/feature

feature(name: string, kernel: AssetRef<computeShader>) -> table

Build the render feature for one technique from its kernel shader.

Parameters

  • name string
  • kernel AssetRef<computeShader>

Returns table

modules/ao.shared/scale

scale() -> number

The fraction of the frame's resolution the occlusion is computed at.

Returns number

modules/ao.shared/setScale

setScale(scale: number)

Parameters

  • scale number

modules/ao/README

require("@builtin/systems/ambientOcclusion/ao") -- ao

Screen-space ambient occlusion — the contact darkening in corners, creases and where objects meet. Without it every concave region is lit exactly like a flat one.

Usage: local ao = require("@builtin/systems/ambientOcclusion/ao")

modules/ao/active

active(): boolean

Whether the occlusion pass is running this frame.

if ao.active() then ... end

modules/ao/clear

clear()

Turn occlusion off and release the pass. The other settings are kept, so a later set({ intensity = ... }) brings back the same look.

ao.clear()

modules/ao/get

get(): AoState

The occlusion settings currently in force.

local r = ao.get().radius

modules/ao/paramsBuffer

paramsBuffer(): any?

The buffer the AO kernel reads. It belongs to whichever technique is running, and is remade when the technique swaps, so the pass binds what this hands it rather than looking it up.

local p = ao.paramsBuffer()

modules/ao/qualityLevels

qualityLevels(): { [string]: { slices: number, steps: number, scale: number } }

What each quality level costs: slices × steps taps per pixel (total slices * steps * 2, the same for every technique), computed on a grid scale of the frame's resolution.

local cost = ao.qualityLevels().ultra

modules/ao/set

set(opts: AoOpts?): AoState

Set the scene's ambient occlusion. Any omitted field keeps its current value, so a call can adjust one knob without restating the rest. An intensity of 0 turns occlusion off and releases the pass.

Parameters

  • opts AoOpts? (optional) — Occlusion settings — see AoOpts.
ao.set({ technique = "hbao", intensity = 0.9, radius = 1.5 })

modules/ao/techniques

techniques(): { [string]: string }

The techniques that can be selected, each with what it does.

for name in pairs(ao.techniques()) do print(name) end

modules/asset/README

require("@builtin/modules/api/engine/asset") -- asset (also available as global 'asset')

Asset resolver, ref envelope builder, sidecar metadata. Public Luau surface over the __asset Internal FFI namespace.

Usage: local asset = require("@builtin/modules/api/engine/asset") Also available as global: asset

modules/asset/add_tag

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

  • ref RefArg — Any name the asset has.
  • tag string — Tag to add.
asset.add_tag("brick", "wip")

modules/asset/alias

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

  • ref RefArg — The asset gaining the name.
  • alias string — The additional name. Any identity form: a bare leaf (standard) or a scope-qualified path (@builtin::shaders.legacy).
asset.alias("@builtin::shaders.pbr", "standard")

modules/asset/aliases

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

  • ref RefArg — Any name the asset has.
for _, n in asset.aliases("pbr") do print(n) end

modules/asset/canCreate

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

  • typeName string — Registered asset type (e.g. "material", "scene").
if asset.canCreate(kind) then asset.create(kind, name) end

modules/asset/categories

categories(): { string }

List every asset category the engine currently recognises. Use to discover valid type argument values for the rest of asset.*.

for _, c in asset.categories() do print(c) end

modules/asset/containing

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

  • path string — VFS path to inspect.
local a = asset.containing("/source/scenes/main.scene/scene.json")

modules/asset/cpuResident

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

  • ref RefArg — Any name the asset has — handle, identity, guid, name or path.
  • typeName string? (optional) — Category to restrict the match to. Omit to search every category.
if asset.cpuResident(ref) then print("bytes are warm") end

modules/asset/create

create(typeName: string, name: string, opts: { [string]: any }?): (AssetRef, { durable: boolean, warning: string?, playShadow: string?, shadowed: { string }? })

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

  • typeName string — The asset category to instance — one of asset.categories().
  • name string — The new asset's name.
  • opts { [string]: any }? (optional) — Optional table forwarded to the type's onCreate hook, minus four framework keys consumed here and never seen by the hook: folder (a relative subfolder under /source to 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), and overwrite (re-author in place, keeping the guid).
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
local clip, d = asset.create("soundClip", "hit", { pcm = buf, sampleRate = 48000, channels = 1 })
if not d.durable then log.warn(d.warning .. " " .. d.playShadow) end

modules/asset/declareReferenceArg

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

  • call string — The callee as it is written at a call site.
  • position number — Which argument holds the name, counting from 1.
  • assetType string
asset.declareReferenceArg("spawnModel", 3, "mesh")

modules/asset/declareReferenceField

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

  • call string — The callee as it is written at a call site.
  • field string — The options-table field holding the name, read at the table's own level.
  • assetType string
asset.declareReferenceField("fx.beam", "material", "material")

modules/asset/declareReferenceKey

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

  • assetType string — The category owning the file, e.g. "material".
  • key string — The top-level key holding the name(s).
  • refType string
asset.declareReferenceKey("material", "shader", "shader")

modules/asset/deps

deps(ref: RefArg, type: string?): DepsResult

Return the asset's outbound dependency graph — every other asset recorded as a content dependency of it.

Parameters

  • ref RefArg — Any name the asset has.
  • type string? (optional) — Category hint (optional).
for _, d in asset.deps("main.scene").deps do print(d.asset_guid) end

modules/asset/describe

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

  • typeName string — Registered asset type to describe (e.g. "texture").
local contract = asset.describe("texture").contract

modules/asset/diagnose

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

  • ref RefArg — Any name the asset has — handle, identity, guid, name or path.
local d = asset.diagnose("myTex") if not d.usable then print(d.reason, d.detail) end

modules/asset/exists

exists(name: string, typeName: string): boolean

Parameters

  • name string
  • typeName string

modules/asset/get_field

get_field(ref: RefArg, key: string): any

Read one top-level field from the asset's .metadata.

Parameters

  • ref RefArg — Any name the asset has.
  • key string — Field name.
local author = asset.get_field("brick", "author")
local settings = asset.get_field("tree", "settings") -- → a Luau table

modules/asset/gpuResident

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

  • ref RefArg — Any name the asset has — handle, identity, guid, name or path.
print(asset.gpuResident("@builtin::models.Sample.DamagedHelmet"))

modules/asset/guid

guid(ref: RefArg, type: string?): string

Return the guid for an asset.

Parameters

  • ref RefArg — Any name the asset has.
  • type string? (optional) — Category hint (optional).
local g = asset.guid("@builtin::components.Camera")

modules/asset/has_field

has_field(ref: RefArg, key: string): boolean

True when the asset's .metadata carries the named field.

Parameters

  • ref RefArg — Any name the asset has.
  • key string — Field name.
if asset.has_field("brick", "author") then end

modules/asset/has_tag

has_tag(ref: RefArg, tag: string): boolean

True when the asset's .metadata.tags contains tag.

Parameters

  • ref RefArg — Any name the asset has.
  • tag string — Tag to check for.
if asset.has_tag("brick", "wip") then end

modules/asset/identity

identity(ref: RefArg, type: string?): string

Return the canonical identity for an asset.

Parameters

  • ref RefArg — Any name the asset has.
  • type string? (optional) — Category hint (optional).
local id = asset.identity("brick")

modules/asset/import

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

  • path string — The raw source VFS path to import (e.g. a just-written .glb).
local bundle = asset.import("/zero/source/generated/chest.glb")

modules/asset/inspect

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

  • ref RefArg — An AssetRef, an identity string, or a path.
  • type string? (optional) — Narrow the resolve to one asset type when assets of several categories answer to the same bare name.
local rec = asset.inspect("@builtin::materials.default")
local rec = asset.inspect(name, "mesh"); print(rec.guid, #rec.tags)

modules/asset/list

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.
  • scope string? (optional) — Scope filter (when first arg is a type).
  • opts ListOpts? (optional) — The query table — see ListOpts.
local hero = asset.list({ path = "/zero/source/props", type = "mesh", fields = { tags = "hero" } }):first()

modules/asset/list_field_values

list_field_values(key: string): { any }

Distinct values seen for the named field across every asset's .metadata.

Parameters

  • key string — Field name.
local authors = asset.list_field_values("author")

modules/asset/list_fields

list_fields(): { string }

Distinct top-level field keys observed across every asset's .metadata. Useful for tooling discovering custom keys in use.

for _, k in asset.list_fields() do print(k) end

modules/asset/meta

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

  • ref RefArg — Any name the asset has.
  • type string? (optional) — Category hint (optional).
local m = asset.meta("brick") -- { guid = ..., checksum = ... }

modules/asset/metadata

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

  • ref RefArg — Any name the asset has.
  • type string? (optional) — Category hint (optional).
local md = asset.metadata("brick")

modules/asset/observe

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.

local r = asset.observe() print(#r.textures, r.totals.textureBytes)

modules/asset/preview

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

  • ref RefArg — Any name the asset has.
  • opts { [string]: any }? (optional) — Optional { size = { width, height }, angle = { yaw, pitch } }.
  • type string? (optional) — Category hint (optional).
local p = asset.preview("@builtin::materials.gold", { size = { width = 512, height = 512 } })

modules/asset/primaryFile

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

  • ref RefArg — Any name the asset has — handle, identity, guid, name or path.
print(asset.primaryFile("myTex").path)

modules/asset/ref

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

  • ref RefArg — The asset to reference — an identity, a guid, a VFS path, or a handle.
  • type string? (optional) — Category hint (optional).
local r = asset.ref("animations.idle", "animation")

modules/asset/reloadPending

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

  • ref RefArg — Any name the asset has — handle, identity, guid, name or path.
  • typeName string? (optional) — Category to restrict the match to. Omit to search every category.
repeat task.wait() until not asset.reloadPending(ref)

modules/asset/reloadSeq

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

  • ref RefArg — Any name the asset has — handle, identity, guid, name or path.
  • typeName string? (optional) — Category to restrict the match to. Omit to search every category.
local at = asset.reloadSeq(ref)
vfs.write(asset.source(ref) .. "/graph.json", encoded)
repeat task.wait() until asset.reloadSeq(ref) > at

modules/asset/remove_field

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

  • ref RefArg — Any name the asset has.
  • key string — Field name.
asset.remove_field("brick", "author")

modules/asset/remove_tag

remove_tag(ref: RefArg, tag: string)

Remove a tag from the asset's .metadata.tags. No-op when the tag isn't present.

Parameters

  • ref RefArg — Any name the asset has.
  • tag string — Tag to remove.
asset.remove_tag("brick", "wip")

modules/asset/resolve

resolve<C>(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

  • ref RefArg — 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.png after the texture importer turned it into wall.texture) resolves to the promoted asset, and says so in the log once per reference.
local a = asset.resolve("@builtin::components.Camera")

modules/asset/set_field

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

  • ref RefArg — Any name the asset has.
  • key string — Field name.
  • value any (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

modules/asset/set_metadata

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

  • ref RefArg — Any name the asset has.
  • data AssetMeta — Full JSON-shaped contents for the sidecar.
asset.set_metadata("brick", { author = "me", tags = { "wip" } })

modules/asset/source

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

  • ref RefArg — Any name the asset has.
  • type string? (optional) — Category hint (optional).
local p = asset.source("brick") -- "/source/brick.material"

modules/asset/storeHoldsIdentity

storeHoldsIdentity(identity: string, typeName: string, reproduced: boolean): boolean

Whether an asset of type typeName that name reaches has files in the store asset.create writes from this context — the authored /source tree, plus the ephemeral /runtime/assets/ store when the caller is a script component or a scene entrypoint, which is where a create there files it. name is any name the registry answers to for that asset: its canonical identity ("a.b.thing"), a registered alias, or the bare leaf ("thing") — the spelling asset.list publishes as an entry's name and the spelling asset.create was given alongside opts.folder. So the if not asset.exists(n, t) then asset.create(t, n, opts) end pairing is satisfied by its own create, spelled the way the create was, in either store and at any folder depth. A plain existence probe — it does NOT resolve a handle or pin a content dependency, so it is safe to call with a COMPUTED name (unlike asset.resolve, whose handle would become a static-pinned dependency).

The name is read the way asset.resolve reads one; the ANSWER comes from the store, so a registered asset whose files live outside the store this context writes — every @builtin:: asset among them — is false. asset.tryResolve(name, typeName) asks the registry the wider question and hands back the handle — and because it reaches the asset, the name it is given follows the reference rule every lookup follows: a literal is pinned, a computed one is a dynamic resolve. So a name this source computed is what asset.exists is for. This call names the asset FIRST and its category second, as every asset.* call taking both does except asset.create(category, name, opts). A call carrying a category where the asset goes and a non-category where the category goes is refused, naming which way round the call reads, rather than reporting the asset absent.

Parameters

  • identity string
  • typeName string — The asset type (e.g. "mesh", "texture", "material") — one of asset.categories().
  • reproduced boolean
if not asset.exists(meshName, "mesh") then asset.create("mesh", meshName, geo) end
if not asset.exists(n, "mesh") then asset.create("mesh", n, { folder = "props", positions = p, indices = i }) end

modules/asset/tags

tags(ref: RefArg): { string }

Convenience read of the .metadata.tags array.

Parameters

  • ref RefArg — Any name the asset has.
for _, t in asset.tags("brick") do print(t) end

modules/asset/tryResolve

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

  • ref RefArg — The asset to look up — an identity, a guid, a VFS path, or a handle.
  • type string? (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 as asset.resolve.
  • base string? (optional) — Referring VFS path a ~ / ~.tail ref expands against, the same as asset.resolve's — so the two answer the same question and differ only in what a miss is.
local mat = asset.tryResolve(name, "material")

modules/asset/typeRef

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

  • target RefArg — Asset handle / identity / guid / VFS path.
local t = asset.resolve(asset.typeRef("brick"))

modules/asset/unusableReasons

unusableReasons(): { string }

Every reason asset.diagnose can report an asset unusable for, sorted.

for _, r in ipairs(asset.unusableReasons()) do print(r) end

modules/asset/validate

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

  • ref RefArg — Any name the asset has.
  • type string? (optional) — Category hint (optional).
local v = asset.validate("@builtin::components.Camera")

modules/asset/warmup

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

  • ref RefArg — Any name the root asset has — handle, identity, guid, or path.
  • opts WarmupOpts? (optional) — Optional { vias, max } — restrict ref-edge kinds / cap closure size.
local w = asset.warmup("@builtin::scenes.test_arena")

modules/asset_tag/README

asset_tag

The assetTag field-constraint validator: a constrained value must be an asset carrying constraint.tag in its .metadata.tags. This is what lets a slot declare the KIND of asset it takes — a camera behavior, a player visual — without naming the assets themselves, so a new asset becomes assignable the moment it is tagged. Registers itself with the generic field_constraints registry on load. nil (no asset) passes — the field is optional.

modules/atmosphere/README

require("@builtin/systems/atmosphere/atmosphere") -- atmosphere

Physically-based atmospheric scattering — aerial perspective on distant geometry, and a sky whose colour follows from the sun's elevation rather than from an authored gradient.

Usage: local atmosphere = require("@builtin/systems/atmosphere/atmosphere")

modules/atmosphere/active

active(): boolean

Whether the atmosphere passes are currently running.

if atmosphere.active() then print("scattering") end

modules/atmosphere/clear

clear()

Turn the atmosphere off and release its passes. The other settings are kept, so a later set({ aerial = ... }) brings back the same look.

atmosphere.clear()

modules/atmosphere/get

get(): AtmosphereState

The atmosphere settings currently in force.

local km = atmosphere.get().worldUnitsPerKm

modules/atmosphere/paramsBuffer

paramsBuffer(): any?

The buffer this system's passes read. The render feature binds what this hands it, so the pass has the values this module packed.

local p = atmosphere.paramsBuffer()

modules/atmosphere/set

set(opts: AtmosphereOpts?): AtmosphereState

Set the scene's atmosphere. Any omitted field keeps its current value, so a call can adjust one knob without restating the rest. With aerial at 0 and sky off nothing is wanted and the passes are released.

Parameters

  • opts AtmosphereOpts? (optional) — Atmosphere settings — see AtmosphereOpts.
atmosphere.set({ aerial = 1, sky = true, worldUnitsPerKm = 1000 })

modules/audio/README

require("@builtin/modules/api/engine/audio") -- audio (also available as global 'audio')

Engine-native audio: encode audio (or raw PCM) into the ZAUD compressed payload, decode/inspect it, set what the mix is heard at — a level per named channel, a master level and a mute — and observe what the mixer is making audible right now: live voices, why a source is silent, master levels, mixer voice accounting, listener state and subsystem cost.

Usage: local audio = require("@builtin/modules/api/engine/audio") Also available as global: audio

modules/audio/decode

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

  • zaud buffer | string — A ZAUD payload — a buffer or a binary string.

modules/audio/device

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.

local d = audio.device(); print(d.state, d.device, d.reopens)

modules/audio/encode

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

  • sourceBytes buffer | 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? }
local zaud = audio.encode(oggBytes, { bitrateKbps = 96 })

modules/audio/encodePcm

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

  • pcm any (optional) — Interleaved f32 samples — a buffer or a binary string of little-endian f32, the shape microphone.samples and audio.decode hand 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.
  • sampleRate number — Source sample rate in Hz.
  • channels number — 1 or 2, and a divisor of the sample count.
  • opts { [string]: any }? (optional) — Same shape as audio.encode.
local s = microphone.status(); local zaud = audio.encodePcm(microphone.samples(), s.sampleRate, 1)

modules/audio/info

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

  • zaud buffer | string — A ZAUD payload — a buffer or a binary string.
local info = audio.info(zaud); print(info.durationMs)

modules/audio/levels

levels(): AudioLevels

The master mix's peak and RMS over the meter's most recent closed window, measured without recording anything.

local l = audio.levels(); print(l.peak, l.rms, l.windowMs)

modules/audio/listener

listener(): AudioListenerState

Where the scene is heard from, how many active listeners exist, and which entity's listener drives the ears.

local l = audio.listener(); print(l.present, l.count, l.entity)

modules/audio/loopSeam

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

  • zaud buffer | string — A ZAUD payload — a buffer or a binary string.
local seam = audio.loopSeam(clipRef:getBytes()); print(seam.ratio, seam.seamless)

modules/audio/mixer

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.

local m = audio.mixer(); print(m.master, m.muted, m.channels.music)

modules/audio/observe

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.

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

modules/audio/peakSince

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

  • window number — A windows count taken from audio.levels() earlier.
local mark = audio.levels().windows
local peak = audio.peakSince(mark)

modules/audio/profile

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.

audio.resetProfile(); task.wait(1); local p = audio.profile()
print("per frame:", (p.pump.totalMs + p.observe.totalMs) / p.frames)

modules/audio/resetProfile

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

modules/audio/setChannelVolume

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

  • channel string — The channel name, matching Audio.channel.
  • volume number — Channel level, 0..1.
audio.setChannelVolume("music", 0.3)
for _, v in audio.voices() do print(v.channel, v.gain.channel) end

modules/audio/setMasterVolume

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

  • volume number — Master level, 0..1.
audio.setMasterVolume(0.5)

modules/audio/setMuted

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

  • muted boolean — Whether the mix is silenced.
audio.setMuted(true)

modules/audio/voice

voice(entityId: string): AudioVoice?

The voice on one entity, or nil when that entity carries no audio source.

Parameters

  • entityId string — The entity's stable id.
local v = audio.voice(e.id); print(v and v.mixerState)

modules/audio/voiceAccounting

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.

local v = audio.voiceAccounting(); print(v.inUse .. "/" .. v.capacity)
local v = audio.voiceAccounting(); print(v.sourcesHolding - (v.inUse + v.spatialInUse))

modules/audio/voices

voices(): { AudioVoice }

Every live audio source with the mixer's opinion of it.

for _, v in audio.voices() do print(v.clip, v.gain.effective) end

modules/audio/whySilent

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

  • entityId string — The entity's stable id.
local why = audio.whySilent(e.id); if why then print(why) end

modules/av/README

require("@builtin/modules/api/engine/av") -- av (also available as global 'av')

Audio/video encode + mux control — live streaming, recording, encoder/muxer primitives. Public Luau surface over the __av Internal FFI namespace.

Usage: local av = require("@builtin/modules/api/engine/av") Also available as global: av

modules/av/is_live

is_live(): boolean

True if a live-stream session is currently active.

if av.is_live() then av.stop_live() end

modules/av/is_recording

is_recording(): boolean

True if a recording session is currently active.

print("recording:", av.is_recording())

modules/av/live

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

  • opts LiveOpts? (optional) — Encoder options.
local url = av.live({ width = 1280, height = 720, fps = 60 })

modules/av/record

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

  • path string — VFS destination path.
  • opts RecordOpts? (optional) — Encoder options (optional).
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" })

modules/av/status

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.

local s = av.status(); print(s.backend, s.hardware, s.recordingAudio)

modules/av/stop_live

stop_live(): boolean

Stop any active live-stream session.

av.stop_live()

modules/av/stop_recording

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

  • handle string? (optional) — Promise handle of a specific recording (optional).
local stopped, ended = av.stop_recording()

modules/base64/README

require("@builtin/modules/api/engine/base64") -- base64 (also available as global 'base64')

Base64 encode / decode between binary and text Luau strings (standard RFC 4648 alphabet, padded). Public Luau surface over the __base64 Internal FFI namespace.

Usage: local base64 = require("@builtin/modules/api/engine/base64") Also available as global: base64

modules/base64/decode

decode(text: string): (string?, string?)

Decode standard-alphabet base64 text back to the original binary string.

Parameters

  • text string — Base64 text to decode.
local bytes = base64.decode(text)

modules/base64/encode

encode(bytes: buffer | string): string

Encode a binary string to standard-alphabet (padded) base64 text.

Parameters

  • bytes buffer | string — Binary bytes to encode.
local text = base64.encode(jpegBytes)

modules/bitmask_bits/README

bitmask_bits

The bitmask field-constraint validator: a constrained value must be a whole number that fits the declared width, so what the field reads back is a mask the system consuming it can actually address. A rejection names the width and why the value is not a mask of it. Registers itself with the generic field_constraints registry on load. nil passes, so a mask field may be left unset.

modules/blend/README

require("@builtin/modules/api/engine/blend") -- blend (also available as global 'blend')

Record-stride blend primitives over Buffer slices — layout registry + weighted/lerp combiners. Public Luau surface over the __blend Internal FFI namespace.

Usage: local blend = require("@builtin/modules/api/engine/blend") Also available as global: blend

modules/blend/destroyLayout

destroyLayout(handle: number): boolean

Drop the layout from the registry.

Parameters

  • handle number — Layout handle.

modules/blend/layout

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.
  • totalStride number? (optional) — Optional explicit record stride.
local l = blend.layout({ { offset = 0, stride = 3, op = "lerp" } })

modules/blend/lerpInto

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

  • outBuffer Substrate.TypedBuffer — The buffer written into.
  • layout number — Layout handle.
  • aBuffer Substrate.TypedBuffer — The A side of the fade.
  • bBuffer Substrate.TypedBuffer — The B side of the fade.
  • t number — Crossfade weight on B (0..1).

modules/blend/weightedInto

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

  • outBuffer Substrate.TypedBuffer — The buffer written into.
  • layout number — Layout handle.
  • inputs { BlendInput } — Array of { buffer, weight }.

modules/buoyancy/README

require("@builtin/systems/oceans/modules/buoyancy") -- buoyancy

Floats a rigid body on the sea the simulation is already drawing — displacement, damping and the righting moment that keeps a hull upright.

Usage: local buoyancy = require("@builtin/systems/oceans/modules/buoyancy")

modules/buoyancy/apply

apply(self, dt: number): boolean

Step the body once. The Buoyancy component calls this every frame; call it yourself only when driving a body without that component.

Parameters

  • self any (optional)
  • dt number — Seconds since the last step.
float:apply(1 / 60)

modules/buoyancy/attach

attach(target: any, options: any?): (any?, string?)

Float a rigid body on the sea. The entity needs a dynamic Physics body; the hull is measured from what it draws unless sizeX/Y/Z say otherwise.

Parameters

  • target any (optional) — The entity id or proxy to float.
  • options any? (optional){ density, damping, angularDamping, columns, sizeX, sizeY, sizeZ, mass, gravityScale, ocean }. density is relative to water, and the lower it is the higher the hull rides.
local float = buoyancy.attach(barrel, { density = 0.4 })

modules/buoyancy/clear

clear()

Stop floating every body. Each one is released, so a body driven by a Buoyancy component stops being pushed as well.

buoyancy.clear()

modules/buoyancy/columns

columns(size: any, columns: number): { { x: number, z: number } }

The local-space footprint of a hull's columns: an evenly spaced grid over its X/Z extent, each column standing for an equal share of the hull.

Parameters

  • size any (optional){ x, y, z } full extents of the hull in its own space.
  • columns number — Grid resolution per horizontal axis. 1 gives a single central column, which floats but cannot right itself; 2 is the smallest grid that produces a righting moment.
local cols = buoyancy.columns({ x = 4, y = 1, z = 2 }, 2)

modules/buoyancy/configure

configure(self, options: any)

Reconfigure a floating body. Any field may be passed; anything omitted is left as it was.

Parameters

  • self any (optional)
  • options any (optional){ density, damping, angularDamping, columns, sizeX, sizeY, sizeZ, mass, gravityScale, ocean }. mass and gravityScale stand in for the rigid body's own, which is what they are read from otherwise.
float:configure({ density = 0.8 })

modules/buoyancy/destroy

destroy(self)

Stop floating this body. The rigid body keeps everything else about it.

Parameters

  • self any (optional)
float:destroy()

modules/buoyancy/forEntity

forEntity(entityId: string): any

The floating body on an entity, if it has one.

Parameters

  • entityId string — The entity's id.
local float = buoyancy.forEntity(barrelId)

modules/buoyancy/list

list(): { any }

Every body currently floating.

print(#buoyancy.list())

modules/buoyancy/restingY

restingY(waterY: number, height: number, density: number): number

The height a hull of this density displaces its own weight at on flat water — the waterline its lift balances at, with no wave motion in it.

Parameters

  • waterY number — World Y of the undisturbed surface.
  • height number — The hull's full height.
  • density number — The hull's density relative to water.
local y = buoyancy.restingY(0, 1, 0.45)

modules/buoyancy/submergedFraction

submergedFraction(centreY: number, waterY: number, height: number): number

How much of a column stands under water: 0 clear of it, 1 fully under.

Parameters

  • centreY number — World Y of the column's mid-height.
  • waterY number — World Y of the surface above it.
  • height number — The column's full height.
local f = buoyancy.submergedFraction(0.2, 0.0, 1.0)

modules/buoyancy/submersion

submersion(self): number

The share of the hull that was under water on the last step, averaged over its columns. 0 is clear of the sea, 1 is fully submerged.

Parameters

  • self any (optional)
print(float:submersion())

modules/buoyancy/waterline

waterline(self): number?

The world Y of the water surface under the hull's centre on the last step — where the waterline stood, wave motion included.

Parameters

  • self any (optional)
print(float:waterline())

modules/cachedIndirect/README

require("@builtin/systems/radianceCache/cachedIndirect") -- cachedIndirect

Indirect diffuse light that is gathered once per patch of world and reused, rather than re-gathered for every pixel of every frame. It is the scene-wide user of @builtin::systems.radianceCache.radianceCache and the worked example of what a cache buys a lighting technique: the same bounce, converged over frames instead of within one, at a fraction of the gathering per frame.

Usage: local cachedIndirect = require("@builtin/systems/radianceCache/cachedIndirect")

modules/cachedIndirect/active

active(): boolean

Whether the cached-indirect passes are running this frame.

if cachedIndirect.active() then ... end

modules/cachedIndirect/cache

cache(): any

The cache this effect drives. The render feature takes its passes from here, and a script can read its counters or invalidate it through the same handle.

cachedIndirect.cache():stats()

modules/cachedIndirect/clear

clear()

Turn the cached bounce off and release the passes, the cache table and the resolve target. The other settings are kept, so a later set({ intensity = ... }) brings the effect back at the same settings and refills the table over history * stride frames.

cachedIndirect.clear()

modules/cachedIndirect/get

get(): { [string]: number }

The cached-indirect settings currently in force.

local s = cachedIndirect.get().stride

modules/cachedIndirect/invalidate

invalidate()

Declare that the lighting changed, so every patch takes its next gather whole instead of averaging it into light that is gone. The cache is rebuilt within stride frames.

cachedIndirect.invalidate()

modules/cachedIndirect/set

set(opts: CachedIndirectOpts?): { [string]: number }

Set the scene's cached indirect light. Any omitted field keeps its current value. An intensity of 0 turns it off and releases the passes.

Parameters

  • opts CachedIndirectOpts? (optional) — Cached-indirect settings — see CachedIndirectOpts.
cachedIndirect.set({ intensity = 1.0, stride = 8 })

modules/camera/README

require("@builtin/modules/api/engine/camera") -- camera (also available as global 'camera')

Script-facing camera queries: the main scene camera, the on-screen render camera, the editor fly-camera, per-frame view data, and the camera observation — which cameras drew this frame, with what projection, into what, and at what cost. Public Luau surface over the __camera Internal FFI namespace.

Usage: local camera = require("@builtin/modules/api/engine/camera") Also available as global: camera

modules/camera/active

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.

local camId = camera.active()

modules/camera/cut

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 }

modules/camera/editor

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.

local camId = camera.editor()
local id = camera.editor(); entity(id).position = { 12, 8, 12 }; tools.use("camera", "lookAt", id, { 0, 0, 0 })

modules/camera/editorOverride

editorOverride(): string?

Entity id currently overriding viewport selection, or nil when the viewport is decided by highest-priority-wins.

local owner = camera.editorOverride()

modules/camera/get

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.
local c = camera.get(camera.active()); print(c.frame.far, c.authored.far)
local c = camera.get("minimapCam"); print(c.rendering, c.reason)

modules/camera/list

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.

for _, c in ipairs(camera.list()) do print(c.name, c.enabled, c.rendering) end

modules/camera/main

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().

local camId = camera.main(); local cam = camId and entity(camId)

modules/camera/motionTally

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.

local before = camera.motionTally().withoutHistory

modules/camera/observe

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.

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

modules/camera/setEditorOverride

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

  • entityId string? (optional) — Entity id of the camera to route the viewport to, or nil to clear.
camera.setEditorOverride(camera.editor())
camera.setEditorOverride(nil)

modules/camera/viewData

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

modules/camera_spawner/README

camera_spawner

DEPRECATED: v7 scenes author their own Camera entity. This procedural primary-camera spawner serves legacy v6 scenes only; M.ensure stands down (returns early) for any scene carrying a string playerIntent.

Spawns the scene's primary Camera entity on non-additive layers.onLoad, for legacy v6 scenes. The per-mode world default (world.camera_default_<mode>) is the fallback; scene-level overrides via sceneProxy.settings.camera win when present. Missing refs → log.error + skip (no hardcoded fallback).

modules/channel/README

require("@builtin/modules/api/engine/channel") -- channel (also available as global 'channel')

Keyframe-channel sampling primitives — registry + sampleInto variants. Public Luau surface over the __channel Internal FFI namespace.

Usage: local channel = require("@builtin/modules/api/engine/channel") Also available as global: channel

modules/channel/create

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

  • opts ChannelOpts{ times, values, stride, interp }.
local h = channel.create({ times = ts, values = vs, stride = 3, interp = "linear" })

modules/channel/destroy

destroy(handle: number): boolean

Drop the channel from the registry.

Parameters

  • handle number — Channel handle.

modules/channel/sampleInto

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

  • ch number — Channel handle.
  • time number — Sample time in seconds.
  • buf Substrate.TypedBuffer — The buffer written into.
  • offset number — Starting f32 index in the buffer.

modules/channel/sampleManyInto

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

  • ch number — Channel handle.
  • time number — Sample time in seconds.
  • buf Substrate.TypedBuffer — The buffer written into.
  • offsets { number } — Array of f32 indices.

modules/channel/sampleQuat

sampleQuat(ch: number, time: number): (number?, number?, number?, number?)

Convenience accessor for stride-4 quaternion channels.

Parameters

  • ch number — Channel handle.
  • time number — Sample time.

modules/channel/sampleVec3

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

  • ch number — Channel handle.
  • time number — Sample time.
local x, y, z = channel.sampleVec3(h, t)

modules/color/README

require("@builtin/modules/api/engine/color") -- color (also available as global 'color')

Color construction, conversion, and perceptual ops (RGB / HSL / HSV / Oklch / hex). Public Luau surface over the __color Internal FFI namespace.

Usage: local color = require("@builtin/modules/api/engine/color") Also available as global: color

modules/color/coerce

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

  • value any (optional) — Value to read as a colour.
local c = color.coerce("#5a5a62") or color.coerce({ 0.2, 0.7, 0.2 })

modules/color/complementary

complementary(c: Color): Color

Complementary color — rotate hue 180° in Oklch space.

Parameters

  • c Color — Input color.
local accent = color.complementary(primary)

modules/color/darken

darken(c: Color, amount: number): Color

Decrease the lightness of a color in Oklch perceptual space.

Parameters

  • c Color — Input color.
  • amount number — Lightness decrease 0-1.
local pressed = color.darken(base, 0.1)

modules/color/desaturate

desaturate(c: Color, amount: number): Color

Decrease the chroma (saturation) of a color in Oklch space.

Parameters

  • c Color — Input color.
  • amount number — Chroma decrease (typically 0-0.2).
local muted = color.desaturate(base, 0.05)

modules/color/hex

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

  • hexString string — Hex color string.
local fromCss = color.hex("#ff8800")

modules/color/hsl

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

  • h number — Hue (degrees, 0-360).
  • s number — Saturation (0-1).
  • l number — Lightness (0-1).
local teal = color.hsl(180, 0.5, 0.5)

modules/color/hsla

hsla(h: number, s: number, l: number, a: number): Color

Build a color from HSLA, returned as sRGB.

Parameters

  • h number — Hue (0-360).
  • s number — Saturation (0-1).
  • l number — Lightness (0-1).
  • a number — Alpha (0-1).
local fadedTeal = color.hsla(180, 0.5, 0.5, 0.3)

modules/color/hsv

hsv(h: number, s: number, v: number): Color

Build a color from HSV (h: 0-360, s: 0-1, v: 0-1).

Parameters

  • h number — Hue (0-360).
  • s number — Saturation (0-1).
  • v number — Value / brightness (0-1).
local primary = color.hsv(220, 0.7, 0.9)

modules/color/lighten

lighten(c: Color, amount: number): Color

Increase the lightness of a color in Oklch perceptual space.

Parameters

  • c Color — Input color.
  • amount number — Lightness increase 0-1.
local hover = color.lighten(base, 0.1)

modules/color/linear

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

  • r number — Linear red (0-1).
  • g number — Linear green (0-1).
  • b number — Linear blue (0-1).
  • a number? (optional) — Alpha (0-1, default 1).
local gpuBlue = color.linear(0.0, 0.0, 1.0)

modules/color/mix

mix(c1: Color, c2: Color, t: number): Color

Perceptually blend two colors in Oklch space — better than RGB mixing for gradients.

Parameters

  • c1 Color — First color.
  • c2 Color — Second color.
  • t number — Blend factor 0-1 (0 = c1, 1 = c2).
local mid = color.mix(color.rgb(255, 0, 0), color.rgb(0, 0, 255), 0.5)

modules/color/mixRgb

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

  • c1 Color — First color.
  • c2 Color — Second color.
  • t number — Blend factor 0-1.
local plain = color.mixRgb(a, b, 0.5)

modules/color/oklch

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

  • l number — Lightness (0-1).
  • c number — Chroma / saturation (0-0.4).
  • h number — Hue (0-360).
local accent = color.oklch(0.7, 0.15, 30)

modules/color/rgb

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

  • r number — Red channel (0-255).
  • g number — Green channel (0-255).
  • b number — Blue channel (0-255).
local red = color.rgb(255, 0, 0)

modules/color/rgba

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

  • r number — Red channel (0-255).
  • g number — Green channel (0-255).
  • b number — Blue channel (0-255).
  • a number — Alpha (0-1).
local halfRed = color.rgba(255, 0, 0, 0.5)

modules/color/rotateHue

rotateHue(c: Color, degrees: number): Color

Rotate the hue of a color by a given number of degrees in Oklch space.

Parameters

  • c Color — Input color.
  • degrees number — Hue rotation (positive or negative).
local triadic = color.rotateHue(base, 120)

modules/color/saturate

saturate(c: Color, amount: number): Color

Increase the chroma (saturation) of a color in Oklch space.

Parameters

  • c Color — Input color.
  • amount number — Chroma increase (typically 0-0.2).
local pop = color.saturate(base, 0.05)

modules/color/toHex

toHex(c: Color): string

Convert a color to a hex string. Returns "#rrggbb" or "#rrggbbaa" if alpha is not 1.

Parameters

  • c Color — Input color.
print(color.toHex(color.rgb(255, 136, 0))) -- "#ff8800"

modules/color/toHsl

toHsl(c: Color): HslColor

Convert a color to HSL.

Parameters

  • c Color — Input color.
local hsl = color.toHsl(base)

modules/color/toLinear

toLinear(c: Color): Color

Convert a color from sRGB to linear RGB space — useful for GPU calculations that need linear-space values.

Parameters

  • c Color — Input sRGB color.
local gpu = color.toLinear(base)

modules/color/toOklch

toOklch(c: Color): OklchColor

Convert a color to Oklch perceptual color space.

Parameters

  • c Color — Input color.
local okl = color.toOklch(base)

modules/color/withAlpha

withAlpha(c: Color, a: number): Color

Return a copy of a color with a different alpha value.

Parameters

  • c Color — Input color.
  • a number — New alpha (0-1).
local ghost = color.withAlpha(base, 0.3)

modules/compute/README

require("@builtin/modules/api/engine/compute") -- compute (also available as global 'compute')

GPU compute pipelines — compile shaders, dispatch workgroups, read back results. Public Luau surface over the __compute Internal FFI namespace. A buffer belongs to the shader that owns it (shaderRef:createBuffer) or to the substrate (substrate.createBuffer), and reaches a dispatch as a handle.

Usage: local compute = require("@builtin/modules/api/engine/compute") Also available as global: compute

modules/compute/absentReasons

absentReasons(): { string }

Every reason compute.diagnose reports, sorted. resident is the one that means the resource is there.

for _, r in ipairs(compute.absentReasons()) do print(r) end

modules/compute/beginBvh

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.
local id = compute.beginBvh(gather.instances)

modules/compute/buildBvh

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.
local bvh = compute.buildBvh(instances)
shader:dispatch({ buffers = { bvh.nodes, bvh.tris }, workgroups = { 64, 1, 1 } })

modules/compute/buildBvhSliced

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).
local built, err = compute.buildBvhSliced(gather.instances, { budgetMs = 4 })

modules/compute/bvhBuilds

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.

print(#compute.bvhBuilds(), "hierarchies in flight")

modules/compute/cancelBvh

cancelBvh(id: number): boolean

Drop a build along with the triangles it has gathered.

Parameters

  • id number — Build id from compute.beginBvh.
compute.cancelBvh(id)

modules/compute/compile

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

  • nameOrHandle string | { [string]: any } | AssetRef — Shader identity to register under (string or asset handle).
  • opts { [string]: any }? (optional){ source, entryPoint?, bindings, params? }bindings is an ordered list of { name, kind, access?, element?, format?, array? }.
compute.compile("my_sim", { source = WGSL, bindings = { { name = "data", kind = "buffer", access = "read_write", element = "f32" } } })

modules/compute/compileByName

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

  • ref string | { [string]: any } | AssetRef — A .computeShader identity/guid string, or a resolved asset handle.
compute.compileByName("@builtin::shaders.compute_double")

modules/compute/copyBufferToTexture

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

  • bufferName string — Source compute buffer.
  • textureKey string — Cache key to register the texture under.
  • width number — Texture width in texels.
  • height number — Texture height in texels.
  • format string? (optional) — Texel format: "rgba16f" (default), "rgba32f", "rgba8".
compute.copyBufferToTexture("gi_resolved", "lm_wall", 128, 128)

modules/compute/createBuffer

createBuffer(name: string, opts: { [string]: any }): boolean

Allocate a buffer under name, sized in bytes.

Parameters

  • name string — The name a dispatch binds it by.
  • opts { [string]: any }{ size, readback? }size in bytes.

modules/compute/createSampler

createSampler(name: string, opts: { [string]: any }?): boolean

Create a named GPU sampler. opts: filter/wrap settings.

Parameters

  • name string
  • opts { [string]: any }? (optional)

modules/compute/createStorageTexture2D

createStorageTexture2D(name: string, opts: { [string]: any }): boolean

Create a 2D storage texture (compute-writable render target). opts: { width, height, format? }.

Parameters

  • name string
  • opts { [string]: any }

modules/compute/createTexture3D

createTexture3D(name: string, opts: { [string]: any }): boolean

Create a 3D texture volume. opts: { width, height, depth, format?, storage? }.

Parameters

  • name string — Unique volume name.
  • opts { [string]: any } — Dimensions + format (r8/r16f/r32f/rgba8/rgba16f/rgba32f).

modules/compute/createTextureHistory

createTextureHistory(name: string, opts: { [string]: any }): boolean

Create a temporal history buffer (ping-pong textures) for a target. opts: { width, height, format? }.

Parameters

  • name string
  • opts { [string]: any }

modules/compute/destroyBuffer

destroyBuffer(name: string): boolean

Release the buffer allocated under name.

Parameters

  • name string — The name it was created under.

modules/compute/destroySampler

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

  • name string — Sampler name.
compute.createSampler("soft", { filter = true }); compute.destroySampler("soft")

modules/compute/destroyShader

destroyShader(name: string): boolean

Destroy a named compute shader pipeline.

Parameters

  • name string — Shader name.

modules/compute/destroyShaderEx

destroyShaderEx(name: string): boolean

Destroy a shader registered via registerShaderEx.

Parameters

  • name string

modules/compute/destroyStorageTexture2D

destroyStorageTexture2D(name: string): boolean

Destroy a named 2D storage texture.

Parameters

  • name string

modules/compute/destroyTexture

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

  • textureKey string — Cache key the texture was registered under.
compute.destroyTexture("lm_wall")

modules/compute/destroyTexture3D

destroyTexture3D(name: string): boolean

Destroy a named 3D volume and free its GPU memory.

Parameters

  • name string

modules/compute/destroyTextureHistory

destroyTextureHistory(name: string): boolean

Destroy a named texture-history buffer.

Parameters

  • name string

modules/compute/diagnose

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

  • key string — The resource key, verbatim.
local d = compute.diagnose(key)
if not d.exists then print(d.reason, d.current) end

modules/compute/dispatch

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

  • shaderNameOrHandle string | { [string]: any } | AssetRef — Shader name or asset handle.
  • opts DispatchOpts{ buffers, workgroups }. A zero in any workgroup dimension is refused and recorded as a dispatch failure — clamp a computed count with math.max(1, math.ceil(n / 64)).
compute.dispatch("blur", { buffers = { "src", "dst" }, workgroups = { 8, 8 } })

modules/compute/dispatchEx

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

  • shaderNameOrHandle string | { [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 with math.max(1, math.ceil(n / 64)).

modules/compute/dispatchOnVertices

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

  • shaderNameOrHandle string | { [string]: any } | AssetRef — Shader name or asset handle.
  • opts DispatchOnVerticesOpts{ model, buffers?, workgroups }. A zero in any workgroup dimension is refused and recorded as a dispatch failure — clamp a computed count with math.max(1, math.ceil(n / 64)).
compute.dispatchOnVertices("flatten", { model = "@user/mesh", workgroups = { 64 } })

modules/compute/failing

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.

for _, d in ipairs(compute.failing()) do print(d.shader, d.lastError) end

modules/compute/finishBvh

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

  • id number — Build id from compute.beginBvh, stepped until "ready".
local built = compute.finishBvh(id)

modules/compute/getReadbackResult

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

  • resultKey string — Key returned by readBuffer().
local floats = compute.getReadbackResult(key)

modules/compute/getReadbackResultBytes

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

  • resultKey string — Key returned by readBuffer().
local bytes = compute.getReadbackResultBytes(key)
local firstTexel = buffer.readf32(bytes, 0)

modules/compute/getReadbackResultU32

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

  • resultKey string — Key returned by readBuffer().

modules/compute/isReadbackReady

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

  • resultKey string — Key returned by readBuffer().

modules/compute/observe

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.

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

modules/compute/program.compile

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

  • key string — The key to register under.
  • spec { [string]: any }{ source, entryPoint?, bindings, params } — the parsed schema.

modules/compute/program.destroy

program.destroy(key: string): boolean

Release the program registered under key.

Parameters

  • key string — The program's key.

modules/compute/program.dispatch

program.dispatch(key: string, opts: { [string]: any }): boolean

Dispatch the program under key with one buffer per declared storage binding, in declaration order.

Parameters

  • key string — 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 with math.max(1, math.ceil(n / 64)).

modules/compute/program.dispatchEx

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

  • key string — 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 with math.max(1, math.ceil(n / 64)).

modules/compute/program.dispatchOnVertices

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

  • key string — 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 with math.max(1, math.ceil(n / 64)).

modules/compute/program.setParam

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

  • key string — The program's key.
  • prop string — Parameter name as declared.
  • value number — New scalar value.

modules/compute/program.status

program.status(key: string): { { [string]: any } }

What the engine did with the dispatches of the program under key, one record per target.

Parameters

  • key string — The program's key.

modules/compute/programState

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

  • ref string | { [string]: any } | AssetRef — A .computeShader identity/guid string, a resolved asset handle, or the name a raw registration chose.
if compute.programState(carve) == "ready" then carve:dispatch(opts) end

modules/compute/readBuffer

readBuffer(name: string): string

Start a GPU→CPU read of the buffer under name.

Parameters

  • name string — The name it was created under.

modules/compute/readTexture3D

readTexture3D(name: string): string

Start a GPU→CPU readback of a named 3D volume. Returns a result key to poll.

Parameters

  • name string

modules/compute/readbackState

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

  • resultKey string — Key returned by readBuffer().
if compute.readbackState(key) == "ready" then ... end

modules/compute/registerShader

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

  • nameOrHandle string | { [string]: any } | AssetRef — Shader name or asset handle.
  • opts ShaderOpts? (optional) — Shader options. bindings comes from the source's own @group(0) @binding(n) declarations when omitted; supplying a count that disagrees with them raises. Every readOnlyBindings entry names one of those declared bindings, as a whole number from 0 to bindings - 1; an entry outside that run raises.
compute.registerShader("blur", { source = WGSL, entryPoint = "main" })

modules/compute/registerShaderEx

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

  • nameOrHandle string | { [string]: any } | AssetRef
  • opts { [string]: any }? (optional)

modules/compute/resources

resources(owner: any?): { any }

The resource rows on their own, optionally narrowed to what one shader owns.

Parameters

  • owner any? (optional) — A .computeShader ref, 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.
for _, r in ipairs(compute.resources(shaderRef)) do print(r.key, r.bytes) end

modules/compute/setParam

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

  • nameOrHandle string | { [string]: any } | AssetRef — Shader identity (the .computeShader asset name), or the handle asset.load / asset.resolve returns — the same forms dispatch takes.
  • prop string — Parameter name as declared in bindings.yaml.
  • value number — New scalar value (numbers only).
compute.setParam("my_sim", "scale", 4.0)

modules/compute/stepBvh

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

  • id number — Build id from compute.beginBvh.
  • budgetMs number? (optional) — Wall time this slice may spend, in milliseconds (default 4).
while compute.stepBvh(id, 4) == "pending" do task.wait() end

modules/compute/textureFormatBytes

textureFormatBytes(format: string): number

Bytes-per-voxel for a texture format string (rgba16f, r8, ...).

Parameters

  • format string

modules/compute/writeBuffer

writeBuffer(name: string, values: { number } | buffer | string, offset: number?): boolean

Write words into the buffer under name.

Parameters

  • name string — The name it was created under.
  • values { number } | buffer | string — The floats to write, or a buffer / binary string already holding them.
  • offset number? (optional) — 32-bit word offset to write at.

modules/compute/writeBufferBytes

writeBufferBytes(name: string, bytes: buffer | string, offsetBytes: number?): boolean

Write packed bytes into the buffer under name.

Parameters

  • name string — The name it was created under.
  • bytes buffer | string — The payload.
  • offsetBytes number? (optional) — Byte offset to write at.

modules/compute/writeBufferU32

writeBufferU32(name: string, values: { number } | buffer | string, offsetBytes: number?): boolean

Write 32-bit words into the buffer under name.

Parameters

  • name string — The name it was created under.
  • values { number } | buffer | string — The words to write.
  • offsetBytes number? (optional) — Byte offset to write at.

modules/compute/writeFloatsTexture3D

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

  • name string
  • floats { number }
  • formatOrOpts (string | { [string]: any })? (optional)

modules/compute/writeTexture3D

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

  • name string — Volume name.
  • data buffer | string | { number } — Voxel bytes as a buffer, a binary string, or an array of bytes.

modules/connected_users/README

connected_users

world.connectedUsers.* — server-replicated user registry. This module is user-scope and user-editable. The file you're reading is /zero/source/libs/@builtin/modules/connected_users.module/init.luau — anyone (any script, any agent, any user with VFS write) can rewrite it; hot-reload picks the change up and the engine uses the new version. There is no security boundary here. The read-only metatable below catches accidental writes loudly (API hygiene); it doesn't enforce anything. Trust boundaries that DO exist: - The Rust FFI (__connected_users.local_identity()) is registered into the VM state by the engine at boot and cannot be replaced from Luau. When it's called, it returns the real JWT sub. - The trusted VM (src/lua/trusted/**) is a separate Luau state, not on the user-writable VFS root, holds the auth.* namespace. User-scope Luau cannot reach it. - The server verifies the JWT on every RPC. Anything the server gates on flows through that check, not through whatever this module reports. What IS load-bearing: - __connected_users.local_identity() (Rust FFI) cannot be replaced. It always returns the real JWT sub of the engine's UserCredential, or nil for anonymous sessions. - The JWT bytes and session token NEVER cross any FFI exposed here; only the extracted identity string does. A malicious user-scope script can lie about identity but cannot exfiltrate the bearer token. - Authority on the server is established by the JWT the SDK presents on every request, not by anything this module reports. Multi-user replication (other connected users' records) plugs in on top of the same User metatable shape; today the registry contains only localUser because the boot path is the only populated source. The list/get/exists/count surface is shaped so it stays correct once server replication populates the remaining records.

modules/contactShadows/README

require("@builtin/systems/contactShadows/contactShadows") -- contactShadows

Short screen-space shadow traces that supply the small-scale contact a shadow map cannot resolve — the grounding under a chair leg, a prop on a table, anything whose contact is finer than one shadow texel.

Usage: local contactShadows = require("@builtin/systems/contactShadows/contactShadows")

modules/contactShadows/active

active(): boolean

Whether the contact-shadow pass is running this frame.

if contactShadows.active() then ... end

modules/contactShadows/clear

clear()

Turn contact shadows off and release the pass. The other settings are kept, so a later set({ strength = ... }) brings back the same look.

contactShadows.clear()

modules/contactShadows/get

get(): ContactState

The contact-shadow settings currently in force.

local l = contactShadows.get().length

modules/contactShadows/lights

lights(): { ContactLight }

The point, spot and distant lights the last refresh packed for the trace pass, ranked by what each is worth at the main camera.

local n = #contactShadows.lights()

modules/contactShadows/lightsBuffer

lightsBuffer(): any?

The light buffer the trace pass reads — one row per light the scene's contact traces reach toward, packed by the last refresh.

local l = contactShadows.lightsBuffer()

modules/contactShadows/paramsBuffer

paramsBuffer(): any?

The buffer this system's passes read. The render feature binds what this hands it, so the pass has the values this module packed.

local p = contactShadows.paramsBuffer()

modules/contactShadows/refresh

refresh()

Re-read the scene's lights and re-push what the trace pass reads: the sun's direction and weight, and the lights supplying contact of their own. The render feature calls this every frame, so the traces follow lights that move, brighten or are spawned while the scene runs.

contactShadows.refresh()

modules/contactShadows/set

set(opts: ContactOpts?): ContactState

Set the scene's contact shadows. Any omitted field keeps its current value. A strength of 0 turns them off and releases the pass.

Parameters

  • opts ContactOpts? (optional) — Contact-shadow settings — see ContactOpts.
contactShadows.set({ strength = 0.9, length = 0.35, steps = 16 })

modules/data_contract/README

data_contract

The dataContract field-constraint validator: a constrained value must be a .data instance whose dataType contract chain includes constraint.contract. Registers itself with the generic field_constraints registry on load.

modules/debanding/README

require("@builtin/systems/debanding/debanding") -- debanding

Gradient debanding. Rebuilds a shallow ramp that eight-bit storage flattened into shelves, so a sky or a light falloff reads as continuous instead of as a stack of contour lines.

Usage: local debanding = require("@builtin/systems/debanding/debanding")

modules/debanding/active

active(): boolean

Whether the debanding pass is running this frame.

if debanding.active() then ... end

modules/debanding/disable

disable()

Turn debanding off and release the pass.

debanding.disable()

modules/debanding/enable

enable(strength: number?): State

Turn debanding on at a given strength.

Parameters

  • strength number? (optional) — Strength in [0, 1]. Omit to keep the current value.
debanding.enable()

modules/debanding/get

get(): State

The debanding settings currently in force.

local s = debanding.get().threshold

modules/debanding/paramsBuffer

paramsBuffer(): any?

The buffer this system's passes read. The render feature binds what this hands it, so the pass has the values this module packed.

local p = debanding.paramsBuffer()

modules/debanding/set

set(opts: DebandOpts?): State

Set the debanding settings. Any omitted field keeps its current value.

Parameters

  • opts DebandOpts? (optional) — Debanding settings — see DebandOpts.
debanding.set({ threshold = 3, radius = 16 })

modules/debugger/README

require("@builtin/modules/api/engine/debugger") -- debugger (also available as global 'debugger')

Luau debugger — breakpoints, stepping, stack inspection, watches. Public Luau surface over the __debugger Internal FFI namespace.

Usage: local debugger = require("@builtin/modules/api/engine/debugger") Also available as global: debugger

modules/debugger/__diagnostics

__diagnostics(): DebuggerDiagnostics

Internal diagnostic counters for debugging the debugger itself: { installs, debugbreakHits }.

modules/debugger/addWatch

addWatch(expr: string): number

Register an expression to re-evaluate on every pause.

Parameters

  • expr string — Luau expression.

modules/debugger/continue_

continue_(): boolean

Resume the paused thread.

modules/debugger/disableAll

disableAll()

Disable every registered breakpoint. Records persist; bytecode BREAK ops are cleared.

modules/debugger/disconnect

disconnect(handle: number): boolean

Disconnect an onBreak or onResume callback.

Parameters

  • handle number — Handle returned by onBreak/onResume.

modules/debugger/enableAll

enableAll()

Enable every registered breakpoint and re-install them in the VM bytecode.

modules/debugger/evaluate

evaluate(expr: string, frame: number?): (string?, string?)

Evaluate an expression against the paused frame's environment. Returns (value, error).

Parameters

  • expr string — Luau expression.
  • frame number? (optional) — 1-based frame index (default 1).

modules/debugger/getLocals

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

  • frame number? (optional) — 1-based frame index (default 1).

modules/debugger/getPauseInfo

getPauseInfo(): PauseInfo?

Info about the active pause, or nil if nothing is paused.

modules/debugger/getStack

getStack(): { Frame }

Captured stack from the active pause, top frame first. Empty when nothing is paused.

modules/debugger/getUpvalues

getUpvalues(frame: number?): { [string]: string }

Upvalues captured at the active pause for the given frame.

Parameters

  • frame number? (optional) — 1-based frame index.

modules/debugger/getWatchValue

getWatchValue(id: number): (string?, string?)

Re-evaluate the watch expression against the paused frame's environment and return (value, error).

Parameters

  • id number — Watch id.

modules/debugger/getWatches

getWatches(): { Watch }

Snapshot of all watches with their last evaluated value and error, sorted by id.

modules/debugger/isPauseOnError

isPauseOnError(): boolean

Current pause-on-error toggle state for this VM.

modules/debugger/isPaused

isPaused(): boolean

Whether the debugger currently has a paused thread.

modules/debugger/listBreakpoints

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.

modules/debugger/onBreak

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.

modules/debugger/onResume

onResume(fn: () -> ()): number

Register a callback invoked when the paused thread is resumed.

Parameters

  • fn () -> () — Callback.

modules/debugger/removeBreakpoint

removeBreakpoint(id: number): boolean

Remove the breakpoint with the given id.

Parameters

  • id number — Breakpoint id returned by setBreakpoint.

modules/debugger/removeWatch

removeWatch(id: number): boolean

Remove the watch with the given id.

Parameters

  • id number — Watch id.

modules/debugger/setBreakpoint

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

  • path string — VFS path, require identity, or chunk name.
  • line number — 1-based source line.
  • opts BreakpointOpts? (optional){ condition?, logMessage?, hitCount?, enabled? }.
local bp = debugger.setBreakpoint("/zero/source/main.luau", 42)
print(bp.pendingReason or ("installed in " .. bp.chunkNames[1]))

modules/debugger/setPauseOnError

setPauseOnError(enabled: boolean)

When true, uncaught Luau errors fire the onBreak callback (observation only — the error still propagates).

Parameters

  • enabled boolean — Toggle state.

modules/debugger/stepInto

stepInto(): boolean

Run until the next line, descending into any function call.

modules/debugger/stepOut

stepOut(): boolean

Run until the current frame returns; pauses in the caller.

modules/debugger/stepOver

stepOver(): boolean

Run until the next line in the current frame. Calls inside the current line are skipped.

modules/debugger/toggleBreakpoint

toggleBreakpoint(path: string, line: number): Breakpoint?

Toggle a breakpoint at the given line: removes if present, adds otherwise.

Parameters

  • path string — VFS path, require identity, or chunk name.
  • line number — 1-based line.

modules/denoiser/README

denoiser

Edge-aware denoising a render feature routes a noisy signal through. A feature that shades from a few stochastic samples per pixel writes a result carrying that sampling noise, and the only way to quieten it in the feature itself is to cast more rays. Filtering the result instead buys the same quality far more cheaply, and the filter is the same one for every such feature, so it lives here rather than being rewritten per effect. A filter smooths in two directions. Across the frame it is an à-trous wavelet: successive passes with a doubling tap stride, so a handful of 5x5 passes reach the radius a single wide blur would need hundreds of taps for. Each tap is weighted by how much the surface under it resembles the surface under the centre pixel — its world position from @scene.depth and its normal from @scene.normal — so the smoothing follows geometry and stops at depth and normal discontinuities instead of bleeding an object's occlusion onto the wall behind it. Across time — with temporal set — it first carries the previous frame's estimate forward through @scene.motion, so the standing average holds far more samples than any one frame casts and the feature feeding it can trace fewer rays for the same quietness. A reprojected estimate is admitted only where the surface it was written on is the surface being shaded now, and never for longer than historyFrames. Each filter owns its targets and its compiled passes, keyed by the name it was created with, so two features denoising in the same frame do not interfere.

modules/denoiser/create

create(name: string, opts: DenoiseOpts?): Filter

Create a filter that owns its own targets and passes. name keys those resources, so two features denoising in the same frame each pass their own name and never share state.

Parameters

  • name string — Identifies this filter's resources. Unique per feature.
  • opts DenoiseOpts? (optional) — Filtering settings — see DenoiseOpts.
local d = denoiser.create("rt_ao", { iterations = 4, worldSigma = 0.5, temporal = true })

modules/denoiser/destroy

destroy(self: Filter)

Release the filter's targets. The filter rebuilds on its next run.

Parameters

  • self Filter
filter:destroy()

modules/denoiser/passes

passes(self: Filter): number

Consecutive order slots run occupies, counted from the order it is given, so a caller knows where its own next pass can sit.

Parameters

  • self Filter
local apply = 51 + filter:passes()

modules/denoiser/reset

reset(self: Filter)

Drop what the accumulation holds, so the next frame starts from its own samples. Call it at a camera cut, where nothing on screen was on the last frame and no reprojection could find it.

Parameters

  • self Filter
filter:reset()

modules/denoiser/run

run(self: Filter, ctx: any, source: string, opts: { phase: string?, order: number? }?): string

Enqueue this filter's passes over source, and answer the guid holding the filtered result. Call it from a render feature's render, passing the same ctx; the result is ready for the phase and order given.

Parameters

  • self Filter
  • ctx any (optional) — The render context the calling feature received.
  • source string — Guid of the texture holding the noisy signal.
  • opts { phase: string?, order: number? }? (optional){ phase, order } — where the filter's passes run. order is the first of passes() consecutive slots.
local clean = filter:run(ctx, noisy.guid, { phase = "afterLighting", order = 55 })

modules/denoiser/stats

stats(self: Filter): DenoiseStats

What the filter is doing right now — whether it accumulates, whether it holds an accumulation, how many times that has been dropped, and the size its targets are built for.

Parameters

  • self Filter
if not filter:stats().warmed then print("first frame of the accumulation") end

modules/depthOfField/README

require("@builtin/systems/depthOfField/depthOfField") -- depthOfField

Depth of field from a lens — focus distance, focal length and aperture, so the falloff behaves the way a camera's does.

Usage: local depthOfField = require("@builtin/systems/depthOfField/depthOfField")

modules/depthOfField/active

active(): boolean

Whether the defocus passes are currently running.

if depthOfField.active() then print("shallow") end

modules/depthOfField/clear

clear()

Turn defocus off and release the passes. The lens is kept, so a later set({ fStop = ... }) brings back the same look.

depthOfField.clear()

modules/depthOfField/get

get(): DepthOfFieldState

The lens currently in force.

local f = depthOfField.get().focusDistance

modules/depthOfField/paramsBuffer

paramsBuffer(): any?

The buffer this system's passes read. The render feature binds what this hands it, so the pass has the values this module packed.

local p = depthOfField.paramsBuffer()

modules/depthOfField/set

set(opts: DepthOfFieldOpts?): DepthOfFieldState

Set the lens. Any omitted field keeps its current value, so a call can rack focus without restating the rest. An fStop of 0 turns defocus off and releases the passes.

Parameters

  • opts DepthOfFieldOpts? (optional) — Lens settings — see DepthOfFieldOpts.
depthOfField.set({ focusDistance = 8, fStop = 1.8, focalLength = 85 })

modules/edui.app/README

edui.app

The edui reactive core: an App owns one ui.* screen, maps author closures to ui.* string callback ids, dispatches the broadcast that ui.registerCallbackEnv delivers, and rebuilds the widget tree on state change. This is the layer that lets editor chrome be written with closures (onClick = function() ... end) instead of hand-managed string ids — the footgun the deprecated zui grew three shapes for.

modules/edui.app/adoptHandlers

adoptHandlers(self: any, bucket: { [string]: (any) -> () })

Re-register a bucket of handlers collected by collectHandlers into the current build, keeping a cached subtree's callbacks live.

Parameters

  • self any (optional)
  • bucket { [string]: (any) -> () } — The handler bucket to adopt.
app:adoptHandlers(cached.bucket)

modules/edui.app/cb

cb(self: any, closure: (any) -> (), key: (string | number)?): string

Allocate (or reuse) a callback id bound to closure for this build. Pass a stable key so the id is identical across rebuilds — required for anything the renderer tracks by id (focus, drag, ui.widgetState). Without a key the id is a per-build sequence number (fine for a fire-and-forget button).

Parameters

  • self any (optional)
  • closure (any) -> () — The handler (data) -> () the id dispatches to.
  • key (string | number)? (optional) — Optional stable key (unique within this screen).
props = { onClick = app:cb(function() doThing() end, "save") }

modules/edui.app/cbRaw

cbRaw(self: any, id: string, closure: (any) -> ()): string

Register closure under the EXACT id id, without the screen-name prefix cb adds. For wiring engine-emitted interaction ids that a widget publishes itself (e.g. a dockArea's <panelId>-close), which arrive unprefixed. Re-issue it each build, like cb.

Parameters

  • self any (optional)
  • id string — The exact callback id the engine will deliver.
  • closure (any) -> () — The handler (data) -> ().
app:cbRaw(panelId .. "-close", function() closePanel(panelId) end)

modules/edui.app/collectHandlers

collectHandlers(self: any, fn: () -> any): (any, { [string]: (any) -> () })

Collect every callback registered while fn runs into a named bucket, returned alongside fn's result. A host that CACHES the subtree fn built re-adopts the bucket on later builds (adoptHandlers), so the cached tree's callback ids stay live across rebuilds that skipped it.

Parameters

  • self any (optional)
  • fn () -> any — The builder to run.

Returns () })fn's result, and the bucket of handlers it registered.

local tree, bucket = app:collectHandlers(function() return panel.build() end)

modules/edui.app/current

current(): any

The app whose builder is currently running, or nil outside a build. The edui.* primitive builders read this to register their closures, so authors don't thread the app through every call.

local app = edui.app.current()

modules/edui.app/dispatch

dispatch(self: any, id: string, data: any): boolean

Route a callback id to its closure and schedule a rebuild. This is what the screen's onCallback broadcast calls. Returns true when the id was handled by this app (an id from another surface returns false so a host can keep routing).

Parameters

  • self any (optional)
  • id string — The callback id the broadcast delivered.
  • data any (optional) — The engine event envelope { value, eventType, widgetId, button, mouseX, mouseY }. The handler receives its value: a scalar for onChange (checkbox bool, input string, select value), a { dx, dy, shift, ctrl, alt } table for a canvas onDrag/onScroll, nil for a bare onClick.
function onCallback(id, data) app:dispatch(id, data) end

modules/edui.app/markDirty

markDirty(self: any)

Mark the app dirty and rebuild its tree. Coalesces a rebuild requested WHILE a build is running into a single follow-up build (so a handler that mutates state and a builder that reads it never recurse), and one requested WHILE a dispatch's handler runs into the single rebuild that dispatch performs after the handler returns.

Parameters

  • self any (optional)

modules/edui.app/mount

mount(self: any, builderFn: (any) -> any): any

Mount the app: register the broadcast callback env and do the first build. builderFn(app) -> widgetTree is called now and on every rebuild; inside it, app:cb(...) (or the edui.* primitives) wire closures.

Parameters

  • self any (optional)
  • builderFn (any) -> any(app) -> widgetTree.
app:mount(function(a) return { type = "vertical", children = { ... } } end)

modules/edui.app/new

new(screenName: string, opts: { [string]: any }?): any

Create an editor app that owns the ui.* screen screenName.

Parameters

  • screenName string — Unique screen id (also the callback-id namespace prefix).
  • opts { [string]: any }? (optional){ layer? = <renderLayerMask>, order? = <number>, callbackKey? = <string> }. layer is a render-layer membership mask (e.g. the EditorUI bit) applied via ui.setScreenRenderLayer on first register; omit for the default. order is the screen's Z-ORDER — higher paints on top; a floating surface (the command palette) states one to stand over the dock.
local app = edui.app.new("myPanel", { layer = editorMask })

modules/edui.app/rebuild

rebuild(self: any)

Run the builder and push the resulting tree to the screen. First call registers the screen (+ render layer); later calls update it. Builder errors are logged, never thrown, so one bad build never wedges the editor.

Parameters

  • self any (optional)

modules/edui.app/unmount

unmount(self: any)

Tear the app down: release the callback env and unregister the screen.

Parameters

  • self any (optional)

modules/edui.argForm/README

require("@builtin/~edui.argForm") -- edui.argForm

The typed argument form — a declared argument list becomes a form of REAL controls, and the filled form becomes the typed values a call takes. A person never types a Luau literal: strings get a text field, numbers a numeric field, booleans a checkbox, string-literal unions a chip row, colours a colour swatch, Vec3 three axis fields, and an options table expands one level into typed rows of its own. Only a type outside the model falls back to the literal field. The command palette renders every tool through this; the Inspector renders an asset type's operations through the same form, so any surface that knows a signature can offer it.

Usage: local edui.argForm = require("@builtin/~edui.argForm")

modules/edui.argForm/fieldRows

fieldRows(self: any, path: string, t: string, optional: boolean,

The label + control rows for one argument (or one expanded table field). depth indents expanded option fields under their parent.

modules/edui.argForm/make

make(W: any): any

Bind the form builder to the widgets barrel W. The barrel calls this once; consumers reach the result as edui.argForm.

Parameters

  • W any (optional)

modules/edui.argForm/new

new(o: { [string]: any }): any

A new form instance. o.ns (REQUIRED — the id namespace every control keys under), o.typeDefs (the schema's named definitions, { { name, definition } }), o.onSubmit (fired when Enter lands in a text field — the run affordance), o.onChanged (fired after any control writes a value, for hosts that gate rebuilds on an epoch).

Parameters

  • o { [string]: any }

modules/edui.argForm/parseSignature

parseSignature(sig: string): { any }

Parse a method signature string — "(self, name: string, opts: T?)" — into the argument list a form renders: { { name, type, optional } }. A leading self is the receiver, not an argument, and is dropped. The arguments are the balanced parenthesised run at the head of the text, so a signature that goes on to declare what the call hands back — "(self, name: string): boolean" — renders the same form as one that stops at the arguments.

Parameters

  • sig string — The signature text.

modules/edui.argForm/rows

rows(self: any, args: { any }): { any }

The rows for a whole argument list (each { name, type, optional, description? }), in order.

Parameters

  • self any (optional)
  • args { any }

modules/edui.argForm/values

values(self: any, args: { any }): (boolean, any, number?)

Assemble the positional values for args from the filled form. Returns (true, values, n)n the last non-nil position for table.unpack(values, 1, n) — or (false, message).

Parameters

  • self any (optional)
  • args { any }

modules/edui.cmdPalette/README

require("@builtin/~edui.cmdPalette") -- edui.cmdPalette

The command palette — search everything under one keystroke. Ctrl+K (or the topbar's Tools button) opens a floating surface whose one query reaches the scene's entities (select + frame), the registered assets (select — the Inspector shows it), the editor's panels (focus), and the whole tool registry (pick one and its typed argument schema becomes a form, Run executing it in place). A t: / e: / a: / p: prefix narrows to one domain. Every domain reads its live registry, so what exists is findable with no UI work per addition.

Usage: local edui.cmdPalette = require("@builtin/~edui.cmdPalette")

modules/edui.cmdPalette/isOpen

isOpen(): boolean

Whether the palette is up. The screen's live visibility is the one answer every scope shares — the menu bar, the entrypoint, and a panel each hold their own copy of this module, and a module-local flag left them disagreeing about whether the palette was open, so a Close click handled by one copy re-showed what another copy had shown.

modules/edui.cmdPalette/mount

mount(layerMask: number?)

Mount the palette (idempotent): its own screen, hidden until toggled.

Parameters

  • layerMask number? (optional) — Optional EditorUI render-layer mask (the menu bar passes its own).

modules/edui.cmdPalette/openTool

openTool(name: string)

Open the palette directly at a tool's form — the deep link another surface uses to hand a person a ready-to-fill tool ("run this operation" from an inspector, a docs page, an agent suggestion). Opens the palette if it is closed, then loads name's schema as the form.

Parameters

  • name string — The tool's full name, toolbox.tool.

modules/edui.cmdPalette/screenName

screenName(): string

The palette's screen name (capture / element addressing).

modules/edui.cmdPalette/toggle

toggle()

Open / close the palette. Opening starts fresh at the search and puts the caret in it. The decision reads the screen's live visibility, never a module-local flag.

modules/edui.cmdPalette/unmount

unmount()

Tear the palette down.

modules/edui.framework/README

require("@builtin/~edui.framework") -- edui.framework

Editor UI framework on the CSS-parity ui.* surface. A small, editor-ONLY toolkit: a reactive app that maps author closures to ui.* callback ids, a dock shell over ui.* native docking, and editor-chrome primitives (panels, trees, toolbars, inspector rows, context menus, drag-and-drop). Gameplay / normal UI authors raw ui.* trees and needs none of this — edui earns its keep only for the editor.

Usage: local edui.framework = require("@builtin/~edui.framework")

modules/edui.framework/createApp

createApp(name: string, opts: { [string]: any }?): any

Create + return an editor app that owns the ui.* screen name. Shorthand for edui.app.new. Call app:mount(builderFn) to bring it up.

Parameters

  • name string — Unique screen id (also the callback-id namespace).
  • opts { [string]: any }? (optional){ layer? = <renderLayerMask> }.
local a = edui.createApp("myPanel"); a:mount(function(app) return tree end)

modules/edui.framework/current

current(): any

The app whose builder is currently running (nil outside a build). The edui.* primitives read this to register their closures.

modules/edui.query/README

require("@builtin/~edui.query") -- edui.query

Resolves the engine-provided query globals edui panels read — queryEntitiesTable, queryLogsTable, getProfilingDataTable, and the like. The engine installs these on _G at boot (before the global table is sealed read-only), so panels read them through this one indirection rather than each touching _G directly. override/restore swap a source for a stub or a mock feed without writing the sealed _G.

Usage: local edui.query = require("@builtin/~edui.query")

modules/edui.query/override

override(name: string, fn: any)

Shadow an engine query global with a function until restored.

Parameters

  • name string — The global name.
  • fn any (optional) — The function to resolve in its place.

modules/edui.query/resolve

resolve(name: string): any

Resolve a named engine query global, honouring any active override.

Parameters

  • name string — The global name (e.g. "queryEntitiesTable").

modules/edui.query/restore

restore(name: string)

Clear an override so resolve falls back to the engine global again.

Parameters

  • name string — The global name.

modules/edui.shell/README

require("@builtin/~edui.shell") -- edui.shell

The editor dock shell on the CSS-parity ui.* surface. One edui app owns a ui.* native dockArea whose children are a dockPanel per registered edui panel — real egui_dock tabs, splits and drag. The shell keeps its OWN panel registry (separate from the deprecated zui editor registry) so the zui editor stays untouched while panels migrate one at a time; the two registries fold into one at cutover.

Usage: local edui.shell = require("@builtin/~edui.shell")

modules/edui.shell/addPanel

addPanel(spec: any): boolean

Register (or replace) an edui dock panel. A duplicate id replaces the prior registration; the live shell rebuilds so the change shows at once.

Parameters

  • spec any (optional){ id (REQUIRED), build (REQUIRED, () -> widget), title?, order?, dock?, badge?, badgeColor?, startClosed? }. build is an edui builder: it may call the edui.* primitives, whose closures register on the shell app automatically. startClosed = true registers into the Window-menu catalog without opening a tab; the panel opens when someone opens it.
edui.shell.addPanel{ id = "entities", title = "Entities", build = fn }

modules/edui.shell/app

app(): any

The mounted shell App, or nil when the shell is not up.

modules/edui.shell/catalog

catalog(): { any }

Every catalogued panel (open or closed), sorted by (order, id), each { id, title, order, dock, open } — the Window menu's source.

modules/edui.shell/exportLayout

exportLayout(): string?

The live dock arrangement, serialized — what saveLayout writes and loadLayout applies. Nil before the dock's first render.

modules/edui.shell/focusPanel

focusPanel(id: string): boolean

Bring a panel's tab to the front of its dock leaf, opening it from the catalog first if its tab is closed — what a menu entry naming a panel does. Unlike openPanel, this acts visibly when the tab is already open behind another.

Parameters

  • id string — The panel id.

modules/edui.shell/layoutMode

layoutMode(): string

The current editor layout mode: "full" (every panel + toolstrip) or "simple" (viewport-first — for sessions driven mainly through agents).

modules/edui.shell/listLayouts

listLayouts(): { string }

The saved layout names, sorted — the files under the layout folder.

modules/edui.shell/loadLayout

loadLayout(name: string): (boolean, string?)

Apply the saved layout name to the live dock. Opens every catalogued panel first so each tab the arrangement references exists, then restores the arrangement one-shot — a drag afterwards owns it, exactly as after a reset.

Parameters

  • name string — A name listLayouts reports.
edui.shell.loadLayout("modeling")

modules/edui.shell/mount

mount(opts: any?): any

Mount the shell (idempotent — returns the existing app if already up). Registers the ui.* screen on the EditorUI layer and does the first build.

Parameters

  • opts any? (optional){ refresh? = <seconds between liveness rebuilds, default 0.5> }.
edui.shell.mount()

modules/edui.shell/openPanel

openPanel(id: string): boolean

Re-open a catalogued panel by id — restores its tab in its preferred dock region. A no-op (returns false) if the id was never registered or is already open.

Parameters

  • id string — The panel id.

modules/edui.shell/panel

panel(id: string): any?

The catalogued panel record for id — the spec table its registration passed to addPanel, extra keys included. This is how one panel reaches another's exported surface: the Files panel opens a file in the Code panel through the openFile its record carries.

Parameters

  • id string — The panel id passed to addPanel.
local rec = edui.shell.panel("code"); if rec then rec.openFile(path) end

modules/edui.shell/panelsSorted

panelsSorted(): { any }

The registered panels sorted by (order, id) — the dock/tab order.

modules/edui.shell/refresh

refresh()

Rebuild every active panel now — each re-queries live data. Call after a change the shell can't observe (a scene edit made outside its own handlers). For a change that concerns ONE panel, refreshPanel rebuilds just that panel.

modules/edui.shell/refreshPanel

refreshPanel(id: string)

Rebuild one panel — its own screen republishes; every other panel and the dock itself are untouched. The scoped path for a data event with a known audience (a selection change concerns the entity tree and the inspector, not the console).

Parameters

  • id string — The panel id to rebuild.
edui.shell.refreshPanel("inspector")

modules/edui.shell/removePanel

removePanel(id: string): boolean

Remove a registered panel by id — closes its tab. The panel stays in the catalog, so the Window menu can re-open it later. A spec that declares close is told: the hook runs as the tab goes, so a panel holding live resources (a render session, a watcher) releases them.

Parameters

  • id string — The panel id passed to addPanel.

modules/edui.shell/resetLayout

resetLayout()

Restore the seeded default dock arrangement — the escape hatch for a layout dragged into an unusable state. Reopens the mode's panel set (every catalogued panel in full mode, the viewport alone in simple) and rebuilds the dock from each panel's dock region hint.

edui.shell.resetLayout()

modules/edui.shell/saveLayout

saveLayout(name: string): (boolean, string?)

Save the live dock arrangement under name — one file at /zero/source/editor/layout/saved/<name>.json, renameable and removable through the Files panel like any other file.

Parameters

  • name string — The layout's name; non-identifier characters fold to _.
edui.shell.saveLayout("modeling")

modules/edui.shell/screenName

screenName(): string

The shell's screen name (for ui.showScreen / capture screen).

modules/edui.shell/setLayoutMode

setLayoutMode(mode: string, opts: any?): boolean

Switch the editor between the full authoring layout and the simple, viewport-first one. "simple" closes every panel but the Scene viewport and hides the toolstrip; "full" restores the panels that were open when simple was entered (or every catalogued panel on a fresh boot into simple). The choice persists across sessions alongside the saved dock arrangement.

Parameters

  • mode string — "full" | "simple".
  • opts any? (optional){ persist? = false } skips the write (the boot restore path).
edui.shell.setLayoutMode("simple")

modules/edui.shell/togglePanel

togglePanel(id: string): boolean

Toggle a catalogued panel open/closed — the Window-menu action.

Parameters

  • id string — The panel id.

modules/edui.shell/unmount

unmount()

Tear the shell down (unregisters the screen + callback env). Panel registrations are kept, so a later mount brings the same set back up.

modules/edui.shell/update

update(dt: number)

Drive the shell's liveness refresh. Call each frame from the editor update loop; every ACTIVE panel rebuilds every refresh seconds so it reflects live scene state without arming its own timer. A tab switch is also caught here: the newly shown panel refreshes on the tick after the switch.

Parameters

  • dt number — Seconds since the last call.

modules/edui.topbar/README

require("@builtin/~edui.topbar") -- edui.topbar

The editor menu bar on edui — a ui.* ctx-level topPanel above the dock: domain dropdown menus on the left (Scene / Content / Debug), the Window menu that opens/closes edui panels, and the play/pause transport on the right. Every menu entry names a panel this editor ships and focuses it. It owns one edui app on its own screen, so it composes above the shell's dock and toolstrip.

Usage: local edui.topbar = require("@builtin/~edui.topbar")

modules/edui.topbar/mount

mount(layerMask: number?): any

Mount the menu bar (idempotent). Registers its ui.* screen on the EditorUI layer and does the first build.

Parameters

  • layerMask number? (optional) — Optional EditorUI render-layer mask; computed when omitted.
edui.topbar.mount()

modules/edui.topbar/screenName

screenName(): string

The bar's screen name (for ui.showScreen / capture screen).

modules/edui.topbar/unmount

unmount()

Tear the menu bar down.

modules/edui.widgets.assetBrowser/README

require("@builtin/~edui.widgets.assetBrowser") -- edui.widgets.assetBrowser

The generic asset browser — one surface for browsing and for picking. Search (name and type), a scope filter (project / builtin), a type filter built from the live registry, list and grid presentation with adjustable tile size and image previews where the asset is one, over the world's asset registry. Browse mode is the Assets panel's body; picker mode is what a REF field or an Add Component flow opens, filtered to the kinds the target accepts, firing onPick with the chosen record.

Usage: local edui.widgets.assetBrowser = require("@builtin/~edui.widgets.assetBrowser")

modules/edui.widgets.assetBrowser/data

data(): { rows: { AssetRecord }, kinds: { { id: string, count: number } } }

The shared registry reading — { rows, kinds }, fetched on first use and held until invalidate. The capability queries above resolve their per-kind probes against it.

modules/edui.widgets.assetBrowser/epoch

epoch(): number

The browser's view epoch — fold into the host panel's build signature.

modules/edui.widgets.assetBrowser/instantiableKinds

instantiableKinds(): { string }

The placeable kind ids in the live registry, sorted — each answered by its type's canInstantiate capability. For an accepts list or a label.

modules/edui.widgets.assetBrowser/invalidate

invalidate()

Drop the shared registry cache; the next build re-reads the registry. Call after authoring/installing content so every open browser sees it.

modules/edui.widgets.assetBrowser/isInstantiable

isInstantiable(kind: any): boolean

Whether assets of kind can be placed into a scene — the asset type's canInstantiate capability, answered once per kind. Drop targets and filters that accept "a placeable asset" resolve the question here, so every surface answers it the same way.

Parameters

  • kind any (optional)

modules/edui.widgets.assetBrowser/make

make(W: any): (any) -> any

Bind the browser builder to the widgets barrel W. The barrel calls this once and exposes the result as edui.assetBrowser.

Parameters

  • W any (optional) — The edui.widgets table.

Returns anyassetBrowser(o) -> widget.

modules/edui.widgets.assetBrowser/previewSrc

previewSrc(path: any): string?

The image source that pictures an asset — its own file when the path is an image, else the preview/source file inside its folder. Nil when nothing drawable is found. Cached per path; M.invalidate() clears it.

Parameters

  • path any (optional)

modules/edui.widgets.controls/README

edui.widgets.controls

edui's OWN interactive controls, drawn on the ui.* canvas substrate (paint commands + pointer/key/scroll events) rather than the generic ui.* widgets — so an editor control looks and behaves like an editor control, independent of general UI, and enforces its type. A number is a drag-scrub field that only ever holds a number (drag to scrub, scroll to nudge, double-click to type digits — letters can't enter); a bool is a toggle switch. Controls.make(W) binds them to the widgets barrel.

modules/edui.widgets.controls/make

make(W: any): any

Bind the canvas controls to the widgets barrel W. Returns { numberField, boolToggle }.

Parameters

  • W any (optional)

modules/edui.widgets.entityPicker/README

require("@builtin/~edui.widgets.entityPicker") -- edui.widgets.entityPicker

The entity picker — the Hierarchy's tree shape wherever an entity is chosen. A search field over a windowed, expandable entity tree (collapsed by default, so skeleton bones and other deep noise stay behind their roots until unfolded or matched), firing onPick with the chosen entity's id. What a REF field opens instead of a flat dump of every entity in the world.

Usage: local edui.widgets.entityPicker = require("@builtin/~edui.widgets.entityPicker")

modules/edui.widgets.entityPicker/epoch

epoch(): number

The picker's view epoch — fold into the host panel's build signature.

modules/edui.widgets.entityPicker/make

make(W: any): (any) -> any

Bind the picker builder to the widgets barrel W. The barrel calls this once and exposes the result as edui.entityPicker.

Parameters

  • W any (optional) — The edui.widgets table.

Returns anyentityPicker(o) -> widget.

modules/edui.widgets.fields/README

edui.widgets.fields

Inspector field rows for edui — a two-column (label + control) row whose control is chosen by the field's declared TYPE (from the component's backing schema), falling back to the value's shape when no type is given. The registry maps: number → numeric field, bool → checkbox, string → text field, enum → a dropdown of its members, color → a colour picker, {x,y,z} → a vec3 triple, a quaternion → an editable euler (X/Y/Z degrees), an asset slot → a live-asset chip, a nested table → a recursing disclosure. Each control wraps the matching ui.* widget and carries the author's onChange closure, so a field writes straight back to the live component. Fields.make(W) binds these to the widgets barrel; the barrel exposes edui.field / edui.section.

modules/edui.widgets.fields/epoch

epoch(): number

The field layer's interaction epoch — moves on every dropdown / nested-table / picker open or close, and on every view mutation inside an open asset or entity picker. Fold it into a panel's build signature so those interactions repaint through a no-change gate.

modules/edui.widgets.fields/field

field(o: any): any

An inspector field row. o = { label, value, kind?, options?, accepts?, onChange?, readOnly?, key }. kind (from the component schema) selects the control; without it the control is inferred from the value's shape. An editable leaf fires onChange(newValue) (vec3/quat fire the full new table).

Parameters

  • o any (optional)
edui.field{ label = "kind", value = c.kind, kind = "enum", options = {"a","b"}, onChange = set, key = "k" }

modules/edui.widgets.fields/make

make(W: any): any

Bind the field builders to the widgets barrel W. Returns { field, section }.

Parameters

  • W any (optional) — The edui widgets barrel.

modules/edui.widgets.fields/section

section(o: any): any

A collapsible inspector section, drawn as a CARD — a bordered, rounded container whose clickable header (chevron + optional icon + uppercase title, with an optional trailing widget) sits over a body shown while open. o = { title, open, onToggle, children, key, headerRight?, icon?, muted? }. headerRight is a SIBLING of the clickable header region, so its click isn't swallowed by the header toggle.

Parameters

  • o any (optional)
edui.section{ title = "Transform", icon = tfIcon, open = st, onToggle = t, children = rows }

modules/edui.widgets.livePreview/README

edui.widgets.livePreview

The ONE live preview session, following the asset selection. Every surface that shows the selected asset live — the Preview panel, the Inspector's Preview section — reads this module instead of holding its own rig, so one camera and one subject serve them all. The session starts when the asset selection lands, swaps with it, and tears down when it clears. subscribe tells a consumer the session's state moved (started, came live, failed) so it can rebuild; epoch folds that state into a build signature.

modules/edui.widgets.livePreview/assetId

assetId(): string? return sessionFor end

The guid the session (or attempt) is for, or nil.

modules/edui.widgets.livePreview/dragOrbit

dragOrbit(d: any)

Apply a pointer-drag delta as an orbit — the shared handler every live-preview surface hands its onDrag.

Parameters

  • d any (optional)

modules/edui.widgets.livePreview/ensure

ensure()

Bring the session in line with the asset selection: a new primary disposes the old rig and assembles one for it, a cleared selection tears down. Idempotent — consumers call it from their builds; the selection subscription below calls it on every change.

modules/edui.widgets.livePreview/epoch

epoch(): number return epoch end

The monotonic state counter — fold into a build signature so the session coming live is a change the panel's no-change gate can see.

modules/edui.widgets.livePreview/isStarting

isStarting(): boolean return starting end

True while a session is assembling.

modules/edui.widgets.livePreview/name

name(): string return sessionName end

The selected asset's display name once known, else "".

modules/edui.widgets.livePreview/reason

reason(): string? return failReason end

Why the last attempt produced no live session, or nil.

modules/edui.widgets.livePreview/resetView

resetView()

Back to the framed opening view.

modules/edui.widgets.livePreview/scrollZoom

scrollZoom(d: any)

Apply a wheel delta as a zoom — the shared handler every live-preview surface hands its onScroll. Wheel-up moves in.

Parameters

  • d any (optional)

modules/edui.widgets.livePreview/session

session(): any return session end

The live session for the selected asset, or nil while there is none (nothing selected, still starting, or the type has no live path).

modules/edui.widgets.livePreview/subscribe

subscribe(fn: () -> ())

Register a consumer's rebuild closure, called whenever the session's state moves. Registration is for the VM lifetime.

Parameters

  • fn () -> ()
livePreview.subscribe(function() edui.shell.refreshPanel("preview") end)

modules/edui.widgets.livePreview/zoomBy

zoomBy(factor: number)

Zoom by a plain factor (toolbar buttons).

Parameters

  • factor number

modules/edui.widgets.tree/README

edui.widgets.tree

The editor hierarchy tree primitive — flattens a nested node graph into the rows that are actually visible under the current expand state, and renders them as indented, selectable ui.* rows inside a virtualized scroll area. The flatten pass is pure and separately exported, so row order is testable without a rendered frame.

modules/edui.widgets.tree/flatten

flatten(o: { [string]: any }): { Row }

Flatten o.roots into the ordered list of rows visible under the current expand state — a node's children are included only while that node is expanded. Pure: no ui.* calls, no app required.

Parameters

  • o { [string]: any }{ roots, childrenOf?, idOf?, expanded?, isExpanded? }. expanded is an { [id] = true } set; isExpanded(node, id) -> boolean overrides it.
local rows = edui.widgets.flattenTree{ roots = roots, expanded = { root = true } }

modules/edui.widgets.tree/make

make(W: any): (any) -> any

Bind the tree builder to a widgets barrel W. The barrel calls this once and exposes the result as edui.tree.

Parameters

  • W any (optional) — The edui.widgets table (supplies palette/text/svgIcon/cbId/merge).

Returns anytree(o) -> widget.

modules/edui.widgets/README

edui.widgets

Editor-chrome primitives for edui — styled ui.* subtree builders (box/text/icon/iconButton/button/toolbar/panel/divider/spacer). Each reads the active theme's editor tokens so chrome looks like the editor by default, and interactive builders register their closures with the currently-building edui app (edui.current()), so an author passes a closure, not a callback id.

modules/edui.widgets/button

button(o: { [string]: any }): any

A text button. o.text, o.onClick, o.key, o.primary, o.icon (SVG path), o.tooltip.

Parameters

  • o { [string]: any }
edui.button{ text = "Add", primary = true, onClick = onAdd, key = "add" }

modules/edui.widgets/divider

divider(o: { [string]: any }?): any

A 1px divider line (horizontal by default).

Parameters

  • o { [string]: any }? (optional)

modules/edui.widgets/group

group(o: { [string]: any }): any

A FLAT collapsible group (chevron + uppercase title + optional count), with none of the section card's border/shadow — for grouping a list inside a panel. o.title, o.count?, o.open, o.onToggle, o.children, o.key.

Parameters

  • o { [string]: any }
edui.group{ title = "@builtin", count = 12, open = o, onToggle = t, children = rows }

modules/edui.widgets/hbox

hbox(o: { [string]: any }): any

A horizontal flex container. Forwards onClick, onDoubleClick, onDrag, dragPayload, onDrop and tooltip.

Parameters

  • o { [string]: any }
edui.hbox{ gap = 6, align = "center", children = { ... } }

modules/edui.widgets/iconButton

iconButton(o: { [string]: any }): any

A compact icon button — a hoverable, optionally-active square holding an SVG icon. o.d (icon path), o.tooltip, o.active, o.onClick, o.key, o.size, o.color, o.id (widget id, e.g. a popup anchor).

Parameters

  • o { [string]: any }
edui.iconButton{ d = eyeIcon, tooltip = "Visible", active = vis, onClick = toggle, key = "vis:"..id }

modules/edui.widgets/input

input(o: { [string]: any }): any

A flat text-entry field — a styled container holding an optional leading icon and a borderless input, so it reads as one clean field instead of the raw widget's heavy frame. o.text, o.placeholder, o.onChange (live per-keystroke), o.onSubmit (command-line mode: the full line on Enter, buffer cleared, caret kept), o.key, o.icon (SVG path), o.style, o.bare (no container frame at all — the input alone, for a surface like a terminal prompt that draws its own ground).

Parameters

  • o { [string]: any }
edui.input{ placeholder = "Search…", icon = searchPath, onChange = fn, key = "search" }

modules/edui.widgets/menu

menu(o: { [string]: any }): any?

A context/action menu — a ui.* popup pinned below an anchor widget, holding clickable action items. o.anchorTo (widget id of the anchor, which must render before this in tree order), o.open (bool), o.items (list of { label, icon?, onClick, danger?, disabled?, hint?, separator?, key? } — a disabled item dims and takes no click; hint renders right-aligned dim text, where a shortcut goes), o.onDismiss (closure, fired on click-outside / Escape, and after an item's onClick runs — a menu closes once it has acted), o.key (id namespace), o.pivot. Returns nil when closed.

Parameters

  • o { [string]: any }
edui.menu{ anchorTo = "rowmenu:"..id, open = open, items = {...}, onDismiss = close }

modules/edui.widgets/notice

notice(o: { [string]: any }): any

An inline status banner. o.text, o.tone ("info" | "ok" | "error"). edui has no timer, so a transient toast is the caller gating this on/off; this draws the styled line.

Parameters

  • o { [string]: any }
edui.notice{ text = "Saved", tone = "ok" }

modules/edui.widgets/panel

panel(o: { [string]: any }): any

A panel frame — an optional title/toolbar header over a body that fills the remaining height. This is the standard dockable-panel shell. o.title (string) OR o.toolbar (a prebuilt toolbar widget); o.body (widget); o.style.

Parameters

  • o { [string]: any }
edui.panel{ title = "Entities", body = tree }

modules/edui.widgets/scroll

scroll(o: { [string]: any }): any

A first-class vertical scroll container filling its parent. o.children, o.height?, o.maxHeight?, o.style?.

Parameters

  • o { [string]: any }
edui.scroll{ children = rows }

modules/edui.widgets/spacer

spacer(): any

A flexible spacer that eats remaining space in a flex row/column.

modules/edui.widgets/stat

stat(o: { [string]: any }): any

A dense read-only telemetry row: label on the left, value on the right (mono by default). good/warn numeric thresholds tint the value (higher = worse: ≥warn is danger, ≥good is warn-hue, else ok); an explicit color overrides. For readouts too light to warrant an editable field.

Parameters

  • o { [string]: any }
edui.stat{ label = "RSS", value = "412 MB", good = 400, warn = 800, mono = true }

modules/edui.widgets/svgIcon

svgIcon(o: { [string]: any }): any

An inline SVG icon. o.d is an SVG path (or full <svg>); o.size, o.color.

Parameters

  • o { [string]: any }
edui.svgIcon{ d = "<path d='M6 9l6 6 6-6'/>", size = 14 }

modules/edui.widgets/switch

switch(o: { [string]: any }): any

A bare toggle switch (pill track + sliding knob) — the switch control on its own, distinct from a checkbox or a power button. o.value (bool), o.onChange, o.key, o.readOnly.

Parameters

  • o { [string]: any }
edui.switch{ value = enabled, onChange = function(v) ... end, key = "comp:en" }

modules/edui.widgets/table

table(o: { [string]: any }): any

A multi-column table with a header row, on the real CSS grid. o.columns is { { key, label?, width?, align?, mono? } } (width defaults to 1fr); o.rows is { [colKey] = value } records. o.key (widget id).

Parameters

  • o { [string]: any }
edui.table{ columns = {{key="name",label="Script"},{key="ms",label="ms",align="right",mono=true}}, rows = rows, key = "vm" }

modules/edui.widgets/text

text(o: { [string]: any } | string): any

A text label. o.text, o.color, o.dim (muted), o.weight, o.size, o.style.

Parameters

  • o { [string]: any } | string
edui.text{ text = "Entities", weight = "600" }

modules/edui.widgets/toolbar

toolbar(o: { [string]: any }): any

A header row — items laid out horizontally with comfortable padding and a hairline rule beneath, on the panel's own surface (no competing fill). o.items (widget list), o.style.

Parameters

  • o { [string]: any }
edui.toolbar{ items = { edui.text{text="Entities"}, edui.spacer(), addBtn } }

modules/edui.widgets/typeBadge

typeBadge(kind: string?): any?

A tiny type badge — a mono uppercase pill that names a field's kind in its own hue (num/str/bool/enum/rgb/vec3/rot/ref/obj), so a row's type reads at a glance. kind is a field kind (number/string/enum/…).

Parameters

  • kind string? (optional)
edui.typeBadge("enum")

modules/edui.widgets/vbox

vbox(o: { [string]: any }): any

A vertical flex container. o.style overrides; o.children (or the positional 1st arg) are the children. Forwards onClick, onDoubleClick, onDrag, dragPayload, onDrop and tooltip.

Parameters

  • o { [string]: any }
edui.vbox{ gap = 4, children = { ... } }

modules/effects/README

require("@builtin/modules/api/engine/effects") -- effects (also available as global 'effects')

Fire a finished visual effect from gameplay code in one line. play puts an effect at a position and playOn sticks it to an entity; both return a handle that stops it early, moves it, or re-tunes a parameter while it runs. The effect owns its own lifetime — nothing here needs the caller to tick it — and repeated firing re-uses what the last one left rather than allocating again.

Usage: local effects = require("@builtin/modules/api/engine/effects") Also available as global: effects

modules/effects/backends

backends(): { string }

The backend kinds an effect can be built out of, in name order. The runtime ships emitter, geometry, material, decal and feature.

print(table.concat(effects.backends(), ", "))

modules/effects/describe

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

  • identity string — The effect's canonical identity, or a short name.
local d = effects.describe("explosion"); print(d.family, d.cost.gpuMs)

modules/effects/drain

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.

print(effects.drain().freed)

modules/effects/families

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.

for _, f in ipairs(effects.families()) do print(f, #effects.list({ family = f })) end

modules/effects/list

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

  • opts table? (optional){ family? = string }. A family is matched without regard to case.
for _, id in ipairs(effects.list()) do print(id) end
for _, id in ipairs(effects.list({ family = "combat" })) do print(id) end

modules/effects/observe

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.

local o = effects.observe(); print(o.live, o.leased, o.pooled, o.bytes)
print(o.peakLive, o.peakLeased)   -- the widest burst the pool covers

modules/effects/play

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

  • identity string — The effect's canonical identity, or a short name that reaches exactly one effect.
  • opts table? (optional){ position? = { x, y, z }, rotation? = quat, direction? = { x, y, z }, params? = { … }, duration? = number, held? = boolean }. Anything params omits takes the effect's declared default, and an effect that declares a duration parameter reads its length from there rather than from duration here.
local h = effects.play("@builtin::systems.effects.combat.explosion", {
position = { 0, 2, 0 }, params = { scale = 4, coreColor = { 1, 0.4, 0.1 } },
})

modules/effects/playOn

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

  • identity string — The effect's canonical identity, or a short name.
  • target any (optional) — An entity proxy or entity id.
  • opts table? (optional) — The same options play takes; position is read from the entity.
local h = effects.playOn("explosion", drum, { params = { scale = 3 } })

modules/effects/registerBackend

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

  • kind string — The kind name a spec asks for.
  • backend table — The backend — key, acquire, seat, start, stop, quiet, place, bytes, active, silence and free.
effects.registerBackend("ribbonTrail", myBackend)

modules/effects/silenceReasons

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.

for _, r in ipairs(effects.silenceReasons()) do print(r.reason, r.means) end

modules/egress/README

require("@builtin/modules/api/engine/egress") -- egress (also available as global 'egress')

Credential-injecting HTTP for BYO-key world egress (Mechanism B). Public Luau surface over the __egress Internal FFI namespace.

Usage: local egress = require("@builtin/modules/api/engine/egress") Also available as global: egress

modules/egress/clearCredential

clearCredential(name: string): boolean

TRUSTED ONLY. Remove a named credential.

Parameters

  • name string — Credential name.
egress.clearCredential("meshy")

modules/egress/credentialNames

credentialNames(): { string }

List the names of configured credentials. Names only — secret values are never exposed to Luau.

for _, n in ipairs(egress.credentialNames()) do print(n) end

modules/egress/fetch

fetch(name: string, method: string, url: 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.

local h = egress.fetch("meshy", "POST", url, nil, { prompt = p })

modules/egress/hasCredential

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

  • name string — Credential name.
if not egress.hasCredential("meshy") then error("set MESHY_API_KEY") end

modules/egress/setCredential

setCredential(name: string, base_url: string,

TRUSTED ONLY. Register a named credential whose header is injected into matching egress.fetch calls. The value is held in Rust and never returned to Luau.

egress.setCredential("meshy", "https://api.meshy.ai/", "Authorization", "Bearer " .. key)

modules/engine.dirty_hot_reload/README

require("@builtin/modules/engine/dirty_hot_reload") -- engine.dirty_hot_reload

Per-entity hot reload on <scene>.dirty/entities/. Subscribes to the layer's dirty entities directory via the generic vfs.watch(folder, callback) primitive and reapplies changed bodies to live entities — the consumer side of the collaborative-via-VFS-sync edit model. Local edits write a dirty file; VFS sync replicates the bytes to every peer's local VFS; the subscription fires for both Local and Remote writes; the callback reads the replicated file and applies it to the peer's live entity. No polling. Local writes fire watchers synchronously inside the FFI call. Peer-driven writes are queued in zero_vfs::write_events and drained by the engine schedule's apply_vfs_mutations system, which fires watchers within the same frame the bytes land. Cost is O(0) when nothing changes. Mark-suppression contract: Body application happens inside __scene_load.begin() / __scene_load.finish() so the Rust mark sites (mark_entity_dirty, mark_manifest_dirty) early-return — without the gate, applying a peer's edit would re-mark the entity dirty locally, the saver would re-write the same body to disk, VFS sync would re-broadcast back, and the feedback loop would saturate the relay. The same gate the scene loader uses on full-scene loads. Echo-loop suppression: Local writes round-trip through the watcher too (lua_vfs_write fires synchronously). The known per-eid content cache short- circuits re-apply when the file content matches the last-seen string — we just wrote it, no need to re-apply our own write. Lifecycle: layers.M.fireLoad(proxy) calls M.install(proxy) for every scene (additive or not) that just loaded — the install seeds known from current dir contents and registers one vfs.watch on <entitiesDir> (folder subscription). M.fireUnload(proxy) calls M.uninstall(proxy) which calls vfs.unwatch(id) and drops the per-layer state. Authored content lives in additive overlays too (editor side-panels, side games, custom HUD overlays) — they get the same hot-reload treatment. Scenes that genuinely don't resolve to a dirty subtree (e.g. an engine-internal scaffolding overlay) early-return inside entitiesDirFor.

Usage: local engine.dirty_hot_reload = require("@builtin/modules/engine/dirty_hot_reload")

modules/engine.dirty_hot_reload/canonicalChanged

canonicalChanged(sceneGuid: string): number

Take the layer to what its canonical scene.json now says, and report what that reached. The entry point for a write this module's own subscription does not see: vfs.watch fires for a write made inside the vm and for one replicated from a peer, and NOT for one made through the host (write_file / edit_file), which is the route an author reaches for first. The scene assetType's onChange sees that one and calls this.

Guarded by the same content cache the subscription uses, so whichever of the two arrives first applies the write and the other reads an echo. A record whose entity already matches costs nothing either way.

Parameters

  • sceneGuid string — The layer's guid.
DirtyHotReload.canonicalChanged(layers.active.guid)

modules/engine.dirty_hot_reload/debugFire

debugFire(guid, path, kind)

Diagnostic: synchronously dispatch a synthetic write event for the given guid + path. Used by tests / debugging — production reads come from vfs.watch callbacks.

Parameters

  • guid any (optional)
  • path any (optional)
  • kind any (optional)

modules/engine.dirty_hot_reload/debugState

debugState(guid)

Diagnostic: return a shallow copy of the per-layer state for inspection. Returns { entitiesDir, knownKeys, active, watcherId, eventCount }.

Parameters

  • guid any (optional)

modules/engine.dirty_hot_reload/install

install(sceneProxy)

Subscribe to the layer's dirty/entities/ directory via vfs.watch (folder subscription — fires for any descendant write/remove, both Local and Remote origins). Called automatically from layers.M.fireLoad for EVERY loaded scene (additive or root) — scenes don't need to install it manually. Scenes that don't resolve to a dirty subtree (engine-internal scaffolding overlays, persistent UI layers with no on-disk authoring path) early-return inside entitiesDirFor.

Parameters

  • sceneProxy any (optional) — SceneProxy for the just-loaded layer.

modules/engine.dirty_hot_reload/installed

installed()

Return the set of currently-installed layer guids. Observability hook for tests / debugging.

modules/engine.dirty_hot_reload/uninstall

uninstall(sceneProxy)

Stop the watcher for the given layer. Called from layers.M.fireUnload. Idempotent — safe to call when no install ran or after a prior uninstall.

Parameters

  • sceneProxy any (optional) — SceneProxy for the layer being unloaded.

modules/entity/README

require("@builtin/modules/api/engine/entity") -- entity (also available as global 'entity')

Entity spawn / despawn / query / hierarchy surface. Calling the table itself — entity(idOrProxy) — resolves an id or proxy to its live entityRef proxy. Public Luau surface over the __entity Internal FFI namespace, composed with the entityRef proxy metatable, the hierarchy swap helper, id/proxy coercion, and the polymorphic batch read/write dispatch.

Usage: local entity = require("@builtin/modules/api/engine/entity") Also available as global: entity

modules/entity/batchAddComponent

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 of entity.batchSpawn or entity.findAll).
  • type_name string — Component type to add to every entity.
  • data table? (optional) — Init data table, applied identically to every entity — the same shape the second arg to entity(id).component.add(type, data) takes.
local n = entity.batchAddComponent(ids, "Debris", { lifetime = 5 })

modules/entity/batchDespawn

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 of entity.batchSpawn / entity.findAll).
local n = entity.batchDespawn(ids)

modules/entity/batchProxy

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.
local proxies = entity.batchProxy(ids)

modules/entity/batchRead

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. target accepts an entity-id array or a ecs.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 from ecs.bindEntities(ids).
  • component string? (optional) — Component type name (e.g. "Transform").
  • field string? (optional) — Field name (e.g. "position").
  • sink buffer? (optional) — Typed CPU buffer from substrate.createBuffer({...}) to memcpy field values into. Required when target is a binding.
local snapshot = entity.batchRead(ids)
local positions = entity.batchRead(ids, "Transform", "position")

modules/entity/batchReadToBuffer

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

  • binding number — Binding id from ecs.bindEntities(ids).id.
  • component string — Component type name.
  • field string — Field name to read.
  • buffer number — Destination buffer id (must be the matching type).
entity.batchReadToBuffer(binding.id, "Transform", "position", buf.id)

modules/entity/batchSpawn

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

  • count number — How many entities to spawn (capped at 1,000,000).
  • name_prefix string? (optional) — Display-name prefix appended with the 1-based index. Defaults to "entity".
local ids = entity.batchSpawn(100, "grass_")

modules/entity/batchWrite

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; values is an array the same length as ids (nil slots are skipped). Use for one-shot writes.
  • entity.batchWrite(binding, comp, field, values) — binding handle from ecs.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 from ecs.bindEntities(ids).
  • component string — Component type name.
  • field string — Field name to write.
  • source { any? } | buffer — Per-entity values array (nil entries are skipped), or a typed CPU buffer from substrate.createBuffer({...}). A buffer source requires a binding target.
entity.batchWrite(ids, "Transform", "position", positions)

modules/entity/batchWriteBound

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

  • binding number — Binding id from ecs.bindEntities(ids).id.
  • component string — Component type name.
  • field string — Field name to write.
  • values { any? } — Per-entity source values (nil = skip). Length must match the binding's entity count.
entity.batchWriteBound(binding.id, "Transform", "position", values)

modules/entity/batchWriteFromBuffer

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

  • binding number — Binding id from ecs.bindEntities(ids).id.
  • component string — Component type name.
  • field string — Field name to write.
  • buffer number — Buffer id from substrate.createBuffer({type="vec3", len=N}).id.
entity.batchWriteFromBuffer(binding.id, "Transform", "position", buf.id)

modules/entity/capture

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.
local ids, err, reproduced = entity.capture(function() entity.spawn("chair") end)

modules/entity/despawn

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

  • target string | 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"

modules/entity/duplicate

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

  • sourceId string | entityRef — Entity id or entity proxy of the source entity to clone.
  • name string? (optional) — Display name for the copy (defaults to source name + " (copy)").
  • opts table? (optional){ includeTemporary?: boolean, name?: string }name is the same field the name argument sets, and wins when both are given.
local copyId = entity.duplicate(id); if copyId then entity(copyId).position = { 1, 0, 0 } end
local copyId = entity.duplicate(id, "Turret", { includeTemporary = true })

modules/entity/exists

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

  • idOrProxy string | entityRef — Entity id or an entity proxy.
if entity.exists(id) then ... end

modules/entity/find

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

  • nameOrGlob string — Exact entity Name or id, or a * / ? glob over Names.
local e = entity.find("enemy_*")

modules/entity/findAll

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

  • nameOrGlob string? (optional) — Exact entity Name or id to filter by, or a * / ? glob over Names. Omit to enumerate every entity.
for _, e in entity.findAll("enemy_*") do e:despawn() end

modules/entity/getChildren

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

  • id string | entityRef — Entity id or entity proxy.
for _, c in entity.getChildren(id) do c.internal = true end

modules/entity/getDescendants

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

  • id string | entityRef — Entity id or entity proxy.
local all = entity.getDescendants(id)

modules/entity/getParent

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

  • id string | entityRef — Entity id or entity proxy.
local p = entity.getParent(id)

modules/entity/instantiate

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

  • handle number — Template handle from entity.template.
  • count number — Number of instances to spawn (capped at 1,000,000).
  • fn ((number) -> EntityInstantiateOverrides?)? (optional) — Per-instance override callback (i) -> table?.
local ids = entity.instantiate(h, 50, function(i) return { position = { i, 0, 0 } } end)

modules/entity/spawn

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.
  • opts SpawnOpts? (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.
local e = entity.spawn("crate", { components = { Model = { model = "cube" } } })
local e = entity.spawn { name = "turret", position = { 1, 2, 3 } }

modules/entity/spawnSynced

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.
  • opts SpawnOpts? (optional) — Same options as entity.spawn (synced is already implied).
if multiplayer.isHost() then entity.spawnSynced("Goblin") end

modules/entity/template

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

  • def EntityTemplateDef — Template definition: components / ecs / temporary / active / internal / attributes. Per-instance name / position / rotation / scale / parent and any override go through the instantiate callback.
local h = entity.template({ components = { Model = { model = "cube" } } })

modules/entity/tree

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? }.
local view = entity.tree({ filter = "crate", limit = 50 })

modules/entityMembers/README

entityMembers

The declarative description of every entityRef member. The runtime dispatch, the accepted-member set, the miss diagnostics and the LSP tree all derive from this table, so they cannot disagree.

modules/enum_values/README

enum_values

The enum field-constraint validator: a constrained value must be one of the strings the field declared. A rejection names every member, so the error carries the whole set the caller may choose from. Registers itself with the generic field_constraints registry on load. nil passes, so an enum field may be left unset.

modules/environment/README

require("@builtin/modules/api/engine/environment") -- environment (also available as global 'environment')

Environment / reflection capture — bake the scene into reflection-probe cube slots from world positions, persist them as faces6 .texture assets, and set per-probe blend data so surfaces reflect the nearest probe(s). Public Luau surface over the __environment Internal FFI namespace. The generic "render the scene into a cubemap from a point" capability the reflection probe system is built on.

Usage: local environment = require("@builtin/modules/api/engine/environment") Also available as global: environment

modules/environment/capture

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

  • x number — World X of the capture position.
  • y number — World Y of the capture position.
  • z number — World Z of the capture position.
environment.capture(0, 2, 0)

modules/environment/captureSky

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

  • x number? (optional) — World X of the capture position. Defaults to 0.
  • y number? (optional) — World Y of the capture position — the altitude a height-dependent atmosphere is sampled at. Defaults to 0.
  • z number? (optional) — World Z of the capture position. Defaults to 0.
environment.captureSky()

modules/environment/captureSlot

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

  • slot number — Reflection-probe slot (0-based).
  • x number — World X of the capture position.
  • y number — World Y of the capture position.
  • z number — World Z of the capture position.
environment.captureSlot(0, 0, 2, 0)

modules/environment/captureSlotToAsset

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

  • name string — Destination asset identity (writes /source/<name>.texture/).
  • slot number — Reflection-probe slot (0-based).
  • x number — World X of the capture position.
  • y number — World Y of the capture position.
  • z number — World Z of the capture position.
  • timeoutFrames number? (optional) — Optional max frames to wait for the readback (default 180).
environment.captureSlotToAsset("probe_lobby", 0, 0, 2, 0)

modules/environment/captureToAsset

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

  • name string — Destination asset identity (writes /source/<name>.texture/).
  • x number — World X of the capture position.
  • y number — World Y of the capture position.
  • z number — World Z of the capture position.
environment.captureToAsset("env_main", 0, 2, 0)

modules/environment/ensureSkyFallback

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.

environment.ensureSkyFallback()

modules/environment/loadFromAsset

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

  • name string — Source asset identity (reads /source/<name>.texture/).
environment.loadFromAsset("env_main")

modules/environment/loadSlotFromAsset

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

  • name string — Source asset identity (reads /source/<name>.texture/).
  • slot number — Reflection-probe slot (0-based).
environment.loadSlotFromAsset("probe_lobby", 0)

modules/environment/setProbes

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. priority defaults to 0.
environment.setProbes({ { x = 0, y = 2, z = 0, radius = 12 } })

modules/environment/setSkyFallback

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

  • active boolean — Whether reflections fall back to the captured sky.
environment.setSkyFallback(false)

modules/exposure/README

require("@builtin/systems/exposure/exposure") -- exposure

Exposure in stops, and eye adaptation — the image settles when the scene gets brighter or darker instead of clipping or going black.

Usage: local exposure = require("@builtin/systems/exposure/exposure")

modules/exposure/active

active(): boolean

Whether the exposure passes are currently running.

if exposure.active() then print("metering") end

modules/exposure/buffers

buffers(): { [string]: any }?

The buffers the exposure passes read: params carries the settings this module packs, state is the adaptation value the GPU owns between frames. The render feature binds what this hands it.

local b = exposure.buffers()

modules/exposure/clear

clear()

Turn exposure off and release the passes. The other settings are kept, so a later set brings back the same behaviour.

exposure.clear()

modules/exposure/get

get(): ExposureState

The exposure settings currently in force.

local ev = exposure.get().compensation

modules/exposure/set

set(opts: ExposureOpts?): ExposureState

Set the scene's exposure. Any omitted field keeps its current value, so a call can adjust one knob without restating the rest. With auto off and compensation at 0 nothing is being asked for and the passes are released. Each setting names the interval it means something in, and a value outside that interval is refused naming the field, the value and the interval — the whole call, so the scene stays on the exposure it already had. The settings this returns are therefore the settings the frame is rendered with.

Parameters

  • opts ExposureOpts? (optional) — Exposure settings — see ExposureOpts.
exposure.set({ auto = true, targetGrey = 0.18, meter = "centre" })

modules/exposure/tick

tick(dt: number)

Advance adaptation by dt seconds. Adaptation is a rate, so the passes need the frame's own delta to move at the same speed whatever the frame rate. The AutoExposure component calls this; code driving the module directly calls it once a frame.

Parameters

  • dt number — Seconds since the last frame.
exposure.tick(dt)

modules/field_constraints/README

field_constraints

Generic field-constraint dispatch. A Field descriptor may carry an opaque constraint table with a kind; the engine calls _G.__zero_check_field_constraint(value, constraint) on every write to (and default of) a constrained field. This module owns that hook and the validator registry — the engine core carries the constraint verbatim and never interprets it.

modules/field_constraints/register

register(kind: string, fn: (value: any, constraint: any) -> (boolean, string?))

Register the validator for a constraint kind. One validator per kind; re-registering a kind is an error (one path).

Parameters

  • kind string — The constraint kind string (matches constraint.kind).
  • fn (value: any, constraint: any) -> (boolean, string?)(value, constraint) -> ok, reason?.
FieldConstraints.register("dataContract", checkDataContract)

modules/fluid/README

require("@builtin/systems/fluidSim/fluid") -- fluid

Grid-based GPU fluid simulation — smoke, fire and gas that curls, rolls and is deflected by forces, rather than translating rigidly the way billboard particles do.

Usage: local fluid = require("@builtin/systems/fluidSim/fluid")

modules/fluid/addForce

addForce(self: any, opts: ForceOpts)

Place a directional force. Same one-step lifetime as a source, so a sustained wind is re-applied each frame.

Parameters

  • self any (optional)
  • opts ForceOpts — Force placement and direction — see ForceOpts.
sim:addForce({ position = { 2, 1, 0 }, direction = { -1, 0, 0 }, strength = 5 })

modules/fluid/addSource

addSource(self: any, opts: SourceOpts)

Place a density/heat source. It emits for one step, so a continuous plume calls this each frame — which is also what lets emission follow a moving object without any separate binding.

Parameters

  • self any (optional)
  • opts SourceOpts — Source placement and emission — see SourceOpts.
sim:addSource({ position = { 0, 1, 0 }, density = 1.0, temperature = 400 })

modules/fluid/create

create(opts: FluidOpts?): any

Create a fluid simulation over a world-space box.

Parameters

  • opts FluidOpts? (optional) — Grid and solver settings — see FluidOpts.
local sim = fluid.create({ resolution = { 64, 64, 64 } })

modules/fluid/destroy

destroy(self: any)

Release every GPU resource the simulation owns. The handle is unusable afterwards.

Parameters

  • self any (optional)
sim:destroy()

modules/fluid/step

step(self: any, dt: number)

Advance the simulation one step. Emitters added since the last step are consumed here, so they act exactly once.

Parameters

  • self any (optional)
  • dt number — Step length in seconds.
sim:step(1 / 60)

modules/fluid/velocityTexture

velocityTexture(self: any): string

The 3D texture currently holding the velocity field. Velocity changes texture each step, so read this when you need it rather than caching it.

Parameters

  • self any (optional)
local vel = sim:velocityTexture()

modules/fog/README

require("@builtin/systems/atmosphere/fog") -- fog

Scene-wide exponential height fog with directional in-scattering. Density falls off with altitude, so valleys pool fog while hilltops clear, and the medium brightens toward the sun so looking into it is not the same as looking away from it.

Usage: local fog = require("@builtin/systems/atmosphere/fog")

modules/fog/active

active(): boolean

Whether the fog pass is running this frame.

if fog.active() then ... end

modules/fog/clear

clear()

Turn fog off and release the pass. The other settings are kept, so a later set({ density = ... }) brings back the same look.

fog.clear()

modules/fog/get

get(): FogState

The fog settings currently in force.

local d = fog.get().density

modules/fog/paramsBuffer

paramsBuffer(): any?

The buffer this system's passes read. The render feature binds what this hands it, so the pass has the values this module packed.

local p = fog.paramsBuffer()

modules/fog/set

set(opts: FogOpts?): FogState

Set the scene's fog. Any omitted field keeps its current value, so a call can adjust one knob without restating the rest. A density of 0 turns fog off and releases the pass.

Parameters

  • opts FogOpts? (optional) — Fog settings — see FogOpts.
fog.set({ density = 0.03, heightFalloff = 0.12, heightRef = 0, inscatter = 0.85 })

modules/font/README

require("@builtin/modules/api/engine/font") -- font (also available as global 'font')

Font primitive — parse a font file ONCE into a baked, vectorized glyph format (ZFNT), then drive every text surface from it. A font is a general CPU resource addressed by name (the CPU counterpart of a renderer GPU resource), so a single registration is usable from UI text, 2D text, and true 3D glyph geometry. Public Luau surface over the __font Internal FFI namespace. Authored fonts are .font assets whose onRegister hook calls font.register.

Usage: local font = require("@builtin/modules/api/engine/font") Also available as global: font

modules/font/glyph

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

  • name string — Registered family name.
  • codepoint number — Unicode codepoint (e.g. string.byte("A")).
local g = font.glyph("Inter", string.byte("A"))

modules/font/list

list(): { string }

List every registered font family name.

for _, fam in font.list() do print(fam) end

modules/font/observe

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().

for _, f in ipairs(font.observe()) do print(f.family, #f.weights) end

modules/font/parse

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

  • bytes buffer | string — Raw font-file bytes (binary-safe) — TTF / OTF.
local zfnt = font.parse(vfs.read("/zero/source/Inter.ttf"))

modules/font/reconcile

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.

for _, f in ipairs(font.reconcile()) do if f.selectable and not f.matched then print(f.family, f.weight) end end

modules/font/register

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

  • name string — Family name to register under.
  • zfnt string — Baked ZFNT payload from font.parse (binary-safe string).
  • opts table? (optional){ family: string?, role: string?, aliases: {string}? } — group key, weight/style role, and case-insensitive selectable aliases.
local info = font.register("Inter", font.parse(vfs.read("/zero/source/Inter.ttf")))

modules/font/textMesh

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

  • name string — Registered family name.
  • text string — String to lay out.
  • opts table? (optional){ size?=1, depth?=0 (extrude, EM units), tolerance?=0.0015, letterSpacing?=0, lineHeight?=0 }.
local geom = font.textMesh("Inter", "Hello", { size = 1, depth = 0.1 })

modules/frameStream/README

require("@builtin/modules/api/engine/frameStream") -- frameStream (also available as global 'frameStream')

Carries an image the GPU drew out to another program — the view a camera renders, read back off its render target and written into a byte stream frame after frame, so pixels that sit in GPU memory reach a process outside the engine while the world runs. Public Luau surface over the __framestream Internal FFI namespace.

Usage: local frameStream = require("@builtin/modules/api/engine/frameStream") Also available as global: frameStream

modules/frameStream/attach

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

  • texture string — Guid of the render target the image was drawn into (a Camera's textureHandle).
  • stream string — Stream handle from stream.open.
  • opts AttachOpts? (optional) — Rate, pixel layout and row order (optional).
local session = frameStream.attach(rt.guid, handle, { fps = 30 })

modules/frameStream/detach

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

  • handle string — Session handle from frameStream.attach.
frameStream.detach(session)

modules/frameStream/list

list(): { string }

Every live session handle, in a stable order.

for _, h in frameStream.list() do frameStream.detach(h) end

modules/frameStream/status

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

  • handle string — Session handle from frameStream.attach.
local s = frameStream.status(session); print(s.frames, s.dropped, s.achievedFps)

modules/gaussianSplats/README

require("@builtin/systems/gaussianSplats") -- gaussianSplats

Registry of live Gaussian splat clouds — the entity-id-keyed table the gaussianSplats render feature reads each frame. GaussianSplat.component writes the declaration into it; the feature owns every GPU buffer and reports back one thing, its verdict on whether it could build the cloud.

Usage: local gaussianSplats = require("@builtin/systems/gaussianSplats")

modules/gaussianSplats/clear

clear(id: string)

Drop an entity's cloud, along with the placement recorded for it. The render feature frees its GPU buffers on the next frame that finds the entry gone.

Parameters

  • id string — Entity id.
gaussianSplats.clear(id)

modules/gaussianSplats/clearFailure

clearFailure(id: string)

Drop the failure standing against an entity's cloud. The feature calls this on the frame it builds the cloud.

Parameters

  • id string — Entity id.
gaussianSplats.clearFailure(id)

modules/gaussianSplats/entries

entries(): { [string]: SplatEntry }

The live registry, keyed by entity id. Read-only for callers other than GaussianSplat.component.

for id, entry in pairs(gaussianSplats.entries()) do ... end

modules/gaussianSplats/failure

failure(id: string): string?

Why the feature could not build an entity's cloud, or nil while it has nothing against it.

Parameters

  • id string — Entity id.
local why = gaussianSplats.failure(id)

modules/gaussianSplats/get

get(id: string): SplatEntry?

The entry registered for one entity, or nil.

Parameters

  • id string — Entity id.
local e = gaussianSplats.get(id)

modules/gaussianSplats/placement

placement(id: string): { number }?

The placement an entity's cloud was last culled and drawn at — 16 column-major numbers, translation in elements 13, 14, 15. The cull transforms every splat position by this matrix and the draw conjugates each covariance by its upper-left 3x3, so it is where the cloud was cut as well as where it was drawn. A fresh table each call, so writing to it leaves the record alone. Nil before the feature has drawn the cloud, on any frame it does not draw it, and once the cloud has left the registry.

Parameters

  • id string — Entity id.
local m = gaussianSplats.placement(id)

modules/gaussianSplats/recordPlacement

recordPlacement(id: string, matrix: { number }?)

Record the placement an entity's splats were culled and drawn at, as 16 column-major numbers. The render feature calls this each frame it draws the cloud, with the matrix it hands the cull pass; nil drops the record. The numbers are copied in, so the caller keeps its own matrix to itself.

Parameters

  • id string — Entity id.
  • matrix { number }? (optional) — The 16 column-major numbers the cloud was placed by.
gaussianSplats.recordPlacement(id, entity(id).worldMatrix())

modules/gaussianSplats/reportFailure

reportFailure(id: string, message: string)

Report that the feature could not build an entity's cloud. The message reaches the entry's status listener, which is how it lands on the component's error surface.

Parameters

  • id string — Entity id.
  • message string — What the feature refused the cloud for.
gaussianSplats.reportFailure(id, "no capture at 'captures/room.gaussianSplat'")

modules/gaussianSplats/set

set(id: string, entry: SplatEntry, onStatus: StatusListener?)

Register or update the cloud an entity renders. Changing source or convention bumps the entry's generation, which makes the render feature re-decode and re-upload on its next frame, and drops any verdict the feature reached about the capture asked for before.

Parameters

  • id string — Entity id.
  • entry SplatEntry — The cloud's declaration.
  • onStatus StatusListener? (optional) — Called with the feature's message when it cannot build the cloud, and with nil once it can. GaussianSplat.component passes the call that puts the message on its own error surface.
gaussianSplats.set(id, { source = "captures/ceramic.spz", opacity = 1 })

modules/gaussianSplats/setVelocity

setVelocity(enabled: boolean): boolean

Whether a moving cloud writes its screen-space displacement into the frame's velocity buffer, which is what makes motion blur, temporal antialiasing, temporal upsampling and a denoiser's history term treat it as moving. On costs two screen-sized targets and two full-screen passes while any cloud is on screen. On by default.

Parameters

  • enabled boolean — Whether the clouds report their motion.
gaussianSplats.setVelocity(false)

modules/gaussianSplats/velocity

velocity(): boolean

Whether the clouds report their motion into the frame's velocity buffer.

if gaussianSplats.velocity() then ... end

modules/http/README

require("@builtin/modules/api/engine/http") -- http (also available as global 'http')

Async HTTP — GET/POST returning JSON or raw bytes. Public Luau surface over the __http Internal FFI namespace.

Usage: local http = require("@builtin/modules/api/engine/http") Also available as global: http

modules/http/get_bytes

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

  • url string — Request URL.
  • headers Headers? (optional) — Header key-value pairs (optional).
local bytes = task.await(http.get_bytes("https://example.com/sound.ogg"))

modules/http/get_json

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

  • url string — Request URL.
  • headers Headers? (optional) — Header key-value pairs (optional).
local data = task.await(http.get_json("https://api.example.com/info"))

modules/http/post_bytes

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

  • url string — Request URL.
  • headers Headers? (optional) — Header key-value pairs (optional).
  • body JsonBody? (optional) — JSON body (optional).
local audio = task.await(http.post_bytes(ttsUrl, nil, { text = "hello" }))

modules/http/post_json

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

  • url string — Request URL.
  • headers Headers? (optional) — Header key-value pairs (optional).
  • body JsonBody? (optional) — JSON body (optional).
local r = task.await(http.post_json(url, nil, { name = "Alice" }))

modules/http/request

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

  • method string — HTTP verb (case-insensitive).
  • url string — Request URL.
  • headers Headers? (optional) — Header key-value pairs (optional).
  • body JsonBody? (optional) — JSON body (optional).
local w = task.await(http.request("PATCH", url, hdrs, { description = "hi" }))

modules/http/request_raw

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

  • method string — HTTP verb (case-insensitive).
  • url string — Request URL.
  • headers Headers? (optional) — Header key-value pairs (optional).
  • body buffer | string | nil (optional) — Raw binary request body (optional).
local r = task.await(http.request_raw("POST", blobsUrl, hdrs, pngBytes))

modules/httpServer/README

require("@builtin/modules/api/engine/httpServer") -- httpServer (also available as global 'httpServer')

Serve HTTP from this engine. A world registers a handler for a method and a path, and the engine's own HTTP server routes matching requests to it — so a browser tab, a curl call or another process on this machine reaches the running world over plain HTTP, with the world deciding what every address answers. The engine's own HTTP server listens on the loopback interface, so a world that asks for nothing is reachable from this machine, and through a forward another program opens to it (adb forward, an SSH tunnel, a reverse proxy). httpServer.listen(target) holds a second address of this world's own choosing: the host in the target is the interface bound and the whole of what decides who can reach it, so "0.0.0.0:8080" answers a phone on the same wifi and "127.0.0.1:8080" answers this machine. httpServer.address(path) is the URL to call, and httpServer.status() reports the host, the port and the reach those routes answer under. Content routes are served under the /app mount: httpServer.route("GET", "/status", h) answers http://<host>:<port>/app/status. The engine's own /engine/* routes are matched first, and every registration lands under /app, so the two trees stay disjoint. A route belongs to the chunk that registered it, and so does an address that chunk opened. When that chunk runs again — a module hot-reload, a cleared require cache — both are released and the new run makes its own, so an edited handler is the one that answers. httpServer.routes() names the owner of every address. A handler runs on the script thread, inside the frame, like any other world code — it may read and write the world, and it holds the tick for as long as it runs. The request waits at most timeoutMs (5 s by default) for its answer; past that the caller is told 504 and the connection closes, while the handler itself carries on to completion.

Usage: local httpServer = require("@builtin/modules/api/engine/httpServer") Also available as global: httpServer

modules/httpServer/address

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

  • path string — Path under the /app mount, e.g. "/status".
print(httpServer.address("/status")) --> http://127.0.0.1:7607/app/status

modules/httpServer/listen

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

  • target string — Interface and port to hold, e.g. "0.0.0.0:8080".
local l = assert(httpServer.listen("0.0.0.0:8080"))

modules/httpServer/route

route(

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.

local h = httpServer.route("GET", "/status", function(req)

modules/httpServer/routes

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.

for _, r in ipairs(httpServer.routes()) do print(r.method, r.url, r.owner) end

modules/httpServer/status

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.

local s = httpServer.status(); print(s.url, s.reach)

modules/httpServer/unlisten

unlisten(): boolean

Release the address listen opened. Returns once the socket is free, so the same port binds again straight after.

httpServer.unlisten()

modules/httpServer/unroute

unroute(handle: number): boolean

Stop serving a route and release its handler. The address is free for another registration once this returns true.

Parameters

  • handle number — The handle httpServer.route returned.
httpServer.unroute(h)

modules/ibl/README

require("@builtin/systems/imageBasedLighting/ibl") -- ibl

Image-based lighting from the scene's own sky — metal reflects what is actually above it, and matte surfaces pick up the sky's colour.

Usage: local ibl = require("@builtin/systems/imageBasedLighting/ibl")

modules/ibl/active

active(): boolean

Whether the environment-lighting passes are currently running.

if ibl.active() then print("lit by the sky") end

modules/ibl/clear

clear()

Turn environment lighting off and release the passes. The other settings are kept, so a later set brings back the same look.

ibl.clear()

modules/ibl/follow

follow()

Go back to reading the sky and sun from the scene, after a call pinned them.

ibl.follow()

modules/ibl/get

get(): IblState

The environment-lighting settings currently in force.

local s = ibl.get().specular

modules/ibl/paramsBuffer

paramsBuffer(): any?

The buffer this system's passes read. The render feature binds what this hands it, so the pass has the values this module packed.

local p = ibl.paramsBuffer()

modules/ibl/refresh

refresh()

Carry the scene's current sky and sun into the running passes. The scales — diffuse, specular, skyIntensity, horizonSharpness and sunSize — stay as they stand, so a caller ticking this every frame keeps a day/night cycle reaching the reflections without restating them. It reads while the passes are running and the scene is on automatic tracking; a colour or direction pinned through set holds it off until follow.

ibl.refresh()

modules/ibl/set

set(opts: IblOpts?): IblState

Set the scene's environment lighting. Any omitted field keeps its current value. With both diffuse and specular at 0 nothing is being asked for and the passes are released.

Parameters

  • opts IblOpts? (optional) — Settings — see IblOpts.
ibl.set({ diffuse = 1, specular = 1 })

modules/ies/README

require("@builtin/systems/photometrics.package/ies") -- ies

Photometric light distributions in the IESNA LM-63 format, turned into a texture a spot light projects through its cone.

Usage: local ies = require("@builtin/systems/photometrics.package/ies")

modules/ies/parse

parse(src: string): (Profile?, string?)

Read an IESNA LM-63 photometric file.

Parameters

  • src string — The file's text.
local p = ies.parse(vfs.read("/source/lights/downlight.ies"))

modules/ies/toLayer

toLayer(profile: Profile, layer: number, coneHalfAngleDeg: number, resolution: number?): (number?, string?)

Rasterize a profile into one layer of the shared feature-texture array, ready for a spot light's cookieLayer to project it.

Parameters

  • profile Profile — A profile from ies.parse.
  • layer number — Which feature-texture layer to fill.
  • coneHalfAngleDeg number — The spot's outer half-angle, so the profile's angles land where the cone actually reaches.
  • resolution number? (optional) — Layer size in pixels, a multiple of 32. Defaults to 256.
ies.toLayer(profile, 0, spot.angle)

modules/layeredMaterial/README

require("@builtin/systems/layeredMaterial/layeredMaterial") -- layeredMaterial

A surface written as a stack of materials that already exist, compiled to one WGSL surface shader. Rust over painted metal, snow over rock, mud over a vehicle, lacquer over paint: each is one material laid over another. The two materials are already authored; what is missing is a way to say "this one, over that one, where the mask says". Written by hand, that means a new fn surface() for every pair, and the pair is what the shader is about — neither half is reusable. A stack names the materials instead, and material() compiles it, writes the shader out as an ordinary .shader asset and hands back a material wearing it. Each layer is read off the material it names — its shading values and the textures it binds — and the emitted shader carries every layer's inputs under its own name, blends them by coverage, and shades once. Shading each layer and adding the results would put more energy back out than arrived; interpolating the inputs and shading the result cannot, which is what keeps a half-covered surface from reading brighter than either layer alone. A layer past the first is revealed by the product of three terms, each of which can be left neutral: a mask texture read through one channel and a contrast, a per-material amount, and one lane of the entity's own shader data. All three are ordinary material properties, so the blend is drivable at runtime, and the instance term is what lets two entities wearing one material carry different amounts of wear in the same frame. property(i, name) spells the naming convention out for a caller driving them, inputs() lists what a layer carries, and limits() reports the bounds a stack is compiled against.

Usage: local layeredMaterial = require("@builtin/systems/layeredMaterial/layeredMaterial")

modules/layeredMaterial/check

check(stack: Stack): (boolean, any)

Compile a stack without raising. The same work compile does, with the error handed back rather than thrown — what an editor wants while the author is still choosing materials.

Parameters

  • stack Stack — The stack to check.
local ok, result = layeredMaterial.check(stack)

modules/layeredMaterial/compile

compile(stack: Stack, name: string?): Compiled

Compile a stack of materials into the two files a .shader asset holds. The layers are read off the materials they name, so this reaches the asset graph; it touches no renderer and no GPU, and a layer written as plain values and textures instead of a material reference needs no asset at all.

Parameters

  • stack Stack{ name, layers = { { material, blend, mask, ... }, ... } }, the bottom layer first.
  • name string? (optional) — Overrides stack.name for the shader this compiles to.
local out = layeredMaterial.compile({ name = "worn", layers = { { material = "@builtin::materials.default" }, { material = "@builtin::materials.gold", blend = 0.5 } } })
print(out.wgsl)

modules/layeredMaterial/inputs

inputs(): {

What a layer carries: the shading inputs the stack blends, the texture slots a layer may bind, the coverage controls a layer past the first declares, and the properties that belong to the whole surface.

for _, decl in ipairs(layeredMaterial.inputs().values) do print(decl.name) end

modules/layeredMaterial/install

install(stack: Stack, opts: { name: string?, into: any? }?): Installed

Compile a stack and write it out as a real .shader asset. The result is an ordinary shader asset — it can be read, hand-edited and shipped like any other. Installing the same stack again rewrites the same asset. The shader is compiled at the next frame boundary, the way any shader edit is; asset.ref(name, "shader"):compileStatus() is what reports the outcome.

Parameters

  • stack Stack — The stack to install.
  • opts { name: string?, into: any? }? (optional){ name = <shader name>, into = { path = <folder> } }. into places the asset as it is created; installing again writes wherever it already is.
local shader = layeredMaterial.install(stack, { name = "worn_barrel" })
local status = asset.ref(shader.name, "shader"):compileStatus()

modules/layeredMaterial/limits

limits(): { maxLayers: number, textureSlots: number }

The bounds a stack is compiled against: how many layers one holds, and how many texture slots the layers may spend between them.

print(layeredMaterial.limits().textureSlots)

modules/layeredMaterial/material

material(

Install a stack and make a material wearing the shader it compiled to, carrying every layer's values and every texture the layers bind. The result is an ordinary material asset: hand it to a Model's material field, or drive its properties afterwards like any other. Calling it again with the same material name is how a stack is iterated on — the shader is rewritten, the material is brought onto it, and the layers are applied over it, so what the call says is what the material holds when it returns.

local mat = layeredMaterial.material(stack, { name = "worn_barrel" })
entity.spawn("barrel").component.add("Model", { model = "cube", material = mat })

modules/layeredMaterial/property

property(index: number, input: string): string

The name the generated shader declares one of a layer's inputs under. The convention is l<index>_<input>, and this is what says so — a caller driving a blend at runtime asks for the name rather than spelling it.

Parameters

  • index number — Which layer, counting the bottom one as 1.
  • input string — The input's name — one of inputs(), or a coverage control. The coverage controls belong to a layer laid over another, so asking for one on the bottom layer raises rather than naming a property no stack declares.
matRef:setProperty(layeredMaterial.property(2, "coverage"), 0.7)

modules/layeredMaterial/read

read(materialRef: any): {

Read a layer's inputs off a material that already exists — the values it carries for everything a stack blends, and the textures it binds for them. This is what compile does with a layer written as { material = ... }, exposed on its own so a caller can see what a material would contribute before stacking it. A slot the material leaves on a built-in fallback (default:white, default:normal) is read as bound to nothing, since that fallback is what a slot left undeclared samples anyway.

Parameters

  • materialRef any (optional) — The material — a name, an identity, or an AssetRef<material>.
local layer = layeredMaterial.read("@builtin::materials.gold")
print(layer.values.metallic, layer.textures.base_color_texture)

modules/layers/README

layers (global)

Top-level scene-management namespace + Scene proxy. Owns the public layer-management API surface exposed on _G.layers. Pure Luau composition over internal __layers.* ECS-glue primitives; the LSP discovers the public shape via this --!global layers directive. Also available as global: layers

modules/layers/cost

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.

layers.resetCostWindow(); task.wait(1); for _, c in layers.cost() do print(c.name, c.avgMs) end

modules/layers/find

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

  • ref AssetRef<scene> | string — A scene AssetRef, or an identity string resolved through asset.ref.
local layer = layers.find("scenes.arena")

modules/layers/fireBeforeLoad

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

  • proxy any (optional) — The scene proxy about to load.
layers.fireBeforeLoad(sceneProxy)

modules/layers/fireLoad

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

  • proxy any (optional) — The loaded scene proxy.
layers.fireLoad(sceneProxy)

modules/layers/fireUnload

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

  • proxy any (optional) — The scene proxy being unloaded.
layers.fireUnload(sceneProxy)

modules/layers/install

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.

layers.install()

modules/layers/inventory

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.

for _, l in layers.inventory() do print(l.name, l.entities, l.ok) end

modules/layers/is_loaded

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

  • ref AssetRef<scene> | string — A scene AssetRef, or an identity string.
if not layers.is_loaded("scenes.hud") then layers.load("scenes.hud", { additive = true }) end

modules/layers/lastLoad

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.

local r = layers.lastLoad(); print(r.name, r.outcome, r.entities.added)

modules/layers/lastUnload

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.

local u = layers.lastUnload(); print(u.name, u.entities.removed)

modules/layers/list

list(): { any }

Every loaded scene layer as a proxy, root and additive alike, in the order the engine reports them.

for _, layer in ipairs(layers.list()) do print(layer.name, layer.additive) end

modules/layers/load

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

  • ref AssetRef<scene> | stringAssetRef<scene> envelope (preferred) or scene identity string.
  • opts LoadOpts? (optional) — Optional load options — additive overlay flag, slot name, persistence flag, world-origin offset, and whether to rebuild.
layers.load(asset.ref("@builtin::scenes.test_arena", "scene"))
layers.load(myAssetRef, { additive = true, name = "hud_overlay" })

modules/layers/loadHistory

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.

for _, r in layers.loadHistory() do print(r.name, r.durationMs) end

modules/layers/loadInFlight

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.

modules/layers/observe

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.

local o = layers.observe(); print(o.lastLoad.outcome, o.lastLoad.durationMs)
for _, c in layers.observe().cost do print(c.name, c.avgMs) end

modules/layers/offBeforeLoad

offBeforeLoad(h: number): boolean return remove(beforeLoadCbs, h) end

Cancel a layers.onBeforeLoad subscription.

Parameters

  • h number — The handle layers.onBeforeLoad returned.
layers.offBeforeLoad(h)

modules/layers/offEntityChanged

offEntityChanged(h: number): boolean

Remove a subscription made with layers.onEntityChanged.

Parameters

  • h number — The handle returned by layers.onEntityChanged.
layers.offEntityChanged(handle)

modules/layers/offLoad

offLoad(h: number): boolean return remove(loadCbs, h) end

Cancel a layers.onLoad subscription.

Parameters

  • h number — The handle layers.onLoad returned.
layers.offLoad(h)

modules/layers/offUnload

offUnload(h: number): boolean return remove(unloadCbs, h) end

Cancel a layers.onUnload subscription.

Parameters

  • h number — The handle layers.onUnload returned.
layers.offUnload(h)

modules/layers/onBeforeLoad

onBeforeLoad(cb: (any) -> ()): number return push(beforeLoadCbs, cb) end

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.
local h = layers.onBeforeLoad(function(scene) print("loading", scene.name) end)

modules/layers/onEntityChanged

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.
layers.onEntityChanged(function(batch)
for _, row in ipairs(batch) do
for _, id in ipairs(row.entities) do rebuild(id) end
end
end)

modules/layers/onLoad

onLoad(cb: (any) -> ()): number return push(loadCbs, cb) end

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.
local h = layers.onLoad(function(scene) spawnPlayerFor(scene) end)

modules/layers/onUnload

onUnload(cb: (any) -> ()): number return push(unloadCbs, cb) end

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.
local h = layers.onUnload(function(scene) releaseHandlesFor(scene) end)

modules/layers/problems

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 scene AssetRef, an identity string, or a scene proxy. Omit for the active root layer.
for _, f in layers.problems() do print(f.reason, f.entity, f.message) end

modules/layers/rebuildInFlight

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.

if not layers.rebuildInFlight() then judge(layers.active) end

modules/layers/reload

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 scene AssetRef, or an identity string. Omit to reload the active root scene.
layers.reload("scenes.arena")

modules/layers/resetCostWindow

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.

layers.resetCostWindow()

modules/layers/unload

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 scene AssetRef, an identity string, or a scene proxy. Omit to unload the active root scene.
layers.unload("scenes.hud")
layers.unload() -- the active root, plus its non-persistent overlays

modules/layers/whyPartial

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 scene AssetRef, an identity string, or a scene proxy. Omit for the active root layer.
local why, detail = layers.whyPartial(); if why then print(why, detail) end

modules/lensFx/README

require("@builtin/systems/lensFx/lensFx") -- lensFx

The lens and the film — flare thrown by bright sources, dirt on the front element, and grain that is redrawn every frame rather than a fixed screen pattern.

Usage: local lensFx = require("@builtin/systems/lensFx/lensFx")

modules/lensFx/active

active(): boolean

Whether the lens passes are running this frame.

if lensFx.active() then ... end

modules/lensFx/clear

clear()

Turn the flare and grain off and release the passes. The other settings are kept, so a later set brings back the same look.

lensFx.clear()

modules/lensFx/get

get(): LensFxState

The lens settings currently in force.

local g = lensFx.get().grainIntensity

modules/lensFx/paramsBuffer

paramsBuffer(): any?

The buffer this system's passes read. The render feature binds what this hands it, so the pass has the values this module packed.

local p = lensFx.paramsBuffer()

modules/lensFx/set

set(opts: LensFxOpts?): LensFxState

Set the lens and film. Any omitted field keeps its current value. With both flareIntensity and grainIntensity at 0 the passes are released.

Parameters

  • opts LensFxOpts? (optional) — Lens settings — see LensFxOpts.
lensFx.set({ flareIntensity = 1.0, grainIntensity = 0.04 })

modules/lensFx/sync

sync()

Bring the buffer's grain clock to the instant this frame draws at. The render feature calls it once a frame, so the field follows a renderer.temporal.hold the moment one is taken or released, whatever is driving the scene's own clock.

lensFx.sync()

modules/lensFx/tick

tick(dt: number)

Advance the clock the grain is drawn against. Grain is a fresh field every frame rather than a fixed screen pattern, so it needs the frame's own time — which cannot arrive through set, since a value that has not changed is not re-pushed.

Parameters

  • dt number — Seconds since the previous frame.
function update(dt) lensFx.tick(dt) end

modules/library/README

require("@builtin/modules/api/engine/library") -- library (also available as global 'library')

Discover, check, and import library assets — the @builtin/* tree the engine ships and the @namespace/* trees imported from other worlds.

Usage: local library = require("@builtin/modules/api/engine/library") Also available as global: library

modules/library/has

has(path: string): boolean

Check if a library asset exists at the given path.

Parameters

  • path string — Library asset path (e.g. "@builtin/models/Sample/DamagedHelmet").
assert(library.has("@builtin/models/Cube"))

modules/library/import

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

  • namespace string — Library namespace, with or without the leading @ (e.g. "@mylib" or "mylib").
  • worldRef string — The upstream world's guid, or its name as it appears in world.list().
library.import("@mylib", "my-shared-world")

modules/library/list

list(assetType: string?): { LibraryAsset }

List all available library assets. Optionally filter by asset type — call asset.categories() for the live set.

Parameters

  • assetType string? (optional) — Asset type filter (optional).
for _, a in ipairs(library.list("model")) do print(a.path) end

modules/lightCookies/README

require("@builtin/systems/lightCookies.package/lightCookies") -- lightCookies

Project an authored image through a spot light's cone — a gobo, a window pattern, a headlight cutoff.

Usage: local lightCookies = require("@builtin/systems/lightCookies.package/lightCookies")

modules/lightCookies/clear

clear(light: any): boolean

Stop projecting a cookie through a light's cone. The layer keeps its contents; the light stops reading it.

Parameters

  • light any (optional) — The spot light's entity id or entity proxy.
lightCookies.clear(lampId)

modules/lightCookies/configure

configure(opts: { resolution: number, layers: number }): (number?, string?)

Size the shared feature-texture array for cookies. Call once, before setting any cookie, with a layer count covering every layer the scene uses — resizing the array zeroes every layer in it, including layers other features own. renderer.featureTexture.state() reports the extent the array carries and the layers holding content, so a cookie that has left filled is one to set again.

Parameters

  • opts { resolution: number, layers: number }{ resolution, layers }resolution is the square extent of each layer in pixels, a multiple of 32; layers is how many the array holds.
lightCookies.configure({ resolution = 256, layers = 4 })

modules/lightCookies/fill

fill(image: any, opts: Options): (number?, string?)

Fill one layer of the shared array with an authored image, ready for a spot light's cookieLayer to project it.

Parameters

  • image any (optional) — A texture AssetRef, a TextureHandle, or any string asset.ref resolves to one — a guid, an identity, a name or a source path.
  • opts Options{ layer, gain?, tint? }.
lightCookies.fill("/textures/cookies/window_blinds.png", { layer = 0 })

modules/lightCookies/layerOf

layerOf(light: any): number?

The layer a light is currently projecting, or nil when it has no cookie.

Parameters

  • light any (optional) — The spot light's entity id or entity proxy.
print(lightCookies.layerOf(lampId))

modules/lightCookies/project

project(light: any, image: any, opts: Options): (number?, string?)

Project an image through a spot light's cone: fill a layer with it and point the light at that layer.

Parameters

  • light any (optional) — The spot light's entity id or entity proxy.
  • image any (optional) — A texture asset, in any form lightCookies.fill accepts.
  • opts Options{ layer, gain?, tint? }.
lightCookies.project(lampId, "/textures/cookies/blinds.png", { layer = 0 })

modules/logs/README

require("@builtin/modules/api/engine/logs") -- logs (also available as global 'logs')

Read-only query surface over the engine's in-memory log ring buffer. Public Luau surface over the __logs Internal FFI namespace.

Usage: local logs = require("@builtin/modules/api/engine/logs") Also available as global: logs

modules/logs/clear

clear(): boolean

Drop all buffered log entries. Lifetime per-level counts (logs.count) are preserved.

logs.clear()

modules/logs/count

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

  • opts LogQueryOpts? (optional) — Filter options, as logs.query takes.
print("errors:", logs.count().errors)
local before = logs.count().last_seq

modules/logs/errors

errors(limit: number?): { LogEntry }

Most-recent ERROR-level entries (newest first). limit defaults to 100.

Parameters

  • limit number? (optional) — Maximum entries to return.
for _, e in ipairs(logs.errors(20)) do print(e.message) end

modules/logs/find

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

  • text string — Substring to search for.
  • limit number? (optional) — Maximum entries to return.
local hits = logs.find("MY_MARKER")

modules/logs/query

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

  • opts LogQueryOpts? (optional) — Filter options.
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

modules/logs/tail

tail(limit: number?): { LogEntry }

Most-recent entries of any level in chronological order. limit defaults to 100.

Parameters

  • limit number? (optional) — Maximum entries to return.
for _, e in ipairs(logs.tail(20)) do print(e.level, e.message) end

modules/logs/template

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

  • message string — Log message to normalize.
local key = logs.template(entry.message)

modules/logs/warnings

warnings(limit: number?): { LogEntry }

Most-recent WARN+ entries (newest first). limit defaults to 100.

Parameters

  • limit number? (optional) — Maximum entries to return.
print(#logs.warnings(), "warnings")

modules/lsp/README

require("@builtin/modules/api/engine/lsp") -- lsp (also available as global 'lsp')

Embedded Luau language server — check / search / inspect Public Luau surface over the __lsp Internal FFI namespace.

Usage: local lsp = require("@builtin/modules/api/engine/lsp") Also available as global: lsp

modules/lsp/check

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

  • path string — VFS path.
  • opts CheckOpts? (optional){ severity?, limit?, context? }.
local diags = lsp.check("/zero/source/main.luau")

modules/lsp/checkAll

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

  • opts CheckAllOpts? (optional){ scope?, severity?, limit? }.

modules/lsp/checkCode

checkCode(source: string, opts: CheckOpts?): DiagnosticsResult

Validate inline Luau source without a backing file. Useful for checking code before writing it to disk.

Parameters

  • source string — Luau source.
  • opts CheckOpts? (optional){ severity?, limit?, context? }.

modules/lsp/checkDirty

checkDirty(): DiagnosticsResult

Drain the dirty-file set populated by the hot-reload hook, validate each, and return the combined diagnostic list.

modules/lsp/describe

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

  • path string — Doc path (e.g. "asset/resolve", "renderer.texture.create").
  • opts DescribeOpts? (optional) — Optional { includeInternal? } — default prefers the published entry.
local doc = lsp.describe("renderer.texture.create")

modules/lsp/describePaths

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

  • path string — Doc path in any spelling ("renderer.texture", "ecs/query").
for _, p in ipairs(lsp.describePaths("renderer.texture")) do print(p) end

modules/lsp/describeTool

describeTool(path: string): string?

Return the full documentation text for a code-mode tool.

Parameters

  • path string — Tool path (e.g. "scene/spawnLight").

modules/lsp/docsByKind

docsByKind(kind: string): { MethodSummary }

List every doc whose registration kind matches kind. Valid: "binding", "runtime_tool", "module", "component", "library", "lua_export".

Parameters

  • kind string — Registration kind.

modules/lsp/getStrictMode

getStrictMode(): StrictMode

Return the current strict mode.

modules/lsp/isStrict

isStrict(): boolean

Is the pre-execute LSP gate fully strict? False when off or in soft mode.

modules/lsp/lastCheckGen

lastCheckGen(): number

Generation counter — bumped each time the cache is rebuilt. UI polls this to know when to redraw.

modules/lsp/methods

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

  • namespace string — Namespace name (e.g. "entity", "modules/Transform").
  • opts MethodsOpts? (optional) — Optional { filter?, namesOnly?, includeInternal? }.
for _, name in ipairs(lsp.methods("ui", { namesOnly = true })) do print(name) end
lsp.methods("renderer", { filter = "shadow" })

modules/lsp/modules

modules(): { ModuleEntry }

List every Luau library module the engine currently knows about — discovered via --!module headers, library scans, and manually-recorded docs.

modules/lsp/namespaces

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

  • opts NamespacesOpts? (optional) — Optional { includeInternal? } — default lists public only.
for _, ns in ipairs(lsp.namespaces()) do print(ns.name) end

modules/lsp/readDirectives

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

  • source string — Luau source text.
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

  • query string — Substring to search for.
  • opts SearchOpts? (optional){ limit? = 50, includeInternal? }.

modules/lsp/setStrict

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

  • enabled boolean — True = strict, false = off.

modules/lsp/setStrictMode

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

  • mode StrictMode"off" | "soft" | "strict".

modules/lsp/summary

summary(): Summary

Counts only — does not re-run validation.

modules/lsp/tools

tools(): { ToolEntry }

List every code-mode tool registered in the VFS under /zero/docs/tools/<category>/<tool>.

modules/lsp/typeOf

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_source string — Luau expression source (no surrounding chunk).
  • context_path string? (optional) — VFS path whose scope should be visible.
local td = lsp.typeOf("Transform.lookAt", "/zero/source/main.luau")

modules/luau_coverage/README

require("@builtin/modules/api/engine/luau_coverage") -- luau_coverage (also available as global 'luau_coverage')

Luau source-based coverage dump (LCOV + sources manifest). Public Luau surface over the __luau_coverage Internal FFI namespace.

Usage: local luau_coverage = require("@builtin/modules/api/engine/luau_coverage") Also available as global: luau_coverage

modules/luau_coverage/dump_lcov

dump_lcov(path: string): DumpResult

Walk every Luau chunk loaded since the VM started (or since the last reset), collect per-line hit counts via Luau's built-in coverage API, render the aggregate as LCOV, and write it to path. Also writes a sibling luau_sources/ directory next to the LCOV file containing one source file per tracked chunk plus a manifest.json mapping chunk names to filenames — used by the proxy /ui/coverage page for per-file source drill-down. Raises a Luau error on failure.

Parameters

  • path string — Absolute filesystem path to write the LCOV tracefile to.
local r = luau_coverage.dump_lcov("/tmp/luau.lcov")

modules/luau_coverage/level

level(): number

The coverageLevel the Luau compiler was configured with (1 = statement coverage, 2 = statement + expression). Returns 0 when coverage is off.

if luau_coverage.level() > 0 then ... end

modules/luau_coverage/reset

reset(): boolean

Drop every tracked chunk ref and clear the coverage accumulator. The VM stops reporting hits for previously loaded scripts — use before exercising a specific scenario to isolate its coverage.

luau_coverage.reset()

modules/luau_profile/README

require("@builtin/modules/api/engine/luau_profile") -- luau_profile (also available as global 'luau_profile')

Sampling profiler + manual regions for Luau scripts. Public Luau surface over the __luau_profile Internal FFI namespace.

Usage: local luau_profile = require("@builtin/modules/api/engine/luau_profile") Also available as global: luau_profile

modules/luau_profile/begin

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

  • name string — Region name; aggregated across opens.
local id = luau_profile.begin("walk"); ...; luau_profile.end_region(id)

modules/luau_profile/dump

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

  • path string — Absolute host filesystem path to write.
local r = luau_profile.dump("/tmp/profile.folded")

modules/luau_profile/dump_regions

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

  • path string — Absolute host filesystem path to write.
luau_profile.dump_regions("/tmp/regions.json")

modules/luau_profile/end_region

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

  • id number — Region id returned by begin().
luau_profile.end_region(id)

modules/luau_profile/folded

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.

vfs.write("/source/tmp/sample.folded", luau_profile.folded())

modules/luau_profile/is_running

is_running(): boolean

True iff the background sampler is currently running.

if luau_profile.is_running() then luau_profile.stop() end

modules/luau_profile/reset

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

modules/luau_profile/sampling_available

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.

if luau_profile.sampling_available() then luau_profile.start() end

modules/luau_profile/snapshot

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_n number? (optional) — Truncate stacks to this many entries; omit for all.
local snap = luau_profile.snapshot(10)

modules/luau_profile/span<T...>

span<T...>(name: string, fn: () -> T..., ...): T...

Call fn(...) inside a manual region named name. The region is closed even if fn raises (the call goes through pcall internally). Returns whatever fn returned.

Parameters

  • name string — Region name.
  • fn () -> T... — Function to invoke with the trailing varargs.
local r = luau_profile.span("walk", function() return walk() end)

modules/luau_profile/start

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

  • hz number? (optional) — Sampling rate in Hz.
luau_profile.start(500)

modules/luau_profile/stop

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

modules/lut/README

require("@builtin/modules/lut") -- lut

Colour lookup tables — the table a lut post-process effect reads. Builds one from a grading description, imports one a colourist exported as a .cube file or a strip image, samples it the way the shader does, and installs it as a .texture asset the effect can bind.

A LUT is an N-entry colour cube: the answer to "what does this colour become" stored for N^3 input colours, with everything in between read by trilinear interpolation. A grade of any complexity therefore costs the same eight texel reads, which is why colourists deliver a look as a table rather than as a chain of operations. The engine stores a cube as a STRIP: N square tiles in one row, so the image measures N*N wide by N tall. Tile b holds the plane of constant blue index, red runs across a tile and green runs down it. The lut post shader reads N from the image's own height, so one shader serves a 16-, 32- or 64-entry cube with nothing to configure. Apply one through the post chain: local lut = require("@builtin::modules.lut") local ref = lut.install("warm_evening", lut.fromGrade({ exposure = 0.3, temperature = 0.25, saturation = 1.1, })) tools.use("pp", "add", "lut", { texture = ref.guid, amount = 1.0 }) A lut effect with no table bound passes the frame through untouched, so adding the effect before its table exists changes nothing.

Usage: local lut = require("@builtin/modules/lut")

modules/lut/build

build(size: number, fn: (number, number, number) -> (number, number, number)): Lut

Build a table by asking fn what each of its N^3 lattice colours becomes. The input is the lattice colour in 0..1, and so is the answer, which is clamped to that range and stored as it stands.

Parameters

  • size number — Cube edge — how many entries each axis holds. 2..90.
  • fn (number, number, number) -> (number, number, number)(r, g, b) -> r, g, b, called once per lattice entry.
local invert = lut.build(16, function(r, g, b) return 1 - r, 1 - g, 1 - b end)

modules/lut/fromCube

fromCube(text: string): Lut

Read a table out of .cube text — the interchange format DaVinci Resolve, Photoshop and every other grading tool writes. LUT_3D_SIZE gives the cube edge and the rows that follow are its entries with red varying fastest, which is the order this reader expects. The table is addressed over 0..1, so a file declaring any other DOMAIN_MIN / DOMAIN_MAX is refused by name rather than read as if it were.

Parameters

  • text string — The file's contents.
local look = lut.fromCube(vfs.read("/source/looks/kodak2383.cube"))

modules/lut/fromGrade

fromGrade(spec: GradeSpec?, size: number?): Lut

Bake a grade into a table. The same look as calling lut.grade's function per pixel, at the cost of eight texel reads however involved the grade is.

Parameters

  • spec GradeSpec? (optional) — What the grade does — see GradeSpec.
  • size number? (optional) — Cube edge. Defaults to 16.
local look = lut.fromGrade({ contrast = 1.2, saturation = 0.85, lift = { 0.02, 0.02, 0.05 } })

modules/lut/fromImage

fromImage(bytes: buffer | string): Lut

Read a table out of a strip image laid out as N*N by N — what a grading tool exports as a "2D LUT". Takes either source image bytes (PNG, JPEG, WebP) or the .texture blob lut.install wrote, so a table can be read back out of the asset it was installed as.

Parameters

  • bytes buffer | string — The encoded image, or a .texture blob.
local look = lut.fromImage(vfs.readBytes("/source/looks/teal_orange.png"))

modules/lut/grade

grade(spec: GradeSpec?): (number, number, number) -> (number, number, number)

The colour transform a GradeSpec describes, as a plain function. Useful on its own — to grade a single colour, to compose two looks, or to hand to lut.build — and it is what lut.fromGrade bakes.

Parameters

  • spec GradeSpec? (optional) — What the grade does. Every field is optional.

Returns (number, number, number)(r, g, b) -> r, g, b over 0..1.

local warm = lut.grade({ temperature = 0.3 }); local r, g, b = warm(0.5, 0.5, 0.5)

modules/lut/identity

identity(size: number?): Lut

The table that changes nothing — every entry answers with the colour that indexed it. The starting point for a hand-authored look, and the control an A/B measures against.

Parameters

  • size number? (optional) — Cube edge. Defaults to 16.
local base = lut.identity(32)

modules/lut/install

install(name: string, table_: Lut, opts: { [string]: any }?): any

Write the table as a .texture asset the lut post effect can bind, and upload it to the GPU under that asset's own guid. Stored as a float raster, unfiltered and with no mip chain: a table's entries are addresses, so they reach the shader as they were written, with no transfer function between and no mip average to land on a colour the table holds nowhere. The upload is what makes the returned guid bindable from the next frame, and what puts a re-baked look in front of the camera under the name it already had.

Parameters

  • name string — The asset's name.
  • table_ Lut — The Lut to write.
  • opts { [string]: any }? (optional){ dest = "<vfs path>", folder = "<vfs folder>", overwrite = true } — placement, forwarded to asset.create. overwrite defaults to true, so installing under a name that exists rewrites that asset and keeps its guid.
local ref = lut.install("dusk", lut.fromGrade({ temperature = -0.2 }))

modules/lut/sample

sample(table_: Lut, r: number, g: number, b: number): (number, number, number)

What the table answers for a colour — the same trilinear read the shader performs, so a table can be checked without rendering a frame.

Parameters

  • table_ Lut — The Lut to read.
  • r number — Red, 0..1. Values outside are clamped, as they are on the GPU.
  • g number — Green, 0..1.
  • b number — Blue, 0..1.
local r, g, b = lut.sample(look, 0.5, 0.5, 0.5)

modules/lut/strip

strip(table_: Lut): ({ number }, number, number)

The table's strip raster — the channel values, its width and its height. What renderer.texture.encode and asset.create("texture", ...) take at rgba32f.

Parameters

  • table_ Lut — The Lut to read.
local px, w, h = lut.strip(look); print(w, h)

modules/lut/toCube

toCube(table_: Lut, title: string?): string

Write a table out as .cube text, so a look built here can be opened in a grading tool or handed to another pipeline.

Parameters

  • table_ Lut — The Lut to write.
  • title string? (optional) — What the file calls itself. Defaults to "zero".
vfs.write("/source/looks/mine.cube", lut.toCube(look, "mine"))

modules/materialGraph/README

materialGraph

modules/materialGraph/check

check(graph: Graph): (boolean, any)

Compile a graph without raising. The same work compile does, with the error handed back rather than thrown — what an editor wants while the author is still typing.

Parameters

  • graph Graph — The graph to check.
local ok, result = materialGraph.check(graph)

modules/materialGraph/compile

compile(graph: Graph, name: string?): Compiled

Compile a graph into the two files a .shader asset holds. Pure text generation: it touches no asset, no renderer and no GPU, so a graph can be checked, compared and tested with nothing installed.

Parameters

  • graph Graph — The graph — nodes, surface, and optionally name + properties.
  • name string? (optional) — Overrides graph.name for the shader this compiles to.
local out = materialGraph.compile(graph)
print(out.wgsl)

modules/materialGraph/install

install(graph: Graph, opts: { name: string?, into: any? }?): Installed

Compile a graph and write it out as a real .shader asset. The result is an ordinary shader asset — nothing keeps a link back to the graph, so it can be read, hand-edited and shipped like any other. Installing the same graph again rewrites the same asset. The shader itself is compiled at the next frame boundary, the way any shader edit is; asset.ref(name, "shader"):compileStatus() is what reports the outcome.

Parameters

  • graph Graph — The graph to install.
  • opts { name: string?, into: any? }? (optional){ name = <shader name>, into = { path = <folder> } }. into places the asset as it is created; installing again writes wherever it already is, and an into naming somewhere else is refused rather than ignored.
local shader = materialGraph.install(graph, { name = "rusty_metal" })
local status = asset.ref(shader.name, "shader"):compileStatus()

modules/materialGraph/material

material(graph: Graph, opts: { name: string?, shader: string?, into: any?, values: any? }?): string

Install a graph and make a material wearing the shader it compiled to. The material is an ordinary material asset: hand it to a Model's material field, or edit its values afterwards like any other. Calling it again with the same material name is how an author iterates: the shader is rewritten from the graph, the material is brought onto that shader's property list, and values is applied over it — so what the call says is what the material holds when it returns.

Parameters

  • graph Graph — The graph to install.
  • opts { name: string?, shader: string?, into: any?, values: any? }? (optional){ name = <material name>, shader = <shader name>, into = ..., values = { <property> = <value> } }.
local mat = materialGraph.material(graph, { name = "rusty", values = { wear = 0.8 } })

modules/materialGraph/nodeTypes

nodeTypes(): { [string]: { ports: { string }, required: { string }, result: string, doc: string } }

Every node operation this compiler knows: the inputs a node of that op is written with, which of them it cannot be written without, the type it produces and a line on what it does. A graph written from this listing compiles.

for op, info in pairs(materialGraph.nodeTypes()) do print(op, info.doc) end
print(table.concat(materialGraph.nodeTypes().sampleTexture.required, ", "))

modules/materialGraph/surfaceChannels

surfaceChannels(): { [string]: { field: string, ty: string } }

The channels the surface terminal accepts, and the shading-model field each one drives.

print(materialGraph.surfaceChannels().roughness.field)

modules/mathx/README

require("@builtin/modules/api/engine/mathx") -- mathx (also available as global 'mathx')

Batch math kernels over buffer slices (damp, lerp/slerp, transform). Public Luau surface over the __mathx Internal FFI namespace.

Usage: local mathx = require("@builtin/modules/api/engine/mathx") Also available as global: mathx

modules/mathx/addScaledVec3

addScaledVec3(dstBuffer: Substrate.TypedBuffer, srcBuffer: Substrate.TypedBuffer,

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.

mathx.addScaledVec3(positions, velocities, n, dt)

modules/mathx/dampScalar

dampScalar(buffer: Substrate.TypedBuffer, offset: number, count: number,

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.

mathx.dampScalar(buf, 0, 16, 0.0, 0.16, dt)

modules/mathx/lerpVec3

lerpVec3(buffer: Substrate.TypedBuffer, offset: number, count: number,

Element-wise linear blend of count vec3s in buffer[offset .. offset+count*3] toward (tx, ty, tz) by t.

mathx.lerpVec3(buf, 0, n, 0, 1, 0, 0.5)

modules/mathx/normalizeQuat

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

  • buffer Substrate.TypedBuffer — The buffer to operate on.
  • offset number — Starting f32 index.
  • count number — Number of quaternions.
mathx.normalizeQuat(buf, 0, n)

modules/mathx/slerpQuat

slerpQuat(buffer: Substrate.TypedBuffer, offset: number, count: number,

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.

mathx.slerpQuat(buf, 0, n, 0, 0, 0, 1, 0.25)

modules/mathx/transformVec3

transformVec3(buffer: Substrate.TypedBuffer, offset: number,

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.

mathx.transformVec3(positions, 0, n, worldMatrix)

modules/mcpLog/README

require("@builtin/modules/api/engine/mcpLog") -- mcpLog (also available as global 'mcpLog')

Read-only MCP tool-call log ring buffer. Public Luau surface over the __mcpLog Internal FFI namespace.

Usage: local mcpLog = require("@builtin/modules/api/engine/mcpLog") Also available as global: mcpLog

modules/mcpLog/clear

clear(): boolean

Clear all entries from the engine's MCP log ring buffer.

mcpLog.clear()

modules/mcpLog/query

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

  • limit number? (optional) — Maximum number of entries to return.
for _, e in ipairs(mcpLog.query(50)) do print(e.tool_name, e.status) end

modules/mediaTransmittance/README

require("@builtin/systems/volumetrics/mediaTransmittance") -- mediaTransmittance

Light attenuation through participating media, in a form both media and surfaces can sample. A fog bank dims what is behind it, and overlapping volumes stack.

Usage: local mediaTransmittance = require("@builtin/systems/volumetrics/mediaTransmittance")

modules/mediaTransmittance/active

active(): boolean

Whether the transmittance passes are running this frame.

if mediaTransmittance.active() then ... end

modules/mediaTransmittance/buffers

buffers(): { [string]: any }?

The buffer(s) this system's passes read. A pass binds what this hands it, so it has the values this module packed.

local b = <module>.buffers()

modules/mediaTransmittance/clear

clear()

Remove every medium and release the passes. The settings are kept.

mediaTransmittance.clear()

modules/mediaTransmittance/get

get(): State

The transmittance settings currently in force.

local s = mediaTransmittance.get().strength

modules/mediaTransmittance/removeMedium

removeMedium(key: string): number

Remove a registered medium.

Parameters

  • key string — The id it was registered under.
mediaTransmittance.removeMedium("bank")

modules/mediaTransmittance/set

set(opts: TransmittanceOpts?): State

Set the scene-wide transmittance settings. Any omitted field keeps its current value.

Parameters

  • opts TransmittanceOpts? (optional) — Settings — see TransmittanceOpts.
mediaTransmittance.set({ sunDirection = { 0.4, 0.8, 0.2 }, steps = 32 })

modules/mediaTransmittance/setMedium

setMedium(key: string, opts: MediumOpts): number

Register (or move) a medium. Pushing the same key again replaces it.

Parameters

  • key string — A stable id for this medium.
  • opts MediumOpts — Placement and density — see MediumOpts.
mediaTransmittance.setMedium("bank", { position = { 0, 8, 0 }, size = { 20, 6, 20 }, density = 0.12 })

modules/microphone/README

require("@builtin/modules/api/engine/microphone") -- microphone (also available as global 'microphone')

Capture from an input device and read what is arriving: a loudness, a magnitude spectrum, and the raw PCM. Public Luau surface over the __microphone Internal FFI namespace.

Usage: local microphone = require("@builtin/modules/api/engine/microphone") Also available as global: microphone

modules/microphone/awaitRunning

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

  • timeout number? (optional) — Seconds to wait at most. Defaults to 10.
microphone.start(); local state, why = microphone.awaitRunning()

modules/microphone/devices

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.

for _, d in ipairs(microphone.devices()) do print(d.name, d.default) end

modules/microphone/frequencies

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.

local hz = microphone.frequencies(); print(hz[#hz]) -- the Nyquist frequency

modules/microphone/level

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.

if microphone.level() > 0.05 then print("someone is talking") end

modules/microphone/peak

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.

local p = microphone.peak(); if p and p.amplitude > 0.05 then print(p.hz) end

modules/microphone/samples

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

  • max number? (optional) — How many samples to take at most. Omitted, everything held comes back.
local pcm = microphone.samples(); if pcm then print(buffer.len(pcm) // 4) end

modules/microphone/spectrum

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.

local bins = microphone.spectrum(); print(#bins, bins[1])

modules/microphone/start

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

  • opts MicOpts? (optional){ device, sampleRate, fftSize }.
local state, why = microphone.start({ fftSize = 2048 })

modules/microphone/status

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.

local s = microphone.status(); print(s.state, s.framesCaptured, s.overruns)

modules/microphone/stop

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.

microphone.stop()

modules/modelImport/README

require("@builtin/modules/api/engine/modelImport") -- modelImport (also available as global 'modelImport')

The model-import pipeline: decompose any assimp-supported 3D model (fbx, obj, dae, gltf, glb, stl, ply, 3ds, …) into meshes, materials, textures, animation clips and a node graph; extract a single clip to its .zanim payload; and derive a .rig from a skinned mesh or from an animation-only file's driven skeleton. Public Luau surface over the __model and __rig Internal FFI namespaces.

Usage: local modelImport = require("@builtin/modules/api/engine/modelImport") Also available as global: modelImport

modules/modelImport/decompose

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

  • bytes buffer | string — Raw model file bytes (from vfs.readAsync).
  • format string — The source file extension (lowercase, no dot).
local h = modelImport.decompose(bytes, "obj"); task.await(h)

modules/modelImport/decomposeFiles

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.
  • mainName string — Basename of the model file to import (one of files' names).
local h = modelImport.decomposeFiles(files, "CesiumMilkTruck.gltf"); task.await(h)

modules/modelImport/extractAnimation

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

  • sourcePath string — VFS path to the source model file.
  • clipName string — Clip name as returned by result(handle).animations[i].name.

modules/modelImport/extractAnimationResult

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

  • handle string — Promise handle from extractAnimation.

modules/modelImport/result

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

  • handle string — Promise handle from decompose.

modules/modelImport/retryHandle

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.
  • retries number? (optional) — Extra attempts after the first (default 3).
  • yield (() -> ())? (optional) — Called between attempts (default task.wait).

modules/modelImport/rigFromMeshSkin

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

  • meshBytes buffer | string — Raw ZMSH mesh bytes carrying a skin.

modules/modelImport/rigFromSkeleton

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

  • skeleton AnimationSkeleton{ names, parents, locals } (a decompose result's skeleton).
local rigJson = modelImport.rigFromSkeleton(data.skeleton)

modules/motionBlur/README

require("@builtin/systems/motionBlur/motionBlur") -- motionBlur

Camera-shutter motion blur. Blur length comes from a shutter angle, and the reconstruction lets a moving object smear past its own silhouette instead of stopping dead at its edge.

Usage: local motionBlur = require("@builtin/systems/motionBlur/motionBlur")

modules/motionBlur/active

active(): boolean

Whether the motion-blur passes are running this frame.

if motionBlur.active() then ... end

modules/motionBlur/buffers

buffers(): { [string]: any }?

The parameter buffer the motion-blur passes read, carrying the settings this module packs. The render feature binds what this hands it.

local b = motionBlur.buffers()

modules/motionBlur/clear

clear()

Close the shutter and release the passes. The other settings are kept, so a later set({ shutterAngle = ... }) brings back the same look.

motionBlur.clear()

modules/motionBlur/get

get(): MotionBlurState

The shutter settings currently in force.

local a = motionBlur.get().shutterAngle

modules/motionBlur/set

set(opts: MotionBlurOpts?): MotionBlurState

Set the camera's shutter. Any omitted field keeps its current value. A shutterAngle of 0 closes the shutter and releases the passes.

Parameters

  • opts MotionBlurOpts? (optional) — Shutter settings — see MotionBlurOpts.
motionBlur.set({ shutterAngle = 180, samples = 16 })

modules/multiplayer/README

require("@builtin/modules/api/engine/multiplayer") -- multiplayer (also available as global 'multiplayer')

Multiplayer sync state and operations — connection, peers, ownership, rooms, undo/redo. Public Luau surface over the __multiplayer Internal FFI namespace.

Usage: local multiplayer = require("@builtin/modules/api/engine/multiplayer") Also available as global: multiplayer

modules/multiplayer/beginOperation

beginOperation(description: string)

Begin recording an undoable operation. All mutations until commitOperation() are grouped into one undo entry.

Parameters

  • description string — Human-readable label.
multiplayer.beginOperation("move cube")

modules/multiplayer/canRedo

canRedo(): boolean

Check if this client has any redoable operations.

modules/multiplayer/canUndo

canUndo(): boolean

Check if this client has any undoable operations.

modules/multiplayer/cancelOperation

cancelOperation()

Cancel the current operation and restore all properties to their values at begin time.

multiplayer.cancelOperation()

modules/multiplayer/claimOwnership

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.

modules/multiplayer/clearHistory

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

modules/multiplayer/commitOperation

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

modules/multiplayer/connect

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

  • relayUrl string — Relay server URL.
multiplayer.connect("https://relay.example.com")

modules/multiplayer/disconnect

disconnect()

Disconnect from the multiplayer relay server.

multiplayer.disconnect()

modules/multiplayer/explain

explain(

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.

local v = multiplayer.explain(player, "Health", "hp")
if not v.arriving then print(v.reason) end

modules/multiplayer/getDiagnostics

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.

local d = multiplayer.getDiagnostics()
if d.packetLoss > 0.05 then warnThePlayer(d.rttMs) end
print(d.messagesAwaitingEntity .. " message(s) waiting for their entity")

modules/multiplayer/getPeerId

getPeerId(): number?

Get this client's peer ID in the current session.

modules/multiplayer/getPeers

getPeers(): { PeerInfo }

Get a list of all connected peers in the current session.

for _, p in ipairs(multiplayer.getPeers()) do print(p.name) end

modules/multiplayer/getRoomPeers

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

  • roomKey string — Fully-qualified room key ({worldGuid}/{profile}/{mode}/{sceneGuid}).
for _, p in ipairs(multiplayer.getRoomPeers(key)) do print(p.id) end

modules/multiplayer/getRooms

getRooms(): { string }

The relay rooms this client has joined, sorted. A broadcast reaches only the peers that share one of these.

for _, key in ipairs(multiplayer.getRooms()) do print(key) end

modules/multiplayer/getTickRate

getTickRate(): number

Get the current sync tick rate (network updates per second).

modules/multiplayer/heldMessages

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.

for _, m in ipairs(multiplayer.heldMessages()) do print(m.kind, m.ageMs) end

modules/multiplayer/isConnected

isConnected(): boolean

Check if a multiplayer session is active and connected to a relay.

modules/multiplayer/isHost

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

if multiplayer.isHost() then enemy = entity.spawnSynced("enemy") end

modules/multiplayer/isOwner

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 to self.entityId in component context).

modules/multiplayer/isRoomCreator

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

  • roomKey string — Fully-qualified room key ({worldGuid}/{profile}/{mode}/{sceneGuid}).
if multiplayer.isRoomCreator(key) ~= false then layers.load(scene) end

modules/multiplayer/joinRoom

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

  • roomKey string — Fully-qualified room key.
multiplayer.joinRoom(worldGuid .. "/" .. engine.profile .. "/play/" .. sceneGuid)

modules/multiplayer/leaveRoom

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

  • roomKey string — Fully-qualified room key.

modules/multiplayer/loopback

loopback(): { [string]: any }

Loopback testing harness. Returns a table with enable(), disable(), flush(), receive() methods for testing sync without a relay server.

modules/multiplayer/observe

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.

local o = multiplayer.observe()
print(#o.entities .. " entities, " .. o.totals.properties .. " synced properties")

modules/multiplayer/observeComponent

observeComponent(

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.

local c = multiplayer.observeComponent(player, "Health")
print(#c.properties .. " declared properties")

modules/multiplayer/observeEntity

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.
local r = multiplayer.observeEntity(e)
for _, c in ipairs(r.components) do print(c.componentType) end

modules/multiplayer/on

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

  • channel string — Channel name to listen on.
  • callback (number, ...any) -> ()function(fromPeerId: number, ...) — the sender's peer id then the sent args.

modules/multiplayer/recordSpawn

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

  • entityId string — Entity id to record as spawned by this operation.
multiplayer.recordSpawn(id)

modules/multiplayer/redo

redo(): boolean

Redo this client's last undone operation.

modules/multiplayer/releaseOwnership

releaseOwnership(entityId: (string | entityRef)?): boolean

Release ownership of an entity.

Parameters

  • entityId (string | entityRef)? (optional) — Entity id or proxy to release.

modules/multiplayer/roomFor

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.
local key = multiplayer.roomFor(player)
if key then multiplayer.joinRoom(key) end

modules/multiplayer/send

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

  • channel string — Channel name listeners subscribe to via multiplayer.on.
  • args any (optional)

modules/multiplayer/syncTotals

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.

print(multiplayer.syncTotals().properties .. " synced properties registered")

modules/multiplayer/undo

undo(): boolean

Undo this client's last edit-mode operation.

modules/notices/README

require("@builtin/modules/api/engine/notices") -- notices (also available as global 'notices')

Post an event record that surfaces to the operating agent on its next tool call. Public Luau surface over the __notices_post Internal FFI global.

Usage: local notices = require("@builtin/modules/api/engine/notices") Also available as global: notices

modules/notices/post

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

  • template string — Fixed sentence identifying the notice.
  • params { [string]: any }? (optional) — Optional table of named values rendered alongside the template.
  • opts NoticeOpts? (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" })

modules/onUnload

modules.onUnload(callback) -> ()

Register a teardown for the calling module's current run. It runs once, at the moment this run ends — immediately before the next run of that chunk replaces it, and when the module leaves the require cache — while this run's module-level locals are still in scope, so the run that spawned the entities, created the particle systems or subscribed to the event bus is the run that releases them. Each run registers its own, and the registration goes with the run that made it.

Parameters

  • callback function — Called with no arguments just before this run of the module is replaced

modules/packages/README

require("@builtin/modules/api/engine/packages") -- packages (also available as global 'packages')

Discover and inspect registered packages (folders marked with package.yaml). Public Luau surface over the __packages Internal FFI namespace.

Usage: local packages = require("@builtin/modules/api/engine/packages") Also available as global: packages

modules/packages/list

list(): { PackageEntry }

List every registered package across scopes.

for _, p in ipairs(packages.list()) do print(p.name, p.scope) end

modules/packages/lookup

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_scope string — Package name, or scope if a second arg is given.
  • name string? (optional) — Package name when the first arg is a scope.
local p = packages.lookup("@builtin", "audio")

modules/particles/README

require("@builtin/modules/api/engine/particles") -- particles (also available as global 'particles')

Particle emitters: create one, list the ones that are live — every emitter, or one creator's own — and read what each is simulating and drawing right now: whose it is, how many particles are alive, what it costs on the GPU, and when it is producing nothing, which reason explains it.

Usage: local particles = require("@builtin/modules/api/engine/particles") Also available as global: particles

modules/particles/create

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

  • spec table? (optional){ maxCount, rate, lifetime, speed, shape, ... } — every field optional, each falling back to the emitter's default. owner and name say whose the emitter is: owner is the key list(owner) matches, so a creator reaches exactly its own emitters after it has lost their handles, and name says 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.create lists every key the spec defines.
local fire = particles.create({ maxCount = 2000, rate = 100 })
local star = particles.create({ owner = "starfield", name = "shell" })

modules/particles/list

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 the owner key a creator stated; a table matches every one of owner, name and source that it names.
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") })

modules/particles/observe

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

  • system any? (optional) — Optional particle system handle to read on its own.
local o = particles.observe(); print(o.count, o.alive, o.silent)
local r = particles.observe(fire); print(r.alive, r.bytes.total)

modules/particles/silenceReasons

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.

for _, r in ipairs(particles.silenceReasons()) do print(r.reason, r.means) end

modules/particles/whySilent

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

  • system any (optional) — The particle system handle to ask about.
local why, detail = particles.whySilent(fire)

modules/persist/README

persist

modules/persist_origin/README

persist_origin

Provenance / origin-context layer for the persist (create-flow) system. origin kinds: "execute" — an agent call (the engine's authoring window is open) "scene" — scene loader / entrypoint onLoad (__scene_load.inProgress()) "component" — a script-component lifecycle callback (awake/update/...) "engine" — engine-driven code that is none of the above (an assetType behavior composing an avatar, a bundle exploding its hierarchy). The DEFAULT: a surface is authored only while the authoring window is open, so a surface added later classifies as code without being enumerated anywhere.

modules/persist_player_camera/README

persist_player_camera

Freeze the LIVE player + camera into a scene's player/camera config. The persist north star is "what you see is what you get": the freeze PRESERVES the exact live state, it never synthesizes. PLAYER. The live player body is layers.active.players.localPlayer.avatar — whatever entity is in that slot right now, however it was built (procedurally spawned, instantiated, hand-assembled). The scene CONFIG (player.avatar_<mode>) is a BUNDLE ref the spawner instantiates on the next load. So the freeze CONVERTS the live avatar entity-tree into a bundle and points the config at it: * If the live avatar is an UNMODIFIED instance of an existing bundle (its bundleProvenance carries exactly one source bundle with no added components/entities), reference THAT bundle — don't duplicate. * Otherwise compose the live tree into a bundle (new on first freeze, re-composed in place on re-freeze) and reference it. The entity-tree -> bundle work goes through the bundle assetType's own composeTemplate path: first freeze = one-step asset.create("bundle", name, { entity = ref }) (onCreate composes the live tree), re-freeze = bundleRef:update(entityId) in place. Both capture LIVE component state via serialized component snapshots — exactly what's on screen. CAMERA. The live primary camera's behavior is a COMPONENT (the agent authors one to drive the camera); the freeze references it as camera.behavior_<mode>. Camera parameters (fov/near/far/follow) are baked into the live Camera component and preserved with it. No synthesis. Bundle CREATION is side-effecting, so it runs from confirm(), never plan() (a cancelled plan must leave no orphan bundle in /source).

modules/persist_serializer/README

persist_serializer

Live-state serializer driver for persist. The missing half of the scene-save story: scene_saver.serializeEntity already turns a LIVE entity into the v6 per-entity body, but it is only ever fed ids from the EDIT-mode dirty-mark queue (empty in play). This driver supplies the play-mode side — it ENUMERATES the active layer's live entities, filters them by PROVENANCE (execute-origin only, via persist.origin) + authored-ness (scene_saver's spawner-managed / temporary filter), and assembles a loadable v6 scene body. Lighting stays ENTITY-PRIMARY: lighting comes from Light components on entities, which serialize as normal entities — so we deliberately do NOT emit scene_saver's global lighting manifest block (it would compete with / override the entity lights). Only the player + camera config blocks are carried over.

modules/playerSetupValidation/README

require("@builtin/modules/api/engine/playerSetupValidation") -- playerSetupValidation

Agent-facing validation for authored player setups. A player-spawn scene is built from PlayerSpawn entities (each naming a PlayerPrototype subtree to instantiate on join) and PlayerPrototype roots (marked PrototypeOnly, with a body and a camera field each naming a DESCENDANT of the prototype — the entity adopted as the avatar and the entity carrying the player's Camera). The prototype is cloned as a self-contained subtree, so both refs must resolve inside it and it may hold no camera other than the one camera names. checkEntity inspects one such entity and returns human-readable messages that name what is wrong and what to do about it; checkScene validates the scene's player intent against its PlayerSpawn / Camera counts. checkActiveScene runs the full rule set across the live active-layer scene, resolving each spawn's prototype wherever the materialisation holds it — the live entity in edit, the captured authored template in play; checkSceneJson runs it statically over a scene.json document — the path the scene assetType's validate hook uses so asset.validate, worldValidation, and the world.push gate all catch a broken player setup at authoring time.

Usage: local playerSetupValidation = require("@builtin/modules/api/engine/playerSetupValidation")

modules/playerSetupValidation/checkActiveScene

checkActiveScene(): { { entity: string, message: string } }

Gather every player-setup validation message for the live active-layer scene: the per-entity rules across all PlayerSpawn / PlayerPrototype entities, the competing-camera rule, and the scene-intent rule. Returns a flat list an agent can read to see what to fix. The verdict belongs to the settled scene, so the call holds while a scene load or a mode-flip transition is rebuilding the live tree, and judges what the rebuild lands on.

modules/playerSetupValidation/checkEntity

checkEntity(entityId: string): { string }

Validate a single PlayerSpawn or PlayerPrototype entity, returning agent-facing messages naming what is wrong and what to do. An entity carrying neither component (or one that does not exist) yields no messages.

Parameters

  • entityId string — The entity to inspect.

modules/playerSetupValidation/checkScene

checkScene(opts: { playerIntent: string, spawnCount: number, cameraCount: number }): { string }

Validate a scene's player intent against its PlayerSpawn / Camera counts, returning agent-facing messages. A "spawns" scene with no PlayerSpawn, or a "none" scene with no Camera, yields a message; every other combination is clean.

Parameters

  • opts { playerIntent: string, spawnCount: number, cameraCount: number }{ playerIntent: string, spawnCount: number, cameraCount: number }.

modules/playerSetupValidation/checkSceneJson

checkSceneJson(sceneJson: { [string]: any }): { { code: string, severity: string, message: string } }

Validate a decoded scene.json document statically: the full player-setup rule set (per-entity, competing-camera, scene-intent) run over the scene's authored entity tree without loading it. This is what the scene assetType's validate hook calls, so asset.validate / worldValidation / the world.push gate all report a broken player setup at authoring time.

Parameters

  • sceneJson { [string]: any } — The decoded scene.json table ({ player, version, entities }).

modules/playerSetupValidation/playReadinessProblems

playReadinessProblems(): { { entity: string, message: string } }

The player-setup problems that must block a flip into play: the per-entity spawn/prototype rules (body + camera refs set, resolving to descendants, a single referenced camera) and the scene-intent rule, over the LIVE active scene. A "spawns" scene with none of these problems is ready to play. The competing-camera rule is deliberately excluded — the editor's own free-fly camera is a live viewport camera outside every prototype, so running it here would false-positive on every edit session; that rule stays a static / publish-time concern. Empty for a non-"spawns" scene (no player requirement), and empty while the active scene is still being materialised — the verdict belongs to the settled scene, so it waits for the layer to finish loading and for any mode-flip transition to converge.

modules/player_lifecycle/README

player_lifecycle

Bridges the UserIdentity component's awake / onDestroy into the per-scene players registry. Installed by the prelude. The UserIdentity component requires this module and calls playerJoined(eid) in awake, playerLeft(eid) in onDestroy, and localAvatarBound(eid, av) when the local avatar slot fills. Each routes into layers.active.players._addPlayer / _removePlayer / _onLocalAvatarBound, which update the per-scene set AND fire onPlayerJoined / onPlayerLeft in one atomic step. onLocalReady is fired separately when the joining entity matches world.connectedUsers.localUser.entity. install() also registers __layers_* dispatch channels the player_spawner uses.

modules/player_prototype_spawn/README

require("@builtin/modules/api/engine/player_prototype_spawn") -- player_prototype_spawn

The deterministic spawn core for authored player prototypes. A PlayerSpawn entity names a PlayerPrototype subtree to instantiate; spawnFor clones that subtree, activates and reveals the clone, prunes clone-subtree nodes whose networkScope excludes the caller's owner/authority role, marks the clone RuntimeOnly, and turns the clone ROOT into the joining user's internal identity: it removes the PlayerPrototype / PlayerSpawn markers from the root and adds the UserIdentity component, so the root plugs into the existing players registry / join-leave / ownership / sync as a first-class local player. The clone root's authored children are detached to standalone world entities — nothing is parented to the identity. The joining user's avatar is the body named by the root's PlayerPrototype.body ref; it is positioned at the PlayerSpawn's world transform (placement at_spawn_transform) and bound by assigning the identity's avatar, which links it back through PlayerAvatar.owner. The clone's Camera node follows the bound body. Runtime provenance attributes (source prototype, owner user, owner player) are stamped on the root and read back with runtimeSpawnedInfo. The attributes are runtime-only: they live on the live entity and are not serialized into the world. installJoinHook wires spawnFor to a scene's user-join event so a joining user is placed from a PlayerSpawn automatically.

Usage: local player_prototype_spawn = require("@builtin/modules/api/engine/player_prototype_spawn")

modules/player_prototype_spawn/applyNetworkScope

applyNetworkScope(cloneRootId: string, isOwner: boolean, isAuthority: boolean)

Prune a clone subtree by each node's networkScope against the caller's role. Walks the subtree from cloneRootId; a node scoped OwnerOnly is despawned when the caller is not the owner, AuthorityOnly when the caller is not the authority, and Replicated (or any other value) is kept.

Parameters

  • cloneRootId string — The clone's root entity id.
  • isOwner boolean — Whether the caller owns this clone.
  • isAuthority boolean — Whether the caller is the simulation authority for this clone.

modules/player_prototype_spawn/authorDefault

authorDefault(): { [string]: string }

Author the canonical default player setup into the active scene — the same shape the default world and the static_player canonical scene ship: a PrototypeOnly prototype whose body adopts the humanoid avatar and whose OwnerOnly camera rig runs the orbital follow behavior, plus a spawn at the origin. Returns the authored entity ids. This is the single builder scene.player("spawns") and the "player" scene template both resolve to, so a joining user's avatar always replaces the same authored body.

modules/player_prototype_spawn/captureTemplatesFromEntities

captureTemplatesFromEntities(entities: { any })

Build the prototype-template registry from a scene's authored entity records (the parsed scene data, not live entities). This is the primary capture path: it is independent of scene-load order and runtime composition, so it captures the clean authored subtree (no composed avatar) and works in the runtime profile, which boots straight to play. Called by the scene loader for v7 scenes.

Parameters

  • entities { any } — The scene's authored entity records (each { id, name, parent, networkScope, renderLayer, transform, components }).

modules/player_prototype_spawn/capturedTemplate

capturedTemplate(prototypeId: string): any

The captured authored subtree for a PlayerPrototype — the clone source spawnFor instantiates for each joining player, keyed by the prototype's authored entity id (the id a PlayerSpawn's prototype field carries). Each node is { id, name, participation, networkScope, renderLayer, position, rotation, scale, components = { [type] = data }, children }. Where the materialisation keeps authored prototype subtrees out of the live scene — play — this template is the authored prototype, and it is the subtree spawnFor clones for each joining player.

Parameters

  • prototypeId string — The PlayerPrototype root's authored entity id.
local proto = player_prototype_spawn.capturedTemplate(spawn.prototype.id)

modules/player_prototype_spawn/chooseSpawn

chooseSpawn(ctx): (string?, { [string]: any }?, string?)

Pick the PlayerSpawn-carrying entity to spawn from. Enumerates entities carrying the PlayerSpawn component in the joining user's ROOT scene, skipping any that live in an additive overlay layer (editor UI, HUD scenes). When the root scene resolves (ctx.rootSceneGuid, else layers.active.guid), only spawns in that scene's layer are considered; otherwise every non-overlay spawn is eligible. Spawns with no layer attribution yet belong to the world root and stay eligible either way. Honors an optional ctx.spawnId override (used for deterministic selection), otherwise returns the first matching spawn.

Parameters

  • ctx any (optional) — A table; ctx.spawnId optionally names the spawn entity to select, ctx.rootSceneGuid optionally names the scene layer to scope the search to.

modules/player_prototype_spawn/clearJoinHook

clearJoinHook(guid: string)

Clear a scene's join-hook flag. Called when a "spawns" scene unloads so the once-registered connect / play-entry handlers stand down (they no-op while no wired scene remains). Idempotent for an unknown guid.

Parameters

  • guid string — The scene guid passed to installJoinHook.

modules/player_prototype_spawn/installJoinHook

installJoinHook(sceneProxy): boolean

Wire spawnFor to the world's connected-user join event. When a user connects, the hook picks a PlayerSpawn and instantiates that user's prototype instance (internal identity + avatar + camera-follow) via spawnFor. The trigger is world.connectedUsers.onConnect — the WORLD-level "a user joined the session" event — not the room players registry, so the internal identity the clone becomes (which folds into that registry) does not re-trigger a spawn. Entering play spawns every already-connected user (their onConnect fired in edit, ignored then). A scene wired while ALREADY in play — the runtime profile boots straight into play, or a scene swapped in mid-play — gets that same sweep immediately, since no play flip follows to trigger it. Every spawn path is per-user idempotent: a user who already owns a live clone is skipped, so overlapping paths and re-flips never produce a second player. Idempotent per scene proxy: a second call for the same scene installs nothing further.

Parameters

  • sceneProxy any (optional) — A non-additive Scene proxy.

modules/player_prototype_spawn/runtimeSpawnedInfo

runtimeSpawnedInfo(id: string): { [string]: any }?

Read back the runtime provenance stamped on a clone root by spawnFor.

Parameters

  • id string — The clone root entity id.

modules/player_prototype_spawn/spawnFor

spawnFor(ctx): string?

Spawn a player instance for a joining user from the chosen PlayerSpawn's prototype. Chooses a spawn (honoring ctx.spawnId), resolves and validates its prototype, clones the prototype subtree, activates and reveals the clone, prunes it by networkScope against the caller's owner/authority role (both default true), marks the clone RuntimeOnly, stamps provenance attributes, places the clone at the spawn's world transform, and registers it with the prototype lifecycle so it is despawned on the return to edit.

Parameters

  • ctx any (optional){ userId, playerEntityId?, spawnId?, isOwner?, isAuthority? }.

modules/player_prototype_spawn/storePrototypeTemplate

storePrototypeTemplate(prototypeId: string)

Capture a PlayerPrototype's authored subtree into the template registry. Called by PlayerPrototype.awake (before its Asset composes and before it deactivates) so spawnFor can instantiate the authored structure per player.

Parameters

  • prototypeId string — The PlayerPrototype root entity id.

modules/player_prototype_spawn/userHasSpawnedPlayer

userHasSpawnedPlayer(userId: string?): boolean

Whether a live clone spawned by spawnFor already carries this user's owner provenance. Scans the live entities for a root whose ownerUserId attribute matches. The idempotency guard the auto-spawn paths use so a user who already has a spawned player never gets a second one.

Parameters

  • userId string? (optional) — The joining user's account id.

modules/player_spawner/README

player_spawner

DEPRECATED: v7 scenes place players through the PlayerSpawn / PlayerPrototype flow. This engine-default-Player avatar path serves legacy v6 scenes only; M.ensure stands down (returns early) for any scene carrying a string playerIntent.

Binds the per-mode avatar bundle to the engine-default identity entity on non-additive layers.onLoad, for legacy v6 scenes. The avatar ref resolves as: per-scene settings.player.avatar_<mode> when set, else the world default world.avatar_default_<mode>. Missing-but-required is log.error + skip — no hardcoded fallback bundle. A single avatar slot combines visual + controller in one bundle. It spawns a fresh body entity from that bundle, then binds it by assigning the identity's avatar field, which marks the body synced + PlayerOwned and attaches its PlayerAvatar link. Scene-level avatar_<mode> = "" is the explicit opt-out — the scene builds its own body in entrypoint.luau::onLocalReady.

modules/players/README

players

Per-scene players registry. Tied to a non-additive Scene proxy. Reached as layers.active.players or layers.find(name).players. Returns curated player handles — never entity or component proxies — so the caller reads/writes player data (.userId, .displayName) and reaches the body via player.avatar (an entity ref) uniformly across all consumers. Both colon (p:onJoin(cb)) and dot (p.onJoin(cb)) call styles are supported on every method; the registry is a namespace surface, not an object, so neither style is canonical. Additive layers do NOT carry a players surface today (multiplayer support for players living inside additive overlays is a follow-up — see the layers.module additive-scene comment). layers.find(<additive>).players returns nil; this module is only instantiated on the root scene proxy.

modules/postprocess/README

require("@builtin/modules/api/engine/postprocess") -- postprocess (also available as global 'postprocess')

Fullscreen post-process effects — register / remove / toggle / named-property updates / list the chain / read one effect's declared property schema and the value each property holds. Public Luau surface over the __postprocess Internal FFI namespace.

Usage: local postprocess = require("@builtin/modules/api/engine/postprocess") Also available as global: postprocess

modules/postprocess/add

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

  • name string — Unique effect name.
  • shader string | AssetRef — A resolved shader asset reference, or author WGSL (fn fragment(in: PostInput) only).
  • opts PostprocessOpts? (optional){ priority = 100, enabled = true, layer = "all"|"scene", properties = {{name, type, default?, min?, max?, textureDefault?}} } — with a shader asset, properties layers over the asset's own schema. layer picks 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. textureDefault is what a type = "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 until setTexture binds a texture that exists.
postprocess.add("vignette", asset.resolve("@builtin::shaders.post.vignette", "shader"))
postprocess.add("vignette", VIGNETTE_WGSL, { properties = {{ name = "intensity", type = "float", default = 0.5 }} })

modules/postprocess/describe

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

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

modules/postprocess/list

list(): { string }

List all registered post-process effect names in renderer priority order (lower priority runs first).

for _, n in ipairs(postprocess.list()) do print(n) end

modules/postprocess/remove

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

  • name string — Effect name to remove.
postprocess.remove("vignette")

modules/postprocess/setEnabled

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

  • name string — Effect name.
  • enabled boolean — True to enable, false to disable.
postprocess.setEnabled("bloom", false)

modules/postprocess/setProperty

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

  • name string — Effect name.
  • prop string — Declared property name.
  • value (number | { number }) — Number or array of numbers.
postprocess.setProperty("vignette", "intensity", 0.6)

modules/postprocess/setSampler

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

  • name string — Effect name.
  • opts { [string]: any }{ filter = "linear"|"nearest", wrap = "clamp"|"repeat"|"mirror" }.
postprocess.setSampler("blur", { filter = "linear", wrap = "clamp" })

modules/postprocess/setTexture

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

  • name string — Effect name.
  • prop string — Declared texture-property name.
  • path string — Texture path / spec.
postprocess.setTexture("__vsky_composite", "cloud", "cloud_target")

modules/postprocess/status

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.

for _, e in ipairs(postprocess.status()) do
print(e.name, e.enabled, e.layer, e.error)
end

modules/profiler/README

require("@builtin/modules/api/engine/profiler") -- profiler (also available as global 'profiler')

Frame-level profiler capture, EMA stats, and named profiling blocks. Public Luau surface over the __profiler Internal FFI namespace.

Usage: local profiler = require("@builtin/modules/api/engine/profiler") Also available as global: profiler

modules/profiler/begin

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

  • name string — Block name (e.g. "MyComponent.update").
profiler.begin("MyComponent.update"); ...; profiler.finish()

modules/profiler/disableRing

disableRing()

Disable the ring buffer and clear its history.

profiler.disableRing()

modules/profiler/enableRing

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

  • seconds number? (optional) — Seconds of history to retain (default 20).
if profiler.enableRing(30) then ... end

modules/profiler/finish

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

  • name string? (optional) — Block name to finish. Omit to pop the top of the stack.
local ms = profiler.finish("MyComponent.update")

modules/profiler/gpuFrame

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.

local g = profiler.gpuFrame(); print(g.frame_span_ms, g.spans[1] and g.spans[1].label)

modules/profiler/hits

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

  • label string? (optional) — Capture label to store under (default "watch_hits").
local json = profiler.hits()

modules/profiler/isCapturing

isCapturing(): boolean

Check if a profiler capture is currently active.

if profiler.isCapturing() then ... end

modules/profiler/lastCapture

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.

local last = profiler.lastCapture()

modules/profiler/measure<T...>

measure<T...>(name: string, fn: () -> T...): T...

Run a function inside a profiling block. Equivalent to a begin/finish pair but handles errors correctly. Returns the function's return values.

Parameters

  • name string — Block name.
  • fn () -> T... — Function to profile.
local count = profiler.measure("walk", function() return walk() end)

modules/profiler/retro

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 retro tool renders the agent-facing report. Latency-immune: the data is historical.

Parameters

  • seconds number? (optional) — How many seconds back to include (default: whole ring).
  • label string? (optional) — Capture label to store under (default "retro").
local agg = profiler.retro(8, "collapse")

modules/profiler/ringStatus

ringStatus(): string

Ring buffer status as a JSON string: { enabled, frames, capacity, span_seconds }.

local s = profiler.ringStatus()

modules/profiler/startCapture

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

  • label string? (optional) — Capture label (default "capture").
if profiler.startCapture("frame-spike") then ... end

modules/profiler/stats

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

  • pattern string? (optional) — Filter pattern (e.g. "schedule.", "system.schedule.render.").
for _, s in ipairs(profiler.stats("schedule.*")) do print(s.name, s.avg_frame_ms) end

modules/profiler/stopCapture

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.

local json = profiler.stopCapture()

modules/profiler/unwatch

unwatch()

Disarm the watchdog. Recorded hits are kept for a final profiler.hits().

profiler.unwatch()

modules/profiler/watch

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

  • ceilingMs number — Effective frame-time ceiling in ms.
  • mode string? (optional) — "record" (default) or "pause".
  • excludeAgent boolean? (optional) — Subtract agent cost before comparing (default true).
  • maxHits number? (optional) — Max frames retained in record mode (default 240).
if profiler.watch(50, "pause") then ... end

modules/profiler/watchStatus

watchStatus(): string

Watchdog status as a JSON string: { armed, ceiling_ms, mode, exclude_agent, hits, dropped_hits, tripped }.

local s = profiler.watchStatus()

modules/prototype_lifecycle/README

require("@builtin/modules/api/engine/prototype_lifecycle") -- prototype_lifecycle

The play-mode invariant for authored player prototypes and EditorOnly entities. In play a PlayerPrototype subtree is the clone source, not a live scene entity: the scene loader spawns it only in edit and the spawn-on-join flow instantiates a clone per joining user in play. This module deactivates and hides any prototype still live when play begins (the fallback when the loader spawned one) and every EditorOnly entity, so neither is simulated nor rendered in play. In edit both are fully live. Runtime clones are tracked here and despawned on the return to edit. Deactivating and hiding a root cascades to its subtree through the hierarchy, so operating on the root covers its children.

Usage: local prototype_lifecycle = require("@builtin/modules/api/engine/prototype_lifecycle")

modules/prototype_lifecycle/activatePrototypes

activatePrototypes()

Reactivate and unhide every prototype root and EditorOnly entity.

modules/prototype_lifecycle/deactivatePrototypes

deactivatePrototypes()

Deactivate and hide every prototype root and EditorOnly entity so play mode neither simulates nor renders them.

modules/prototype_lifecycle/despawnClones

despawnClones()

Despawn every still-existing tracked clone and clear the tracking list.

modules/prototype_lifecycle/enterEdit

enterEdit()

Enter edit mode: despawn runtime clones, then reactivate prototypes and EditorOnly entities.

modules/prototype_lifecycle/enterPlay

enterPlay()

Enter play mode: deactivate and hide prototypes and EditorOnly entities.

modules/prototype_lifecycle/hideEditorSurface

hideEditorSurface()

Deactivate and hide the EditorOnly authoring surface without touching player-prototype roots. Used when play mode resumes so the surface disappears and gameplay cameras take the viewport back.

modules/prototype_lifecycle/install

install()

Keep the play-mode invariant applied for every flip, in every world. enterPlay / enterEdit are what make PrototypeOnly and EditorOnly mean something at runtime, and until something calls them on the flip a template stays live: its camera competes for the viewport with the camera of the player cloned from it, carries no follow target, and holds the shot at the spawn point; its body answers the same input as a second character.

Registered from the prelude beside the other engine installs rather than from a scene-load path — a load that does not run leaves the invariant unapplied with nothing reporting it, and a flip that reloads no scene never reaches a loader hook at all. Idempotent: a second call registers nothing, and the current mode is applied once on install so a world opened straight into play does not start with its templates live.

PrototypeLifecycle.install()

modules/prototype_lifecycle/prototypeRoots

prototypeRoots(): { string }

Ids of every active-layer entity carrying the PlayerPrototype component.

modules/prototype_lifecycle/registerClone

registerClone(id: string)

Track a runtime clone root so it can be despawned on the return to edit.

Parameters

  • id string — The clone's root entity id.

modules/prototype_lifecycle/showEditorSurface

showEditorSurface()

Reactivate and unhide the EditorOnly authoring surface (free camera + editor-only visualizers) without touching player-prototype roots. Used when play mode is paused so the editor camera returns over the frozen world.

modules/proxyOcclusion/README

require("@builtin/systems/proxyOcclusion/proxyOcclusion") -- proxyOcclusion

Analytic occlusion from coarse proxy shapes — grounding shadow for subjects a shadow map does not reach, at a cost that scales with the number of proxies rather than with scene geometry.

Usage: local proxyOcclusion = require("@builtin/systems/proxyOcclusion/proxyOcclusion")

modules/proxyOcclusion/active

active(): boolean

Whether the occlusion pass is currently running.

if proxyOcclusion.active() then print("occluding") end

modules/proxyOcclusion/buffers

buffers(): { [string]: any }?

The buffer(s) this system's passes read. A pass binds what this hands it, so it has the values this module packed.

local b = <module>.buffers()

modules/proxyOcclusion/clear

clear()

Drop every proxy and release the pass. The settings are kept.

proxyOcclusion.clear()

modules/proxyOcclusion/configure

configure(opts: ProxyOcclusionOpts?): ProxyOcclusionState

Adjust how the occlusion is applied. Any omitted field keeps its current value. An intensity of 0 releases the pass.

Parameters

  • opts ProxyOcclusionOpts? (optional) — Settings — see ProxyOcclusionOpts.
proxyOcclusion.configure({ intensity = 0.8, minDistance = 40 })

modules/proxyOcclusion/count

count(): number

How many proxies are registered.

print(proxyOcclusion.count())

modules/proxyOcclusion/remove

remove(key: string): boolean

Remove the proxy registered under key.

Parameters

  • key string — The identifier the proxy was registered with.
proxyOcclusion.remove("boulder")

modules/proxyOcclusion/set

set(key: string, shape: ProxyShape): number

Add or replace a proxy under key. Re-submitting the same key moves that proxy rather than adding another, which is what lets a component push its shape every frame as its entity moves.

Parameters

  • key string — Stable identifier for this proxy — an entity id works well.
  • shape ProxyShape — The capsule — see ProxyShape.
proxyOcclusion.set("boulder", { a = { 0, 1, 0 }, radius = 2 })

modules/proxyOcclusion/settings

settings(): ProxyOcclusionState

The settings currently in force.

local i = proxyOcclusion.settings().intensity

modules/radianceCache/README

require("@builtin/systems/radianceCache/radianceCache") -- radianceCache

A world-space store of lighting results that survives the frame that produced it. A lighting technique that gathers per pixel throws its answer away when the frame ends and pays for it again the next one; a technique that writes into a cache pays once per patch of world and reuses the answer for every later frame and every pixel standing on that patch. The cache is a fixed table of slots addressed by hashing a world-space cell, so it covers an unbounded world in bounded memory: capacity slots at 68 bytes each, and nothing about the size of the scene changes that. Cells coarsen with distance from the camera, and a slot no pixel has asked for in maxAge frames is reclaimed, so what the table holds tracks the view rather than accumulating everything ever seen. Each cache is created by name and owns its own table, so two techniques caching in the same frame do not share slots. Either drive the built-in screen-space producer with run, or enqueue your own producing pass over bindings() and let this own the table, the addressing and the eviction.

Usage: local radianceCache = require("@builtin/systems/radianceCache/radianceCache")

modules/radianceCache/allocate

allocate(self: Cache, ctx: any, opts: { phase: string?, order: number? }?)

Claim a slot for every patch of world the frame is looking at, and keep the ones already held alive. Every technique using the cache runs this first: it is what decides what the table holds, and a producing pass can only fill patches this has stamped.

Parameters

  • self Cache
  • ctx any (optional) — The render context the calling feature received.
  • opts { phase: string?, order: number? }? (optional){ phase, order } — where the pass runs.
cache:allocate(ctx, { phase = "afterLighting", order = 40 })

modules/radianceCache/bindings

bindings(self: Cache): { [string]: any }

The buffers a pass binds to reach this cache. Hand them to ctx.enqueue's buffers and #include "@builtin::systems.radianceCache.radiance_cache" in the shader — that is the whole contract for a technique that wants to produce into this cache or read out of it with its own pass.

Parameters

  • self Cache
ctx.enqueue { kind = "compute", program = mine, buffers = cache:bindings(), ... }

modules/radianceCache/create

create(name: string, opts: CacheOpts?): Cache

Create a cache that owns its own table. name keys its buffers and its resolve target, so two techniques caching in the same frame each pass their own name and never share slots.

Parameters

  • name string — Identifies this cache's resources. Unique per technique.
  • opts CacheOpts? (optional) — How the cache is sized and how it fills — see CacheOpts.
local c = radianceCache.create("indirect", { capacity = 65536, stride = 8 })

modules/radianceCache/destroy

destroy(self: Cache)

Release the table, the read-back and the resolve target. The cache rebuilds — empty — on its next use.

Parameters

  • self Cache
cache:destroy()

modules/radianceCache/gather

gather(self: Cache, ctx: any, opts: { phase: string?, order: number? }?)

Gather light into the slots this frame is scheduled to visit, from the scene that was just drawn. The built-in producer: it needs no acceleration structure and no bake, so it runs wherever the deferred path does. A technique with its own way of computing radiance enqueues that instead and skips this.

Parameters

  • self Cache
  • ctx any (optional) — The render context the calling feature received.
  • opts { phase: string?, order: number? }? (optional){ phase, order } — where the pass runs.
cache:gather(ctx, { phase = "afterLighting", order = 41 })

modules/radianceCache/get

get(self: Cache): CacheState

This cache's settings.

Parameters

  • self Cache
local n = cache:get().capacity

modules/radianceCache/invalidate

invalidate(self: Cache)

Declare that the lighting which produced what is in the cache is gone. Every slot takes its next gather whole instead of averaging it into light that no longer exists, so the cache is rebuilt within stride frames rather than over history * stride. Call it after moving, recolouring or switching off a light.

Parameters

  • self Cache
lighting.setSunIntensity(0); cache:invalidate()

modules/radianceCache/memoryBytes

memoryBytes(self: Cache): number

What this cache's TABLE costs, in bytes: capacity slots of payload plus one key word each. Fixed at creation and independent of the scene — the resolve target the cache also owns is screen-sized and scales with the viewport instead.

Parameters

  • self Cache
print(cache:memoryBytes() // 1024, "KiB")

modules/radianceCache/resolve

resolve(self: Cache, ctx: any, opts: { phase: string?, order: number? }?): string

Read the cache back out per pixel and answer the guid of the texture holding it. The light in it arrived over however many frames have visited the patches on screen, so a pixel drawn for the first time still gets the converged answer.

Parameters

  • self Cache
  • ctx any (optional) — The render context the calling feature received.
  • opts { phase: string?, order: number? }? (optional){ phase, order } — where the pass runs.
local gi = cache:resolve(ctx, { phase = "afterLighting", order = 42 })

modules/radianceCache/run

run(self: Cache, ctx: any, opts: { phase: string?, order: number? }?): string

Allocate, gather and resolve in three consecutive slots — the whole cache for a technique that wants the built-in screen-space producer.

Parameters

  • self Cache
  • ctx any (optional) — The render context the calling feature received.
  • opts { phase: string?, order: number? }? (optional){ phase, order }order is the first of three consecutive slots.
local gi = cache:run(ctx, { phase = "afterLighting", order = 40 })

modules/radianceCache/set

set(self: Cache, opts: CacheOpts?): CacheState

Change this cache's settings. Any omitted field keeps its current value. capacity is fixed at creation — a table cannot be resized under the patches already in it — so changing it is refused rather than silently ignored.

Parameters

  • self Cache
  • opts CacheOpts? (optional) — The settings to change — see CacheOpts.
cache:set({ stride = 4, samples = 16 })

modules/radianceCache/stats

stats(self: Cache): CacheStats?

What the cache did on the most recent frame a read-back has landed for, or nil before the first one arrives. Counted on the GPU by the passes.

Parameters

  • self Cache
local s = cache:stats(); print(s.live, s.dropped)

modules/range_bounds/README

range_bounds

The range field-constraint validator: a constrained value must be a finite number inside the interval the field declared. A rejection names the interval, so the error carries the range the caller may write in. Registers itself with the generic field_constraints registry on load. nil passes, so a ranged field may be left unset.

modules/referenceView/README

require("@builtin/systems/pathTracing/referenceView") -- referenceView

A progressive path-traced view of the scene through the active camera, accumulating samples while the view holds still, so the real-time image has a converged reference to be judged against.

Usage: local referenceView = require("@builtin/systems/pathTracing/referenceView")

modules/referenceView/__frame

__frame(): { [string]: any }?

Advance one frame of accumulation: read the camera, restart if it has moved, and write the parameter block the trace pass reads. Called by the pathTrace render feature once per frame.

modules/referenceView/active

active(): boolean

Whether the reference view is tracing.

if referenceView.active() then ... end

modules/referenceView/disable

disable()

Stop tracing and release the accumulation buffer and scene snapshot. The render feature's own targets are released with it.

referenceView.disable()

modules/referenceView/enable

enable(opts: ViewOpts?): ({ [string]: any }?, string?)

Start path tracing the active camera. Allocates the accumulation buffer and snapshots the scene's geometry and lights; the image begins converging on the next frame the camera holds still for.

Parameters

  • opts ViewOpts? (optional) — Trace settings — see the fields below. All are optional.
referenceView.enable({ samplesPerFrame = 4, bounces = 5 })

modules/referenceView/refresh

refresh(): (boolean, string?)

Retake the scene snapshot — geometry and lights as they now stand — and begin converging again. What to call after moving, adding or removing anything the tracer must see.

referenceView.refresh()

modules/referenceView/reset

reset()

Throw away every sample gathered so far and begin converging again from the current camera. The camera is watched automatically — this is for a change the view cannot see, such as a light being retuned.

referenceView.reset()

modules/referenceView/settings

settings(): { [string]: any }?

The settings in force, the size of the scene snapshot, and how far the image has converged — samples is the count folded into the displayed image, and converged is true once it has reached maxSamples.

local s = referenceView.settings(); print(s.samples, s.converged)

modules/reflectionProbe/README

require("@builtin/modules/api/engine/reflectionProbe") -- reflectionProbe (also available as global 'reflectionProbe')

Reflection-probe system — multiple proximity-blended reflection probes in a scene. Each probe bakes the scene into its own cube slot from its position; surfaces reflect the probes covering them, gathered highest priority first and blended by proximity within a rank (the renderer's per-fragment probe blend), with the scene's sky under whatever coverage the probes leave. One-liner authoring (reflectionProbe.add) and a one-call bake-everything (reflectionProbe.bakeAll).

Usage: local reflectionProbe = require("@builtin/modules/api/engine/reflectionProbe") Also available as global: reflectionProbe

modules/reflectionProbe/add

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

  • x number — World X.
  • y number — World Y.
  • z number — World Z.
  • opts { [string]: any }? (optional) — Optional { radius = 12, probeId = "...", name = "..." }. probeId is 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 — call bakeAll() once the scene is built (baking is an authoring step).
reflectionProbe.add(0, 3, 0, { radius = 15, probeId = "lobby" })

modules/reflectionProbe/apply

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.

modules/reflectionProbe/bake

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

  • id string — Probe entity id.

modules/reflectionProbe/bakeAll

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

reflectionProbe.bakeAll()

modules/reflectionProbe/count

count(): number

Number of registered probes.

modules/reflectionProbe/ensureSkyFallback

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.

reflectionProbe.ensureSkyFallback()

modules/reflectionProbe/list

list(): { any }

List every registered probe: { { id, slot, radius, priority, asset, position }, ... }.

modules/reflectionProbe/loadBaked

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

  • id string — Probe entity id.

modules/reflectionProbe/register

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

  • id string — Probe entity id.
  • radius number — Influence radius (world units) — surfaces within blend it.
  • key string? (optional) — Optional STABLE asset identity (the probe's probeId). Defaults to id. The baked cube persists at probe_<key>.texture so an authored probe keeps the same asset across reloads even though its runtime entity id changes.

modules/reflectionProbe/setPriority

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

  • id string — Probe entity id.
  • priority number — Blend rank. Defaults to 0 on every probe.
reflectionProbe.setPriority(interiorId, 1)

modules/reflectionProbe/setProxy

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

  • id string — Probe entity id.
  • kind string — "box" (sized by all three half-extents), "sphere" (sized by x), or "none" to sample along the raw reflection vector.
  • x number — Half-extent along X, in world units — the sphere radius for "sphere".
  • y number — Half-extent along Y.
  • z number — Half-extent along Z.
reflectionProbe.setProxy(id, "box", 5, 3, 4)  -- a 10x6x8 room

modules/reflectionProbe/setRadius

setRadius(id: string, radius: number)

Update a probe's influence radius and re-apply.

Parameters

  • id string — Probe entity id.
  • radius number — New influence radius.

modules/reflectionProbe/unregister

unregister(id: string)

Unregister entity id's probe, freeing its cube slot, and re-apply.

Parameters

  • id string — Probe entity id.

modules/renderer/README

require("@builtin/modules/api/engine/renderer") -- renderer (also available as global 'renderer')

GPU-resource factory + CPU codec/store wrappers — the GPU/CPU half of the asset↔resource split. This module (under modules/api/) is the SOLE caller of the internal __mesh / __meshcpu / __meshgpu / __splat / __texture / __texturecpu / __texturegpu / __instancedata FFI; assetType behaviours, components, and every other module call renderer.*, never the __ internals.

Usage: local renderer = require("@builtin/modules/api/engine/renderer") Also available as global: renderer

modules/renderer/anisotropy

anisotropy(): number

The maximum anisotropy material textures are sampled with right now — the requested level clamped to what this device honours.

if renderer.anisotropy() < 4 then ... end

modules/renderer/atmospherics.held

atmospherics.held(): boolean

Whether a hold is standing on the air right now.

if renderer.atmospherics.held() then print("clear air") end

modules/renderer/atmospherics.hold

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

  • share number? (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()

modules/renderer/atmospherics.onChange

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)

modules/renderer/atmospherics.share

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.

local density = state.density * renderer.atmospherics.share()

modules/renderer/blendedBatching

blendedBatching(): boolean

Whether blended neighbours sharing a draw key draw together.

modules/renderer/bounds.clear

bounds.clear(id: string): boolean

Withdraw the box an entity published, so it stops contributing to the entity's reported extent.

Parameters

  • id string — Entity id.
renderer.bounds.clear(id)

modules/renderer/bounds.set

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.

modules/renderer/captureView.channelId

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

  • name string — The view name.
ctx.enqueue { ..., debugChannel = renderer.captureView.channelId("lightmap") }

modules/renderer/captureView.list

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.

for _, v in ipairs(renderer.captureView.list()) do ... end

modules/renderer/captureView.ready

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

  • name string — The view name.
repeat task.wait() until renderer.captureView.ready("zfighting")

modules/renderer/captureView.register

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

  • name string — The view name, selected via capture pass=<name>.
  • config any (optional){ description?, ensure?, warmup?, renderLayers? }. ensure is called before a capture of this view so the feature that draws it is live (e.g. create it on demand). warmup is 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. renderLayers is 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.
local ch = renderer.captureView.register("lightmap", { description = "...", ensure = fn })

modules/renderer/captureView.resolve

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

  • name string — The view name.
local v = renderer.captureView.resolve("lightmap")

modules/renderer/captureView.unregister

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

  • name string — The view name.
renderer.captureView.unregister("lightmap")

modules/renderer/clearShadowHero

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.

renderer.clearShadowHero()

modules/renderer/clearShadowProxy

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

  • mesh string? (optional) — The mesh to stop proxying. Omit to clear all of them.
renderer.clearShadowProxy(statueMesh)
print(renderer.clearShadowProxy(), "proxies dropped")

modules/renderer/collect

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.

local c = renderer.collect() print(c.released.texture, c.kept)

modules/renderer/compiledShaders

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.

for _, name in renderer.compiledShaders() do print(name) end

modules/renderer/compiledSource

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

  • shader string — Any name a shader compiled under — identity, guid, alias, or a shaderVariants() program name.
local status = asset.resolve("myShader", "shader"):compileStatus()
if status.status == "failed" then
print(status.error)
print(renderer.compiledSource("myShader"))
end

modules/renderer/compositeSize

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.

local c = renderer.compositeSize()

modules/renderer/cullStats

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().

local s = renderer.cullStats(); print(s.visible - s.occlusionCulled, "drawn")

modules/renderer/debugPass.builtins

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, …).

for _, n in ipairs(renderer.debugPass.builtins()) do ... end

modules/renderer/debugPass.channel

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

  • name string — A debug-pass name (e.g. "normal", "depth", "lightmap").
local ch = renderer.debugPass.channel("normal")   -- 7

modules/renderer/debugPass.list

debugPass.list(): { string }

Every selectable debug-pass name: the built-in passes plus every registered content capture view. What a debug-pass selector offers.

local passes = renderer.debugPass.list()

modules/renderer/debugPass.name

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

  • channel number — The channel number.
local name = renderer.debugPass.name(7)   -- "normal"

modules/renderer/depthPrepass

depthPrepass(): boolean

Whether the opaque depth pre-pass is currently enabled.

modules/renderer/depthPrepassOrder

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.

local o = renderer.depthPrepassOrder()  -- o.reordered > 0 → it sorted

modules/renderer/depthPrepassOrdering

depthPrepassOrdering(): boolean

Whether the depth pre-pass is submitted nearest-first.

modules/renderer/destroy

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

  • handleOrKind any (optional) — A MeshHandle, TextureHandle, MaterialHandle or feature handle; the id itself; or the kind ("texture", "material", "mesh", "feature") with the id as the second argument.
  • id string? (optional) — The guid or registry key, when the first argument is a kind.
renderer.destroy(meshHandle); renderer.destroy(materialHandle)
renderer.destroy(renderer.texture.list()[1].guid)
renderer.destroy("mesh", guid)

modules/renderer/deviceGeneration

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.

local generation = renderer.deviceGeneration()
engine.onDeviceRebuilt(function(g) print("device " .. g) end)

modules/renderer/deviceState

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.

if renderer.deviceState() == "rebuilding" then return end

modules/renderer/drawDiagnostics

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.

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

modules/renderer/drawStats

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.

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

modules/renderer/feature.create

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

  • ref any (optional) — An AssetRef<renderFeature>, or a string identity/guid resolved via asset.resolve(ref, "renderFeature").
  • guid string? (optional) — Optional explicit handle guid (minted when omitted).
local h = renderer.feature.create(asset.resolve("acrylic", "renderFeature"))
local h = renderer.feature.create("acrylic")

modules/renderer/feature.destroy

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

  • handleOrGuid any (optional) — A RenderFeatureHandle or its guid string.
renderer.feature.destroy("eb623975-d8bd-47a1-a0a4-5c0e56238e2f")

modules/renderer/feature.list

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

for _, f in renderer.feature.list() do print(f.identity, f.guid) end

modules/renderer/feature.shaded

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.

local px = renderer.feature.shaded(); for name, n in pairs(px) do print(name, n) end

modules/renderer/featureTexture.configure

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

  • width number — Layer width in pixels.
  • height number — Layer height in pixels.
  • layers number — How many layers the array holds.
renderer.featureTexture.configure(512, 512, 4)

modules/renderer/featureTexture.setLayer

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

  • layer number — Which layer of the array to write into.
  • textureKey string — The source texture's name — the one it was created under. A compute.createStorageTexture2D target, a compute.createTextureHistory pair (its current side), and a texture a compute.copyBufferToTexture wrote all answer to the name they were given.
  • x number — Left edge of the destination rectangle, in pixels.
  • y number — Top edge of the destination rectangle, in pixels.
compute.createStorageTexture2D("gobo", { width = 256, height = 256, format = "rgba16f" })
renderer.featureTexture.setLayer(0, "gobo", 0, 0)

modules/renderer/featureTexture.state

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.

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

modules/renderer/framePacing

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.

local p = renderer.framePacing()
print(("%d of %d frames in flight, paced by %s"):format(p.framesInFlight, p.maxFramesInFlight, p.mechanism))

modules/renderer/getRaytrace

getRaytrace(): boolean

Whether ray tracing is currently enabled.

modules/renderer/gpuMemory

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 to categorised — are the renderer's own accounting of what it asked for on purpose. Always present, on every backend.
  • allocator is the device allocator's ledger, with a row per creation label largest first, which is what names an allocation no category claims. It exceeds categorised by 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, so reservedBytes runs above allocatedBytes by what those blocks hold unused; blocks lists them emptiest first with the labels that keep each one alive, and emptyBytes plus slackBytes is that distance exactly — the pool held in empty blocks, and the room pinned inside blocks something still sits in.
  • driver.deviceLocalBytes is 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 reports available = false and the reason.
  • driver.outsideAllocatorBytes is 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 and renderer.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.

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

modules/renderer/hold

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

  • handleOrKind any (optional) — The resource's handle, or its kind ("texture", "material", "mesh", "feature") with the guid or key as the second argument.
  • id string? (optional) — The guid or key, when the first argument is a kind.
renderer.hold(tex)
renderer.hold("material", "swatch")

modules/renderer/instanceData.clear

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

  • target string | entityRef — The entity — a proxy from entity(...) / entity.spawn(...), or an entity-id string.
renderer.instanceData.clear(subject)

modules/renderer/instanceData.laneCount

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.

for lane = 0, renderer.instanceData.laneCount() - 1 do
renderer.instanceData.set(subject, lane, 0)
end

modules/renderer/instanceData.set

instanceData.set(

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.

modules/renderer/loseDevice

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

modules/renderer/mainCameraView

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.

modules/renderer/material.animatedTexture

material.animatedTexture(

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.

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)

modules/renderer/material.create

material.create(content: MaterialContent, key: string): MaterialHandle

Parameters

  • content MaterialContent
  • key string

modules/renderer/material.describe

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

  • key string | { [string]: any } | AssetRef — The material's registry key, the MaterialHandle from renderer.material.create, or an AssetRef from asset.resolve.

modules/renderer/material.destroy

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

  • key string | { [string]: any } | AssetRef — The material's registry key (the one passed to create), the MaterialHandle create returned, or an AssetRef from asset.resolve.
renderer.material.destroy("__preview_swatch_" .. texGuid)

modules/renderer/material.list

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.

for _, m in ipairs(renderer.material.list()) do print(m.guid, m.shader) end

modules/renderer/material.renderState

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

  • key string | { [string]: any } | AssetRef — The material's registry key, the MaterialHandle from renderer.material.create, or an AssetRef from asset.resolve.
local r = renderer.material.renderState("water"); print(r.renderState.blend, r.keyBuilt)

modules/renderer/material.sessionKeyFor

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

  • entityId string — The entity carrying the material.
local key = renderer.material.sessionKeyFor(entityId)

modules/renderer/material.setProperty

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

  • key string | { [string]: any } | AssetRef — The material's registry key, the MaterialHandle from renderer.material.create, or an AssetRef from asset.resolve.
  • name string — Property name.
  • value any (optional) — New value.
renderer.material.setProperty(asset.resolve("wall", "material"), "roughness", 0.2)

modules/renderer/material.setTexture

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

  • key string | { [string]: any } | AssetRef — The material's registry key, the MaterialHandle from renderer.material.create, or an AssetRef from asset.resolve.
  • slot string — Texture slot name ("base_color_texture", …).
  • ref string | { [string]: any } | AssetRef — Texture reference — a .texture guid / identity / name / path, the image path it was imported from, a color: / default: form, a live GPU handle, or a texture AssetRef carrying one. An asset reference is materialised (Disk→CPU→GPU) and bound by the key the upload lands under.
renderer.material.setTexture("sky", "sky_texture", "panorama.texture")

modules/renderer/materialCost

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.

for _, m in renderer.materialCost() do print(m.material, m.draws, m.binds, m.bindsElided) end

modules/renderer/materialIdentity

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.

local id = renderer.materialIdentity()
for _, r in id.renderables do print(r.entity, r.slot, r.index, r.material) end

modules/renderer/materialIndex

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

  • name stringstring Material name, as renderer.material.create filed it.
local red = renderer.materialIndex("brick_red")

modules/renderer/maxAnisotropy

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.

local best = renderer.maxAnisotropy()

modules/renderer/maxViewportExtent

maxViewportExtent(): number

The largest number of pixels this device draws a surface at, per axis — the bound renderer.setViewportSize refuses past. 0 before the first frame has drawn.

local bound = renderer.maxViewportExtent()

modules/renderer/mesh.boundsSource

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

  • mesh string | { [string]: any } | AssetRef — The mesh — a MeshHandle, a MeshCpuHandle, a guid, or a mesh AssetRef.
print(renderer.mesh.boundsSource(mesh))

modules/renderer/mesh.buildClusters

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

  • mesh string | { [string]: any } | AssetRef — A mesh loaded into the CPU store (renderer.mesh.loadCpu) — the MeshCpuHandle, a MeshHandle, a guid, or a mesh AssetRef.
local cb = renderer.mesh.buildClusters(cpu)

modules/renderer/mesh.canBuildClusters

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.

if renderer.mesh.canBuildClusters() then ... end

modules/renderer/mesh.clusterBakeBudget

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

  • ms number? (optional) — New per-frame budget in milliseconds, capped at 1000. A value that is not a positive, finite number raises.
renderer.mesh.clusterBakeBudget(2)

modules/renderer/mesh.clusterBakes

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.

print(renderer.mesh.clusterBakes().heldBytes)

modules/renderer/mesh.clusterComponents

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

  • clusterBytes buffer | string — Serialized cluster bytes (binary-safe).
local c = renderer.mesh.clusterComponents(cb)

modules/renderer/mesh.clusters

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

  • mesh string | { [string]: any } | AssetRef — The mesh to read — a MeshHandle, a MeshCpuHandle, a guid, or a mesh AssetRef.
local c = renderer.mesh.clusters(gpu) ; if c then print(c.levelCount) end

modules/renderer/mesh.create

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

  • src any (optional) — A MeshCpuHandle, geometry, compute buffers, or a MeshHandle.
  • guid string? (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).
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 } } })

modules/renderer/mesh.decode

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

  • zmsh buffer | string — Engine-native ZMSH bytes (binary-safe).
local geom = renderer.mesh.decode(meshRef:getBytes())

modules/renderer/mesh.destroy

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

  • mesh string | { [string]: any } | AssetRef — The mesh to release — a MeshHandle, a guid, a MeshCpuHandle or a mesh AssetRef.
local m = renderer.mesh.create(cpu); renderer.mesh.destroy(m)
renderer.mesh.destroy(renderer.mesh.list()[1].guid)

modules/renderer/mesh.drawInstanced

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

  • mesh string | { [string]: any } | AssetRef — The mesh the population draws — a MeshHandle, the guid renderer.mesh.list hands out, a MeshCpuHandle or a mesh AssetRef. 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.
  • opts any (optional){ transformBuffer, instanceCount, material, instanceDataBuffer?, reserveCount?, renderLayer?, castsShadows?, mobility? }.
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 })

modules/renderer/mesh.dropClusters

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

  • mesh string | { [string]: any } | AssetRef — The mesh to detach — a MeshHandle, a MeshCpuHandle, a guid, or a mesh AssetRef.
renderer.mesh.dropClusters(gpu)

modules/renderer/mesh.dropInstanced

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

  • draw InstancedDraw — The InstancedDraw to release.
renderer.mesh.dropInstanced(draw)

modules/renderer/mesh.encode

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

  • geom MeshGeometryMeshGeometry — flat per-vertex float / u32 arrays plus optional skinning and skins.
local bytes = renderer.mesh.encode({ positions = {...}, indices = {...} })
local bytes = renderer.mesh.encode(renderer.mesh.geometry(meshHandle))

modules/renderer/mesh.encodeCpu

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

  • mesh string | { [string]: any } | AssetRef — The mesh — a MeshHandle, a MeshCpuHandle, a guid, or a mesh AssetRef.
local bytes = renderer.mesh.encodeCpu(handle)

modules/renderer/mesh.geometry

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

  • mesh string | { [string]: any } | AssetRef — The mesh — a MeshHandle, a MeshCpuHandle, a guid, or a mesh AssetRef.
local geom = renderer.mesh.geometry(renderer.mesh.create({ positions = P, indices = I }))
local tangents = renderer.mesh.geometry(handle).tangents

modules/renderer/mesh.getVertices

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

  • mesh string | { [string]: any } | AssetRef — The mesh — a MeshHandle, a MeshCpuHandle, a guid, or a mesh AssetRef.

modules/renderer/mesh.instanceInfo

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

  • draw InstancedDraw — The InstancedDraw to report on.
print(renderer.mesh.instanceInfo(draw).instanceCount)
local info = renderer.mesh.instanceInfo(draw)
if info.status == "refused" then error(info.error) end

modules/renderer/mesh.instanceTransforms

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

  • draw InstancedDraw — The InstancedDraw whose copies to locate.
local pending = renderer.mesh.instanceTransforms(draw)
while not pending:ready() do task.wait() end
local floats = pending:result()

modules/renderer/mesh.isCpuResident

mesh.isCpuResident(mesh: string | { [string]: any } | AssetRef): boolean

True if this mesh has a resident CPU copy in the guid-keyed CPU store.

Parameters

  • mesh string | { [string]: any } | AssetRef — The mesh — a MeshHandle, a MeshCpuHandle, a guid, or a mesh AssetRef.
if renderer.mesh.isCpuResident(handle) then ... end

modules/renderer/mesh.isResident

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

  • mesh string | { [string]: any } | AssetRef — The mesh — a MeshHandle, a MeshCpuHandle, a guid, or a mesh AssetRef.
print(renderer.mesh.isResident(handle))

modules/renderer/mesh.list

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.

for _, m in ipairs(renderer.mesh.list()) do print(m.guid, m.bytes) end

modules/renderer/mesh.listInstanced

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.

for _, pop in ipairs(renderer.mesh.listInstanced()) do
renderer.mesh.dropInstanced(pop.draw)
end

modules/renderer/mesh.loadCpu

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

  • ref string | AssetRef — A mesh AssetRef (carries .guid and reads its primary via getBytes), or any string asset.ref resolves to one — the guid encodeCpu takes, an identity, a name or a source path.
local cpu = meshRef:load(); local gpu = renderer.mesh.create(cpu); cpu:unload()
local cpu = renderer.mesh.loadCpu(gpuMesh.guid)

modules/renderer/mesh.morphTargets

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

  • mesh string | { [string]: any } | AssetRef — The mesh — a MeshHandle, a MeshCpuHandle, a guid, or a mesh AssetRef.
for i, name in renderer.mesh.morphTargets(meshRef) do print(i, name) end

modules/renderer/mesh.morphWeights

mesh.morphWeights(

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.

local w = renderer.mesh.morphWeights(meshRef, { Eyes_Blink = 1, Mouth_Smile = 0.4 })
ecs.set(face, ecs.MorphWeights { weights = w })

modules/renderer/mesh.readback

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

  • mesh string | { [string]: any } | AssetRef — The mesh — the MeshHandle renderer.mesh.create returned, a guid, or a mesh AssetRef.
local cpu = renderer.mesh.readback(handle); local zmsh = cpu:encode(); cpu:unload()

modules/renderer/mesh.readbackPosed

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, MeshHandle or mesh AssetRef).
local posed = renderer.mesh.readbackPosed({ { entity = id, mesh = ecs.get(id, ecs.Mesh).mesh } })
local tris = posed[id]:getTriangles()

modules/renderer/mesh.scheduleClusters

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

  • mesh string | { [string]: any } | AssetRef — A mesh loaded into the CPU store (renderer.mesh.loadCpu) — the MeshCpuHandle, a MeshHandle, a guid, or a mesh AssetRef.
renderer.mesh.scheduleClusters(cpu) ; cpu:unload()

modules/renderer/mesh.setInstanceCount

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

  • draw InstancedDraw — The InstancedDraw to reconfigure.
  • count number — Instances to draw, at least 1 and within the reservation.
renderer.mesh.setInstanceCount(draw, visibleCount)

modules/renderer/mesh.setInstanceRenderLayer

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

  • draw InstancedDraw — The InstancedDraw to reconfigure.
  • renderLayer number — The membership bitmask, the same value drawInstanced takes as renderLayer. At least one bit must be set.
renderer.mesh.setInstanceRenderLayer(draw, mask)

modules/renderer/mesh.setVertices

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

  • mesh string | { [string]: any } | AssetRef — The mesh — a MeshHandle, a MeshCpuHandle, a guid, or a mesh AssetRef.
  • 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

modules/renderer/mesh.unloadCpu

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

  • mesh string | { [string]: any } | AssetRef — The mesh — a MeshHandle, a MeshCpuHandle, a guid, or a mesh AssetRef.

modules/renderer/mesh.update

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

  • mesh string | { [string]: any } | AssetRef — The mesh to update — a MeshHandle, a guid, a MeshCpuHandle or a mesh AssetRef.
  • src any (optional) — New geometry {positions, indices, ...} or compute buffers {vertexBuffer, indexBuffer, vertexCount, indexCount, prevVertexBuffer?}.

modules/renderer/mesh.uploadClusters

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

  • mesh string | { [string]: any } | AssetRef — The mesh the clusters belong to — a MeshHandle, a MeshCpuHandle, a guid, or a mesh AssetRef.
  • clusters string — Serialized cluster bytes (binary-safe).
renderer.mesh.uploadClusters(gpu, cb)

modules/renderer/minScreenSize

minScreenSize(): number

The on-screen radius, in pixels, an object must reach to be drawn. 0 while the cutoff is off.

local px = renderer.minScreenSize()

modules/renderer/morphStats

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.

local m = renderer.morphStats()
print(("%d instances carrying %d blends over %d meshes"):format(m.instances, m.blends, m.meshes))

modules/renderer/observe

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.

local o = renderer.observe(); print(o.placeholderDraws, "renderables drew the placeholder in frame", o.frame)

modules/renderer/occlusionCulling

occlusionCulling(): boolean

Whether occlusion culling is currently enabled.

modules/renderer/passSchedule

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.

local s = renderer.passSchedule()
for _, v in s.violations do print(v.message) end

modules/renderer/pipelineCache

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.

local c = renderer.pipelineCache()
print(("%d pipelines in %.1f ms, %d bytes restored"):format(c.pipelinesBuilt, c.buildMs, c.restoredBytes))

modules/renderer/pointShadowBudget

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.

local p = renderer.pointShadowBudget()
print(("%d shadowed point lights, %.1f MiB"):format(p.slots, p.bytes / 1024 / 1024))

modules/renderer/projectionOffset

projectionOffset(): (number, number)

The sub-pixel projection offset in force for the main camera, in NDC.

local ox, oy = renderer.projectionOffset()

modules/renderer/raycast

raycast(

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.

local hit = renderer.raycast({ x, 100, z }, { 0, -1, 0 }, 200)
if hit then camera.position = { x, hit.point.y + 1.7, z } end

modules/renderer/raycastAll

raycastAll(

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.

for _, hit in renderer.raycastAll(eye, look, 500) do print(hit.entityId, hit.distance) end

modules/renderer/raytraceCapability

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.

if renderer.raytraceCapability() == "hardware" then ... end

modules/renderer/raytraceStats

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.

local before = renderer.raytraceStats().trianglesRebuilt

modules/renderer/references

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

  • handleOrKind any (optional) — The resource's handle, or its kind with the id second.
  • id string? (optional) — The guid or key, when the first argument is a kind.
local s = renderer.references(mat) for _, r in s.references do print(r.by, r.id) end

modules/renderer/reflectionEnvironment

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().

local env = renderer.reflectionEnvironment()
print(("%d probes, sky fallback %s"):format(env.probes, tostring(env.sky)))

modules/renderer/release

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

  • handleOrKind any (optional) — The resource's handle, or its kind with the id second.
  • id string? (optional) — The guid or key, when the first argument is a kind.
renderer.release(tex)

modules/renderer/renderTargetLimits

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.

local lim = renderer.renderTargetLimits()
local w = math.min(want, lim.maxSquare)

modules/renderer/renderTargets

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.

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

modules/renderer/resolutionScale

resolutionScale(): number

The fraction of the display resolution the scene is currently rendered at. 1 until something sets it.

local s = renderer.resolutionScale()

modules/renderer/setAnisotropy

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

  • level number — One of 1, 2, 4, 8, 16. Any other value is an error.
renderer.setAnisotropy(16)

modules/renderer/setBlendedBatching

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

  • enabled booleanboolean
renderer.setBlendedBatching(false)  -- a draw per blended renderable

modules/renderer/setDepthPrepass

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

  • enabled booleanboolean
renderer.setDepthPrepass(false) -- shade every layer, for comparison

modules/renderer/setDepthPrepassOrdering

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

  • enabled booleanboolean
renderer.setDepthPrepassOrdering(false) -- submit in registration order

modules/renderer/setGpuMemoryTracking

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

  • frames number? (optional)number? Frames between readings; 0 turns the sampler off. Omit to read the interval without changing it.
renderer.setGpuMemoryTracking(60)
local was = renderer.setGpuMemoryTracking()
renderer.setGpuMemoryTracking(1)
-- ... take a tight reading ...
renderer.setGpuMemoryTracking(was)

modules/renderer/setMaxFramesInFlight

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

  • frames number — number Frames of GPU work that may be outstanding, 1 through 8.
renderer.setMaxFramesInFlight(1) -- lowest latency
print(renderer.setMaxFramesInFlight(99), "was clamped")

modules/renderer/setMinScreenSize

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

  • pixels numbernumber — smallest on-screen radius still drawn; 0 disables.
renderer.setMinScreenSize(4) -- drop anything under 4 px of radius
print(renderer.drawStats().compactedDrawn, "instances survived it")

modules/renderer/setOcclusionCulling

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

  • enabled booleanboolean
renderer.setOcclusionCulling(true); print(renderer.cullStats().occlusionCulled)

modules/renderer/setPointShadowBudget

setPointShadowBudget(cfg: {

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.

renderer.setPointShadowBudget({ megabytes = 96 })
renderer.setPointShadowBudget({ resolution = 1024, megabytes = 96 })

modules/renderer/setPresentMode

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

  • mode string — string One of "fifo", "fifo_relaxed", "mailbox", "immediate", "auto_vsync", "auto_no_vsync".
renderer.setPresentMode("mailbox")
print(renderer.framePacing().presentMode, "is what the surface took")

modules/renderer/setProjectionOffset

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

  • x number — Horizontal offset in NDC. One pixel is 2.0 / width.
  • y number — Vertical offset in NDC. One pixel is 2.0 / height.
renderer.setProjectionOffset(0.5 * 2 / w, -0.25 * 2 / h)

modules/renderer/setRaytrace

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

  • enabled booleanboolean
renderer.setRaytrace(true); renderer.feature.create("rt_shadows")

modules/renderer/setResolutionScale

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

  • scale numbernumber — fraction of the display resolution, clamped to [0.25, 1].
renderer.setResolutionScale(0.7)

modules/renderer/setShadowCaching

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

  • enabled booleanboolean
renderer.setShadowCaching(false)  -- draw every shadow view, every frame

modules/renderer/setShadowCasterBatching

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

  • enabled booleanboolean
renderer.setShadowCasterBatching(false)  -- draw the runs the scene presents

modules/renderer/setShadowCasterCutoff

setShadowCasterCutoff(cfg: {

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.

renderer.setShadowCasterCutoff({ minRadiusPx = 2 })
renderer.setShadowCasterCutoff({ minRadiusPx = 1.5, maxDistance = 120 })

modules/renderer/setShadowConfig

setShadowConfig(cfg: {

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.

renderer.setShadowConfig({ softness = 0.65, fadeFraction = 0.28 })
renderer.setShadowConfig({ cascades = 4, resolution = 2048, distance = 240 })

modules/renderer/setShadowHero

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

  • entity string — The entity whose renderables the view is fit around.
  • padding number? (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.
renderer.setShadowHero(player.id)
print(("hero shadow: %.1f texels/unit vs %.1f"):format(
renderer.shadowHero().texelsPerUnit, renderer.shadowHero().cascadeTexelsPerUnit))

modules/renderer/setShadowProxy

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

  • mesh string — The mesh a caster draws, as a guid or any mesh reference.
  • proxy string — The mesh it rasterizes into shadow views instead.
renderer.setShadowProxy(statueMesh, statueHullMesh)
print(renderer.shadowProxies().triangles, "vs", renderer.shadowProxies().sourceTriangles)

modules/renderer/setSkinnedBatching

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

  • enabled booleanboolean
renderer.setSkinnedBatching(false)  -- a draw per skinned instance

modules/renderer/setSkinningPoseHold

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

  • enabled booleanboolean
renderer.setSkinningPoseHold(false)  -- dispatch every pose, every frame

modules/renderer/setSpotShadowBudget

setSpotShadowBudget(cfg: {

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.

renderer.setSpotShadowBudget({ megabytes = 64 })
renderer.setSpotShadowBudget({ resolution = 2048, megabytes = 64 })

modules/renderer/setTextureBudget

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

  • opts TextureBudgetOpts{ megabytes: number?, bias: number? }
renderer.setTextureBudget({ megabytes = 128 })
renderer.setTextureBudget({ megabytes = 128, bias = -1 })  -- one level finer everywhere
renderer.setTextureBudget({ megabytes = 0 })               -- leave residency alone

modules/renderer/setTransmissionShadows

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

  • enabled booleanboolean
renderer.setTransmissionShadows(true)  -- stained glass tints the floor

modules/renderer/setViewportSize

setViewportSize(width: number, height: number): { width: number, height: number }

Draw at this many pixels. The engine's drawing surface is resized to it, and everything measured against that surface follows within a frame: getViewportSize(), ui.screenSize(), the layout every UI screen rebuilds from it, each camera's aspect, and what capture encodes. This is how one session checks a responsive layout at a second shape — a HUD written against ui.screenSize() is re-laid-out at the new size, so an anchored element is drawn where that shape puts it rather than scaled from where the boot size put it.

Who honours the size depends on who owns the surface. A headless engine and a browser canvas own theirs and are resized exactly. Where an OS window owns it, the window manager is asked and has the last word — a tiled or maximized window keeps the size it has. Read renderer.surfaceSize() on a later frame for what was realized.

The new size is in force from the NEXT frame, so read it back on a later call — renderer.surfaceSize() read in the same call still reports the size that call started at.

The logical UI space is normalised to about 1280 points wide, so a resize to a size of the same aspect moves ui.pixelRatio() and leaves ui.screenSize() where it was, while a resize that changes the aspect changes that shape too. Both resize the surface.

A size larger than the device draws is refused naming the bound, which renderer.maxViewportExtent() reports.

Parameters

  • width numbernumber — width in pixels, at least 1.
  • height numbernumber — height in pixels, at least 1.
renderer.setViewportSize(1920, 1080)
task.waitFrames(1); print(renderer.surfaceSize()) -- what was realized

modules/renderer/shaderCache

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.

local c = renderer.shaderCache()
print(("%d hits, %d misses, %.1f ms saved"):format(c.hits, c.misses, c.savedMs))

modules/renderer/shaderCost

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.

local top = renderer.shaderCost()[1]; print(top.shader, top.variants, top.buildMs)

modules/renderer/shaderVariants

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.

for _, s in renderer.shaderVariants() do print(s.shader, #s.variants, s.budget) end

modules/renderer/shadingOf

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

  • subject string | { [string]: any } — The entity — a proxy from entity(...) or an entity-id string — or the material, as its registry key or the MaterialHandle renderer.material.create returned.
local s = renderer.shadingOf(emitter); if s.state ~= "itsMaterial" then print(s.means) end

modules/renderer/shadowCacheStats

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.

local s = renderer.shadowCacheStats()
print(("shadow views: %d drawn, %d cached"):format(s.rendered, s.cached))

modules/renderer/shadowCaching

shadowCaching(): boolean

Whether a shadow view may keep the depth it already holds.

modules/renderer/shadowCasterBatching

shadowCasterBatching(): boolean

Whether a shadow view draws every caster of one mesh together.

modules/renderer/shadowCasterCutoff

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.

local c = renderer.shadowCasterCutoff(); print(c.minRadiusPx, c.maxDistance)

modules/renderer/shadowConfig

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.

print(renderer.shadowConfig().cascades)

modules/renderer/shadowHero

shadowHero(): ShadowHeroReport

The registered hero caster and what the last frame's fit produced. A frame that fit nothing says why in decline.

local h = renderer.shadowHero()
print(h.active, h.zoom, h.decline)

modules/renderer/shadowMemory

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.

local m = renderer.shadowMemory()
print(("shadows: %.1f MiB"):format(m.total / 1024 / 1024))

modules/renderer/shadowProxies

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.

local p = renderer.shadowProxies()
print(("shadow triangles: %d of %d"):format(p.triangles, p.sourceTriangles))

modules/renderer/shadowViews

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.

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

modules/renderer/skinnedBatching

skinnedBatching(): boolean

Whether skinned instances holding one pose draw together.

modules/renderer/skinningPoseHold

skinningPoseHold(): boolean

Whether a pose already written into its slice skips its dispatch.

modules/renderer/skinningStats

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.

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

modules/renderer/splat.components

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

  • bytes any (optional) — Capture bytes — .spz or .ply, as a buffer or a binary string.
  • convention string? (optional) — Source axis convention: "rightDownFront" (the default, what COLMAP-trained captures use) or "engineNative" for a capture already in engine space.
local c = renderer.splat.components(vfs.readBytes("captures/ceramic.spz"))

modules/renderer/spotShadowBudget

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.

local s = renderer.spotShadowBudget()
print(("%d layers of %d, %.1f MiB"):format(s.layers, s.resolution, s.bytes / 1024 / 1024))

modules/renderer/surfaceSize

surfaceSize(): { width: number, height: number }

The whole drawing surface in pixels — the window, the browser canvas or the headless framebuffer. This is the size renderer.setViewportSize sets and the size every UI screen is laid out over, so it is what says whether a resize was realized, on every layout including an editor one whose viewport panel holds a smaller rect than the window. { width = 0, height = 0 } before the first frame has drawn.

local s = renderer.surfaceSize()

modules/renderer/temporal.held

temporal.held(): boolean

Whether a hold is pinning the per-frame clock right now.

if renderer.temporal.held() then print("frame is pinned") end

modules/renderer/temporal.hold

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

  • at number? (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.
  • options TemporalHoldOptions? (optional)owner is 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. exclusive takes 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" })

modules/renderer/temporal.now

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.

local params = { grainTime = renderer.temporal.now() }

modules/renderer/temporal.onChange

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)

modules/renderer/temporal.owner

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.

local who = renderer.temporal.owner()
if who ~= nil and who.exclusive then print(who.name, "holds the clock at", who.at) end

modules/renderer/temporal.release

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

  • owner string — The key the holds to release were taken under — what owner stated when they were taken, which renderer.temporal.owner reports.
renderer.temporal.release("stage-air")

modules/renderer/texture.capture

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

  • texture string | { [string]: any } | AssetRef — The texture to read back — a TextureHandle, a guid, a TextureCpuHandle or a texture AssetRef.
local key = renderer.texture.capture(cameraTarget)

modules/renderer/texture.cpuCreate

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

  • width number — number Canvas width in pixels.
  • height number — number Canvas height in pixels.
  • fill any? (optional) — Optional { r, g, b, a } (0-255) solid fill; defaults to opaque white.
local c = renderer.texture.cpuCreate(1024, 576, { 255, 255, 255, 255 })

modules/renderer/texture.cpuFromBytes

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

  • bytes buffer | string — The ZTEX or image bytes.
  • encodeOpts any? (optional){ format?, srgb?, generateMipmaps?, maxDimension? } applied when the bytes are an encoded image and need the engine-native encode.
local cpu = renderer.texture.cpuFromBytes(vfs.read(path)); local png = cpu:encodePng(); cpu:unload()

modules/renderer/texture.create

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

  • src any (optional) — A TextureCpuHandle, raw pixels, a TextureHandle, or render-target dimensions.
  • guid string? (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.
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" })

modules/renderer/texture.createFromAsset

texture.createFromAsset(

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.

local h = renderer.texture.createFromAsset(texRef) -- bind h.guid on a material; it draws once resident

modules/renderer/texture.decode

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 ZTEX payload — 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. A ZTEX holding 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

  • bytes buffer | string — A ZTEX payload or source image bytes.
local pixels, w, h = renderer.texture.decode(vfs.read("/source/tmp/shot.png"))

modules/renderer/texture.destroy

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

  • texture string | { [string]: any } | AssetRef — The texture to release — a TextureHandle, a guid, a TextureCpuHandle or a texture AssetRef.
renderer.texture.destroy(rt)
renderer.texture.destroy(renderer.texture.list()[1].guid)

modules/renderer/texture.encode

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

  • rgba any (optional) — Pixel payload at opts.format's native byte width — a buffer, a binary string, or a number array.
  • width number — number
  • height number — number
  • opts any (optional){ format?, srgb?, generateMipmaps?, maxDimension? }

modules/renderer/texture.encodeFromImage

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

  • bytes buffer | string — source image bytes.
  • opts any (optional){ format?, srgb?, generateMipmaps?, maxDimension? }

modules/renderer/texture.frameSchedule

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

  • texture string | AssetRef — The texture — a guid, an identity, a name, a path, or a texture AssetRef.
local ends = renderer.texture.frameSchedule(texRef) -- {0.1, 0.15, 0.35}

modules/renderer/texture.info

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

  • ztex buffer | string — ZTEX bytes.
local i = renderer.texture.info(bytes); if i.animated then print(i.layers, "frames", i.durationMs, "ms") end

modules/renderer/texture.isResident

texture.isResident(texture: string | { [string]: any } | AssetRef): boolean

True if a GPU texture is resident under this texture's guid.

Parameters

  • texture string | { [string]: any } | AssetRef — The texture — a TextureHandle, a TextureCpuHandle, a guid, or a texture AssetRef.
print(renderer.texture.isResident(handle))

modules/renderer/texture.list

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.

for _, t in ipairs(renderer.texture.list()) do print(t.guid, t.bytes) end

modules/renderer/texture.loadCpu

texture.loadCpu(

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().

local cpu = texRef:load(); local r,g,b,a = cpu:readPixel(3, 4); cpu:unload()

modules/renderer/texture.readback

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

  • texture string | { [string]: any } | AssetRef — The texture — the TextureHandle renderer.texture.create returned, a guid, or a texture AssetRef.
local cpu = renderer.texture.readback(handle); local ztex = cpu:encode(); cpu:unload()

modules/renderer/texture.tone

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

  • histogram any (optional) — A histogram from cpu:histogram().
local t = cpu:tone(); if t.spread < 24 then error("the shot is flat") end

modules/renderer/texture.update

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

  • texture string | { [string]: any } | AssetRef — The texture to update — a TextureHandle, a guid, a TextureCpuHandle or a texture AssetRef.
  • src any (optional) — New raw pixels {rgba, width, height, srgb?, format?}rgba as a buffer, a binary string, or a number array.

modules/renderer/textureMemory

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.

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

modules/renderer/textureStreaming

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.

local ts = renderer.textureStreaming()
print(("textures: %.1f MiB resident of %.1f MiB demanded, %d starved"):format(
ts.residentBytes / 1048576, ts.demandedBytes / 1048576, ts.starved))

modules/renderer/transmissionShadows

transmissionShadows(): boolean

Whether translucent casters tint the directional light they block.

modules/renderer/uploadStats

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.

local u = renderer.uploadStats()
print(("instance upload: %d B of %d B in %d writes"):format(u.bytes, u.fullBytes, u.writes))

modules/renderer/variantSource

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

  • program string — A program name from renderer.shaderVariants().
local row = renderer.shaderVariants()[1]
local base = renderer.variantSource(row.base.program)

modules/renderer/viewportSize

viewportSize(): { width: number, height: number }

The rect the scene was drawn into in the frame just drawn, in pixels. That is the whole drawing surface in a runtime window, a browser canvas and a headless engine, and the editor's viewport panel — smaller than the surface — in an editor layout. Follows a renderer.setViewportSize, a window drag and a browser page resize alike, so it reports what is realized rather than what was asked for.

local v = renderer.viewportSize()

modules/restirLighting/README

require("@builtin/systems/restirLighting/restirLighting") -- restirLighting

Direct light from many small sources at a cost that does not grow with how many there are. A shading pass that loops every light pays for every light at every pixel, which is why the count has to be capped; resampled importance sampling instead draws a few candidates per pixel, keeps one in proportion to what it would contribute, and carries a weight that makes the survivor stand for the whole set. What a pixel keeps is reused — by its neighbours this frame and by itself on the next one — so the number of candidates each pixel has to draw stays small while the set it effectively samples keeps growing. A cave of glowing crystals or a street of practicals costs what a handful of lights costs. Sources registered here are additional to the lights the engine holds rows for: this lights what it is given, on top of the scene as it was drawn. Register a source with set, drop it with remove, and the pass starts and stops with the registry.

Usage: local restirLighting = require("@builtin/systems/restirLighting/restirLighting")

modules/restirLighting/active

active(): boolean

Whether the resampling pass is currently running.

if restirLighting.active() then print("resampling") end

modules/restirLighting/beginFrame

beginFrame(width: number, height: number): { [string]: any }?

Size the buffers to the frame, advance the frame counter and push the settings. The render feature calls this once per frame before it enqueues the passes; it is what gives the temporal reuse a frame to count and the grid a size.

Parameters

  • width number — Viewport width in pixels.
  • height number — Viewport height in pixels.
local f = restirLighting.beginFrame(ctx.viewport.w, ctx.viewport.h)

modules/restirLighting/capacity

capacity(): number

The most sources the registry holds.

print(restirLighting.capacity())

modules/restirLighting/clear

clear()

Drop every source and release the pass. The settings are kept.

restirLighting.clear()

modules/restirLighting/configure

configure(opts: Settings?): State

Change how the resampling is run. Any omitted field keeps its current value.

Parameters

  • opts Settings? (optional) — The settings to change — see Settings.
restirLighting.configure({ candidates = 16, mode = "reference" })

modules/restirLighting/count

count(): number

How many sources are registered.

print(restirLighting.count())

modules/restirLighting/defaults

defaults(): State

The settings a registry runs under until something changes them. Pass this to configure to put every one of them back.

restirLighting.configure(restirLighting.defaults())

modules/restirLighting/memoryBytes

memoryBytes(): number

What the reservoir grid costs, in bytes: two vec4 per cell, twice over because reuse reads one grid and writes the other.

print(restirLighting.memoryBytes() // 1024, "KiB")

modules/restirLighting/perPixelCost

perPixelCost(): number

How many candidate evaluations a pixel pays for, per frame. It is the answer the whole technique exists to give: fresh candidates plus borrowed neighbours plus the one history, and no term in it is the source count.

print(restirLighting.perPixelCost(), "evaluations regardless of light count")

modules/restirLighting/remove

remove(key: string): boolean

Remove the source registered under key.

Parameters

  • key string — The identifier the source was registered with.
restirLighting.remove("crystal")

modules/restirLighting/set

set(key: string, source: Source): number

Add or replace the source registered under key. Re-submitting the same key moves that source rather than adding another, which is what lets a component push its position every frame as its entity moves.

Parameters

  • key string — Stable identifier — an entity id works well.
  • source Source — Where it is and what it emits — see Source.
restirLighting.set("crystal", { position = { 2, 1, 0 }, color = { 0.4, 0.8, 1 }, intensity = 6 })

modules/restirLighting/settings

settings(): State

The settings currently in force.

local c = restirLighting.settings().candidates

modules/restirLighting/stats

stats(): Stats?

What the resampling did on the most recent frame a read-back has landed for, or nil before the first one arrives.

local s = restirLighting.stats(); print(s.lit, "of", s.cells, "cells lit")

modules/retarget/README

require("@builtin/modules/retarget") -- retarget

Skeletal animation retargeting — map a clip authored on one rig onto another humanoid rig, preserving the target's shape. Pure, readable Luau: the behavior an agent follows and tweaks. bake is the cold, cached transform (clip + source rig + target rig -> a clip in the target's bone space); the hot path just samples the baked clip. plan reports which canonical roles map across the two rigs and which don't.

Usage: local retarget = require("@builtin/modules/retarget")

modules/retarget/animation

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

  • clipRef any (optional) — Animation asset to retarget.
  • targetMeshRef any (optional) — Target rig mesh whose skin defines the destination skeleton.
  • sourceMeshRef any? (optional) — Source rig mesh the clip was authored for; omit to use the clip's embedded rig.
local ok, path = retarget.animation(clipRef, targetMeshRef)

modules/retarget/aux

aux(bakeKey: string)

Read what was cached beside the bake at bakeKey.

Parameters

  • bakeKey string — A key from bakeKey().

modules/retarget/bake

bake(clip, srcRig, tgtRig, opts)

Retarget a decoded clip from its source rig onto a target rig, producing a clip in the TARGET's bone space (channels named for target bones). Per frame it forward-kinematics the source pose on the rig AS AUTHORED (the bind the clip's channel rotations are local to), measures each mapped role's world rotation as a deviation from the source's CANONICAL bind, re-applies that deviation to the target's CANONICAL bind, and converts back to a target-local rotation through the target's ANIMATED parent. The target chain is rebuilt top-down, so error never accumulates down a limb. Both rigs are re-posed to the geometry-derived canonical T-pose (normalizeToTPose) for that deviation, so the result depends only on the T-pose the two rigs share — never on whatever arbitrary pose either was authored in (an A-pose source idle lands the target's arms down, not splayed out at the A-pose offset). The apply path seeds that same canonical rest. Translation routes through the role and is size-scaled by the hip-height ratio; the apply path makes it relative to the target's bind. This is the cold step — bake once per (clip, target rig) and cache (see bakeBytes).

Parameters

  • clip any (optional) — A decoded clip table { name, duration, channels, bone_names } (e.g. json.decode(skeleton.clipDecode(bytes))).
  • srcRig any (optional) — Source rig the clip was authored on (parsed rig / table / JSON).
  • tgtRig any (optional) — Target rig to retarget onto (parsed rig / table / JSON).
  • opts any (optional) — Optional { symmetrize: boolean } forwarded to normalizeToTPose.

modules/retarget/bakeBytes

bakeBytes(clipBytes, srcRig, tgtRig, cacheKey, opts)

Bake from clip BYTES to retargeted clip BYTES — clipDecode -> bake -> clipEncode — with an in-memory cache. Retarget is cold: pass a stable cacheKey (e.g. clip identity + target rig identity) and the second call for the same pair returns the cached bytes. The hot path then just samples the result like any native clip.

Parameters

  • clipBytes any (optional) — The source clip's zanim payload bytes.
  • srcRig any (optional) — Source rig (parsed rig / table / JSON).
  • tgtRig any (optional) — Target rig (parsed rig / table / JSON).
  • cacheKey any (optional) — Optional stable key; when given, the result is cached and reused.
  • opts any (optional) — Optional { symmetrize: boolean } forwarded to normalizeToTPose (absolute vs relative bind correction). Each mode caches separately.

modules/retarget/bakeKey

bakeKey(cacheKey: string, opts): string

The key a bake is stored under, so a caller that derives something FROM a bake can hold it under the same key and have it dropped at the same time. Each correction mode bakes separately, which is what the suffix carries.

Parameters

  • cacheKey string — The stable key passed to bakeBytes.
  • opts any (optional) — The same { symmetrize } passed to bakeBytes.

modules/retarget/clearCache

clearCache(cacheKey: string?)

Drop every cached bake and everything derived from it (or just cacheKey when given). Call after editing a rig's profile so clips re-bake against the corrected mapping.

Parameters

  • cacheKey string? (optional) — Optional single key to evict; omit to clear all. Accepts either the key passed to bakeBytes or an already-composed bakeKey().

modules/retarget/extractRig

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

  • meshBytes buffer | string — Raw ZMSH mesh bytes carrying a skin.
local rig = retarget.extractRig(meshBytes)

modules/retarget/humanoidProfile

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

  • meshBytes buffer | string — Raw ZMSH mesh bytes carrying a skin.
local holder = retarget.humanoidProfile(meshBytes)

modules/retarget/isHumanoid

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

  • meshBytes buffer | string — Raw ZMSH mesh bytes carrying a skin.
if retarget.isHumanoid(meshBytes) then ... end

modules/retarget/loadRig

loadRig(ref)

Resolve a .rig asset and return its parsed, FK-enriched rig (ready for plan / bake). The rig payload is the asset's rig.json.

Parameters

  • ref any (optional) — A .rig asset ref (identity / guid / path / handle).

modules/retarget/normalizeToTPose

normalizeToTPose(rig, opts)

Re-pose a rig's bind to the EXACT canonical T-pose, so a clip's source rig and the avatar it drives share one reference pose. Retarget transfers RELATIVE motion, so any residual bind mismatch (hands rolled the wrong way, feet pointing askew, an A-pose vs a T-pose) shows up as broken hands/feet in the result. Each bone is posed to a canonical world frame for its role — direction AND roll: arms horizontal palms-down, legs straight down, spine up. Only the limb bones whose bind direction differs between an A-pose and a T-pose are aimed; limb ENDPOINTS (hands, feet, toes) and bones with no role keep their authored orientation relative to the re-posed parent (their pose is mesh-defined, so aiming them twists the hand / tips the foot). The whole body is also rigidly de-rotated into an upright, forward-facing frame (handling a baked root rotation) and centered on its sagittal plane. Bind correction is RELATIVE by default — bone offsets/lengths/inverse-bind are untouched, so the rig keeps its own proportions and any authored left/right asymmetry. Pass opts.symmetrize = true for ABSOLUTE correction: left/right bones are mirrored across the sagittal plane for a perfectly symmetric bind, overriding the rig's authored asymmetry/proportions.

Parameters

  • rig any (optional) — The rig to normalize (parsed rig / table / JSON).
  • opts any (optional) — Optional { symmetrize: boolean }. symmetrize=true = absolute correction (force symmetry); default/false = relative (preserve proportions).

modules/retarget/plan

plan(srcRig, tgtRig)

Report how a source rig's clips map onto a target rig: which canonical roles both rigs fill (mapped), which the source has but the target lacks (unmappedSource — those channels are dropped), and which the target has spare (unmappedTarget). Use it to see why a retarget is partial and which bone to hand-map in a rig's profile.

Parameters

  • srcRig any (optional) — Source rig — a parsed rig, a .rig table, or its JSON string.
  • tgtRig any (optional) — Target rig — same forms.

modules/retarget/serializeProfile

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

  • holder HumanoidHolder — A holder from humanoidProfile.
files["humanoid.profile"] = retarget.serializeProfile(holder)

modules/retarget/setAux

setAux(bakeKey: string, value)

Cache a value beside the bake at bakeKey. It is dropped whenever that bake is, so it cannot outlive the bytes it was derived from.

Parameters

  • bakeKey string — A key from bakeKey().
  • value any (optional) — The value to hold.

modules/rt_ao/README

require("@builtin/modules/rt_ao") -- rt_ao

Ray-traced ambient occlusion — the settings the rt_ao render feature runs on, and the lifetime of the pass that draws it. A hemisphere of short rays per pixel is traced against the scene acceleration structure, and the fraction that hits nearby geometry darkens the frame.

Usage: local rt_ao = require("@builtin/modules/rt_ao")

modules/rt_ao/active

active(): boolean

Whether the occlusion pass is running. Read from the live feature registry, so a feature created directly through renderer.feature.create counts the same as one this module started.

if rt_ao.active() then print(rt_ao.stats().raysPerFrame) end

modules/rt_ao/clear

clear()

Turn occlusion off and release the pass. The other settings are kept, so a later set({ strength = ... }) brings back the same look.

rt_ao.clear()

modules/rt_ao/get

get(): RtAoState

The occlusion settings currently in force.

local rays = rt_ao.get().rays

modules/rt_ao/publish

publish(built: { traceWidth: number, traceHeight: number, dispatches: number, traceTarget: string? })

Record the grid the feature allocated and the dispatches it enqueues. The feature calls this as it builds, which is what gives stats the shape of the chain that is running.

Parameters

  • built { traceWidth: number, traceHeight: number, dispatches: number, traceTarget: string? } — The grid and dispatch count the feature just created.
rt_ao.publish({ traceWidth = 960, traceHeight = 540, dispatches = 9, traceTarget = rt.guid })

modules/rt_ao/qualityLevels

qualityLevels(): { [string]: { rays: number, resolution: number } }

What each quality level costs: rays hemisphere rays per pixel, cast from a grid resolution of the frame's own. The rays a frame traces is those two multiplied by the frame's pixels, and the trace pass's cost is linear in it.

local shape = rt_ao.qualityLevels().ultra

modules/rt_ao/set

set(opts: RtAoOpts?): RtAoState

Set how the occlusion is traced. Any omitted field keeps its current value, so a call can move one knob without restating the rest. A strength of 0 turns occlusion off and releases the pass.

Parameters

  • opts RtAoOpts? (optional) — Occlusion settings — see RtAoOpts.
rt_ao.set({ quality = "low", radius = 1.5 })

modules/rt_ao/stats

stats(): RtAoStats

What the running feature built — the grid it traces from, the rays that grid costs each frame, and the dispatches it enqueues. The feature writes these as it allocates its targets, so they describe the pass that exists this frame; traceTarget is that grid as a resource, for a reader that wants to measure it.

local s = rt_ao.stats(); print(s.traceWidth, s.traceHeight, s.raysPerFrame)

modules/runtime_participation/README

require("@builtin/modules/api/engine/runtime_participation") -- runtime_participation

Reads and writes the RuntimeParticipation lifecycle axis for an entity, and answers the save / edit-liveness / play-liveness questions that follow from a mode. One source of truth for the four modes (WorldEntity, PrototypeOnly, EditorOnly, RuntimeOnly) so save and play-mode code agree on what each mode means.

Usage: local runtime_participation = require("@builtin/modules/api/engine/runtime_participation")

modules/runtime_participation/isSaved

isSaved(mode: string): boolean

Whether an entity with this mode is written to the persisted world. True for WorldEntity, PrototypeOnly, and EditorOnly; false for RuntimeOnly.

Parameters

  • mode string — A RuntimeParticipation mode string.

modules/runtime_participation/liveInEdit

liveInEdit(mode: string): boolean

Whether an entity with this mode is live while authoring in edit mode. True for WorldEntity, PrototypeOnly, and EditorOnly; false for RuntimeOnly.

Parameters

  • mode string — A RuntimeParticipation mode string.

modules/runtime_participation/liveInPlay

liveInPlay(mode: string): boolean

Whether an entity with this mode is live during play. True for WorldEntity and RuntimeOnly; false for PrototypeOnly and EditorOnly.

Parameters

  • mode string — A RuntimeParticipation mode string.

modules/runtime_participation/modeOf

modeOf(entityId: string): string

The entity's RuntimeParticipation mode. Defaults to "WorldEntity".

Parameters

  • entityId string — Entity id to read.

modules/runtime_participation/set

set(entityId: string, mode: string)

Sets the RuntimeParticipation mode on an entity. A mode that is not saved marks the entity temporary so the scene-save exclusion drops it.

Parameters

  • entityId string — Entity id to write.
  • mode string — The mode to store.

modules/runtime_participation/standsDown

standsDown(mode: string, engineMode: string): boolean

Whether an entity with this mode stands down — stops rendering and ticking — when an EDITOR session is in engineMode. This is the question a mode flip actually asks, and it is not liveInPlay: that answers which entities a SHIPPED RUNTIME contains, where there is no authoring surface at all. A session able to flip modes is an editor session by construction (the runtime profile forbids mode swaps), so the editor's own cameras, panels and gizmos are present in both of its modes and stand down in neither. What stands down in play is a template, whose clones are what runs; what stands down in edit is a runtime entity.

Parameters

  • mode string — A RuntimeParticipation mode string.
  • engineMode string — The engine mode the session is in, "play" or "edit".
if rp.standsDown(entity(id).participation, tostring(engine.mode)) then ... end

modules/sceneProxy/README

require("@builtin/systems/sceneProxy/sceneProxy") -- sceneProxy

A coarse volumetric stand-in for the scene's geometry: a grid that answers how far the nearest surface is from any point, cheaply enough to ask along a whole ray.

Usage: local sceneProxy = require("@builtin/systems/sceneProxy/sceneProxy")

modules/sceneProxy/build

build(opts: ProxyOpts?): ({ [string]: any }?, string?)

Build the proxy over the scene as it currently stands. Allocates the grid, scatters the scene's triangles into it, and floods the result into a distance field.

Parameters

  • opts ProxyOpts? (optional) — Grid settings — see the fields below. All are optional.
sceneProxy.build({ resolution = 64 })

modules/sceneProxy/building

building(): boolean

Whether a build or refresh is running right now. Both yield while the GPU answers, so this is what a caller checks before starting one of its own over the same scene.

if not sceneProxy.building() then sceneProxy.build() end

modules/sceneProxy/built

built(): boolean

Whether a field is built.

if sceneProxy.built() then ... end

modules/sceneProxy/claim

claim(key: string, opts: TrackOpts?): boolean

Ask for the field to be kept current, on behalf of something that will say when it no longer wants it. Tracking follows the claims that stand: it is on while there is at least one, and the settings in force are those of the claim made most recently. Two scene objects each wanting a proxy therefore share one grid, and neither turns the other's off.

Parameters

  • key string — What is asking — anything that names the claimant, an entity id for a component.
  • opts TrackOpts? (optional) — What to follow and how closely, as track takes them.
sceneProxy.claim(entityId, { resolution = 64 })

modules/sceneProxy/claimed

claimed(key: string): boolean

Whether a claim made under this key still stands. A release from elsewhere — destroy drops every claim there is — is what this answers false after, so something whose presence IS the request for a proxy can make it again.

Parameters

  • key string — The key the claim was made under.
if not sceneProxy.claimed(id) then sceneProxy.claim(id, opts) end

modules/sceneProxy/destroy

destroy()

Release the proxy's grid and its working volumes, and with them every claim on it and any build or refresh still in flight — what that work was making is a grid nothing asked for any more.

sceneProxy.destroy()

modules/sceneProxy/distanceAt

distanceAt(point: { [string]: number }): number?

The world-space distance from point to the nearest surface. Reads the field the way a shader does — interpolated between voxel centres, and far outside the extent the grid covers. Yields while the GPU answers, so call it from a task or execute; a shader samples the field directly instead.

Parameters

  • point { [string]: number }{ x, y, z } world position.
local d = sceneProxy.distanceAt({ x = 0, y = 2, z = 0 })

modules/sceneProxy/gridParams

gridParams(): { number }?

The grid a shader needs to read the field: the two vec4 that scene_proxy.shaderModule's spGridFrom unpacks, as eight floats ready to write into a consuming pass's own parameter buffer.

local p = sceneProxy.gridParams(); myParams:write(p)

modules/sceneProxy/maintain

maintain(): boolean

One maintenance tick: build the field when tracking is armed and nothing is built yet, otherwise walk a slice of the scene and start a refresh once a whole walk has found that the geometry the proxy covers moved. Cheap on a scene standing still, bounded on a big one, and never yields — the work it starts runs as its own task, which is what lets a component update drive it.

function update() sceneProxy.maintain() end

modules/sceneProxy/refresh

refresh(): (boolean, string?)

Rebuild the field from the scene's current geometry, over the grid the proxy already has. What to call after something moves.

sceneProxy.refresh()

modules/sceneProxy/release

release(key: string): boolean

Drop a claim. The grid is released when it was the last one standing; otherwise the claim made most recently before it takes the settings back.

Parameters

  • key string — The key the claim was made under.
sceneProxy.release(entityId)

modules/sceneProxy/sampleAt

sampleAt(points: { any }): ({ { distance: number, normal: { number } } }?, string?)

Ask the field about a batch of world points in one dispatch. Yields while the GPU answers, so call it from a task or execute.

Parameters

  • points { any } — A list of world positions, each { x, y, z } or { X, Y, Z }.
local s = sceneProxy.sampleAt({ { x = 0, y = 2, z = 0 } })

modules/sceneProxy/settings

settings(): { [string]: any }?

The grid in force, the world extent it covers, and what it cost. boundsMin and boundsMax are the corners of the box the grid spans — resolution voxels along each axis from boundsMin, and what a point has to fall inside to have an answer. The geometry the grid was fitted to sits one voxel inside them at each end. gridParams() is what a consuming shader wants — the same numbers in the layout spGridFrom reads.

local s = sceneProxy.settings(); print(s.voxelSize, s.memoryBytes)

modules/sceneProxy/track

track(opts: TrackOpts?): boolean

Keep the field current by itself: from here on the proxy watches the geometry it covers and refreshes when that geometry moved. Returns at once — it records what to follow and leaves the work to maintain, which something has to drive each frame; SceneProxy.component is that driver for an authored scene. A proxy that is not built yet is built by the first tick, at resolution.

Parameters

  • opts TrackOpts? (optional) — What to follow and how closely — see the fields below. All are optional.
sceneProxy.track({ resolution = 64, interval = 0.2 })

modules/sceneProxy/tracking

tracking(): boolean

Whether the proxy is following the scene.

if not sceneProxy.tracking() then sceneProxy.track() end

modules/sceneProxy/trackingStats

trackingStats(): { [string]: any }

What tracking is set to follow, what it has done, and what it costs: the settings in force, how many claims stand, how many entities the last completed walk over the scene looked at and how long the last slice of one took, how many GPU-driven populations the field standing in the grid covers, how many builds, refreshes and refitting rebuilds it has started, how many ticks it stood aside for work already running, and the last error a walk or a refresh reported.

print(sceneProxy.trackingStats().refreshes)

modules/sceneProxy/untrack

untrack()

Stop keeping the field current. The grid stays built and readable; it simply stops following the scene.

sceneProxy.untrack()

modules/scene_instantiable/README

scene_instantiable

The instantiation contract's shared half. An asset type opts into scene instantiation by defining instantiate(self, target?, opts?) on its behaviour ref table. Both halves of that call are shared, so a caller writes the same code against every type. IN — the base opts position, rotation, scale, name, temporary mean the same thing for every type, so their implementation lives here. rotation takes three numbers as pitch/yaw/roll in DEGREES, or four as a quaternion. root stands the root entity (parented, born temporary, named, placed) and place applies the placement opts to a root the type adopted (a bundle exploding onto its target). OUT — every type returns (root, idMap) through result: the composed root as a LIVE EntityRef, and the originalId -> runtimeId map naming what the composition spawned ({} for a type with no addressable children). Composition is synchronous — the root is usable the moment the call returns. AssetRef.instantiate is dispatched through this same check whether or not the type called it, so the two values a caller gets back never depend on which asset it held. Also registers the sceneInstantiable field-constraint validator: a constrained value must be an asset whose type defines instantiate (ref:canInstantiate()), which is what makes Field.instantiableRef accept by CAPABILITY instead of a hardcoded type list. nil (no asset) passes — the field is optional.

modules/scene_instantiable/isOwned

isOwned(opts: { [string]: any }?): boolean

Whether this instantiate call already has an owner. The Asset / SceneModule components drive instantiate themselves and tag the call with sourceTag; they hold the asset reference, persist the composition's idMap, and re-run the composition on every load. A call with no tag came straight from ref:instantiate(...) and has no such owner, so a type whose composition must survive a reload composes, then hands the result one.

Parameters

  • opts { [string]: any }? (optional) — The instantiate opts table (nil-safe).
if not Instantiable.isOwned(opts) then ... end

modules/scene_instantiable/own

own(root: any, self: any, idMap: { [string]: string }?): any

Hand an ALREADY-COMPOSED root to an Asset component pointing at self. The type composes first and calls this last: the component adopts the composition standing on root rather than building a second one, and from then on owns the reference — it keeps idMap in a persisted field and re-composes with those same ids on the next load, so cross-entity references into the composition (SkinnedModel.skeletonRoot) stay valid. Composed children never reach scene.json; the scene stores the reference and re-composes from it.

Parameters

  • root any (optional) — The root entity, as an EntityRef proxy, with the composition live.
  • self any (optional) — The asset ref being instantiated.
  • idMap { [string]: string }? (optional) — The original_id -> runtime id map naming that composition.
if not Instantiable.isOwned(opts) then Instantiable.own(root, self, freshMap) end

modules/scene_instantiable/place

place(root: any, opts: { [string]: any }?): any

Apply the base placement opts to a root the type already has — the adopt path (a bundle exploding onto its target, a sceneModule reconciling under one). position / rotation / scale land on the root's local transform; name renames it. rotation takes three numbers as pitch/yaw/roll in DEGREES, or four as a quaternion.

Parameters

  • root any (optional) — The root entity, as an EntityRef proxy.
  • opts { [string]: any }? (optional) — The instantiate opts table (nil-safe).
return Instantiable.place(target, opts), idMap
Instantiable.place(root, { rotation = { 0, 90, 0 } })  -- yaw 90°

modules/scene_instantiable/result

result(self: any, root: any, idMap: { [string]: string }?): (any, { [string]: string })

Return an instantiate through the contract — the OUT half, the counterpart of root / place. Checks that root is a live entity ref and normalises a missing map to {}, so every type hands its caller the same two values: the composed root, live on return, and the originalId -> runtimeId map naming what it spawned. AssetRef runs every instantiate through this on the way out, so a type that returns something else fails at its own call rather than handing a caller a nil root or a map that is sometimes absent.

Parameters

  • self any (optional) — The asset ref being instantiated — named in the error.
  • root any (optional) — The composed root, as an EntityRef proxy.
  • idMap { [string]: string }? (optional) — The original_id -> runtime id map, or nil for a type that spawns no addressable children.
return Instantiable.result(self, root, freshMap)
return Instantiable.result(self, Instantiable.root(self, target, opts))

modules/scene_instantiable/root

root(self: any, target: any?, opts: { [string]: any }?): any

Stand the root entity for an asset type's instantiate — the whole base contract in one call. Validates target (an owning entity ref, or nil), spawns the root as its child (born temporary when opts.temporary, named opts.name else the asset's own name), and applies the placement opts. The type adds its components to the returned root; what it returns from instantiate is (thisRoot, idMap).

Parameters

  • self any (optional) — The asset ref being instantiated.
  • target any? (optional) — Optional owning entity ref — the root spawns as its child.
  • opts { [string]: any }? (optional) — The instantiate opts table (nil-safe). rotation takes three numbers as pitch/yaw/roll in DEGREES, or four as a quaternion.
local root = Instantiable.root(self, target, opts)

modules/scene_loader/README

scene_loader

Luau-side scene loader. Reads scene.json v6 and v7, refuses versions outside that range with a typed error, dispatches to entity.spawn / component.add / lights.setup, auto-discovers the sibling entrypoint.luau via vfs.exists. v6 attaches the declarative player + camera blocks to the Scene proxy for procedural resolution; v7 stashes the string player intent and does no procedural player/camera spawn. Replaces the legacy Rust __layers.load FFI path; the Rust FFI remains for one release as a safety net but is no longer invoked by any in-tree caller. Delta-overlay aware: when a scene_dirty/ directory exists next to canonical scene.json, the loader hands the merge off to scene_saver.composeMerged — canonical scene.json + dirty manifest overrides + per-entity overlay files = the assembled body. Single merge point so the load + promote paths can never disagree. See § 10 of the player-camera-unification integration design for the architecture rationale (pure Luau, performance budget ~5 ms per 1k-entity scene, ECS FFI is sufficient without new primitives).

modules/scene_saver/README

scene_saver

Luau-side scene saver. Persists scene state via a delta-overlay model: Canonical (committed work) /scene.json Dirty (work-in-progress, deltas only) /scene_dirty/manifest.json scene-level config overrides (lighting, player, camera, format, version) /scene_dirty/entities/.json one file per CHANGED entity: * full body — modified or newly spawned * tombstone — { tombstone = true } for despawn The dirty directory carries ONLY the deltas. Entities unchanged since the last canonical save have NO entry under entities/. Editing one cube in a 100k-entity scene writes exactly one ~1KB file per drain; the manifest is touched only when scene-level config changes. The entity-id list is NOT carried in the manifest — it's derived at load / promote time from canonical.entities[] plus a filesystem listing of entities/. Concurrent writes to per-entity files therefore never produce manifest-conflict orphans (the conflict surface is one entity at a time, not the whole-scene index). Tombstones survive in dirty until the next save (when clearDirty wipes the directory). An orphan tombstone — one targeting an entity that wasn't in canonical either (i.e. the entity was spawned and deleted inside the same edit session before save) — is a no-op at compose time, so the file shape is robust regardless of operation order. The composer (composeMerged) is the canonical merge point used by both this saver's promote/save paths AND scene_loader.M.load, so there's exactly one place that knows how to combine canonical with the overlay. Captures authored intent only: no spawner-managed entities (player identities, primary camera), no temporary entities, no runtime player position. FFI shapes (discovered at runtime — differ from plan): - entity.findAll() → array of {id: string, name: string} tables. - localPosition / localScale expose number components .x/.y/.z. - localRotation exposes quaternion components .x/.y/.z/.w. - entity(id).getParent() → parent entity proxy or nil; its .id is the id string. - entity(id).name → string name (direct field, no function call). - lighting snapshot is read directly from settings.lighting.

modules/scene_saver/componentShortName

componentShortName(t: string): string

The component type name without its library prefix, so a saved record (Model) and a live one (@builtin::components.Model) name the same type.

Parameters

  • t string — A component type name in either form.
require("@builtin::modules.api.engine.scene_saver").componentShortName("@builtin::components.Model")

modules/scene_saver/discardSceneEdits

discardSceneEdits(name: string, select: (string | { string })?): SceneEditDiscard

Take a scene's unbaked overlay edits back out, leaving the canonical file as what the scene carries. This is the second verdict on the edits pendingSceneEdits reports and a publication call refuses over: staging bakes them into the scene, this drops them. Each edit's overlay record is deleted and its entity withdrawn so no writer re-states it, whichever session wrote the record and however long it has stood.

Parameters

  • name string — The scene — a .scene folder path, a scene.json path, or a bare scene name.
  • select (string | { string })? (optional) — Which edits to drop: an entity id or entity name, or a list of them. Omitted, every edit the scene has pending.
require("@builtin::modules.api.engine.scene_saver").discardSceneEdits(layers.active.name)
require("@builtin::modules.api.engine.scene_saver").discardSceneEdits("/zero/source/scenes/main.scene", "look")

modules/scene_saver/hasPendingSceneEdits

hasPendingSceneEdits(name: string): boolean

Whether baking the scene's dirty overlay into its canonical file would change the scene. False when the overlay is absent, and false when it holds only records the canonical file already states.

Parameters

  • name string — The scene — a .scene folder path, a scene.json path, or a bare scene name.
require("@builtin::modules.api.engine.scene_saver").hasPendingSceneEdits(layers.active.name)

modules/scene_saver/pendingSceneEdits

pendingSceneEdits(name: string): ({ PendingSceneEdit }, string?)

Every edit a scene's dirty overlay holds that baking it would write into the canonical scene file: an added entity, an updated one, a removed one, or a change to the scene's own lighting / player / camera config. An EMPTY result is the verdict that the canonical scene already says what the overlay says, whatever rows the overlay carries. Each entry names the entity and describes the difference.

Parameters

  • name string — The scene — a .scene folder path, a scene.json path, or a bare scene name.
require("@builtin::modules.api.engine.scene_saver").pendingSceneEdits("/zero/source/scenes/main.scene")

modules/scene_saver/recordDiffLines

recordDiffLines(oldRec: any, newRec: any, maxLines: number?): { string }

Human lines describing how a live entity record differs from the record the scene carries: transform channels, component fields, component additions and removals, and attribute additions, changes and removals. Compares authored intent — a component field participates where the scene's record declares it, numbers compare within a float-noise epsilon, and asset references compare by the asset they name rather than by the shape they are written in. An EMPTY result is the "unchanged" verdict.

Parameters

  • oldRec any (optional) — The scene's entity record.
  • newRec any (optional) — The live entity record.
  • maxLines number? (optional) — How many lines to report before summarizing the rest as "…".
local ss = require("@builtin::modules.api.engine.scene_saver"); ss.recordDiffLines(saved, ss.serializeEntity(id))

modules/scene_saver/recordFieldValue

recordFieldValue(v: any): (any, string?)

The durable form of one component field value — what a record states for it. An asset reference keeps the identity a later session re-resolves it from; a live GPU resource handle names a slot in this session's GPU registry, so it is left out and named in the second return instead. Reading a live field through this is what lets a consumer of a record compare what it holds against what the record says in one form.

Parameters

  • v any (optional) — A field value read off a live component.
require("@builtin::modules.api.engine.scene_saver").recordFieldValue(model.material) -- { __ref = "..." }, nil

modules/scene_saver/trackingReason

trackingReason(): string?

Why an authored scene edit made from THIS call's own context would not be recorded right now, or nil while it would be. A scene records its edits in edit mode, outside a scene load, outside the world entrypoint's respawn pass, and outside a component or entrypoint callback tick; inside any of those a spawn or a change reaches the live session and no file. This is the question an empty pendingSceneEdits list cannot answer on its own — the same list stands for "the scene is saved" and for "nothing is watching it".

require("@builtin::modules.api.engine.scene_saver").trackingReason()

modules/scene_saver/transformDiffLines

transformDiffLines(oldRec: any, newRec: any): { string }

Diff lines for an entity's transform channels (position / rotation / scale), comparing the live record against the record the scene carries. A channel the saved record OMITTED defaults to the identity transform, so a move away from origin registers even for an entity saved at identity (which carries no transform block) — while an unmoved entity whose live record materializes the identity transform produces no line.

Parameters

  • oldRec any (optional) — The scene's entity record (may omit transform or channels).
  • newRec any (optional) — The live entity record.
require("@builtin::modules.api.engine.scene_saver").transformDiffLines({}, { transform = { position = { 0, 1, 0 } } })

modules/scene_swap_orchestrator/README

scene_swap_orchestrator

Drives multiplayer room transitions + the scene-swap gate around scene load/unload events. Subscribes to layers.onUnload + layers.onBeforeLoad and translates them into a leaveRoom -> gate.begin -> teardown -> gate.finish -> joinRoom sequence. Room key is {worldGuid}/{profile}/{mode}/{sceneGuid} — never name-based; world and scene segments are always GUIDs (stable across renames and collaborators), and {profile} (runtime/editor) keeps published and live peers apart. Without this orchestrator, scene swaps world would broadcast EntityDespawn to peers still subscribed to the old room, and transient teardown state would land in the dirty file.

modules/scopes/README

require("@builtin/modules/scopes") -- scopes (also available as global 'scopes')

The owning contexts live resources are registered under — what each context holds right now, and how to end everything one holds. -- What is live, and what will never be reached by a seam? for _, r in scopes.list() do if not r.endsAtASeam then print(r.kind, r.handle, r.scope, r.detail) end end -- End everything an earlier execute chunk left behind. scopes.release("exec:__exec_12")

A resource reached through a handle — a substrate job, and every other handle-based resource as it adopts this — outlives the call that made it. Each one records the context it was registered from, and the engine ends what a context left behind once that context's run is over. Three contexts own resources, and they differ in what ends them:

  • chunk:<identity> — a module. Ends when the module runs again (a VFS write to its source) or is dropped from the require cache (vfs.reload). The next run registers its own.
  • component:<instanceId> — one component instance. Ends when that instance is destroyed: its entity despawned, its scene cleared, the component removed.
  • exec:<chunk> — a chunk submitted for a single run, such as an execute call. It is never re-entered, so nothing ends it on its own. That last one is what this module is for. A chunk that registered a resource and lost the handle — it raised before storing it, or simply finished — left something live that no variable names. list() finds it by the context that registered it, and release(scope) ends it. Explicit release is unchanged: destroy / cancel / close end a resource the moment content calls them. This is what happens to the ones nobody ended.

Usage: local scopes = require("@builtin/modules/scopes") Also available as global: scopes

modules/scopes/current

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.

print("resources I register follow", scopes.current())

modules/scopes/list

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.

for _, r in scopes.list() do print(r.scope, r.kind, r.detail) end

modules/scopes/release

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

  • scope string — A scope tag, as the scope field of a list() row carries it.
local ended = scopes.release("exec:__exec_12")

modules/screenSpaceGI/README

require("@builtin/systems/screenSpaceGI/screenSpaceGI") -- screenSpaceGI

Indirect diffuse light gathered from the scene that was just drawn, so anything on screen bounces light onto its neighbours — including geometry that moved this frame, which a baked lightmap cannot follow.

Usage: local screenSpaceGI = require("@builtin/systems/screenSpaceGI/screenSpaceGI")

modules/screenSpaceGI/active

active(): boolean

Whether the screen-space GI pass is running this frame.

if screenSpaceGI.active() then ... end

modules/screenSpaceGI/clear

clear()

Turn screen-space GI off and release the pass. The other settings are kept, so a later set({ intensity = ... }) brings back the same look.

screenSpaceGI.clear()

modules/screenSpaceGI/get

get(): SSGIState

The screen-space GI settings currently in force.

local r = screenSpaceGI.get().radius

modules/screenSpaceGI/paramsBuffer

paramsBuffer(): any?

The buffer this system's passes read. The render feature binds what this hands it, so the pass has the values this module packed.

local p = screenSpaceGI.paramsBuffer()

modules/screenSpaceGI/set

set(opts: SSGIOpts?): SSGIState

Set the scene's screen-space GI. Any omitted field keeps its current value. An intensity of 0 turns it off and releases the pass.

Parameters

  • opts SSGIOpts? (optional) — Screen-space GI settings — see SSGIOpts.
screenSpaceGI.set({ intensity = 1.0, radius = 2.0 })

modules/service/README

require("@builtin/modules/api/engine/service") -- service (also available as global 'service')

Credit-metered service invoke. Public Luau surface over the __service Internal FFI namespace.

Usage: local service = require("@builtin/modules/api/engine/service") Also available as global: service

modules/service/authenticated

authenticated(): boolean

Whether a platform identity (JWT) is available to attach to service calls. Returns only a boolean — never the token.

if not service.authenticated() then error("link ZeroMind") end

modules/service/balance

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.

local h = service.balance(); local raw = h and task.await(h)

modules/service/configureGateway

configureGateway(baseUrl: string): boolean

TRUSTED ONLY. Set the ZeroMind base URL that service.invoke and service.balance target. The trusted-VM auth bootstrap calls this with the resolved issuer.

Parameters

  • baseUrl string — ZeroMind base URL (e.g. "https://origozero.ai").
service.configureGateway("https://origozero.ai")

modules/service/configureWorld

configureWorld(guid: string): boolean

TRUSTED ONLY. Set the bound world guid attached to metered service invocations, so the credit ledger attributes each charge to the world it happened in. The trusted-VM world-load hook calls this on every bind so a runtime world switch re-points attribution.

Parameters

  • guid string — The bound world's guid.
service.configureWorld(world.guid())

modules/service/gatewayConfigured

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.

if not service.gatewayConfigured() then error("no gateway") end

modules/service/invoke

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

  • offering string — Fully-qualified offering identity provider/name (e.g. "origozero/mesh_gen").
  • endpoint string — Logical endpoint name (e.g. "create_preview").
  • opts InvokeOpts? (optional){ params?, headers?, body?, idempotency_key? }.
local h = service.invoke("origozero/mesh_gen", "create_preview", { body = { prompt = p } })

modules/service/jobStatus

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

  • jobId string — Job id returned by submitJob.
local h = service.jobStatus(jobId); local raw = h and task.await(h)

modules/service/submitJob

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

  • offering string — Fully-qualified offering identity provider/name (e.g. "origozero/mesh_gen").
  • endpoint string — Logical endpoint name (e.g. "create_preview").
  • opts InvokeOpts? (optional){ params?, headers?, body?, idempotency_key? }.
local h = service.submitJob("origozero/mesh_gen", "create_preview", { body = { prompt = p } })

modules/shader/README

require("@builtin/modules/api/engine/shader") -- shader (also available as global 'shader')

Shader compilation — turning authored WGSL into registered GPU programs, and the reusable modules those programs include. Public Luau surface over the __shader Internal FFI namespace.

Usage: local shader = require("@builtin/modules/api/engine/shader") Also available as global: shader

modules/shader/compile

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

  • keys string | { 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.
shader.compile({ ref.guid, ref.identity }, { source = wgsl, properties = props })

modules/shader/registerModule

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

  • keys string | { string } — One key, or the array of keys this module answers to.
  • source string — The module's WGSL.
shader.registerModule({ ref.guid, ref.identity }, wgsl)

modules/shader/status

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

  • name string — Shader identity or guid — the key it compiled under.
local status, err = shader.status(ref.guid)

modules/shader_includes/README

require("@builtin/modules/shader_includes") -- shader_includes

Resolves the #include lines of a WGSL source against the asset system, so a shader includes a .shaderModule by every name form require and asset.resolve accept.

Usage: local shader_includes = require("@builtin/modules/shader_includes")

modules/shader_includes/canonicalize

canonicalize(source: string, base: string): string

Rewrite every #include in source whose literal names a .shaderModule to that module's guid, resolved against base, and bring every module the source reaches up to date with what the VFS serves. A literal that resolves to no asset is left as it was written, so a framework name reaches the expander unchanged.

Parameters

  • source string — The WGSL as authored.
  • base string — VFS path of the asset the source belongs to — what a ~ / ~.tail / .relative.tail literal expands against.
local wgsl = ShaderIncludes.canonicalize(vfs.read(path .. "/shader.wgsl"), path)

modules/sharpening/README

require("@builtin/systems/sharpening/sharpening") -- sharpening

Contrast-adaptive sharpening. Restores local acuity to the finished frame, backing off wherever the neighbourhood has no headroom left, which is what keeps a bright fringe from forming along high-contrast edges.

Usage: local sharpening = require("@builtin/systems/sharpening/sharpening")

modules/sharpening/active

active(): boolean

Whether the sharpening pass is running this frame.

if sharpening.active() then ... end

modules/sharpening/disable

disable()

Turn sharpening off and release the pass.

sharpening.disable()

modules/sharpening/enable

enable(sharpness: number?): State

Turn sharpening on at a given strength.

Parameters

  • sharpness number? (optional) — Strength in [0, 1]. Omit to keep the current value.
sharpening.enable(0.5)

modules/sharpening/get

get(): State

The sharpening settings currently in force.

local s = sharpening.get().sharpness

modules/sharpening/paramsBuffer

paramsBuffer(): any?

The buffer this system's passes read. The render feature binds what this hands it, so the pass has the values this module packed.

local p = sharpening.paramsBuffer()

modules/sharpening/set

set(opts: SharpenOpts?): State

Set the sharpening strength. Any omitted field keeps its current value.

Parameters

  • opts SharpenOpts? (optional) — Sharpening settings — see SharpenOpts.
sharpening.set({ sharpness = 0.6 })

modules/shell/README

require("@builtin/modules/api/engine/shell") -- shell (also available as global 'shell')

Engine emulated Unix shell — same shell that powers the MCP bash tool. Public Luau surface over the __shell Internal FFI namespace.

Usage: local shell = require("@builtin/modules/api/engine/shell") Also available as global: shell

modules/shell/run

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

  • command string — Shell command to execute.
local r = shell.run("ls /zero/source")

modules/shell/runAsync

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

  • command string — Shell command to execute.
local json = task.await(shell.runAsync("find /zero -name '*.luau'"))

modules/skeleton/README

require("@builtin/modules/api/engine/skeleton") -- skeleton (also available as global 'skeleton')

The skeleton pose-data pipeline: sample a clip into a pose buffer, and bind/apply a pose buffer onto a Skeleton + Model entity. Public Luau surface over the __skeleton and __clip Internal FFI namespaces.

Usage: local skeleton = require("@builtin/modules/api/engine/skeleton") Also available as global: skeleton

modules/skeleton/applyPose

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

  • sinkHandle number — Sink handle from bindPose.
  • poseBuffer Substrate.TypedBuffer — The pose buffer to apply.

modules/skeleton/bindClip

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

  • zanimBytes buffer | string — The clip's data.zanim payload bytes (binary-safe).
  • boneOrder { string } — Output bone names — one stride-10 record per bone.

modules/skeleton/bindPose

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.
  • opts SkeletonLayout{ boneOrder, stride }.
local h = skeleton.bindPose(nil, { boneOrder = bones, stride = 10 })

modules/skeleton/clipBones

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

  • zanimBytes buffer | string — The clip's data.zanim payload bytes (binary-safe).
local names = skeleton.clipBones(vfs.read(path .. "/data.zanim"))

modules/skeleton/clipDecode

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

  • zanimBytes buffer | string — The clip's data.zanim payload bytes (binary-safe).
local clip = json.decode(skeleton.clipDecode(bytes))

modules/skeleton/clipEncode

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

  • jsonString string — A clip JSON document.
local bytes = skeleton.clipEncode(json.encode(clip))

modules/skeleton/jointTransforms

jointTransforms(entityId: string | entityRef): table

Read a skinned entity's per-joint world transforms for the current animated pose.

Parameters

  • entityId string | entityRef — Engine entity id or proxy of a skinned entity.
local joints = skeleton.jointTransforms(meshId)

modules/skeleton/sampleClip

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

  • handle number — Sampler handle from bindClip.
  • time number — Sample time in seconds.
  • poseBuffer Substrate.TypedBuffer — The stride-10 pose buffer written into.

modules/skeleton/unbindClip

unbindClip(handle: number): boolean

Drop the bound clip sampler from the registry.

Parameters

  • handle number — Sampler handle to remove.

modules/skeleton/unbindPose

unbindPose(sinkHandle: number): boolean

Remove the sink from the registry.

Parameters

  • sinkHandle number — Sink handle to remove.

modules/sky/README

require("@builtin/modules/api/engine/sky") -- sky (also available as global 'sky')

Sky configuration — type, time of day, day/night cycle, procedural parameters, presets, explicit sun direction. Public Luau surface over the __sky Internal FFI namespace.

Usage: local sky = require("@builtin/modules/api/engine/sky") Also available as global: sky

modules/sky/get

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

local cfg = sky.get(); print(cfg.time_of_day)

modules/sky/getTimeOfDay

getTimeOfDay(): number

Get the current time of day in hours (0-24).

local t = sky.getTimeOfDay()

modules/sky/installFallback

installFallback()

Register the engine fallback sky material and install it as the engine-level fallback (rendered when a scene has no sky entity). Idempotent; requires a live renderer — the scene loader calls it.

sky.installFallback()

modules/sky/preset

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

  • name string — Preset name (case-sensitive).
sky.preset("sunset")

modules/sky/set

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

  • opts SkyOpts — Sky configuration properties.
sky.set({ type = "procedural", time_of_day = 14, sync_sun_to_light = true })

modules/sky/setSunDirection

setSunDirection(dir: SkyColor)

Set an explicit sun direction and disable time-based sun positioning. The directional light is updated to match.

Parameters

  • dir SkyColor — Normalized sun direction vector.
sky.setSunDirection({ 0.5, -1, 0.3 })

modules/sky/setTimeOfDay

setTimeOfDay(time: number)

Set the time of day (0-24 hours). 0 = midnight, 6 = sunrise, 12 = noon, 18 = sunset.

Parameters

  • time number — Time of day in hours.
sky.setTimeOfDay(18.5)

modules/spatialStreaming/README

spatialStreaming

modules/spatialStreaming/add

add(records: { Record }, opts: { [string]: any }?): string

Register a group of records for content that is not spawned. This is the path that lets a world hold more than it can spawn at once: with store = "vfs" the records are written to a file under the configured directory and only the cell's manifest stays in memory until a source arrives.

Parameters

  • records { Record } — Array of records, root first. A record's parent indexes another record of the same array.
  • opts { [string]: any }? (optional){ layer = "default", store = "memory" | "vfs" }.
spatialStreaming.add({ { name = "rock", position = { x = 300, y = 0, z = 0 }, components = { Model = { model = "@builtin::meshes.cube" } } } }, { store = "vfs" })

modules/spatialStreaming/addSource

addSource(target: any, opts: { [string]: any }?): number

Add a streaming source. A cell is resident while ANY source wants it resident and released only once every source wants it released, so several sources — a player and a spectator camera, two split-screen players — each keep the region they stand near.

A source carries its own radii, which is what lets a distant spectator stream a thin shell while the player streams a deep one. Radii left out follow the layer's, and the layer's follow the configuration.

Parameters

  • target any (optional) — An entity id, an entity proxy, an entity name, or a fixed world position { x, y, z }.
  • opts { [string]: any }? (optional){ loadRadius, unloadRadius, proxyRadius, proxyUnloadRadius } — this source's own radii, for the content and for the far field it sees proxies in. A source that names an unload radius of its own sees a far field measured from it unless it names that too.
spatialStreaming.addSource(entity.find("Player"), { loadRadius = 90, unloadRadius = 140 })

modules/spatialStreaming/capture

capture(targets: any, opts: { [string]: any }?): { [string]: any }

Hand live entities to the streaming store. Each target is the ROOT of a group: it and its descendants are serialized together and placed in the cell the root stands in. The entities are left standing — residency passes to the streaming rule, which releases them on the first tick that puts them past the unload radius.

A target that is replicated to other peers is refused: residency is a local decision, and despawning a replicated entity on one client would reach the others. A target that already has a parent is refused too — its root is the group, and capturing a branch of one would leave the rest behind. A target the store already holds is refused as well, and counted: a second group over the same live entities would despawn them on the first release and leave the second group naming content that is gone.

Parameters

  • targets any (optional) — Array of entity ids, proxies, or names, or a single name glob.
  • opts { [string]: any }? (optional){ layer = "default", store = "memory" | "vfs" }.
spatialStreaming.capture(entity.findAll("rock_*"), { layer = "props" })

modules/spatialStreaming/cellBounds

cellBounds(key: string, cellSize: number): { minX: number, minZ: number, maxX: number, maxZ: number }

Ground footprint of a cell.

Parameters

  • key string — Cell key.
  • cellSize number — Edge length of a cell in world units.
local b = spatialStreaming.cellBounds("2:-1", 64)

modules/spatialStreaming/cellCoords

cellCoords(key: string): (number, number)

Grid coordinates a cell key names.

Parameters

  • key string — Cell key from cellKeyAt.
local cx, cz = spatialStreaming.cellCoords("2:-1")

modules/spatialStreaming/cellDistance

cellDistance(key: string, cellSize: number, x: number, z: number): number

Distance from a point to the nearest edge of a cell's footprint, zero when the point stands inside it. Measuring to the centre instead would put a cell's near corner inside the load radius while the cell itself reads as far, so a source walking along a boundary would see the ground it is on released.

Parameters

  • key string — Cell key.
  • cellSize number — Edge length of a cell in world units.
  • x number — World-space X of the point.
  • z number — World-space Z of the point.
local d = spatialStreaming.cellDistance("2:-1", 64, 10, 10)

modules/spatialStreaming/cellKeyAt

cellKeyAt(x: number, z: number, cellSize: number): string

Key of the cell a world-space point stands in. Cells are square columns on the ground plane: height never enters, because a world's content is spread over its ground rather than through its air, and a column keeps a tower and its foundation in one cell.

Parameters

  • x number — World-space X.
  • z number — World-space Z.
  • cellSize number — Edge length of a cell in world units.
local key = spatialStreaming.cellKeyAt(130, -40, 64)

modules/spatialStreaming/cells

cells(): { any }

Per-cell view of the store, for inspection and for tests.

local c = spatialStreaming.cells()

modules/spatialStreaming/clearProxy

clearProxy(target: any, opts: { [string]: any }?): boolean

Take a cell's proxy away, despawning it if it is standing.

Parameters

  • target any (optional) — A cell key or a { x, y, z } world position.
  • opts { [string]: any }? (optional){ layer = "default" }.
spatialStreaming.clearProxy("4:0")

modules/spatialStreaming/clearSources

clearSources()

Drop every streaming source. Cells hold whatever residency they have — nothing loads or releases while no source stands anywhere.

spatialStreaming.clearSources()

modules/spatialStreaming/config

config(): { [string]: any }

The configuration now in force. The far field is reported as the distances it is measured at, whether they were given or follow the content radii.

local c = spatialStreaming.config()

modules/spatialStreaming/configure

configure(opts: { [string]: any }?): { [string]: any }

Set the grid and the radii every layer inherits. Keys left out keep their current value.

A new cellSize re-buckets everything the store already holds, by each group's own root position. A cell key means nothing without the size it was measured with, so content filed under the old grid would otherwise be measured against footprints it never stood in.

Parameters

  • opts { [string]: any }? (optional){ cellSize, loadRadius, unloadRadius, proxyRadius, proxyUnloadRadius, budget, dir }. budget is how many entity records one tick may spend on loading and releasing; the proxy pair is the far field a released cell's stand-in covers, and follows unloadRadius unless it is given — passing 0 for either hands it back to that; dir is the VFS directory a file-backed cell writes under.
spatialStreaming.configure({ cellSize = 50, loadRadius = 100, unloadRadius = 160 })

modules/spatialStreaming/decide

decide(distance: number, loadRadius: number, unloadRadius: number): string

Residency a cell whose nearest edge is distance from the closest source should be in. Inside loadRadius it is wanted resident, past unloadRadius it is wanted released, and between the two it keeps whatever it already is — the band is what stops a source resting on a boundary from loading and releasing the same cell every frame.

Parameters

  • distance number — Distance from the nearest source to the cell's nearest edge.
  • loadRadius number — Distance within which a cell is wanted resident.
  • unloadRadius number — Distance past which a cell is wanted released.
local want = spatialStreaming.decide(140, 128, 192)

modules/spatialStreaming/deriveProxies

deriveProxies(opts: { [string]: any }?): { [string]: any }

Build bounding-silhouette proxies from the content the store already holds — one box over a whole cell, or one over each group in it. This is the automatic path; setProxy is the one that takes an authored mesh or impostor.

Each record is measured as a box of its own scale, placed through the whole of its parent's transform, so a group standing rotated and scaled is enclosed where it actually stands. A record whose mesh is larger than one unit at scale 1 is measured as the unit box it is scaled from — padding is what covers the difference, and setProxy is what takes an authored silhouette instead.

A file-backed cell's records are read to measure them and dropped again, so deriving over a world that is held in files costs one read per cell and leaves the store as it found it.

Parameters

  • opts { [string]: any }? (optional){ layer, mesh = "@builtin::meshes.cube", material, granularity = "cell" | "group", padding = 0, replace = false }. layer limits the walk to one layer; replace overwrites a proxy a cell already has.
spatialStreaming.deriveProxies({ granularity = "group", material = "@builtin::materials.default" })

modules/spatialStreaming/flush

flush(): number

Write every file-backed cell that has no group standing out to its file and drop its records. A cell already written is left alone. This is what bounds memory while a world's content is being registered; tick calls it each step, so a world that streams needs it only to measure the store between an add batch and the first step.

spatialStreaming.flush()

modules/spatialStreaming/layers

layers(): { [string]: any }

Settings of every configured layer, keyed by name.

local l = spatialStreaming.layers()

modules/spatialStreaming/proxies

proxies(): { any }

Per-cell view of the proxies the store holds. records is what the stand-in is made of; entities is how much of it is standing right now, which is zero while the cell's content covers it.

local p = spatialStreaming.proxies()

modules/spatialStreaming/proxyDecide

proxyDecide(distance: number, proxyRadius: number, proxyUnloadRadius: number): string

Residency a cell's PROXY should be in, for a cell whose content is not standing. Same shape as decide and for the same reason: the band between the two radii is what keeps a source resting at the far edge from spawning and despawning the same stand-in every frame.

The rule reads a distance and the far-field radii alone. Whether the cell's own content is standing is the tick's question, and the tick lets the content's answer win: a proxy stands where the content does not.

Parameters

  • distance number — Distance from the nearest source to the cell's nearest edge.
  • proxyRadius number — Distance within which a released cell's proxy stands.
  • proxyUnloadRadius number — Distance past which the proxy is dropped too.
local want = spatialStreaming.proxyDecide(560, 512, 640)

modules/spatialStreaming/proxyIds

proxyIds(): { string }

Entity ids the proxies currently have standing, across every cell.

local ids = spatialStreaming.proxyIds()

modules/spatialStreaming/removeSource

removeSource(target: any): boolean

Remove a streaming source by the entity it follows.

Parameters

  • target any (optional) — Entity id, proxy, or name the source was added with.
spatialStreaming.removeSource(entity.find("Player"))

modules/spatialStreaming/reset

reset()

Release every resident group, forget the store, and drop every source. Live entities the store owns are despawned; nothing else in the scene is touched.

spatialStreaming.reset()

modules/spatialStreaming/residentIds

residentIds(): { string }

Entity ids the store currently has standing, across every cell.

local ids = spatialStreaming.residentIds()

modules/spatialStreaming/setLayer

setLayer(name: string, opts: { [string]: any }?): { [string]: any }

Configure a content layer. A layer streams on its own radii and can be switched off entirely, so decorative content can be released long before the content a player interacts with. Each layer keeps its own grid, so two layers standing in the same square still stream apart.

Parameters

  • name string — Layer name. Content lands in "default" unless capture / add say otherwise.
  • opts { [string]: any }? (optional){ enabled, loadRadius, unloadRadius, proxyRadius, proxyUnloadRadius }. A nil radius follows the global configuration, and a layer that names an unload radius of its own gets a far field measured from it. A disabled layer holds nothing standing — neither its content nor its proxies.
spatialStreaming.setLayer("props", { loadRadius = 60, unloadRadius = 90 })

modules/spatialStreaming/setProxy

setProxy(target: any, records: { Record }, opts: { [string]: any }?): string

Give a cell the stand-in it is drawn as while its content is released. The proxy is a group of records like any other, so what it represents is the caller's: a merged low-detail mesh, a billboard impostor, a bounding silhouette. It stands whenever the cell's content does not and the cell is inside the proxy radius, and it is exchanged for the content in an order that leaves no frame with neither standing.

A cell that has no content yet takes a proxy too, which is what lets a skyline be registered before — or instead of — the content it stands for.

Parameters

  • target any (optional) — A cell key from cellKeyAt / cells(), or a { x, y, z } world position the cell is looked up from.
  • records { Record } — Array of records, the same shape add takes. Positions are world-space for a record with no parent.
  • opts { [string]: any }? (optional){ layer = "default" }.
spatialStreaming.setProxy("4:0", { { name = "skyline", position = { x = 288, y = 8, z = 32 }, scale = { x = 64, y = 16, z = 64 }, components = { Model = { model = "@builtin::meshes.cube" } } } })

modules/spatialStreaming/sourcePositions

sourcePositions(): { Vec3 }

World positions of the live sources. An entity source that has been despawned is dropped here rather than reporting a stale position.

local p = spatialStreaming.sourcePositions()

modules/spatialStreaming/stats

stats(): { [string]: any }

What the store holds and how much of it is standing. bytesResident is the encoded size of the groups that are live; bytesStored is the encoded size of everything the store knows about, resident or not. The gap between the two is what streaming bought.

The proxy figures stand apart from the content's: entitiesProxy and bytesProxy are what the far field costs right now, against the entitiesTotal / bytesStored it stands in for.

local s = spatialStreaming.stats()

modules/spatialStreaming/tick

tick(_dt: number?): { [string]: any }

Advance streaming by one step. Loads and releases at most budget entity records, so crossing a cell boundary spreads its cost over frames instead of spending it all in one. A group is atomic: a group larger than the whole budget still moves in one piece, so the budget is the floor a step stops at rather than a ceiling it never passes.

Releases are spent before loads, which keeps the peak residency at what the radius bought rather than at the sum of the cells on both sides of a boundary.

A cell that has a proxy raises it before the first of its groups is released and drops it once the cell's own content has landed, so the exchange between the two representations leaves neither a gap nor a silhouette standing over the content it covered for.

One step belongs to the frame, not to the caller: a second call inside the same engine frame reports stepped = false and changes nothing, so a world carrying several StreamingSource components spends one budget rather than one per source. Waiting a frame is what advances it again.

Parameters

  • _dt number? (optional) — Frame delta, accepted so a component can pass its own and ignored — the step is driven by distance, not by time.
spatialStreaming.tick(dt)

modules/ssr/README

require("@builtin/systems/reflections/ssr") -- ssr

Screen-space reflections — polished floors reflect what is standing on them, and wet ground reflects what is above it, from the frame the engine has already drawn.

Usage: local ssr = require("@builtin/systems/reflections/ssr")

modules/ssr/active

active(): boolean

Whether the reflection passes are currently running.

if ssr.active() then print("reflecting") end

modules/ssr/clear

clear()

Turn reflections off and release the passes. The other settings are kept, so a later set({ intensity = ... }) brings back the same look.

ssr.clear()

modules/ssr/get

get(): SsrState

The reflection settings currently in force.

local d = ssr.get().maxDistance

modules/ssr/paramsBuffer

paramsBuffer(): any?

The buffer this system's passes read. The render feature binds what this hands it, so the pass has the values this module packed.

local p = ssr.paramsBuffer()

modules/ssr/set

set(opts: SsrOpts?): SsrState

Set the scene's screen-space reflections. Any omitted field keeps its current value, so a call can adjust one knob without restating the rest. An intensity of 0 turns reflections off and releases the passes.

Parameters

  • opts SsrOpts? (optional) — Reflection settings — see SsrOpts.
ssr.set({ intensity = 1, maxDistance = 40, quality = "high" })

modules/state

modules.state(path?) -> table

The calling module's durable state table — the same table on every call for the life of the VM, kept across an in-place hot reload of that module and across the re-run a reloaded dependency triggers in it. A module-level local is an upvalue of the chunk that declared it, so it starts again from its initial value each time that chunk runs; a field on this table is held by the engine and does not. Put here what a reload has to survive: a built flag, the entities and particle systems a build owns, an unsubscribe list, a generation counter. Pass a module require path to read another module's table.

Parameters

  • path string (optional) — Module require path. Omit inside a module to get that module's own table.

Returns table — The module's durable state table

modules/stream/README

require("@builtin/modules/api/engine/stream") -- stream (also available as global 'stream')

Named byte streams by URL scheme — TCP, tty, Bluetooth Low Energy and loopback transports, dialled out or listened for, with a common read/write/status/close surface over every one of them. Public Luau surface over the __stream Internal FFI namespace.

Usage: local stream = require("@builtin/modules/api/engine/stream") Also available as global: stream

modules/stream/accept

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

  • listener string — Listener handle from stream.listen.
while true do local h = stream.accept(listener); if not h then break end; table.insert(peers, h) end

modules/stream/close

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

  • handle string — Stream handle from stream.open or stream.accept, or listener handle from stream.listen.
stream.write(peer, response); stream.close(peer)

modules/stream/listen

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

  • url string — Listen URL — scheme://host:port.
  • opts StreamListenOpts? (optional) — Per-connection capacities and the accept backlog (optional).
local pending = stream.listen("tcp://127.0.0.1:9000"); local listener = task.await(pending)

modules/stream/listenerStatus

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

  • listener string — Listener handle from stream.listen.
local s = stream.listenerStatus(listener); print(s.address, s.reach, s.pending)

modules/stream/listeners

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.

for _, l in stream.listeners() do if l.address == want then stream.close(l.handle) end end

modules/stream/open

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>&notify=<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

  • url string — Stream URL — scheme://target[?k=v&k=v].
  • opts StreamOpenOpts? (optional) — Buffer capacities (optional).
local pending = stream.open("loopback://echo"); local handle = task.await(pending)
local paw = task.await(stream.open("ble://Paw*?service=ffe0&write=ffe1&notify=ffe1"))

modules/stream/read

read(handle: string, max: number?): string

Drain up to max buffered inbound bytes from the stream.

Parameters

  • handle string — Stream handle from stream.open.
  • max number? (optional) — Maximum bytes to drain (optional). Omit to drain everything buffered.
local chunk = stream.read(handle)

modules/stream/serialPorts

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.

for _, p in stream.serialPorts().ports do if p.vendorId == 0x303A then print(p.url, p.product) end end

modules/stream/status

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

  • handle string
local s = stream.status(handle); print(s.pending, s.capacity)

modules/stream/streams

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.

for _, s in stream.streams() do print(s.handle, s.transport, s.pending, s.owner) end

modules/stream/transports

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.

local t = stream.transports(); if not t.tcp.listen then warn(t.tcp.listenReason) end

modules/stream/write

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

  • handle string — Stream handle from stream.open.
  • bytes string — Bytes to queue, byte-safe.
local outcome = stream.write(handle, data)

modules/streaming/README

require("@builtin/modules/api/engine/streaming") -- streaming (also available as global 'streaming')

What the engine has resident of a world's detail right now, and why a piece of it is not on screen. Covers the four systems that decide how much of a world stands at any moment: terrain's LOD cut, a voxel world's chunk meshes, spatial streaming's cell store, and mesh LOD's level selection. Reading a Terrain, VoxelWorld, StreamingSource or MeshLod component's fields back tells you what was asked for. streaming.observe() answers the other question: what is standing, what it costs, and for a chunk that is not drawn, which of a closed set of reasons it is not drawn for. The same reading is served at /zero/runtime/observations/streaming. It is built by the call and published as its last act, so both transports carry the document one pass produced.

Usage: local streaming = require("@builtin/modules/api/engine/streaming") Also available as global: streaming

modules/streaming/cells

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.

local s = streaming.cells()

modules/streaming/levels

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

  • scene any? (optional) — The scene walk to read against. Omitted, the call takes its own.
local l = streaming.levels()

modules/streaming/observe

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.

local r = streaming.observe()

modules/streaming/reasons

reasons(): { string }

Every reason whyNotDrawn can answer with, so a caller can enumerate the set rather than meeting it one failure at a time.

local r = streaming.reasons()

modules/streaming/terrain

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

  • scene any? (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.
local t = streaming.terrain()

modules/streaming/voxel

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

  • scene any? (optional) — The scene walk to read against. Omitted, the call takes its own.
local v = streaming.voxel()

modules/streaming/whyNotDrawn

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

  • subject any (optional) — The entity, chunk, level or cell to answer about.
local w = streaming.whyNotDrawn({ entity = "Vox", chunk = "0_0_0" })

modules/stringx/README

require("@builtin/modules/api/engine/stringx") -- stringx (also available as global 'stringx')

Batch text-scanning kernels — read a whole run of numbers out of a string in one call. Public Luau surface over the __stringx Internal FFI namespace.

Usage: local stringx = require("@builtin/modules/api/engine/stringx") Also available as global: stringx

modules/stringx/scanNumbers

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

  • s string — The text to read.
  • pos number? (optional) — 1-based index to start at. Defaults to 1.
-- 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)

modules/subscriptions/README

require("@builtin/modules/api/engine/subscriptions") -- subscriptions (also available as global 'subscriptions')

Runtime inspection and cancellation of component-event subscriptions. Public Luau surface over the __event_inspect_* / __event_track_* Internal FFI globals; the same data is browsable at /zero/runtime/events/.

Usage: local subscriptions = require("@builtin/modules/api/engine/subscriptions") Also available as global: subscriptions

modules/subscriptions/cancel

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

  • id string — Subscription id to cancel.
subscriptions.cancel(conn.id)

modules/subscriptions/get

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

  • id string — Subscription id (conn.id, or a /zero/runtime/events/subscriptions/ entry).
local row = subscriptions.get(conn.id); print(row and row.deliveries)

modules/subscriptions/list

list(filter: SubscriptionFilter?): { SubscriptionRow }

Every tracked subscription row, optionally filtered by publisher instance id, publisher entity id, event name, and/or connected state.

Parameters

  • filter SubscriptionFilter? (optional) — Optional filter table.
for _, s in ipairs(subscriptions.list({ connected = true })) do print(s.id, s.event, s.deliveries) end

modules/subscriptions/publishers

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.

for _, p in ipairs(subscriptions.publishers()) do print(p.component, p.entityName) end

modules/substrate/README

require("@builtin/modules/api/engine/substrate") -- substrate (also available as global 'substrate')

Typed data buffers, on the CPU or the GPU — the engine's one buffer primitive. Public Luau surface over the __substrate Internal FFI namespace.

Usage: local substrate = require("@builtin/modules/api/engine/substrate") Also available as global: substrate

modules/substrate/createBuffer

createBuffer(opts: BufferOpts): TypedBuffer?

Allocate a typed buffer and return its handle.

A "gpu" buffer is storage a compute shader binds; usage adds "vertex", "index", "indirect" or "readback" on top of the storage it always has. A "cpu" buffer lives in the scripting heap and reads back as a flat array of floats.

The handle's write answers whether the words landed: a payload whose end falls past the end of the buffer is refused whole on both kinds, so the buffer keeps what it held and the call answers false. writeU32 and writeBytes answer the same way, against the same extent.

Parameters

  • opts BufferOpts{ type, len, kind?, usage?, name? }type is "f32", "vec3", "vec4", "quat" or "mat4"; kind is "cpu" (the default) or "gpu". name is the name a dispatch binds a "gpu" buffer by, and the name substrate.getBuffer and substrate.destroyBuffer reach it under.
local pose = substrate.createBuffer({ type = "mat4", len = boneCount })
local field = substrate.createBuffer({ type = "vec3", len = 4096, kind = "gpu" })
local values = pose:read(0, 16):result()

modules/substrate/destroyBuffer

destroyBuffer(name: string): boolean

Free the GPU buffer name denotes, whatever else still holds a handle to it.

The allocation goes and the name is free to be created again at any type and length; every handle that pointed at it answers :alive() false. This is what releases a name whose creating handle is gone, so a build that re-runs at a different size gets its name back.

Parameters

  • name string — The name the buffer was created under.
substrate.destroyBuffer("env.town.xf")

modules/substrate/getBuffer

getBuffer(name: string): TypedBuffer?

The GPU buffer name denotes, as a handle you now hold.

A name is how a dispatch binds a buffer, so the name is what an owner asks by once the handle it created with has gone out of scope — a .module that hot-reloaded, a build that ran in an earlier execute. The handle carries everything createBuffer's does and releases its reference with :destroy().

Parameters

  • name string — The name the buffer was created under.
local xf = substrate.getBuffer("env.town.xf")
local shape = xf and { xf:type(), xf:length() }

modules/substrate/gpuReadback

gpuReadback(key: string?): Readback?

Wrap the key an FFI read handed back as the Readback that polls it. Every GPU→CPU read reaches the caller through this, so a texture's read and a buffer's read answer with the same thing.

Parameters

  • key string? (optional) — The key the read returned.
local pending = substrate.gpuReadback(compute.readTexture3D(handle))

modules/substrate/listBuffers

listBuffers(): { NamedBuffer }

Every named GPU buffer the engine holds, in name order.

Each record states id, name, type ("F32", "Vec3", "Vec4", "Quat", "Mat4"), len in records, and refs — how many holders it has. This is what states which names are taken and at what shape.

for _, b in ipairs(substrate.listBuffers()) do print(b.name, b.type, b.len) end

modules/substrate/ready

ready(self): boolean

Whether this read has arrived.

Parameters

  • self any (optional)

modules/substrate/result

result(self): { number }?

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

Parameters

  • self any (optional)

modules/substrate/resultBytes

resultBytes(self): buffer?

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

Parameters

  • self any (optional)

modules/substrate/resultU32

resultU32(self): { number }?

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

Parameters

  • self any (optional)

modules/substrate/state

state(self): string

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

Parameters

  • self any (optional)

modules/subsurface/README

require("@builtin/systems/subsurfaceScattering/subsurface") -- subsurface

Light that enters a surface at one point and leaves at another — the transport that softens and reddens the terminator on skin, and that a per-pixel shading model cannot produce.

A surface shaded per-pixel answers only for the light that arrived at that pixel. Skin does not work that way: light entering the lit side keeps travelling beneath the surface and emerges past where a cosine has already reached zero, and it emerges red, because red travels furthest through flesh. That is what makes the terminator on a face soft and warm instead of a hard line, and it is why a face with no scattering reads as plastic. The transport is a spread of exitant light across the surface, so it is computed on the drawn frame: marked meshes write their scattering into a mask, and the lit colour is spread along that mask. This module owns which entities are marked and what they scatter, and ties the pass's lifetime to whether anything is marked at all.

Usage: local subsurface = require("@builtin/systems/subsurfaceScattering/subsurface")

modules/subsurface/active

active() -> boolean

Returns boolean

modules/subsurface/clear

clear()

modules/subsurface/groups

groups() -> table

Returns table

modules/subsurface/list

list() -> { string }

Returns { string }

modules/subsurface/mark

mark(entityRef: string, opts: MarkOpts?) -> table

Give an entity's meshes a scattering tint and radius.

Parameters

  • entityRef string
  • opts MarkOpts? (optional)

Returns table

modules/subsurface/marked

marked(entityRef: string) -> table?

Parameters

  • entityRef string

Returns table?

modules/subsurface/paramsBuffer

paramsBuffer(): any?

The buffer this system's passes read. The render feature binds what this hands it, so the pass has the values this module packed.

local p = subsurface.paramsBuffer()

modules/subsurface/setStrength

setStrength(strength: number) -> number

Parameters

  • strength number

Returns number

modules/subsurface/strength

strength() -> number

Returns number

modules/subsurface/unmark

unmark(entityRef: string) -> boolean

Parameters

  • entityRef string

Returns boolean

modules/surfaceLight/README

require("@builtin/systems/areaLights/surfaceLight") -- surfaceLight

Rectangular area lights — a screen, a window or a strip that emits from its whole surface, so its highlight is a shape rather than a dot.

Usage: local surfaceLight = require("@builtin/systems/areaLights/surfaceLight")

modules/surfaceLight/active

active(): boolean

Whether the area-lighting pass is currently running.

if surfaceLight.active() then print("emitting") end

modules/surfaceLight/buffers

buffers(): { [string]: any }?

The buffers the area-lighting pass reads: the settings and the packed light records. The render feature binds what this hands it.

local b = surfaceLight.buffers()

modules/surfaceLight/clear

clear()

Drop every area light and release the pass. The scales are kept.

surfaceLight.clear()

modules/surfaceLight/configure

configure(opts: SurfaceLightOpts?): SurfaceLightState

Adjust how the two terms are weighted. Any omitted field keeps its current value.

Parameters

  • opts SurfaceLightOpts? (optional) — Settings — see SurfaceLightOpts.
surfaceLight.configure({ specular = 0.5 })

modules/surfaceLight/count

count(): number

How many area lights are registered.

print(surfaceLight.count())

modules/surfaceLight/remove

remove(key: string): boolean

Remove the area light registered under key.

Parameters

  • key string — The identifier the light was registered with.
surfaceLight.remove("tv")

modules/surfaceLight/set

set(key: string, shape: SurfaceLightShape): number

Add or replace the area light registered under key. Re-submitting the same key moves that light rather than adding another, which is what lets a component push its rectangle every frame as its entity moves.

Parameters

  • key string — Stable identifier — an entity id works well.
  • shape SurfaceLightShape — The emitting rectangle — see SurfaceLightShape.
surfaceLight.set("tv", { position = { 0, 2, 0 }, right = { 1, 0, 0 }, up = { 0, 1, 0 }, width = 2, height = 1.2 })

modules/surfaceLight/settings

settings(): SurfaceLightState

The settings currently in force.

local d = surfaceLight.settings().diffuse

modules/temporalAntiAliasing/README

require("@builtin/systems/antiAliasing/temporalAntiAliasing") -- temporalAntiAliasing

Antialiasing that accumulates one sample per pixel per frame. The projection samples a different point inside each pixel every frame, and the frames are carried forward through the scene's own motion, so edges, highlights and fine texture settle instead of crawling.

Usage: local temporalAntiAliasing = require("@builtin/systems/antiAliasing/temporalAntiAliasing")

modules/temporalAntiAliasing/active

active(): boolean

Whether the temporal pass is running this frame.

if temporalAntiAliasing.active() then ... end

modules/temporalAntiAliasing/buffers

buffers(): { [string]: any }?

The parameter buffer the accumulation reads, carrying the settings this module packs. The render feature binds what this hands it.

local b = temporalAntiAliasing.buffers()

modules/temporalAntiAliasing/disable

disable()

Turn temporal antialiasing off and release the pass. The projection goes back to sampling pixel centres. The settings are kept, so a later enable() brings back the same tuning.

temporalAntiAliasing.disable()

modules/temporalAntiAliasing/enable

enable(opts: TemporalAntiAliasingOpts?): TemporalAntiAliasingState

Turn temporal antialiasing on and set it. Any omitted field keeps its current value.

Parameters

  • opts TemporalAntiAliasingOpts? (optional) — Temporal antialiasing settings — see TemporalAntiAliasingOpts.
temporalAntiAliasing.enable({ historyWeight = 0.9 })

modules/temporalAntiAliasing/get

get(): TemporalAntiAliasingState

The temporal antialiasing settings currently in force.

local w = temporalAntiAliasing.get().historyWeight

modules/temporalUpscale/README

require("@builtin/systems/temporalUpscale/temporalUpscale") -- temporalUpscale

Reconstruction of a display-resolution image from a lower-resolution render. The projection samples a different point inside each pixel every frame and the frames are carried forward through the scene's own motion, so a scene rasterized at a fraction of the display's pixels resolves detail no single one of those frames holds.

Usage: local temporalUpscale = require("@builtin/systems/temporalUpscale/temporalUpscale")

modules/temporalUpscale/active

active(): boolean

Whether the reconstruction is running this frame.

if temporalUpscale.active() then ... end

modules/temporalUpscale/buffers

buffers(): { [string]: any }?

The parameter buffer the reconstruction reads, carrying the settings this module packs. The render feature binds what this hands it.

local b = temporalUpscale.buffers()

modules/temporalUpscale/disable

disable()

Turn temporal upsampling off and release the pass. The projection goes back to sampling pixel centres, and the render scale returns to what it was before this module first moved it — unless something else has steered it since, which keeps that number. The settings are kept, so a later enable() brings back the same tuning.

temporalUpscale.disable()

modules/temporalUpscale/enable

enable(opts: TemporalUpscaleOpts?): TemporalUpscaleState

Turn temporal upsampling on and set it. Any omitted field keeps its current value. Passing renderScale also moves the resolution the scene rasterizes at, and disable puts back the scale that was in force before the first such move.

Parameters

  • opts TemporalUpscaleOpts? (optional) — Temporal upsampling settings — see TemporalUpscaleOpts.
temporalUpscale.enable({ renderScale = 0.5 })

modules/temporalUpscale/get

get(): TemporalUpscaleState

The temporal upsampling settings currently in force.

local s = temporalUpscale.get().renderScale

modules/text/README

require("@builtin/modules/api/engine/text") -- text (also available as global 'text')

Text rasterisation resource — create a text handle, set its content and style, then rasterise it to a texture for display, and observe what the text system is holding. Public Luau surface over the __text and __textObserve Internal FFI namespaces.

Usage: local text = require("@builtin/modules/api/engine/text") Also available as global: text

modules/text/alive

alive(handle: any): boolean

Whether the text system still holds this handle — true between text.create and the text.destroy that released it.

Parameters

  • handle any (optional) — Text handle from text.create.
if not text.alive(h) then h = text.create({ content = "again" }) end

modules/text/count

count(): number

How many text objects the text system is holding — the number that moves when text.create and text.destroy are called.

local before = text.count()

modules/text/create

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

  • options table — Table of content plus style fields (fontSize, color, alignment, richText, maxWidth, ...).
local h = text.create({ content = "Hello", fontSize = 48 })

modules/text/destroy

destroy(handle: any): boolean

Destroy a text handle and release its raster + glyph layout.

Parameters

  • handle any (optional) — Text handle from text.create.
text.destroy(h)

modules/text/face

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

  • handle any (optional) — Text handle from text.create.
local r = text.face(h).reason; if r == "familyUnknown" or r == "familyNotSelectable" then print(r) end

modules/text/faceReasons

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.

for _, r in ipairs(text.faceReasons()) do print(r) end

modules/text/listFonts

listFonts(): { string }

List the font families currently available to the text system.

local fonts = text.listFonts()

modules/text/loadFont

loadFont(ref: any): any

Load a font from an asset reference so it becomes available to setStyle's fontFamily.

Parameters

  • ref any (optional) — Font asset reference or path.
text.loadFont(asset.ref("fonts.inter", "font"))

modules/text/measure

measure(handle: any): any

Measure the rasterised text in pixels without producing a texture.

Parameters

  • handle any (optional) — Text handle from text.create.
local size = text.measure(h)

modules/text/observe

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.

local live = text.observe().count

modules/text/orphans

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.

print(#text.orphans() .. " labels outlived their entity")

modules/text/rasterMemory

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.

local r = text.rasterMemory(); print(r.bytes .. " of " .. r.poolBytes)

modules/text/rasterize

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

  • handle any (optional) — Text handle from text.create.
  • texture any (optional) — Destination GPU texture handle (renderer.texture.create) or its guid string — WHERE the raster lands.
  • scale number? (optional) — World/pixel scale factor for the raster (default 1.0).
local tex = renderer.texture.create({ width = 256, height = 64 })
local r = text.rasterize(h, tex, 1.0)

modules/text/setStyle

setStyle(handle: any, style: table): boolean

Replace the handle's style. Fields not present keep their current value.

Parameters

  • handle any (optional) — Text handle from text.create.
  • style table — Style table (fontSize, color, alignment, outline, ...).
text.setStyle(h, { fontSize = 64, color = "yellow" })

modules/text/setText

setText(handle: any, content: string): boolean

Replace the handle's text content.

Parameters

  • handle any (optional) — Text handle from text.create.
  • content string — New text string.
if not text.setText(h, "HP: 100") then h = text.create({ content = "HP: 100" }) end

modules/text/textureGuid

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

  • handle any (optional) — Text handle from text.create.
entity(id).component.get("Material"):setTexture("base_color_texture", text.textureGuid(h))

modules/trusted/README

require("@builtin/modules/trusted_client") -- trusted

User-space client for the trusted -> user call bridge (trusted.call).

Lets user-space scripts TRIGGER orchestration that lives in the engine's trusted VM (auth / connection / service flows we author in trusted Luau) WITHOUT exposing the trusted source or the privileged primitives those flows use. Backed by the single __trustedBridge.invoke Rust transport: the named call is marshalled into the trusted VM, run against its __TRUSTED_EXPORTS registry, and the result marshalled back as plain data. Trusted methods are registered on the other side by the trusted module /zero/trusted/exports (its expose(name, handler)). This module is auto-injected as the global trusted by the prelude.

Usage: local trusted = require("@builtin/modules/trusted_client")

modules/ui/README

require("@builtin/modules/api/engine/ui") -- ui (also available as global 'ui')

UI system — screens, styles, themes, widget responses, focus, custom-widget builders, per-widget state. Public Luau surface over the __ui Internal FFI namespace.

Usage: local ui = require("@builtin/modules/api/engine/ui") Also available as global: ui

modules/ui/blur

blur()

Surrender keyboard focus from whichever widget currently holds it.

modules/ui/bringAreaToFront

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

  • id string — Area widget id.

modules/ui/captureWindow

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

  • screen string — Screen id containing the target Window.
  • window string — Widget id of the Window.
  • opts CaptureOpts? (optional){ width, height } (optional).

modules/ui/click

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

  • callbackId string — Callback id assigned to the widget.
  • value any? (optional) — Optional value to pass with the callback.

modules/ui/defineStyle

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

  • name string — Style name.
  • style StyleProps — Style properties table.

modules/ui/defineStyles

defineStyles(styles: { [string]: StyleProps })

Define multiple named styles at once.

Parameters

  • styles { [string]: StyleProps } — Map of style name to style properties.

modules/ui/defineWidget

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

  • name string — Custom widget kind name.
  • builderFn (WidgetTree, { WidgetTree }) -> WidgetTree — Builder closure (props, children) -> widgetTable.

modules/ui/diagnose

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

  • widgetId string — The id the widget records layout under.
"hud-healthbar"

modules/ui/dragState

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.

modules/ui/elementTree

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

  • screenName string — Screen id passed to ui.registerScreen.

modules/ui/focus

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

  • widgetId string — Widget id to focus.

modules/ui/focusedWidget

focusedWidget(): string?

Return the widget id of whichever widget currently holds keyboard focus, or nil. Snapshotted post-render each frame.

modules/ui/getAreaPos

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

  • id string — Area widget id.

modules/ui/getAreaSize

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

  • id string — Area widget id.

modules/ui/getDockLayout

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

  • id string — DockArea widget id.

modules/ui/getLayoutInfo

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

  • widgetId string? (optional) — Optional widget id to query.

modules/ui/getScreenTree

getScreenTree(screenName: string): WidgetTree?

Return the last widget tree table passed to registerScreen / updateScreen for screenName.

Parameters

  • screenName string — Screen name to query.

modules/ui/getTheme

getTheme(): string

Get the name of the currently active theme.

modules/ui/getToken

getToken(name: string): string?

Look up a single design token value from the active theme.

Parameters

  • name string — Token name (without $ prefix).

modules/ui/getTokens

getTokens(): { [string]: string }

Get all design tokens from the active theme as a key-value map.

modules/ui/getWidgetProps

getWidgetProps(typeName: string): { WidgetPropDescriptor }?

Get the property definitions for a widget type.

Parameters

  • typeName string — Widget type name.

modules/ui/getWidgetTypes

getWidgetTypes(): { string }

Get all available widget type names that can be used in widget trees.

modules/ui/hideScreen

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

  • name string — Screen identifier to hide.

modules/ui/hitTest

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

  • x number — Logical X.
  • y number — Logical Y.
640, 360

modules/ui/invisibilityReasons

invisibilityReasons(): { string }

Every verdict ui.diagnose can report, as a closed list.

modules/ui/lastRegistration

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.

modules/ui/lastValidation

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, and wrap-label-no-width reports a label with props.wrap whose box came out narrower than its own longest word, once per label 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

  • screenName string? (optional) — Optional screen name.

modules/ui/listFonts

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.

for _, f in ui.listFonts() do print(f.family) end

modules/ui/listScreens

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.

modules/ui/listThemes

listThemes(): { string }

List all registered theme names.

modules/ui/observe

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

  • screenName string? (optional) — Narrow the rows to one screen. Omit for every screen.
"hud"

modules/ui/paintOrder

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

  • a string — First widget id.
  • b string — Second widget id.
"panel-a", "panel-b"

modules/ui/pixelRatio

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.

modules/ui/pointerWidget

pointerWidget(): PointerRead?

Whether the UI is consuming the pointer, and which widget holds it — the pointer counterpart of ui.focusedWidget().

modules/ui/registerBackgroundShader

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

  • shaderHandle any (optional) — The screen .shader's asset handle, from asset.resolve.
  • width number? (optional) — Render target width (default 1280).
  • height number? (optional) — Render target height (default 720).

modules/ui/registerCallbackEnv

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

  • key string — Stable identifier for this registration (re-register replaces).
  • env { [string]: any } — Environment table whose onCallback receives the broadcasts.

modules/ui/registerScreen

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

  • name string — Unique screen identifier.
  • widgetTree WidgetTree — Root widget table.
  • layer number? (optional) — Z-order layer (optional).
ui.registerScreen("hud", tree)

modules/ui/registerTheme

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

  • name string — Theme name to register.
  • theme ThemeDefinition — Flat-resolved theme table.

modules/ui/removeScreen

removeScreen(name: string): boolean

Alias for ui.unregisterScreen.

Parameters

  • name string — Screen identifier to remove.

modules/ui/resetAreaSize

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

  • id string — Area widget id.

modules/ui/response

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

  • widgetId string — The widget id (NOT the onClick / onChange callback id).

modules/ui/screen

screen(name: string): { [string]: any }?

Get a screen proxy with methods like setResolution and rasterize.

Parameters

  • name string — Screen name.

modules/ui/screenSize

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.

It follows the size the engine draws at, and renderer.setViewportSize changes that size at runtime — which is how a layout written against this is checked at a second shape without rebooting the engine. The pixel size behind these points is renderer.surfaceSize() — the whole drawing surface a screen is laid out over, which an editor layout makes larger than the rect renderer.viewportSize() draws the scene into — and ui.pixelRatio() is the factor between the two spaces.

modules/ui/scroll

scroll(deltaX: number, deltaY: number)

Simulate a mouse-wheel scroll event on the UI.

Parameters

  • deltaX number — Horizontal scroll delta.
  • deltaY number — Vertical scroll delta.

modules/ui/setAreaPos

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

  • id string — Area widget id.
  • x number — Target pivot x (screen coords).
  • y number — Target pivot y (screen coords).

modules/ui/setAreaSize

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

  • id string — Area widget id.
  • w number — Target width (screen coords).
  • h number — Target height (screen coords).

modules/ui/setDockWindowRect

setDockWindowRect(

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.

modules/ui/setScreenRenderLayer

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

  • name string — Screen identifier.
  • mask number — Render-layer membership bitmask.

modules/ui/setScrollPosition

setScrollPosition(widgetId: string, offsetY: number)

Set the scroll offset of a scrollArea widget.

Parameters

  • widgetId string — Scroll area widget id.
  • offsetY number — Vertical scroll offset in pixels.

modules/ui/setShaderUniforms

setShaderUniforms(name: string, uniforms: { [string]: number })

Set uniform values on a registered background shader.

Parameters

  • name string — Shader name identifier.
  • uniforms { [string]: number } — Map of uniform name to number value.

modules/ui/setTheme

setTheme(name: string)

Switch the active global theme by name.

Parameters

  • name string — Theme name to activate.

modules/ui/showScreen

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

  • name string — Screen identifier to show.

modules/ui/unregisterCallbackEnv

unregisterCallbackEnv(key: string)

Remove an environment registered with ui.registerCallbackEnv. Its onCallback stops receiving broadcasts. No-op if key isn't registered.

Parameters

  • key string — The key passed to ui.registerCallbackEnv.

modules/ui/unregisterScreen

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

  • name string — Screen identifier to unregister.

modules/ui/unregisterWidget

unregisterWidget(name: string)

Drop a registered custom widget kind. Subsequent references produce an unknown-widget-type diagnostic.

Parameters

  • name string — Custom widget kind name.

modules/ui/updateScreen

updateScreen(name: string, widgetTree: WidgetTree)

Replace the widget tree of an already-registered screen.

Parameters

  • name string — Screen identifier to update.
  • widgetTree WidgetTree — New root widget table.

modules/ui/useStyles

useStyles(themeName: string)

Apply a registered style file's classes additively without changing the active theme.

Parameters

  • themeName string — Name of the registered style / theme asset.

modules/ui/widgetState

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

  • widgetId string — Widget id whose state to read.
  • key string — State key.
  • default any? (optional) — Value to return when nothing has been written.

modules/ui/widgetStateClear

widgetStateClear(widgetId: string, key: string)

Remove a per-widget state entry.

Parameters

  • widgetId string — Widget id whose state to clear.
  • key string — State key.

modules/ui/widgetStateSet

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

  • widgetId string — Widget id to scope the state under.
  • key string — State key.
  • value any (optional) — Value to store (must be non-nil).

modules/unwatch

modules.unwatch(watcherId) -> boolean

Remove a previously registered module source watcher by its ID.

Parameters

  • watcherId number — Watcher ID returned by modules.watch()

Returns boolean — true if watcher was found and removed

modules/userfile/README

require("@builtin/modules/api/engine/userfile") -- userfile (also available as global 'userfile')

User-system → engine file upload. Opens the user's own file picker (native OS dialog / Android / web browser) and brings the chosen file(s) into the engine. Public Luau surface over the __userfile Internal FFI namespace.

Usage: local userfile = require("@builtin/modules/api/engine/userfile") Also available as global: userfile

modules/userfile/pick

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

  • opts PickOpts? (optional) — Picker options (optional): multiple, folder, title, filters, writeTo.
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

modules/userfile/pickFolder

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

  • opts PickOpts? (optional) — Picker options (optional); folder is forced true.
local r = userfile.pickFolder({ writeTo = "/source/imported/" })

modules/velocityDilation/README

require("@builtin/systems/velocityDilation/velocityDilation") -- velocityDilation

How far a moving surface's velocity reaches past its own silhouette in the frame's velocity buffer. Every temporal technique reads that buffer one texel per pixel, so widening it is what lets a reprojection and a blur follow a moving object over the pixels it is about to cover.

Usage: local velocityDilation = require("@builtin/systems/velocityDilation/velocityDilation")

modules/velocityDilation/active

active(): boolean

Whether the dilation passes are running this frame.

if velocityDilation.active() then ... end

modules/velocityDilation/clear

clear()

Set the radius to 0 and release the passes, leaving the velocity buffer as the geometry passes wrote it.

velocityDilation.clear()

modules/velocityDilation/get

get(): VelocityDilationState

The dilation settings currently in force.

local r = velocityDilation.get().radius

modules/velocityDilation/maxRadius

maxRadius(): number

The widest neighbourhood set accepts, in pixels.

local ceiling = velocityDilation.maxRadius()

modules/velocityDilation/paramsBuffer

paramsBuffer(): any

The parameter buffer the dilation pass reads, carrying the radius this module packs. The render feature binds what this hands it.

local b = velocityDilation.paramsBuffer()

modules/velocityDilation/set

set(opts: VelocityDilationOpts?): VelocityDilationState

Set how far velocity reaches past a silhouette. Any omitted field keeps its current value. A radius of 0 releases the passes and leaves the velocity buffer as the geometry passes wrote it.

Parameters

  • opts VelocityDilationOpts? (optional) — Dilation settings — see VelocityDilationOpts.
velocityDilation.set({ radius = 2 })

modules/vfs/README

require("@builtin/modules/api/engine/vfs") -- vfs (also available as global 'vfs')

Virtual filesystem — read, write, list, watch. Public Luau surface over the __vfs Internal FFI namespace.

A write to authored /source WHILE PLAY IS RUNNING lands on the play shadow: the bytes are live in the session immediately, disk source is untouched, and a guarded play-exit discards them unless they were kept. This is why a session's work can read back correctly and still stage nothing — zm add and a commit look at disk, and the shadow is not on it. vfs.durability(path) answers where any write's bytes went: durable, the state in warning, the routes out in playShadow, and which of the paths you asked about the shadow holds in shadowed. vfs.playShadowPaths() lists everything the session is holding, and vfs.playShadowAuthors() says whose each one is. Three routes keep a write, differing in what they cost the session: vfs.promotePlayShadow(path) -- or a list of paths: THOSE -- paths onto canonical source, -- play keeps running, the rest -- of the set untouched vfs.write(path, bytes, { durable = true }) -- the write skips the -- shadow, nothing to promote tools.use("sceneAuthoring", "changes") -- then acceptChanges: reaches -- ENTITY changes too, session -- paused until the verdict vfs.revertPlayShadow takes the same path or list and does the opposite. Promote the whole SET the asset landed, not the path you passed: a path carrying an asset-type suffix names the asset, so one .component write shadows its entry file, its README and its metadata together, and a play-exit refuses over whichever of them are left behind. Read that set out of vfs.playShadowPaths(), filtered to the paths under the asset. The core/vfs guide has the model and what each route costs.

Usage: local vfs = require("@builtin/modules/api/engine/vfs") Also available as global: vfs

modules/vfs/clearPlayShadow

clearPlayShadow(): boolean

Forget the entire play-shadow set after a bulk promote or discard. Tracking only — never touches the bytes.

vfs.clearPlayShadow()

modules/vfs/copy

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

  • src string — Source absolute VFS path (file or directory).
  • dst string — Destination absolute VFS path.
vfs.copy("/zero/runtime/recordings/take1.mp4", "/zero/source/clips/take1.mp4")

modules/vfs/currentAuthor

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.

local me = vfs.currentAuthor()
print(if me ~= nil then me.id else "unattributed")

modules/vfs/durability

durability(paths: string | { string }): { durable: boolean, warning: string?, playShadow: string?, shadowed: { string }? }

Did that write save to disk, and if not how is it kept? What became of the bytes just written at paths — one path, or an array of them answered as ONE write. durable is true when they are where the call filed them, and false when the play shadow took any of them: live in this session, disk source untouched, discarded on a guarded play-exit unless kept. durable is present whatever the answer is, so its absence is never a reading. A non-durable answer carries warning (the state, for a reader scanning values rather than checking a field), playShadow (the routes to disk and what each costs a session other people are running in) and shadowed (which of the given paths the shadow holds).

This is the answer, off the same shadow set and in the same words, that the write_file / edit_file / capture tools attach to their own results and that asset.create reports as its second return value. Ask it here at any other site that lands files, so every write surface states where the bytes went in one set of terms.

Parameters

  • paths string | { string } — One VFS path, or an array of paths answered together as one write. A path resolves the way vfs.write resolves its own — absolute or @-rooted as it stands, a bare one under /source/ — so the answer is about the file that write landed. A value that is not a path — an AssetRef, a record, a number, a string with nothing in it — raises rather than being answered off the empty set it reads as; a list with no entries names nothing and answers durable.
local ok = vfs.write(path, body)
local d = vfs.durability(path)
if not d.durable then log.warn(d.warning .. " " .. d.playShadow) end

modules/vfs/evict

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

  • path string — VFS path whose bytes should be evicted.
  • opts VfsOpts? (optional){ root = "/source/" }.
vfs.evict("/zero/source/textures/imported_big.png")

modules/vfs/exists

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

  • path string — VFS path to check.
  • opts VfsOpts? (optional){ root = "/source/" }.
assert(vfs.exists("@builtin/models/Cube"))

modules/vfs/isDirectory

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

  • path string — VFS path to classify.
  • opts VfsOpts? (optional){ root = "/source/" }.
if vfs.isDirectory("/zero/source/Goblin.dynamicAsset") then print("folder asset") end

modules/vfs/isSaveExcluded

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

  • path string — VFS path to classify.
  • opts VfsOpts? (optional){ root = "/source/" }.
if not vfs.isSaveExcluded(p) then table.insert(publishable, p) end

modules/vfs/list

list(path: string?): { VfsListEntry }

List entries in a VFS directory.

Parameters

  • path string? (optional) — Directory path (defaults to /zero).
for _, e in ipairs(vfs.list("/zero/source")) do print(e.name) end

modules/vfs/memResident

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.

for _, e in ipairs(vfs.memResident()) do print(e.path, e.bytes, e.kind) end

modules/vfs/mkdir

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

  • path string — Directory path.
  • opts VfsOpts? (optional){ root = "/source/" }.
vfs.mkdir("/zero/source/scenes/")

modules/vfs/move

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

  • src string — Source absolute VFS path.
  • dst string — Destination absolute VFS path.
  • opts { quiet: boolean? }? (optional) — Optional { quiet: boolean? }.
vfs.move("/zero/source/a.luau", "/zero/source/b.luau")

modules/vfs/mutationSeq

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.

local at = vfs.mutationSeq()
vfs.write("/zero/source/tmp/note.txt", "hi")
repeat task.wait() until vfs.mutationSeq() > at

modules/vfs/pendingWrites

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.

for _, p in ipairs(vfs.pendingWrites()) do print(p) end

modules/vfs/playShadowAuthors

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.

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

modules/vfs/playShadowPaths

playShadowPaths(): { string }

Every source write made during play that is not durable yet — the set to keep or discard before leaving play. Lists 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.

for _, p in ipairs(vfs.playShadowPaths()) do print(p) end

modules/vfs/promotePlayShadow

promotePlayShadow(path: string | { string }): string | { string }

KEEP a source write made while play was running — the call that saves a play-mode edit onto disk and makes it durable. Promotes play-shadow edits into canonical writes: re-asserts the live overlay bytes through the full write pipeline, then unmarks each path. The bytes stay in the engine end to end, so binary content promotes exactly. It answers while play is RUNNING and leaves the mode, the clock and every shadow entry it did not name exactly where they stood. Takes ONE path, or an ARRAY of them — a single folder-asset write shadows the entry file, the README and the metadata together, so a slice is tens of paths, and naming them keeps the call to the caller's own work on an engine other sessions are running in. Every named path is attempted; one that refuses does not stop the ones after it. A promotion that cannot happen raises with the reason: the path is not shadowed, the path is a folder covering shadowed edits, 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

  • path string | { string } — A shadowed VFS path to promote, or an array of them (from vfs.playShadowPaths()).
local promoted = vfs.promotePlayShadow("/zero/source/cover.jpg")
local mine = {}
for _, p in ipairs(vfs.playShadowPaths()) do
if string.find(p, "/zero/source/mine/", 1, true) == 1 then table.insert(mine, p) end
end
local settled = vfs.promotePlayShadow(mine)

modules/vfs/read

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

  • path string — VFS path.
  • opts VfsOpts? (optional){ root = "/source/" }.
local src = vfs.read("@builtin/components/Camera.luau")

modules/vfs/readAsync

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

  • path string — VFS path.
  • opts VfsOpts? (optional){ root = "/source/" }.
local data = task.await(vfs.readAsync("/zero/runtime/screenshots/last.png"))

modules/vfs/reload

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

  • modulePath string? (optional) — Module identity to reload (omit to reload all).
vfs.reload("@mylib/utils.helpers")

modules/vfs/remove

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

  • path string — VFS path to remove.
  • opts VfsOpts? (optional){ root = "/source/", recursive = false }.
vfs.remove("/zero/source/scratch.luau")

modules/vfs/revertPlayShadow

revertPlayShadow(path: string | { string }): string | { string }

DISCARD a source write made while play was running, keeping nothing — the opposite of promoting it. Reverts play-shadow edits: restores 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. It answers while play is RUNNING and takes ONE path or an ARRAY of them, on the same terms as vfs.promotePlayShadow. A revert that cannot happen raises with the reason.

Parameters

  • path string | { string } — A shadowed VFS path to revert, or an array of them (from vfs.playShadowPaths()).
local reverted = vfs.revertPlayShadow("/zero/source/Foo.component/init.luau")
local dropped = vfs.revertPlayShadow({ "/zero/source/a.md", "/zero/source/b.md" })

modules/vfs/unmarkPlayShadow

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

  • path string — VFS path to unmark.
vfs.unmarkPlayShadow("/zero/source/Foo.component/init.luau")

modules/vfs/unwatch

unwatch(watcherId: number): boolean

Remove a previously registered VFS watcher.

Parameters

  • watcherId number — Watcher id returned by vfs.watch.
vfs.unwatch(id)

modules/vfs/watch

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

  • path string — Exact path, or folder path ending in /.
  • callback (string, string) -> ()(mutated_path, kind) -> (), kind "write" or "remove".
local id = vfs.watch("/zero/source/", function(path, kind) print(kind, path) end)

modules/vfs/write

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

  • path string — VFS path to write to.
  • content string — File content (binary-safe).
  • opts VfsOpts? (optional){ root = "/source/", overwrite = true, quiet = false, durable = false }.
vfs.write("/zero/source/notes.md", body)
vfs.write("/zero/source/game/Vent.component/init.luau", src, { durable = true })

modules/video/README

require("@builtin/modules/api/engine/video") -- video (also available as global 'video')

Video playback — create / play / pause / seek / setRate / setLoop / destroy on render-target-backed video players. Public Luau surface over the __video Internal FFI namespace.

Usage: local video = require("@builtin/modules/api/engine/video") Also available as global: video

modules/video/create

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

  • url string — URL or asset path to an MP4 video file.
  • options VideoOptions? (optional) — Playback options: loop (default false), autoplay (default false), rate (default 1.0).
local tex = video.create("http://example.com/clip.mp4", { autoplay = true })

modules/video/destroy

destroy(handle: string): boolean

Destroy a video player and free the render target and all resources.

Parameters

  • handle string — Video handle from video.create.
video.destroy(rt)

modules/video/getInfo

getInfo(handle: string): VideoInfo?

Get video information and current playback state.

Parameters

  • handle string — Video handle.
local i = video.getInfo(rt); print(i.currentTime, "/", i.duration)

modules/video/pause

pause(handle: string): boolean

Pause video playback. Can be resumed with video.play.

Parameters

  • handle string — Video handle.
video.pause(rt)

modules/video/play

play(handle: string): boolean

Start or resume video playback.

Parameters

  • handle string — Video handle from video.create.
video.play(rt)

modules/video/seek

seek(handle: string, time: number): boolean

Seek to a specific time (seconds) in the video.

Parameters

  • handle string — Video handle.
  • time number — Target time in seconds.
video.seek(rt, 30.5)

modules/video/setLoop

setLoop(handle: string, loop: boolean): boolean

Enable or disable looping.

Parameters

  • handle string — Video handle.
  • loop boolean — Whether to loop playback.
video.setLoop(rt, true)

modules/video/setRate

setRate(handle: string, rate: number): boolean

Set the playback speed multiplier. 1.0 = normal, 2.0 = double speed, 0.5 = half speed.

Parameters

  • handle string — Video handle.
  • rate number — Playback rate.
video.setRate(rt, 2.0)

modules/video/stop

stop(handle: string): boolean

Stop video playback and reset to the beginning.

Parameters

  • handle string — Video handle.
video.stop(rt)

modules/volume/README

require("@builtin/systems/volumetrics/volume") -- volume

3D textures as volumes: build one procedurally or from imported voxels, page it into bricks so it costs what it holds, and raymarch it through an entity's transform.

Usage: local volume = require("@builtin/systems/volumetrics/volume")

modules/volume/Volume:buildSparse

Volume:buildSparse(opts: SparseOpts?)

Store this volume as bricks, keeping only the ones holding anything over threshold. What the volume costs on the GPU becomes what it contains rather than the size of its bounding box, and the page table built along the way is the grid the raymarcher skips empty space with. Counting the bricks reads the allocation counter back from the GPU, so this yields — twice unless capacity says how many slots to take; refreshSparse re-pages without waiting on either.

Parameters

  • opts SparseOpts? (optional) — See SparseOpts — brick size, density threshold, which channel carries density, and how much slot headroom to leave for a volume that will grow.
local stats = v:buildSparse({ brickSize = 8, threshold = 0.01 })
print(stats.residentBytes, stats.denseBytes)

modules/volume/Volume:destroy

Volume:destroy()

Free the GPU resources backing this Volume — the named 3D texture, any companion occupancy R8 texture, and the brick pool and page table if the volume is paged. Subsequent operations on the wrapper return false silently.

v:destroy()

modules/volume/Volume:readSparseCounts

Volume:readSparseCounts(): { [string]: number }

Read how many bricks the last paging pass stored and how many found the pool full. Yields until the counter arrives.

local counts = v:readSparseCounts()

modules/volume/Volume:refreshSparse

Volume:refreshSparse(): boolean

Re-page this volume from its current contents. Two dispatches and no wait, so a volume a compute shader rewrites every frame keeps a page table that matches what it now holds. The brick counts the pass produces are collected on a later call rather than waited on, so a volume that grows past its pool reports the overflow — and warns — a frame or two after the refresh that caused it.

v:fillCompute(myShader); v:refreshSparse()

modules/volume/Volume:releaseDense

Volume:releaseDense(): boolean

Free the dense volume, leaving the brick pool as the only copy. The volume goes on rendering from the pool; re-paging it needs the dense copy, so this is for a volume that is finished changing.

v:buildSparse(); v:releaseDense()

modules/volume/Volume:releaseSparse

Volume:releaseSparse(): boolean

Free the brick pool and page table, returning the volume to rendering from its dense texture. The dense copy has to still be there, so a volume that released it stays paged.

v:buildSparse(); v:releaseSparse()

modules/volume/Volume:render

Volume:render(opts: RenderOpts): string

Dispatch the shared compute raymarcher to render this Volume through the entity's transform AABB. Output goes to a 2D storage texture (auto-created) — composite from there via a post-process effect, a material sampler, or another GPU copy.

Parameters

  • opts RenderOpts — See RenderOpts above.
v:render({ entity = entity(id), targetWidth = 1280, targetHeight = 720, density = 8.0 })

modules/volume/Volume:sparseStats

Volume:sparseStats(): { [string]: any }

What this volume costs on the GPU, and what it would cost dense.

local s = v:sparseStats(); print(s.residentBytes / s.denseBytes)

modules/volume/box

box(opts: { [string]: any })

Build a 3D volume filled with a solid-box SDF.

Parameters

  • opts { [string]: any }{name, size, color={1,1,1}, format="rgba16f"}.
local v = M.box({ name = "cube_volume", size = 64 })

modules/volume/compositeShaderSource

compositeShaderSource(): (string, string)

WGSL source for the post-process composite that blends the volume render target over the scene with premultiplied-alpha. Zero-scaffolding: author only fragment(); vol_rt is a declared texture property. Register with postprocess.add(name, src, { properties = {{ name="vol_rt", type="texture", textureDefault="transparent" }} }) and bind the target via postprocess.setTexture(name, "vol_rt", <render-target>).

local src, name = M.compositeShaderSource()

modules/volume/create

create(opts: CreateOpts)

Allocate a fresh, zero-filled 3D texture and return a Volume wrapper bound to its name. The volume answers to that name in M.get from here on; building over a name a live volume already holds releases that volume first, so one volume owns the name and its brick pool.

Parameters

  • opts CreateOpts — Creation options; see CreateOpts.
local v = M.create({ name = "cloud", width = 64, height = 64, depth = 64, storage = true })

modules/volume/get

get(name: string): any

The live volume built under name, with everything it knows about itself: its voxel dimensions, its channel count, its occupancy grid, and the brick pool it is paged into. Every volume this module builds answers to its texture name here, so a script or a component that has only the name renders the volume as it stands — including a paged one, whose voxels live in the pool rather than under that name.

Parameters

  • name string — The texture name the volume was built under.
local v = M.get("cloud")
if v then v:render({ entity = e }) end

modules/volume/names

names(): { string }

The names every live volume is registered under, sorted.

for _, name in ipairs(M.names()) do print(name, M.get(name):sparseStats().residentBytes) end

modules/volume/noise

noise(opts: { [string]: any })

Build a 3D volume filled with FBM value-noise. Good base for procedural clouds, smoke, nebulae.

Parameters

  • opts { [string]: any }{name, size=64, scale=4, octaves=4, threshold=0.5, seed=42, color={1,1,1}, format="rgba16f"}.
local cloud = M.noise({ name = "cloud", size = 64, scale = 4, threshold = 0.55 })

modules/volume/raymarchShaderIdentity

raymarchShaderIdentity(): string

Return the built-in raymarch shader's asset identity. Callers that want to diff or override the default raymarcher pull source via asset.inspect(Volume.raymarchShaderIdentity()) and register their variant under a different name.

local src = vfs.read(asset.inspect(M.raymarchShaderIdentity()).source .. "/shader.wgsl")

modules/volume/register

register(volume: any): any

Publish a wrapped volume under its own texture name, so holders of the name reach this object rather than building a wrapper of their own. Volumes this module builds are published as they are created; a wrapper over a texture another system owns is published by this call.

Parameters

  • volume any (optional) — A Volume — the value M.wrap or a builder returned.
M.register(M.wrap({ name = "imported", width = 64, height = 64, depth = 64 }))

modules/volume/releaseTarget

releaseTarget(name: string): boolean

Destroy the 2D storage texture a render names as its target and forget the size this module built it at, so the next render builds it again. A target is created on the first render that names it and shared by every volume marching into it.

Parameters

  • name string — The render-target name, as passed to Volume:render.
M.releaseTarget("scene_volume_rt")

modules/volume/sphere

sphere(opts: { [string]: any })

Build a 3D volume filled with a soft sphere SDF (centred at the volume's middle).

Parameters

  • opts { [string]: any }{name, size, falloffStart=0.3, falloffEnd=0.5, color={1,1,1}, format="rgba16f"}.
local v = M.sphere({ name = "blob", size = 64, falloffStart = 0.3 })

modules/volume/unregister

unregister(name: string): any

Drop the registration under name and hand back the volume that held it, leaving its GPU resources alone. The returned volume is the way back to the data — destroy it to free the texture and any brick pool.

Parameters

  • name string — The texture name to stop answering for.
local v = M.unregister("imported")
if v then v:destroy() end

modules/volume/wrap

wrap(opts: { [string]: any })

Wrap an existing named 3D texture as a Volume without creating a new one. Use when a producer outside this module owns the texture lifetime (e.g. the .zvol viewer uploads voxels through compute.writeFloatsTexture3D and then wraps the result for rendering).

Parameters

  • opts { [string]: any }{name, width?, height?, depth?, format?, occupancyName?, occupancyBrick?}.
local v = M.wrap({ name = "imported", format = "rgba16f", occupancyName = "imported_occ", occupancyBrick = 8 })

modules/volume/zvolHeader

zvolHeader(bytes: string): (ZvolHeader?, string?)

Read a .zvol file's header. The payload it describes uploads verbatim — payloadOffset and payloadBytes cut the voxel bytes out of the same string, and writeBytes takes them as they are.

Parameters

  • bytes string — The file's contents.
local h = M.zvolHeader(vfs.read(path))
v:writeBytes(string.sub(bytes, h.payloadOffset, h.payloadOffset + h.payloadBytes - 1))

modules/volumeSequence/README

require("@builtin/systems/volumetrics/volumeSequence") -- volumeSequence

An ordered run of .zvol frames played back through a fixed ring of resident volumes: frames are read from the VFS a little ahead of the playhead and uploaded into whichever ring slot is furthest from it, so what the sequence costs on the GPU is the ring, whatever the run's length.

Usage: local volumeSequence = require("@builtin/systems/volumetrics/volumeSequence")

modules/volumeSequence/Sequence:bounds

Sequence:bounds(): ({ number }, { number })

The world-space bounds frame 1's header declared, as the importer wrote them.

local mn, mx = seq:bounds()

modules/volumeSequence/Sequence:close

Sequence:close(): boolean

Cancel any read still in flight, free the ring and drop the sequence from the registry.

seq:close()

modules/volumeSequence/Sequence:current

Sequence:current()

The volume holding the frame currently on screen. Render it the way any other volume renders — the wrapper it answers with is one of the ring's slots, so the same handle comes back for as long as that frame is shown.

local v = seq:current(); if v then v:render({ entity = e, density = 6 }) end

modules/volumeSequence/Sequence:pause

Sequence:pause(): boolean

Hold the playhead where it is. Reads already in flight still land.

seq:pause()

modules/volumeSequence/Sequence:play

Sequence:play(): boolean

Resume advancing the playhead.

seq:play()

modules/volumeSequence/Sequence:residency

Sequence:residency(): { { frame: number?, loading: boolean } }

What each ring slot holds, in slot order — one entry per slot, whether or not it has ever been filled. A closed sequence holds no slots and answers with an empty array.

for i, slot in ipairs(seq:residency()) do print(i, slot.frame, slot.loading) end

modules/volumeSequence/Sequence:seek

Sequence:seek(frame: number): number

Put the playhead on a frame. The clock moves with it, so playback resumes from there rather than jumping back.

Parameters

  • frame number — 1-based frame index; wrapped for a looping sequence, clamped otherwise.
seq:seek(12)

modules/volumeSequence/Sequence:setFps

Sequence:setFps(fps: number): number

Set the playback rate. The playhead keeps the frame it stands on and the fraction of the way through it, so playback carries on from there at the new rate. Zero holds the playhead while leaving playing alone.

Parameters

  • fps number — Frames per second.
seq:setFps(12)

modules/volumeSequence/Sequence:stats

Sequence:stats(): { [string]: any }

What the sequence costs and how playback is keeping up. residentBytes counts the ring, allBytes counts what holding every frame at once would cost, stalls counts the frames playback reached before their read did, and errors counts the frames of the run that could not be read — each one counted and logged once, and stepped over from then on.

local s = seq:stats(); print(s.residentBytes, s.allBytes)

modules/volumeSequence/Sequence:tick

Sequence:tick(dt: number): number?

Advance the playhead by dt seconds, start the reads the new position calls for, and promote the newest frame whose slot has filled. The playhead moves once per engine frame however many callers tick the sequence, so two entities can play one run without it running double speed; every call promotes and reads ahead.

Parameters

  • dt number — Seconds since the last call.
seq:tick(dt)

modules/volumeSequence/all

all(): { [string]: any }

Every sequence currently open, by name.

for name, seq in pairs(M.all()) do print(name, seq:stats().residentBytes) end

modules/volumeSequence/close

close(name: string): boolean

Close the sequence open under this name.

Parameters

  • name string — Name passed to open.
M.close("plume")

modules/volumeSequence/get

get(name: string)

The sequence open under this name.

Parameters

  • name string — Name passed to open.
local seq = M.get("plume")

modules/volumeSequence/open

open(opts: OpenOpts)

Open a run of .zvol frames for playback. Frame 1's header sizes the ring, so every frame of the run has to carry the same dimensions; the ring itself is resident volumes and is the whole GPU cost of the sequence. Reading frame 1 yields.

Parameters

  • opts OpenOpts — See OpenOpts — the frame paths, the ring size, and the playback rate.
local seq = M.open({ name = "plume", frames = paths, resident = 3, fps = 24 })
seq:tick(dt); local v = seq:current(); if v then v:render({ entity = e }) end

modules/volumetricLighting/README

require("@builtin/systems/volumetrics/volumetricLighting") -- volumetricLighting

Light scattered by the air between the camera and the scene — a spotlight cone visible in fog, a shaft where the sun cuts past an occluder, haze that brightens toward a source.

Usage: local volumetricLighting = require("@builtin/systems/volumetrics/volumetricLighting")

modules/volumetricLighting/active

active(): boolean

Whether the volumetric passes are running this frame.

if volumetricLighting.active() then ... end

modules/volumetricLighting/clear

clear()

Empty the air and release the passes. The other settings are kept, so a later set({ density = ... }) brings back the same medium.

volumetricLighting.clear()

modules/volumetricLighting/get

get(): VolumetricState

The medium settings currently in force.

local d = volumetricLighting.get().density

modules/volumetricLighting/paramsBuffer

paramsBuffer(): any?

The buffer the marching passes read. lightScattering.renderFeature binds what this hands it, so both passes carry the values this module packed.

local p = volumetricLighting.paramsBuffer()

modules/volumetricLighting/set

set(opts: VolumetricOpts?): VolumetricState

Set the scene's participating medium. Any omitted field keeps its current value. A density of 0 empties the air and releases the passes.

Parameters

  • opts VolumetricOpts? (optional) — Medium settings — see VolumetricOpts.
volumetricLighting.set({ density = 0.06, anisotropy = 0.7 })

modules/watch

modules.watch(path, callback) -> number

Register a callback that fires when a module's source code changes via VFS write. Use for live-reload: re-require the module inside the callback. Returns a watcher ID.

Parameters

  • path string — Module require path to watch (e.g. '@builtin/modules/terminal')
  • callback function — Called with (path) when the module source changes

Returns number — Watcher ID (pass to modules.unwatch to remove)

modules/weather/README

require("@builtin/systems/weather/weather") -- weather

How surfaces respond to weather — wetness darkens and sharpens, snow settles on what faces up, and anything under cover stays clear.

Usage: local weather = require("@builtin/systems/weather/weather")

modules/weather/active

active(): boolean

Whether the weather pass is currently running.

if weather.active() then print("wet") end

modules/weather/bakeShelter

bakeShelter(): ShelterField

Re-derive the cover field the shelter test reads from the geometry the renderer currently draws. The field is baked when weather starts and re-derived when the count of renderables moves; call this after moving or reshaping cover that left that count where it was.

weather.bakeShelter()

modules/weather/clear

clear()

Clear the weather and release the pass. The other settings are kept, so a later set brings back the same look.

weather.clear()

modules/weather/coverBuffer

coverBuffer(): any?

The cover field the shelter test reads, as a buffer a pass binds.

local b = <module>.coverBuffer()

modules/weather/get

get(): WeatherState

The weather currently in force.

local w = weather.get().wetness

modules/weather/paramsBuffer

paramsBuffer(): any?

The parameter buffer this system's passes read. A pass binds what this hands it, so it has the values this module packed.

local b = <module>.paramsBuffer()

modules/weather/refresh

refresh()

Re-pack the buffer against the light the scene is standing in now — its ambient and its sun — and bring the cover field along with the scene. The pass runs every frame and the scene's lighting moves between frames, so the sheen follows the light rather than the value it had when the weather was last set. Writes only when something in the buffer has moved.

weather.refresh()

modules/weather/set

set(opts: WeatherOpts?): WeatherState

Set the scene's weather. Any omitted field keeps its current value, so a call can move one knob without restating the rest. With both wetness and snow at 0 nothing is falling and the pass is released.

Parameters

  • opts WeatherOpts? (optional) — Weather settings — see WeatherOpts.
weather.set({ wetness = 0.8, snow = 0 })

modules/weather/shelterField

shelterField(): ShelterField

Where the cover field the shelter test reads currently stands. A resolution of 0 means no field is baked, and every surface is then open to the sky.

print(weather.shelterField().filled, "columns hold a surface")

modules/world_defaults/README

world (global)

Grafts the per-world defaults + args + lifecycle-callback registry onto the world global. Companion to world_vcs.module. Backed by .world_settings for the persisted fields; callbacks live in in-process registries. Slot fields exposed on world.* (4 per-mode + 1 mode-agnostic): - avatar_default_edit AssetRef (tag: playerAvatar) - avatar_default_play AssetRef (tag: playerAvatar) - camera_default_edit AssetRef (tag: cameraBehavior) - camera_default_play AssetRef (tag: cameraBehavior) - startup_scene AssetRef — engine auto-loads this scene at boot when set Per-mode slots are selected by wld.mode() at spawner-resolution time (player_spawner / camera_spawner). Worlds that don't care about the split set both halves to the same value. All setters are play-mode-gated: writes target .world_settings (a source-VFS file) which is locked in play mode. The gate raises a typed error at the API boundary instead of letting the call hit a downstream "VFS write refused" error. Set engine.mode = "edit" first. All bundle-typed setters enforce the slot's required tag (§ 7 of the player-camera-unification plan). Writing a wrongly-tagged ref is refused with a clear message naming the missing tag. Also available as global: world

modules/world_defaults/offLoaded

offLoaded(handle: number): boolean

Stop a callback registered with world.onLoaded from running.

Parameters

  • handle number — The handle world.onLoaded returned.
world.offLoaded(h)

modules/world_defaults/offSaved

offSaved(handle: number): boolean

Stop a callback registered with world.onSaved from running.

Parameters

  • handle number — The handle world.onSaved returned.
world.offSaved(h)

modules/world_defaults/offUnloaded

offUnloaded(handle: number): boolean

Stop a callback registered with world.onUnloaded from running.

Parameters

  • handle number — The handle world.onUnloaded returned.
world.offUnloaded(h)

modules/world_defaults/onLoaded

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.
local h = world.onLoaded(function() log.info("loaded") end)

modules/world_defaults/onSaved

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.
local h = world.onSaved(function() log.info("saved") end)

modules/world_defaults/onUnloaded

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.
local h = world.onUnloaded(function() log.info("unloaded") end)

modules/world_status/README

world_status

Composes world.status() + world.status_text() — a snapshot of world identity, mode, running flag, primary scene, all loaded scenes, and players across all scenes. Pure-Luau (no Rust); uses existing globals: world.guid / world.branch / world.title, engine.mode, layers.active, layers.list, scene proxy's players / camera registries, world.connectedUsers.localUser. Installed onto the world global by world_defaults.module/init.luau via the world-namespace metatable machinery (see § 4b of the player-camera-unification integration design).

modules/zerojs.compiler/README

zerojs.compiler

Compiles a JavaScript AST (zerojs.parser) to Luau source that executes against the SAME realm value model as the interpreter (zerojs.runtime). Objects, prototypes, the stdlib, typed arrays and the bridge are shared, so compiled code interoperates with interpreted code and host marshaling with no second value model. Speed comes from eliminating per-node dispatch and environment lookups, plus integer-indexed array fast paths (rt.iget/iset). compile returns Luau source; load returns a chunk taking the runtime companion rtc (built by makeRtc from a realm + its global object). A construct outside the covered subset raises "compile bail: " so the caller can fall back to the interpreter for that program.

modules/zui/README

zui

DEPRECATED: Author screens as raw widget trees ({ type, style, props, children }) styled with the engine's CSS-parity ui.* surface; read @builtin::examples.ui.* for complete worked screens. This convenience layer predates CSS parity and writes unlike CSS.

modules/zui/defineWidget

defineWidget(name: string, builderFn: (any, any) -> any)

Register a custom widget kind. builderFn(props, children) is called at registerScreen / updateScreen time and its return value is decoded in place — the renderer never sees the custom name. Validates args locally so misuse fails immediately rather than silently. Requires the engine ui global; off-host execution raises a clear error.

Parameters

  • name string — Non-empty widget name (the value of { type = name, ... }).
  • builderFn (any, any) -> any(props, children) -> widget builder.
Z.defineWidget("myCard", function(props, children) return ... end)

modules/zui/set

set(widgetId: string, key: string, value: any)

Per-widget state write. Persists value against (widgetId, key) so a Luau-defined widget can own state across frames without threading it through the app's reactive store. No-op when the engine ui.widgetStateSet global is missing.

Parameters

  • widgetId string — The widget instance id.
  • key string — State key within the widget.
  • value any (optional) — New value.
Z.widgetState.set("toggle:1", "on", true)

modules/zui/unregisterWidget

unregisterWidget(name: string)

Unregister a previously-defined custom widget. No-op when the engine ui global is absent.

Parameters

  • name string — Non-empty widget name.
Z.unregisterWidget("myCard")
  • api
  • reference