Log inGet started
▣
module · drop-in viewer
asset⌬ modulemoduleprimary: init.luau·part ofmodule proc.module·originates fromworld 07158574-5…●1problem

eval

Lazy topological evaluator of a graph target, with per-eval memoization and cycle detection.

byzero-proxy @ DESKTOP-DB3UJOJ·posted 2mo ago
What it does

proc.eval

Lazy topological evaluator of a graph target, with per-eval memoization and cycle detection.

Part of @builtin::systems.procgen.proc.

Graphs built in the call form

A graph built with proc.define(function(g) ... end) has surface = "call". It records nodes (ops and node assets), scopes (for, fold, step), declared state and the refs between them. It has no output the evaluator is asked for: every node nothing reads is cooked from.

local play = eval.play(graph, { inputs = { count = 8 } })
local report = play:present({ time = 0, frame = 0, fps = 30, duration = 0 })
play:release()

eval.play(graph, opts) plans the graph, cooks from every node nothing reads and keeps what those nodes return. play:present(clock) cooks at a clock ({ time, frame, fps, duration }; a duration above 0 is a timeline with an end, where a seek replays from the start), play:setInputs(inputs) gives the graph other inputs, play:handle(id) answers what a node cooked from answered last, play:report() says what happened and play:release() lets go of everything the play holds, the Handles its output nodes answered included.

Every call-form graph cooks through the plan, whoever asks: proc.eval(graph, target, opts) of a graph with surface = "call" plans it (opts.inputs, opts.clock), cooks what the plan waits on and then the target through the flat graph, and a node asset used as an op cooks its graph the same way. output:<name> names an output of the graph (flat.outputs holds each one lowered), and the nodes the eval cooked into its own cache are released apart from what the answer reaches.

eval.plan(graph, opts) is the plan alone, and eval.references(graph) the assets and ops the graph's calls reach, by guid and id, without cooking.

The document

graph.surface   "call"
graph.nodes[id] { op, inputs = { socket = ref }, params = { name = ref | value }, scope = scopeId? }
graph.scopes    { [id] = { kind = "for", list = ref } | { kind = "fold", count, init, result }
                          | { kind = "step", dt, substeps, result }, parent = scopeId? }
graph.states    { [name] = { columns = ref } }
graph.inputs    { [name] = { type, default, ... } }
graph.outputs   { [name] = { ref } }     -- what a graph used as a node gives

op is an op id ("device.kernel", or "op.device.kernel"), a node asset ({ __ref = guid, type = "procNode", name }) or "construct.<kind>".

The refs are the v1 refs (const, graph_input, node_output, list, record, animated) and:

refstands for
loop_item { loop }the item of the for scope in the copy being written out
loop_collect { loop, ref }the list of ref in every copy of the for scope
loop_index { loop }the place (from 1) of the item in the copy being written out
loop_carry { loop, carry }a value built outside the for scope as a pass begins: the value before the loop for the
first copy, what the copy before left for every other
loop_result { loop, carry }the same value after the last copy
fold_value { scope }, fold_index { scope }the value and the index (from 1) of the pass of a fold
fold_result { scope }the value after the last pass
step_state { scope }, step_index { scope }the state record and the step record of a step body
step_result { scope }, state { name }the state the step has reached
clock { field }time, frame, fps, duration or delta of the graph's clock
enginethe engine API namespace

The plan

  • A node asset built in the call form is written out where it is called, its inputs bound to what the call was given. A call nothing reads is cooked from through every output it declares.
  • A for body is written out once per item of its list, a fold body n times in a chain, a pick takes the one alternative its choice names. A copy is keyed by its content, so reordering, inserting or removing items cooks only what is new.
  • A count or a list a scope needs is a plan value: a constant, an input, or the answer of a node, which the plan cooks and lists in report.waits. A column left on the device is not a plan value; reading it back with transfer.readback is, and the wait names the readback. The length a scope was written out for is watched: a cook that answers another length plans the graph again.
  • A where whose condition the plan knows takes one alternative. What only the other alternative stands on is not written out, and a call nothing reads that stands on any of it is not written out either (the nodes a node asset leaves unread are written after every alternative of the plan is decided), so nothing reachable only through the untaken alternative is cooked while the plan is made. A node of the graph itself that nothing reads is always cooked.
  • A for scope's carries ({ [id] = { init, result } }) chain the copies: copy 1 starts a carried value at init and copy n+1 at what copy n's result lowered to; a loop_result read after the loop is what the last copy left.
  • A step becomes one solver node. Its state is one record of device Columns per play (g:state(name, { column = { type, count } }) allocates them zeroed). The columns a state allocates are stated through plan values: a literal record, or a record a fold fills or constructs read off a list or an input (per-source columns, counts from list.count, names from geometry.names), whose counts the plan cooks and lists in report.waits. A member or item read inside the body of a value the plan knows (a count or a list a fold inside the step is written out for) is answered while the plan is made. The step record carries dt, time, graphTime, index and first. The body runs substeps times in each fixed step of dt seconds. Pausing holds the state, a seek back replays from the start on a timeline with an end, and a changed value the step reads begins it again.

A plan is made once

A graph is planned once for the values its plan stands on, and every later cook of it reads that plan.

  • A plan stands on the registry (registry.revision()), on the graph its plain inputs build, on the values of the inputs it read (as keys), on the animated values it read at the clock, on the answers it waited on and the nodes it cooked for a count or a list, and on the modules it required (by what their files hold). A cook reads a kept plan after asking each of those again (expand.stillStands); a plan that no longer holds is made again, and a plan that does is read.
  • Each answer and node the plan waited on is held by the key of its cone, without a cook. The leaves a cone stands on are named once, when the plan is kept (expand.keyWatches): the graph inputs it reads, the animated values, the constants that hold a device value (a Geometry, a Column) or an asset reference, and the nodes that read live state; the ops, their versions, the plain constants and the wiring are fixed by the plan and the registry. A read asks the evaluator for the key of each leaf as it stands now (opts._leafKeys: a device value is keyed by the run it stands on and the writes the run has had, an asset reference by what the asset's files hold) and compares them with the keys the watch stood at. A watch whose leaves are unmoved holds and cooks nothing; a watch whose leaf moved is cooked again, only its cone, and holds when it answers what it answered, and the keys it stands at are renewed. A plan that cooked a node reading the clock, a play's input or a live run, or whose cone stands on a solver or an op that always cooks, is not keyed: each of its watches is cooked again on every read.
  • A kept plan also holds the answer of each target it was cooked for (up to four a plan, and sixteen plans at a time), filed by the keys of the target's cone: a cook of proc.eval that shares no node cache with its caller, finds the cone unmoved and passes no stats or cookRecord is answered with a copy of the held answer (a Geometry or a Column is cloned, a list or a record of them rebuilt), and cooks nothing. An answer holding anything else (a Handle, a Field) is not held. The held answers are let go of with the plan.
  • A plan is kept unless it registered a step solver or wrote a run of the device into itself (a for over geometries). A plan that stands on a run of the device holds by the key of its cone, and is made again when the key moves. eval.planCounts() answers how many plans were made and read, the milliseconds each took, notKept, the plans that could not be kept by what stood in the way, served, the cooks answered from a held answer, and notKeyed, the kept plans whose watches are cooked again on every read, by why.
  • engine is the engine namespace, a constant. A member read or an item read out of constants, out of a record or a list a call wrote, is the ref written there and costs no node; engine.require of a module named by a constant is the module, required while the plan is made and asked for again each time the plan is read.
  • The plan is made in slices of budgetMs (5 ms) when the caller can yield; a cook over the plan hands a frame back after cookBudgetMs (100 ms).
  • A cook that shares no node cache with its caller keeps none: nothing is keyed or filed, what the plan cooked while it was made (or asked again) and what the cook makes stand in one table by node, and all of it is released as the cook ends but what the answer reaches. A Field that is the answer keeps the runs its leaves read; they stand as long as it does.
  • A node of a plan that stands is held to the contexts its producers operate in once, takes the label of its refusals from one string, and takes its params prepared once when every one is a constant.

A cook after a change is run from a list (cookrun)

A kept plan whose cone stands on numbers (graph inputs holding a number or a Bool) is cooked node by node while those numbers move. The recording wraps the plan's cone re-check and the cook together: the first such cook after the plan was made is recorded as a compute list and the next one is recorded beside it; the words that differ between the two recordings (a dispatch param, a word of a write) must each be the value of one leaf in both, and become the slots of the second list. A cook after that runs the list with the leaves' values in its slots, asks the recorded plan-time read-backs again, runs the watches that are not nodes (modules, animated values, the registry stamp) and answers a device copy of the list's columns (cookrun.counts(): recorded, diffed, replayed, verified, mismatched, readbackChecks, dispatches, refused by why): no node is cooked and no buffer is allocated but the copy. What makes a recording stand, and what ends it:

  • Two recordings are one cook when they make the same dispatches over the same programs and workgroups, bind buffers one for one, and write the same number of words; a run placed at another offset (zero_base_*, a write offset) is a placement. A first cook that uploads a mirror of a CPU column makes the pair differ once, so the comparison is tried again on the next cook (twice at most). A moved leaf that is no word of the list is refused by name, since its value reaches the device by a write the recording did not hear.
  • A cook that read the clock, a play's input or a live run, and an answer that cannot be copied are not recorded.
  • A cpu.call is first handed the geometries it is given with their columns on the device standing in as columns that answer their type, width, length and run (substrate.structureOf): a call that asks for no numbers (the names of the columns a class holds, the types a join decides) moves nothing off the device and records no read-back. A call that asks a stand-in for its numbers, fails, answers a stand-in, declares a return that is no plain value, or is handed a geometry counted on the device or a part is made again over the drained inputs, and its read-back is recorded.
  • Every node whose cook read back from the device (transfer.readback, a drained cpu.call) is recorded as a read-back row: its inputs, whether they were drained, and a digest of its plain outputs. After the list has run each row is asked again; one that answers another value than it did ends the recording and the cook is made node by node, answering correctly. A read-back whose outputs are not plain data is not recorded.
  • The first replay of a list is held to the cook of the same values made node by node, column for column; a difference ends the recording (mismatched).
  • A changed key of any other leaf (a Geometry input, a constant device value, an animated value), a plan made again and a registry change let go of the recordings; at most six plans hold recordings.
  • The recordings of two threads do not meet: a dispatch and a write belong to the recording of the thread that made it.
  • An answer a recording makes is copied word for word onto runs of their own (cookrun.deviceCopy) for the caller and for the held answer, so the buffers the list writes in place are never shared.

cookrun.setEnabled(false) cooks every cook node by node.

A g:fold of three passes or more over device state is planned as one construct.fold_run node (foldrun): the body is built once as a flat graph with the inputs state and index, the first pass is cooked while a compute list records it, and every later pass queues that list over a copy of the state, so a pass after the first cooks no node. foldrun.counts() answers how many passes were recorded, replayed and cooked, and how often a body was refused a list, by why. A body cooks each pass on its own when it reads index, when the state holds something the list cannot follow (a record, a text, a readback), when a pass makes a state of another shape, or when the fold sits inside another recording. foldrun.setEnabled(false) writes the folds of the plans made after out once per pass.

How a step runs

A step solver plans its body once (proc.eval.stepplan), when the state takes its first step.

  • A node of the body that depends on neither the state, the step record, an animated value nor the scene is STATIC: it is cooked once, with the first step, and read as it stands by every later step (opts.fixed of the evaluator). The plan reads only the nodes the next state stands on.
  • What is left is the device work of a step. The first step of each distinct set of step values the body reads (its dt, first, progress, ..., and the animated values it reads, as the device sees them: the f32 they round to) is cooked with every dispatch and every write it makes into a device buffer kept as one compute list over buffers the solver holds, and the next state written back into the state's own columns by the last dispatches of the list. Every later step with those values queues the list: the dispatches read the state's columns where they stand and write the next state into them. A step that runs from a list cooks no node and allocates no buffer.
  • The columns of the state are written in place, so what a step presents is a copy of them made on the device.
  • Up to six sets of step values are kept. A body the plan cannot stand on is cooked a step at a time with its static nodes still read as they stand, and says why in report.steps[<step id>].reason: a node of the body reads the clock, a play's input, a live run or the scene; the body reads values back from the device to the CPU; the next state is not a record of Columns the state's own shape; or a step value the body reads takes a new value in more than six steps (a body that adds step.time to a column, say).
  • play:report().steps[<step id>] holds steps, replays, recorded, interpreted (steps cooked node by node), plannedDispatches (what one list queues), dispatches (queued in all), allocations and uploads (buffers made and uploads done by steps), ops (nodes cooked in steps, by op), staticNodes, variants, mode (replay or cook), reason and keyValues (the step values of the last step).
  • stepplan.setPlanning(false) makes the solvers begun after cook every step node by node, which is how a planned body is held to the numbers a cooked one gives.

A state column the next state names as a Field, a number or a vector is evaluated into a column of the state's own length and type.

Constructs

A construct is a node the evaluator itself knows: construct.member, construct.item, construct.member_set, construct.column, construct.column_set, construct.column_remove and construct.engine and construct.select are cooked; construct.pick, construct.compare and construct.where are written out in the plan (a compare of two constants is their answer, a compare otherwise calls the math.compare node, and a where over a condition the plan does not know is a construct.select node). construct.select chooses a where cond holds and b where it does not: numbers, vectors and Fields element by element (math.where), and a Geometry, a record or a text by a condition that is one value, which a condition over the elements of a column is not (it is refused naming g.pick). A Column or a Geometry a construct reads from a record or a list is a clone of it.

construct.member reads count and keys of a plain list or record (the number of members, the sorted names of a record or the places of a list; a value that is nothing has none), count, type and lanes of a Field (the length of the longest column its chain reads, its element type, its components; a Field that reads no column has no length, and the read refuses naming geometry.point.count, column.count and g.field.index({ domain })), and count, type and lanes of a Column. construct.member also reads x, y, z and w of a vector (a Field of that component for a Field, a Column of that component for a Column of Vec2, Vec3, Vec4 or Quat), the classes of a Geometry, its material and its names (the attribute names each class holds). On a class of a Geometry a read of a detail column answers the value the detail holds. count of a Column or of a class of a Geometry is the live count when the elements are an appended or compacted output (a Column of one word, which a kernel with a domain_count binding reads the live count through, and a kernel without one runs over the capacity); capacity of either is the number of elements the run is allocated for. A record a graph holds under a name a class goes by (point, vertex, ...) is read and written as a record: x.point.name is the member of the member. construct.column, construct.column_set and construct.column_remove take edge beside point, vertex, primitive and detail: an edge is a unique point pair of the primitives, so the class counts them and a column written on it is one value per edge. count_set takes the four stored classes. construct.column_set writes a Column, a Field evaluated over the class it is written into, a vector or a number, a record or a text (one value for every element, a constant Record or String column) or a list of records or texts (one to an element, held in a Record or String column on the CPU); a record whose fields are numbers is a record, and a list of numbers is a vector. A column that is the structure of a geometry (point.id, vertex.pt, primitive.vstart, vcount, curve and closed) holds whole numbers: a number, a Field or a Column written to it is stored as an Int, and so is a value written to a column that already holds Int. A detail value is held as itself, a record among them, and construct.column_remove removes it. The inputs of every construct take nothing as a value: an optional input a call left out reaches them as nothing. construct.scene { what } answers where the graph stands in the scene it is cooked in (entity, placement, inverse, rotation); it is read as g.clock.placement and the other names, and as g.transform.

What the plan answers

A construct that answers while the plan is made cooks the nodes it reads (a wait in report.waits, a readback named as one) and records the answer, which is what the play compares to decide whether the plan still stands.

  • check { cond } message: a condition that is false refuses the play with the message.
  • has { geometry, class, name } is the Bool of whether the class holds the column; names { geometry, class } is the list of attribute names the class holds. A for over the names has one copy per name.
  • fold with stop: after each pass but the last the stop is answered, and the fold ends at the first pass whose stop holds. "changed" holds when the pass left its state as it found it (numbers by value, a Column or a Geometry by what its runs hold: a device run read for the comparison is a readback wait). A stop function's scope reads fold_before and fold_after and its result is the Bool. The report's fold scope states copies, stop ("changed" or "function") and stopped; each answer is a wait named "the stop of fold '<id>' after pass <n>".
  • A node asset called with plain inputs (an input with options, or of type Bool or String) is the graph its source builds for those values (builder.forInputs); a play built for other plain inputs plans again.

Constructs cooked as nodes:

  • count_set { geometry, class, count }: the geometry with the class's element count set. A number sets it; a live count Column (an appended output's) becomes the live count over the capacity it carries; arithmetic over a live count (kept.indices_count * 6, a Field over live counts) is evaluated on the device as one word, and the class is as large as that arithmetic reaches when every count it reads is full (substrate.atCapacity).
  • material_set { geometry, material }: the geometry carrying the material (a Material value, an asset reference or a name; nothing for none).
  • step_between { step, index }: how far from 0 to 1 the step has come through the span of graph time the present covers; step_sample { step, value } is the value, which a step body samples at the step's time.

The step record carries dt, time, graphTime, index, first, start (the time the step began at) and progress.

An element ref is the Field the graph's g.element stands for: id and P are fields the field compiler fuses into the chain they sit in; a column of the evaluation domain is fieldc.element(field, class).

Cooking

Every play cooks into one store of the world, filed by content key (the op, its version, the seed, the params and the keys of the inputs): a node of any graph that cooks the same op over the same inputs is cooked once. A value that holds an asset reference is keyed by what the asset's files hold, so editing a module a node calls cooks the node again. The nodes a play pulls from and every step are the play's own.

An output node answers a Handle (a table with a release function). cpu.call owns its list of results: a Handle in it is released when the cook is superseded and when the play ends.

Interface

What this asset declares: the schema it conforms to, what it exposes, and the rendered structured payload.

conforms to

zero/source-extract/v2

proc.eval — lazy topological evaluation of a graph target. Evaluates only the nodes reachable from the requested target, memoizing each node's outputs for the duration of one eval call. Intra-graph cycles error with the offending node; composite (graph-of-graphs) cycles error with the full path chain. Params that are InputRefs (e.g. graph_input) are resolved like inputs; raw scalars/tables pass through. A `list` ref — the wiring an array-typed socket (`Geometry[]`) takes — resolves element by element, so the op receives the values its producers yield. The per-node ctx carries graphId/nodeId/seed/compositePath/hash so ops can draw stable deterministic values. Time is the evaluation's: `opts.clock` names the graph's current time, an `animated` ref (`proc.anim`) resolves against it to the plain value it holds there, and that resolved value is what a cook key folds. A node whose animated params hold the same values at two times has the same key at both. An op receives the plain value, except on an input that takes a field: there it receives a live field (`proc.fieldc.live`) holding that value, which a compiled chain reads from its params. A node whose own code needs the time asks the cook context — `ctx:getFrame()`, `ctx:getTime()` — and a node that has asked is keyed by the time from then on, with nothing declared. A node's cook key is derived from its op, its params and its upstream keys, plus the `liveKey(params)` digest a def declares when it reads state none of those record — a source op reading the scene, whose params are constant while what it produces moves. Composite nodes: a `composite_output` ref resolves its child `.procGraph` by path (in-memory registry first, then opts.resolveGraph → proc.load), binds the composite's inputs into the child's graph inputs, and evaluates the requested child output. A visited-set of child paths detects cross-graph cycles; depHash exposes a child's structural hash for cache invalidation.

functionKey(fn: any) → void

argtypedescription
fnany

keyShape(v: any, depth: number, flags: any?, walking: { [any]: boolean }?) → any

argtypedescription
vany
depthnumber
flagsany?
walking{ [any]: boolean }?

keyOf(v: any) → number

argtypedescription
vany

plainStatic(v: any, depth: number) → boolean

Whether a param value is the same in every eval of the graph that holds it: plain numbers, text, booleans and tables of them.

argtypedescription
vany
depthnumber

constKey(ref: any) → number

argtypedescription
refany

spent(stats: any, id: string, seconds: number) → void

Whether `def`'s input `socket` takes a field — where an animated value is handed over as a live field rather than as the value it holds now. The shape a point cloud is handed to `def`'s input `socket` in. A socket declared as `PointCloud` (and not `Geometry`) is written against the PointCloud value and receives one; every other socket receives a geometry. Add `seconds` of CPU to what `stats` holds for node `id`, in milliseconds.

argtypedescription
statsany
idstring
secondsnumber

pointShapeFor(def: any, socket: string, value: any) → any

argtypedescription
defany
socketstring
valueany

socketTakesField(def: any, socket: string) → boolean

argtypedescription
defany
socketstring

isPlainValue(t: any) → boolean

Whether a table is a value that happens to have a `kind` field rather than a wiring. A MeshHandle carries `kind = "MeshHandle"` and nothing a wiring carries, and a component's value holding one reaches an op's param intact.

argtypedescription
tany

watchFolder(path: string) → void

argtypedescription
pathstring

listOf(category: string) → void

argtypedescription
categorystring

pathOfRef(ref: any, guid: string) → string

argtypedescription
refany
guidstring

assetStamp(ref: any, memo: { [string]: any }) → any

argtypedescription
refany
memo{ [string]: any }

walk(p: string, depth: number) → void

argtypedescription
pstring
depthnumber

assetStampOf(ref: any) → any

What the content of the asset a reference names stands at: a number that moves when a file of the asset's folder is edited, added or removed.

argtypedescription
refanyThe asset's reference, which carries its path.

examples

local before = evalM.assetStampOf(ref)

moduleStamp(name: string) → any

What the content of the module a graph requires by name stands at. The name is a module's identity, path or guid as it arrived in the graph; the module is found in the list of modules, each of which carries its own path.

argtypedescription
namestring

referencedStamps(v: any, memo: { [string]: any }, depth: number) → void

The stamps of every asset reference a value holds, or nil when it holds none.

argtypedescription
vany
memo{ [string]: any }
depthnumber

registerGraph(path: string, graph: any) → void

Register an in-memory graph at `path` for composite resolution.

argtypedescription
pathstringComposite reference path (e.g. "lib/cash_pile").
graphanyA Graph.

resolveGraph(path: any, opts: any?) → any

Resolve a composite child graph by path: in-memory registry first, then `opts.resolveGraph` (VFS-backed `.procGraph`). A graph document written in place (a loop body) is the graph it describes. Returns nil when unresolvable.

argtypedescription
pathany
optsany?

setGraphLoader(fn: (string) → void

Install the `.procGraph` graph loader (`proc.load`). Called once from `proc.module/init.luau`.

argtypedescription
fn(string`function(path) -> Graph`.

loadGraphFromRef(ref: any) → void

Load the graph a `.procGraph` ref points at — the ref's own `.path`, or the captured `asset.list("procGraph")` entry for its guid.

argtypedescription
refany

contentVersionDigest(graph: any, seen: { [string]: boolean }) → void

Fold the resolved versions of every content-ref op a graph references, transitively, into a stable structure. A graph's `depHash` captures its own JSON — including the `__ref` guids of the content ops it uses — but NOT those ops' current versions, so an edit to a nested op-style procNode leaves the ancestor's structural hash unchanged. Walking the content refs and folding each resolved version closes that gap: editing any op the subgraph reaches re-keys the wrapping node. Guids are visited once (`seen`), so a self or transitive cycle bounds the walk instead of looping. Node ids are sorted for a deterministic digest.

argtypedescription
graphany
seen{ [string]: boolean }

wrapLoadedGraph(child: any, childPath: string, childGuid: string?) → any

The fold both public wrappers below return: (graph, path identity, guid) → an op def. `params` mirror the graph's declared inputs (defaults carried); `outputs` mirror its declared outputs (a sole output is the default). `version` folds the graph's structural `depHash` with the resolved versions of every content op it references (transitively), so editing the graph OR any op-style procNode nested beneath it re-keys every cache that reaches the wrapping node. `eval` evaluates the graph's outputs with the node's params bound as its graph inputs, sharing the parent's cook state and extending the composite path so child node keys never collide with the parent's. A graph that references itself (directly or transitively) raises loudly via the visited-path guard, keyed on `childPath`.

argtypedescription
childany
childPathstring
childGuidstring?

socketType(decl: any) → string

A graph built in the call form states an input's type by its default or by name (`List<Float>`, `Value`, `Column`); a socket takes the type when the registry knows it and any value otherwise.

argtypedescription
declany

eval(ctx: any, wired: any, evalParams: any) → void

argtypedescription
ctxany
wiredany
evalParamsany

wrapGraphAsOp(ref: any) → any

Wrap a `.procGraph` graph asset as an op def, so a graph can be used as a node. The full folding contract is on `wrapLoadedGraph` above.

argtypedescription
refanyA `.procGraph` asset ref (`{ __ref = guid, type = "procGraph" }`).

wrapGraphValueAsOp(child: any, path: string, guid: string?) → any

Wrap an in-memory Graph as an op def — the same folding `wrapGraphAsOp` performs, for a graph that lives INSIDE the asset being registered rather than in a `.procGraph` of its own. A `.procNode` whose entry returns `proc.define(...)` registers through this: the node IS its graph, params mirror the graph's declared inputs, outputs its declared outputs. identity, so a graph that reaches its own asset raises instead of recursing.

argtypedescription
childanyThe Graph (from `proc.define` / `proc.graph`).
pathstringThe owning asset's VFS path — the cycle-guard and composite-path
guidstring?The owning asset's stable guid, seeded into the version walk.

mapRefs(v: any, map: (any) → void

`v` with every InputRef inside it passed through `map`: a ref itself, the items of a list, the fields of a record ref, and a ref sitting in a plain table's field. An asset reference, a constant and an animated value pass through whole.

argtypedescription
vany
map(any

childGraphOf(node: any) → any

The graph a node runs as a whole, or nil for a node that runs one op.

argtypedescription
nodeany

flatten(graph: any, opts: any?) → any

The graph with every graph used inside it as a node written out in place: a node that runs a `.procGraph`, a graph-backed `.procNode`, or a composite becomes the nodes of the graph it runs, each named `<node>/<inner node>`, wired to what the node was given for each of that graph's inputs (its default where it was given nothing), and every read of the node's outputs reads the inner node that produces it. Graphs inside graphs are written out the same way, however deep. A loop stays a loop. What the graph computes is unchanged; how it is cooked is not — every node is cached, placed on the device and moved on in time as a node of the one graph, which is what lets an effect built from graphs play as cheaply as the same nodes written in one. function called as `onInline(id, child)` for each graph written out in place.

argtypedescription
graphanyA Graph.
optsany?Optional `{ resolveGraph = fn, onInline = fn }` — how a composite's path loads, and a

examples

local g = proc.eval.flatten(proc.load("/zero/source/fx/frost.procGraph"))

flattenInto(g: any, depth: number) → any

argtypedescription
gany
depthnumber

writeOut(id: string, child: any, given: (string) → void

argtypedescription
idstring
childany
given(string

bind(name: string) → any

argtypedescription
namestring

lift(ref: any) → any

argtypedescription
refany

resolve(ref: any, hops: number?) → any

Every read of an expanded node's output becomes a read of what produces it — followed through, since that may be a ref given to another expanded node.

argtypedescription
refany
hopsnumber?

depHash(graph: any) → number

argtypedescription
graphany

cacheKey(opId: string, opVersion: number, resolvedInputs: any, depHashes: any) → number

Content-addressed cache key for one op evaluation: hash(opId, opVersion, resolvedInputs, composite_dep_hashes).

argtypedescription
opIdstringOp id.
opVersionnumberOp version (bumped on behaviour change).
resolvedInputsanyThe op's resolved input table.
depHashesanyComposite dep_hashes in scope (array or table).

targetRef(graph: any, target: string) → any

Parse a target string into the root InputRef. "output:<name>" -> the graph output's ref "node:<id>" -> that node's default output "node:<id>:<output>" -> that node's named output

argtypedescription
graphany
targetstring

cloneArray(t: { any }) → void

argtypedescription
t{ any }

structureReturnsOnly(params: any) → boolean

argtypedescription
paramsany

releaseOuts(outs: any) → void

Call `__proc_release` on every value in `outs` that declares one — a node cache entry's outputs, dropped because a param edit superseded it or because `M.releaseCache` is tearing the whole cache down. A value with no release hook on its metatable is left alone (nothing to release).

argtypedescription
outsany

releaseSolverState(def: any, state: any) → void

Release a solver's state: through the solver's own `release` when it declares one, else through the state's `__proc_release`.

argtypedescription
defany
stateany

releaseEntry(entry: any) → void

Release everything one cache entry holds: its outputs, the outputs it keeps per key or per frame, and a solver's state.

argtypedescription
entryany

outsStand(outs: any) → boolean

Whether any of `outs` stands something in the world (a call's Handle): what the cook that made it owns.

argtypedescription
outsany

releaseNodes(nodeCache: { [string]: any }?, keep: { [string]: boolean }) → void

Release and drop every entry of a node cache whose node id is not in `keep`: what a graph that was planned again no longer holds.

argtypedescription
nodeCache{ [string]: any }?A persistent per-node cache from a prior `M.eval` call.
keep{ [string]: boolean }The node ids still in the graph, as a set.

examples

proc.eval.releaseNodes(nodeCache, { ["a"] = true })

plan(graph: any, opts: any?) → any

The plan of a graph built in the call form: the flat graph its nodes, scopes and constructs are written out as, the nodes cooked from, and what the plan waited on (`expand.plan`).

argtypedescription
graphanyA Graph from `proc.define(function(g) ... end)`.
optsany?`{ inputs, clock, cook }`.

play(graph: any, opts: any?) → any

Start a play of a graph built in the call form (`play.start`).

argtypedescription
graphanyA Graph from `proc.define(function(g) ... end)`.
optsany?`{ inputs, seed, scene }`.

references(graph: any) →

The assets a call-form graph's calls reach, by guid, and its ops by id (`expand.references`).

argtypedescription
graphanyA Graph from `proc.define(function(g) ... end)`.

releaseCache(nodeCache: { [string]: any }?) → void

Release every value a persistent node cache holds, calling each entry's `__proc_release` and emptying the cache. A `Generator` (or any owner of a persistent `nodeCache`) calls this from its teardown so a cached GPU-backed value (e.g. a `Field.gpu` whose readback never ran, or a `Raster`) is freed instead of leaking with the cache. A solver's state and the frames it kept go with it.

argtypedescription
nodeCache{ [string]: any }?A persistent per-node cache from a prior `M.eval` call.

outsHoldLive(outs: any) → boolean

Whether any of `outs` holds a run something rewrites in place — directly, or inside a record or list it carries: a source record holding its points is made from those points' runs as much as the points are.

argtypedescription
outsany

markKept(nodeCache: { [string]: any }?) → any

The keys every entry of a node cache holds outputs under right now — the one it holds and the ones it keeps from earlier cooks — and the outputs it keeps because a play or a cooked frame may still stand on them, taken out of the cache. Taken when the inputs of every cook to come change, so none of those keys is cooked again; `releaseMarked` lets them go once nothing stands on them.

argtypedescription
nodeCache{ [string]: any }?A persistent per-node cache from a prior `M.eval` call.

examples

local mark = proc.eval.markKept(nodeCache)

releaseMarked(nodeCache: { [string]: any }?, mark: any) → void

Release what a node cache kept under the keys `markKept` recorded, apart from the output an entry still holds as its own, and the outputs the mark took out of the cache.

argtypedescription
nodeCache{ [string]: any }?The node cache the mark was taken of.
markanyWhat `markKept` returned.

examples

proc.eval.releaseMarked(nodeCache, mark)

dropLive(nodeCache: { [string]: any }?) → void

Release and drop every entry of a node cache whose values hold a live run (`proc.fieldc.liveRuns`). A rewrite for a later time changes what such a run holds while its entry is still filed under the key it was cooked at, so after `proc.fieldc.tick` the entry no longer holds what its key names, and the next cook that reaches it has to make it again.

argtypedescription
nodeCache{ [string]: any }?A persistent per-node cache from a prior `M.eval` call.

examples

proc.eval.dropLive(nodeCache)

animatedIn(ref: any) → number

How many animated values an input ref holds: itself, or those in the records and lists it is made of.

argtypedescription
refany

onDeviceShape(graph: any, rewinds: boolean) → any

argtypedescription
graphany
rewindsboolean

walk(g: any, path: string) → void

The nodes of `g` and of every graph a node of it runs, whose nodes are cooked the same way the graph's own are.

argtypedescription
gany
pathstring

timeOnDevice(graph: any, opts: any?) →

Whether a new time is all a cook of `graph` would differ by in its live runs alone — so rewriting them for that time (`proc.fieldc.tick`) presents it without a cook. True when every animated value the graph holds reaches the device as a live value — wired into an input that takes a field, set on a param its op declares `live`, or handed over as the curve it is through a param or input declared `curve` — while a solver steps on to the time and an op that moves with time reads it as a live field (`ctx:liveTime`); no composite, graph-as-node, graph input or node reading time in its own code carries time any other way; and the graph holds at least one animated value, one solver or one node reading live time. Whether the cook's values then read a live value on the CPU is the caller's to check, through `proc.substrate.liveReads`. `rewinds = true` asks for time going back as well: every solver must then begin again in the runs it holds (it declares `reset`). An input given as `proc.anim.input` reads no time, and is allowed. false and why, naming the node or value that holds it.

argtypedescription
graphanyA Graph.
optsany?Optional `{ inputs = {..}, nodeCache = cache, rewinds = boolean }` — what the cook ran under.

examples

local onDevice, why = proc.eval.timeOnDevice(g, { nodeCache = cache })

eachColumn(value: any, visit: (any) → void

A node whose inputs read a live run is brought to a later time by a registration on the runs it wrote: a solver steps on to that time and presents again, any other op over device runs cooks again over the same inputs, and either way what it made is copied into the runs its first cook wrote, which are what everything downstream binds. Calls `visit` with every column a value holds: a geometry's columns and live counts in every domain, a column itself, and those inside the tables a record, a list or a field keeps them in.

argtypedescription
valueany
visit(any

walk(v: any, depth: number) → void

argtypedescription
vany
depthnumber

adopt(value: any, owner: string) → void

Hold every device run `value` holds for `owner` from now on (`Column.adopt` over each column it holds): what a cache kept apart from the owner that cooks into it calls on what it takes in, so the runs go with the cache rather than with the cook.

argtypedescription
valueanyA value a graph produced.
ownerstringThe owner to hold its runs for.

examples

proc.adopt(values, proc.sharedCacheOwner(key))

liveDepsOf(value: any) → void

The live registrations of the runs a value holds, each once.

argtypedescription
valueany

copyRun(standing: any, made: any, label: string, what: string) → void

Copy a run a present made into the one standing in its place. A run not on the device stands as it is; RAISES, naming what changed, when the two are no longer the same shape — the output is then cooked again instead.

argtypedescription
standingany
madeany
labelstring
whatstring

copyInto(standing: any, made: any, label: string) → void

Copy every run of a node's outputs made again into the outputs standing.

argtypedescription
standingany
madeany
labelstring

followLive(outs: any, inputs: any, rerun: (, label: ?) →

Make what a computation over the device's runs made outside a cook follow the live runs it read, the way an op's outputs follow its inputs in a cook: when a rewrite brings `inputs` to a later time, `rerun()` makes the outputs again and they are copied into the runs `outs` stands on. The runs `outs` shares with `inputs`, and the ones something else already rewrites, are left to what rewrites them.

argtypedescription
outsanyA record of what was made: geometries and columns by name.
inputsanyA record of what it was made from.
rerun(Makes the same record again from the inputs as they stand.
label?What the computation is, for errors.

examples

evalMod.followLive({ geometry = kept }, { geometry = pop }, function() return { geometry = keep(pop) } end, "source 1")

rewrite(_at: any) → void

argtypedescription
_atany

makeCook(opts: any) → any

The cook state shared by one whole evaluation, composites included: the cancellation token, the frame budget, and the yield clock. `checkpoint` is the single pause point — called between node cooks and, via `ctx.checkpoint`, from inside heavy ops' hot loops — so a long cook never freezes the engine and a stale cook stops at the next checkpoint instead of running to the end.

argtypedescription
optsany

origin( ) → void

checkpoint( ) → void

claim(root: any, claimed: { [any]: boolean }, seen: { [any]: boolean }, deep: boolean?) →

Evaluate `target` of `graph`. `target` is "output:<name>" or "node:<id>[:<output>]". `opts` may carry `{ inputs, seed, compositePath, resolveGraph, cancel, budgetMs, nodeCache, cacheBudgetBytes, cookRecord }` (the recursion also threads `visitedPaths`/`pathChain`/`_cook`). `cancel` is a `{ cancelled: boolean }` token checked at every cook checkpoint; `budgetMs` bounds how long the eval runs before yielding a frame (when the caller can yield), counted from `slice.lastYield` when the caller hands over a `slice` its evals share; `sharedCache` is a store several callers playing one graph hand over alike, which keeps the nodes that cook alike for all of them (standing on no solver, reading no time, input or live value, holding no rewritten run); `nodeCacheOwner` and `sharedCacheOwner` name whom the device runs of what `nodeCache` and `sharedCache` keep are held for, when a cache outlives the owner that cooks into it (`proc.adopt`); `nodeCache` is a caller-owned persistent per-node cache — nodes whose derived key is unchanged since the last eval reuse their cooked outputs, so a param edit re-cooks only the nodes it actually reaches. A node's `def.cache` policy ("always" | "never" | "budgeted", default "always") governs whether it participates: "never" always recooks, "budgeted" participates only while its accounted bytes (`__proc_bytes` on its cached values) fit `cacheBudgetBytes` — the cheapest `def.cost` budgeted entries evict first when it doesn't. A superseded or evicted entry's values release via `__proc_release`. `clock` is the graph's current time, `{ time, frame, fps, duration }` (seconds; time zero when absent): every animated value resolves against it before a cook key is built. A clock with a `duration` is a timeline with an end: `nodeCache` then keeps a cooked value per distinct key, so a frame cooked once is served from then on. A clock without one runs on without end, and each node keeps its latest value alone. `keepFrames = false` has a timeline with an end cooked the way one without is: each node and solver keeps its latest value alone and a later time is a rewrite of what stands — for a graph played once through, where no frame comes round. `stats` is a caller-owned table the eval counts into: `cooked`, `hits`, `nodes[id]` cooks per node, `ms[id]` the CPU milliseconds spent in a node's cooks (its eval, or its solver's start, steps and output; a node that evaluates a graph of its own counts the nodes inside it), and for solvers `steps` and `restarts`. While an eval stands yielded to the frame loop over its budget, `waitingSince` is when it yielded, and `waitedMs` adds up the milliseconds every such wait took. A solver node (`kind = "solver"`) holds state: its output at a time is the state its steps have reached, so it keeps that state in `nodeCache` between evals and steps it forward from where it stands. Its history is keyed by everything upstream of it with animated values taken as the curves they are, so a change upstream restarts it and a new time does not. On a timeline with an end it keeps the output of every frame it has produced, a frame reached again is served from that memory, and a frame before the one its state stands at restarts it from `start`; each frame it steps through reads the settings of that frame, so a frame names one state however it is reached, and without a `nodeCache` it simulates from `start` within the eval. On one without an end it is live: it keeps its current state alone, begins at the time it is first asked for (a restart, and a time behind its state, begin it again there) with the prewarm `start` gives it, and a present catches up at most `maxStepsPerFrame` steps, a quarter of a second of steps when the solver declares none. What it presents there is live: a rewrite for a later time steps it on and presents again into the same runs — through `output(ctx, state, params, into)` handed the outputs standing when the solver declares `outputInto`, and by copying a fresh present into them when it does not. `cookRecord` is a `proc.cooks` record the eval reports into: each node cooked marks progress, an op failure closes it "failed" naming the node, and a mid-op cancellation closes it "cancelled"; errors raised outside an op (between-node cancel, input-resolution failure, bad target, unknown op) leave the record open — the record's owner closes it (finish is first-close-wins, so an unconditional owner close is safe). `fixed` is `{ [nodeId] = outs }`: nodes whose outputs the caller already holds, read as given and never cooked, keyed or released by the eval. `notes` is a table the eval writes `dynamic` into, naming the first node whose cook read graph time, a play's input or a live run, which no later cook can stand on. `stats.ops[op]` counts the nodes cooked by op, and `stats.self` the nodes cooked with the time each took apart from the nodes it read. An eval owns what it cooks: a value a node produced on the way to the target, the private copy a mutating op was given, and the value a composite child handed back all release through `__proc_release` when the eval ends — every one of them except what the returned value reaches, what arrived in `inputs`, and what a `nodeCache` holds. So a GPU-backed intermediate frees itself, the returned value is the caller's to `destroy`, the inputs stay the caller's, and the cache's share goes with `M.releaseCache`. An op that keeps one of its outputs somewhere the returned value does not reach holds a value this release frees.

argtypedescription
rootany
claimed{ [any]: boolean }
seen{ [any]: boolean }
deepboolean?

examples

proc.eval(g, "output:mesh")

releaseCacheExcept(cache: { [string]: any }, result: any, kept: any?) → void

Release what a node cache holds, apart from the values `result` reaches.

argtypedescription
cache{ [string]: any }
resultany
keptany?

drop(outs: any) → void

argtypedescription
outsany

releaseExcept(values: { any }, keep: any) → void

Let go of the values of a list, the tables they hold included, apart from what `keep` reaches: a state a pass read and nothing reads any more.

argtypedescription
values{ any }A list of values.
keepanyThe value whose parts stay.

examples

proc.eval.releaseExcept({ previous }, next)

drop(v: any, depth: number) → void

argtypedescription
vany
depthnumber

planCounts( ) →

What the plans of call-form graphs have cost since the module loaded: how many plans were made, how many cooks read a plan made before, and the milliseconds each took (a plan made is the expansion and the cooks its counts and lists asked for; a plan read is finding it again and asking whether what it stood on still holds). `notKept` counts the plans that could not be kept for another cook, by what stood in the way. `served` counts the cooks answered from the result a kept plan holds for the target, and `notKeyed` the plans whose watches are held by cooking them again, by why.

examples

local before = proc.eval.planCounts()

planFor(graph: any, inputs: any) → any

The plan the graph stands on for `inputs`, when one was made before and what it stood on has not moved.

argtypedescription
graphany
inputsany

copyAnswer(value: any) → any

A copy of a cooked answer that is its own to release, or nil when the answer holds something that cannot be copied (a handle, a field, a function): a Geometry or a Column is cloned, a list or a record of them is rebuilt.

argtypedescription
valueany

walk(v: any, depth: number) → void

argtypedescription
vany
depthnumber

releaseAnswer(value: any) → void

Let go of the copies a plan holds as answers.

argtypedescription
valueany

walk(v: any, depth: number) → void

argtypedescription
vany
depthnumber

dropAnswers(plan: any) → void

argtypedescription
planany

holdAnswer(plan: any, target: string, snap: { number }, value: any) → void

argtypedescription
planany
targetstring
snap{ number }
valueany

retirePlan(plan: any) → void

A kept plan let go of: the answers it holds, and the step solvers it registered.

argtypedescription
planany

dropGraphPlans(graph: any) → void

argtypedescription
graphany

forgetPlan(graph: any, plan: any) → void

argtypedescription
graphany
planany

refuseKeeping(why: string) → void

argtypedescription
whystring

keepPlan(graph: any, plan: any, keyIt: () → void

argtypedescription
graphany
planany
keyIt(

planHasSolver(plan: any) → boolean

Whether a plan stands a solver among its nodes, which keeps its state in a node cache.

argtypedescription
planany

planCooksAlike(plan: any) → boolean

Whether every node of a plan cooks to the same answer over the same key: none is a solver, and none is an op that always cooks (a sink).

argtypedescription
planany

keyOf(v: any) → number

The key of a value: a number that is the same for two values that stand on the same runs, whatever table holds them; a Column is keyed by the buffer it lives in and the writes it has had, and never walked.

argtypedescription
vanyThe value.

examples

local key = evalM.keyOf(inputs.geometry)

releaseValue(v: any) → void

argtypedescription
vany

releaseHeld(memo: { [string]: any }, result: any, keep: any) → void

argtypedescription
memo{ [string]: any }
resultany
keepany

evalCallGraph(graph: any, target: string, opts: any) → any

argtypedescription
graphany
targetstring
optsany

letGo(result: any) → void

What the cook made and holds: the nodes of `memo`, and the entries of a node cache a solver keeps its state in.

argtypedescription
resultany

cookAsked(flat: any, id: string, output: string?) → any

argtypedescription
flatany
idstring
outputstring?

leafKeysOf(plan: any) → void

argtypedescription
planany

keysFn( ) → void

holdFor(value: any) → void

argtypedescription
valueany

cook(flat: any, id: string, output: string?) → void

argtypedescription
flatany
idstring
outputstring?

holdOf(value: any) → void

argtypedescription
valueany

heldOf(store: any) → void

The values `store` holds, walked once from its entries the first time it is asked about and kept up from then on by `holdIn`.

argtypedescription
storeany

holdIn(store: any, outs: any) → void

Mark what `outs` reaches as held by `store`, which an entry of it now holds.

argtypedescription
storeany
outsany

entryValues(entry: any) → void

The values one entry's outputs reach, each once.

argtypedescription
entryany

countIn(store: any, outs: any) → void

Count the values a new entry of a shared store holds.

argtypedescription
storeany
outsany

dropSharedEntry(store: any, at: string, dropped: { [any]: boolean }?) → void

Take an entry out of a shared store and release every value it holds that no other entry holds.

argtypedescription
storeany
atstring
dropped{ [any]: boolean }?

letGoShared(store: any, at: string, retired: { [string]: boolean }?, dropped: { [any]: boolean }?) → void

A play's node lets go of the entry of a shared store it stood on. The entry goes once no node of any play stands on it: into `retired` when the caller releases it later (a play, once what it presents stands on what replaced it), else now.

argtypedescription
storeany
atstring
retired{ [string]: boolean }?
dropped{ [any]: boolean }?

releaseRetiredShared(store: any, retired: { [string]: boolean }) → void

Release the entries `unclaimShared` or a cook retired that no node of any play stands on again since.

argtypedescription
storeanyThe store the plays share.
retired{ [string]: boolean }The retired entries, emptied.

examples

proc.eval.releaseRetiredShared(store, retired)

storeBytes(nodeCache: { [string]: any }?, frames: { [any]: any }?) → number

The device bytes the values a node cache and its kept frames hold state for themselves through `__proc_bytes`: the tiles of a raster, the transform rows of a population. Walked from the entries as they stand, so a value the cache let go of is not counted.

argtypedescription
nodeCache{ [string]: any }?A persistent per-node cache from a prior `M.eval` call.
frames{ [any]: any }?A frames table kept beside it, or nil.

examples

local bytes = proc.eval.storeBytes(cache, frames)

eval(graph: any, target: string, opts: any) → any

argtypedescription
graphany
targetstring
optsany

evalFlat(graph: any, target: string, opts: any) → void

argtypedescription
graphany
targetstring
optsany

inputsRead(entry: any) → void

The inputs a node read, as its cache entry remembers them.

argtypedescription
entryany

digestOfInputs(names: { [string]: boolean }) → number

argtypedescription
names{ [string]: boolean }

dropOuts(outs: any) → void

argtypedescription
outsany

standShared(id: string, at: string?) → void

Node `id` of the caller's play stands on the shared entry filed at `at` (nil: on none); the one it stood on before is let go of.

argtypedescription
idstring
atstring?

opDef(id: string) → any

argtypedescription
idstring

refOnSolver(ref: any) → boolean

argtypedescription
refany

standsOnSolver(id: string) → void

argtypedescription
idstring

opLabel(op: any) → string

A readable label for a node's op in error text: the id for a string op, or `<type>:<guid>` for a typed asset ref (never a raw table address).

argtypedescription
opany

sceneFor(def: any, id: string) → any

The scene, handed to a node that declares it pulls from the scene and refused to any other: reaching for the scene is a node's one job.

argtypedescription
defany
idstring

holdContexts(id: string, node: any, label: string) → void

Hold what a node reads to the contexts its producers operate in: what a node of an enclosed context makes is read by a node of that context or by an input that opens it (see `proc.registry.contextRefusal`).

argtypedescription
idstring
nodeany
labelstring

sampleAnimated(ref: any) → any

argtypedescription
refany

animatedParam(def: any, name: string, ref: any) → any

The plain value an animated PARAM holds now, shaped to what the param declares: a whole-number param reads a curve rounded to the nearest whole number, so a count eased from 10 to 400 is a legal count on every frame and every frame that rounds to the same count is one cook key.

argtypedescription
defany
namestring
refany

curveSocket(def: any, socket: string) → boolean

Whether an op's input socket is declared `curve`: every animated value in it — the value itself, a record's field, a list's item — is handed to the op as the curve, which the op samples with `ctx:sample` and may keep for whatever reads it at a later time.

argtypedescription
defany
socketstring

resolveCurves(ref: any) → any

A `curve` socket's value: its animated values as curves, everything else resolved as any input is.

argtypedescription
refany

refKey(ref: any) → number

The key of one InputRef: a child node's key, or the hash of the value it resolves to (graph inputs and consts are small user values — cheap to hash). Composite refs fold in the child graph's structural hash plus the keys of everything bound into it.

argtypedescription
refany

curvesKey(ref: any) → number

The key of a `curve` socket's value: each animated value in it by the curve it is, everything else by its key as any input.

argtypedescription
refany

liveDigest(id: string, def: any, node: any) → any

The live state a node reads that neither its params nor its wiring record. A source op reading the scene has no inputs and constant params, so its key would be constant while the value it produces moved, and every node downstream would keep serving what was cooked against the old state. A def declares `liveKey(params)` — its params prepared exactly as `eval` receives them — returning a digest of what it reads; folding that into the node's key re-keys the node and, through the producer key its consumers fold, the whole cone downstream of it, while every branch beside it keeps its cache entry. The role `contentVersionDigest` plays for a composite's content refs, for a node's scene reads.

argtypedescription
idstring
defany
nodeany

keysHeld( ) → any

forgetHeldKeys( ) → void

A node found reading time or a play's input moves the key of everything downstream: the graph holds none.

staticRef(ref: any, timeless: boolean) → boolean

Whether a ref's key is the same in every eval of the graph: a constant holding no value a use changes (and, for a cook key, no asset reference), a curve taken whole (for a time-free key), or a node whose key is held, and lists and records of those.

argtypedescription
refany
timelessboolean

nodeKey(id: string) → void

A node's cook key: everything that determines its output — the op and its version, the eval seed, every param (literal value or upstream key), every input's upstream key, and the live state the op declares it reads. Derived recursively over the DAG and memoized per eval; no cooked DATA is ever hashed, only structure and small values.

argtypedescription
idstring

timelessRefKey(ref: any) → number

argtypedescription
refany

historyKey(id: string) → void

A node's time-free key: its op and version, the seed, every param and input by its time-free key, and the live state an op declares it reads. A solver's live inputs are read at each step and are no part of it.

argtypedescription
idstring

cachePolicy(def: any) → string

A def's cache policy: an explicit `def.cache` (validated and defaulted to "always" by the registry at registration), the legacy `nocache = true` mapped to "never" for a def that predates it, else "always".

argtypedescription
defany

refUses(ref: any, targetNode: string, targetOutput: string?) → number

How many times `ref` wires (targetNode, targetOutput) — `output` resolved to the producer's actual default-flagged output when the ref itself leaves it nil. A list ref counts each of its elements, so an array-typed socket is as much a consumer as a scalar one.

argtypedescription
refany
targetNodestring
targetOutputstring?

countConsumers(targetNode: string, targetOutput: string?) → number

How many nodes (node inputs, node params, and graph outputs) in `graph` wire to (targetNode, targetOutput). A `mutatesInput` op consults this before running so a value with more than one consumer is never mutated out from under a sibling branch — it is structural (every declared wire counts, whether or not that consumer is reached by THIS eval's target), so the guard holds regardless of evaluation order.

argtypedescription
targetNodestring
targetOutputstring?

outsBytes(outs: any) → number

Bytes a stored outputs table spends against the cache budget: the sum of every output value's `__proc_bytes()`, for values that declare one (a value with none does not count against the budget).

argtypedescription
outsany

evictForBudget(newId: string, newBytes: number, budget: number) → void

Evict "budgeted" entries from `nodeCache` — cheapest to recompute (lowest `def.cost`) first — until storing `newBytes` more for `newId` (excluded from the eviction pool: it is the entry being written) fits within `budget`. Only "budgeted" entries count toward the total or are eviction candidates; an "always" entry is never counted or evicted.

argtypedescription
newIdstring
newBytesnumber
budgetnumber

solverEntry(id: string, def: any) → any

argtypedescription
idstring
defany

resetSolver(entry: any) → void

Put a solver back where it has never stepped: its state, the frames it kept and its current output go.

argtypedescription
entryany

wantedOutputs( ) → void

note(node: string, output: string) → void

argtypedescription
nodestring
outputstring

walk(v: any) → void

argtypedescription
vany

wants(id: string, def: any, name: string) → boolean

Whether output `name` of node `id` is read by anything this eval serves.

argtypedescription
idstring
defany
namestring

evalSolverAt(id: string, frame: number) → void

The graph evaluated again at frame `frame` of this timeline, for solver `id` to step through it with that frame's settings.

argtypedescription
idstring
framenumber

solverCall(id: string, node: any, what: string, fn: any, ...: ?) → any

argtypedescription
idstring
nodeany
whatstring
fnany
...?

evalSolver(id: string, node: any, def: any) → any

A solver node at this eval's time: the state its steps have reached, through `output`. The settings it steps with are its inputs and params at this time.

argtypedescription
idstring
nodeany
defany

getFrame(_self: any) → void

argtypedescription
_selfany

getTime(_self: any) → void

argtypedescription
_selfany

scene(_self: any) → void

The scene, for a solver that pulls from it.

argtypedescription
_selfany

stepTime(_self: any) → void

The graph time the step running now ends at, or this eval's time outside a step.

argtypedescription
_selfany

presentSpan(_self: any) → void

`{ from, to }`: the graph time the present being advanced to starts and ends at, or the eval's time twice outside an advance.

argtypedescription
_selfany

sample(_self: any, value: any) → void

A value as it stands at the time of the step running now, or at this eval's time outside a step: an animated value sampled there, anything else as it is. What a solver reads a `curve` param with.

argtypedescription
_selfany
valueany

wants(_self: any, name: string) → void

Whether anything reads the solver's output `name`, so an output that costs work to keep is kept only while it is wired.

argtypedescription
_selfany
namestring

begin( ) → any

`init`, held to the state type the solver declares.

present(state: any) → any

What it outputs, kept per frame on a timeline with an end and alone on one without. A solver presenting into the runs it holds (`outputInto`) presents a state it has presented before into them, on a timeline with no end: what reads them keeps reading them. Runs made anew are a new generation of its output, which its key carries.

argtypedescription
stateany

beginsFrom(at: number) → number

Where a simulation begun at graph time `at` starts stepping from. On a timeline with no end it begins at the time it is first asked for, less the prewarm `start` gives it before the timeline's first frame: a graph edited a minute in runs that prewarm again, and the minute before it is not replayed.

argtypedescription
atnumber

rewind( ) → boolean

The state begun again: through the solver's own `reset`, in the runs it holds, when it declares one that takes this state, and otherwise made anew. Answers whether the state was reset in place.

advance(check: () → void

Advance the state to the time it presents at in the solver's fixed steps, `check` run between them.

argtypedescription
check(

rewrite(at: any) → void

argtypedescription
atany

storedOuts(id: string, key: number) → any

The outputs stored for a node under `key`: this graph's own cache entry (a key it cooked before becomes the current one again), else what another play of it stored alike.

argtypedescription
idstring
keynumber

evalNode(id: string) → any

argtypedescription
idstring

evalNodeInner(id: string) → void

argtypedescription
idstring

protectShared(val: any, ref: any, socket: string) → any

`def.mutatesInput` names the socket an op writes in place. A value with more than one structural consumer must never see another consumer's in-place write, so it is copied (via its own `__proc_copy`) before this op runs — the shared cached value stays intact for the sibling branch. An array-typed socket protects each of its elements the same way; the array itself is built per eval.

argtypedescription
valany
refany
socketstring

noteSample(a: any) → void

argtypedescription
aany

sampleWired(v: any) → any

A node's output that is an animated value, wired into a param — itself, or a field of a record or an item of a list the param holds — reaches a param the op does not declare `curve` as the value it holds at this eval's time, which makes this node a function of that time.

argtypedescription
vany

getFrame(_self: any) → void

argtypedescription
_selfany

scene(_self: any) → void

The scene the graph is evaluated in, for a node that pulls from it: the entity the graph stands on, and `origin()`, that entity's world matrix now — the graph's own origin.

argtypedescription
_selfany

input(_self: any, socket: string) → void

The value wired into a lazy input, cooked the first time it is asked for; nil when nothing is wired there.

argtypedescription
_selfany
socketstring

getTime(_self: any) → void

argtypedescription
_selfany

liveTime(_self: any) → void

The graph time as a live field: the same value at every element, which a compiled chain reads from its params run. An op whose output moves with time reads it here rather than through `getTime`, and a present for a later time rewrites what it made without a cook; a cook still keys the node on the time.

argtypedescription
_selfany

followTime(_self: any) → void

The graph time and frame for an op over device runs (`deviceColumns`) that folds them into what it makes, such as a compute shader's `time` param: a present for a later time cooks the op again at that time and lands what it makes in the runs this cook wrote, without a cook of the graph.

argtypedescription
_selfany

sample(_self: any, value: any) → void

A value as it stands at this eval's time: an animated value sampled there, anything else as it is. What an op reads a value handed to it through a `curve` socket with.

argtypedescription
_selfany
valueany

live(_self: any, name: string) → void

The animated value behind param `name` when the op declares that param `live` and the graph animates it — on the param itself or through what is wired there — else nil. The param itself arrives as its value at this eval's clock; this is what the op marks the runs it writes from it live with, so a rewrite brings them to a later clock without a cook.

argtypedescription
_selfany
namestring

rewrite(at: any) → void

argtypedescription
atany

release(_self: any) → void

argtypedescription
_selfany

shadow(outs: any) → void

Two cooks sharing a cache can cook one key at once. The later value stands under the key; the earlier one may be standing in a scene or a cooked frame already, so it is kept until the cache itself is released.

argtypedescription
outsany

resolveRef(ref: any) → void

argtypedescription
refany

evalChild(name: string, with: any, pass: number?) → any

argtypedescription
namestring
withany
passnumber?

graphLiveDigest( ) → any

The live state the WHOLE graph reads, rather than the value of one target: every node declaring `liveKey`, folded together, and every composite's child graph asked the same question so a live source nested inside a subgraph counts as much as one at the top. Nil when no node anywhere reads live state, which is what lets a caller poll only the graphs for which polling can ever answer differently.

releaseUnclaimed(result: any) → void

Release what this eval owns and nothing claims: the result is the caller's, the inputs were the caller's all along, and whatever the persistent node cache holds is the cache's to release when it is superseded or torn down.

argtypedescription
resultany

held(v: any) → boolean

argtypedescription
vany

releaseOwnSolverStores(result: any) → void

The solver stores this eval made for itself go with it: each solver's state, and every output it kept that the result does not reach.

argtypedescription
resultany

nodesReaching(g: any, roots: { any }) → void

The ids of the nodes a graph's `roots` reach, following every node output ref their inputs hold. Nil when one of them is a composite's output, which this does not follow.

argtypedescription
gany
roots{ any }

visit(v: any) → void

argtypedescription
vany

timeShape(graph: any, opts: any?) → void

argtypedescription
graphany
optsany?

walk(g: any) → void

argtypedescription
gany

timeDigest(graph: any, clock: any, opts: any?) → number

One number standing for what every animated value a graph reaches holds at `clock` — its own, those of the graphs it composes and the graphs it uses as nodes, and any handed in through `opts.inputs`. Two clocks with equal digests cook identically, so a caller advancing time evaluates only when this moves. A graph holding a solver folds in the time itself: each time names a different simulation state. Nil for a graph that reaches no animated value and holds no solver: time cannot change what it cooks. Cooks nothing. `targets` (`"output:<name>"`, `"node:<id>"`) are what the caller cooks: only the animated values and solvers reaching them count. `nodeCache` is the cache the graph was cooked into: a node whose own code read graph time (`ctx:getFrame()` / `ctx:getTime()`) is remembered there, and makes the time part of the answer.

argtypedescription
graphanyA Graph.
clockany`{ time, frame, fps, duration }`.
optsany?Optional `{ inputs = {..}, resolveGraph = fn, nodeCache = cache, targets = {..} }`.

examples

if proc.timeDigest(g, clock) ~= last then recook() end

historyKey(graph: any, target: string, opts: any?) → number

The time-free key of `target`: everything that decides what it cooks at any time — the graph, the ops and nodes it uses at their versions, the params and inputs, and the live state its sources read — with an animated value taken as the curve it is. Two evals whose history keys match cook the same value at every frame. Cooks nothing.

argtypedescription
graphanyA Graph.
targetstring`"output:<name>"` or `"node:<id>[:<output>]"`.
optsany?Optional `{ inputs = {..}, seed = N, resolveGraph = fn }`.

examples

local key = proc.eval.historyKey(g, "output:mesh", { inputs = params })

liveDigest(graph: any, opts: any?) → any

A digest of every piece of live state a graph reads — the scene entities its `entity.*` nodes stand on, and whatever else an op declares through `liveKey` — folded into one value, composites included. It answers with a different value once any of that state has moved, and with nil for a graph that reads none, so a caller polling for "has the world under this graph changed" pays nothing for the graphs where the answer can never be yes. Cooks nothing: only the declared live reads run. inputs the eval would run under, since a live source can be named by one.

argtypedescription
graphanyA Graph.
optsany?Optional `{ inputs = {..}, seed = N, resolveGraph = fn }` — the same

examples

local live = proc.liveDigest(g, { inputs = { pathEntity = "FencePath" } })

Sub-parts

Everything contained inside this part. Assets are composite children (clickable cards). Files are leaf payloads. Expand any row to view its source.

Showing the 199 items nearest this part. It holds more than this page lists.

199+items
·
assettype · born here
❒asset
# Module (asset type) A module is a reusable, stateless (or globally-stateful) Luau library that other code pulls in with `require()`. Modules are the unit of factored-out logic in a world: helpers, math libraries, format parsers, registries, shared algorithms. ## When to use one - You have logic you want to share across multiple components, services, scenes, or tools. - You have a registry, lookup table, or shared cache that callers all read from / write to. - You want a stable API surface (`M.fn`) other code can depend on. If the logic is bound to one entity, use a `.component`. If you want a named UI of fields rather than functions, use a `.preset`. ## Where it lives - Source: `/zero/source/.../<name>.module/` - Identity: the source path with `/` written as `.` and each segment's type suffix dropped — `/zero/source/demo/beats.module` is `demo.beats`, and `/zero/source/kit.package/beats.module` is `kit.beats`, the container's name kept and its `.package` suffix gone. `asset.create` returns this string, `asset.resolve(...).identity` reads it back, and the `require` forms below write it as `<identity>`. - Folder shape: - `init.luau` (or `init.lua`) — module body. **Required.** Returns a table (conventionally named `M`). - `README.md` — instance-level documentation. **Required.** - `.metadata` — agent-editable tags + free-form fields. **Required.** ## How to create one ```luau asset.create("module", "<name>") -- Creates: /zero/source/<name>.module/ -- init.luau (canonical `local M = {} … return M`) -- README.md (instance README template) -- `folder` places it in a subfolder of /source, and that subfolder -- leads the identity: `{ folder = "lib" }` produces `lib.<name>`. -- `into` authors it inside a resolved container (a package, a toolbox), -- whose own identity leads the same way: asset.create("module", "<name>", { folder = "lib" }) asset.create("module", "<name>", { into = asset.resolve("myPkg.package") }) ``` ## How it operates 1. **Registration.** Writing `init.luau` into a `.module/` folder indexes the asset and registers the module path with the resolver. 2. **Loading.** The first `require("<identity>")` runs the file's body and caches the returned table. Subsequent requires from anywhere return the **same table** — this is the shared-state lever for "module-local" registries. 3. **Hot reload.** Editing the file re-runs the module body and folds what it returns into the table already loaded, so that table keeps its identity: a caller that captured it with `require` reads the new source's functions and values through the reference it already holds, and a later `require` returns that same table. A key the new source drops goes away; a key something set at runtime stays. The body's own `local`s start again from their initial values, so state that must outlive an edit lives in `modules.state()` (`guides { path: "core/scripting-and-tasks" }`). 4. **Identity resolution.** A module is reached by its `<identity>`, and where the caller lives decides which spelling of it resolves. For a module in a **world**, from a caller in that same world — another module, a component, a scene entrypoint, or an `execute` chunk: - `require("<identity>")` — the identity on its own, resolved against the caller's own root. It carries every folder segment, so a module created with `{ folder = "demo" }` is `require("demo.beats")`. - `require("@root::<identity>")` — the same root-relative resolution, written out. - `require(".sibling")` — a module in the same folder, by its bare name; `..name` steps up one folder, `...name` two. Logs, stack traces and hot-reload notices name that same module `@local.source.<identity>` — its key under the world source root. The `<identity>` tail of that key is the form above, so a trace reading `@local.source.demo.beats` is `require("demo.beats")`. For a module in a **library** — content under `/zero/source/libs/@<lib>/` — `@<lib>::` addresses that library's root from anywhere: `require("@builtin::modules.transform")`. Mounting a world as a library moves its files under such a root and flips the caller root to `@<lib>::`, which is what keeps the root-relative forms above resolving across the move. ## Discovery - `asset.list("module")` — every registered module. - `asset.inspect("<name>")` — public functions, source path, this type README. - `ls /zero/source/<name>.module` lists the files the folder holds. ## Authoring conventions - Return a single `M` table from `init.luau`. Top-level statements with side effects run on first require — useful for one-time initialization, dangerous if they touch the engine before it's ready. - Write exported functions as `typed function`, not `function`: the argument types are enforced at the call and the checker knows the signature, so a caller's mistake is reported where they made it. - Annotate exported functions with `--!desc` / `--!arg` / `--!return` / `--!example` so the LSP, `tools.list`, and `asset.inspect` surface them. A function with no `--!desc` is reachable only by someone already reading this file. - Declare a VALUE with `Field.<kind>(default, mode, description)`, and a table whose members are reached through a metatable with `--!members <TypeName>` — the latter gives the surface its accepted member set, its type, and its documentation from one table. See `guides { path: "core/authoring" }` for all four rules and `modules/api/engine/entity.module/members.module` for a worked member table. - Keep module state in module-local upvalues or in `modules.state()`. Globals leak across reloads. Module-locals start again from their initial values with each hot reload; the table `modules.state()` returns is the same table on both sides of every reload. - Prefer focused modules over kitchen-sink modules. If two halves of a module have no shared state, split them. ## Common pitfalls - **Cyclic requires.** A `require` chain that loops will return the partial table (the half built before the cycle was detected). Design for one-way dependencies. - **Engine-time side effects.** Don't `entity.spawn` at module top-level — the engine may not be ready. Expose an `M.init` and call it from a scene entrypoint. - **Hot-reload + module-local cache.** A module that caches expensive computation in upvalues loses that cache on reload — fine for dev, but be aware in performance work. - **`init.lua` vs `init.luau`.** Either works, `.luau` is canonical. - **Writing one while play runs.** A `/source` write made while play is running lands on the play shadow, and a guarded play-exit discards it unless it is kept. One `.module` write shadows `init.luau`, the `README.md` and the `.metadata` together. `vfs.write(path, bytes, { durable = true })` skips the shadow, and `vfs.promotePlayShadow` keeps what is already on it — the `core/vfs` guide has the model and what each route costs. ## Related types - `.component` — for entity-bound state + lifecycle hooks. - `.service` — generates content you don't have yet (a mesh, sound, texture, …) via a metered provider. - `.tool` — for a single agent-callable function with a YAML schema. - `.package` — to group several modules + components + scenes into one shippable folder. ## In the Inspector Exports: what the module exports (name, kind, signature), parsed from its source without running it, with **Check** (the LSP's diagnostics for its entry file) and **Open**.
▲ 0↑ born
▣
module · born here
❒asset
# proc Code-authored, graph-backed procedural generation for the Zero engine. A graph is one function of `g`: `proc.define(function(g) ... end)`. Nodes are called by family and name (`g.<family>.<name>({ ... })`), inputs are declared with `g:input(name, default, opts)`, the returned table is the graph's outputs, and `io.output` is how the graph stands in the world. `builder.module/README.md` holds every rule of that form. This module owns the whole procedural model: the graph builder + data model, the operation registry, the type system, the lazy cached evaluator, deterministic scoped hashing, and the node-family op implementations. Authored `.procGraph` graphs are persisted via `procGraph.assetType`; the `procgen.preview` / `procgen.spawn` operations live in `procgen.toolbox` and are invoked as tools: `use_tool` from an agent, or `tools.use("procgen", "preview", args)` from a script. Generated meshes/materials/textures/bundles are persisted through their own `.assetType`. An op family can also come from outside this package: a `.procPack` asset registers its ops in the same registry, called as `g.op.<id>(...)` exactly like the ones shipped here (`proc.registerPack(name, ops)` is what it registers through, and what a test or a tool calls to hold a family for the life of one VM). `terrain.*` is one such pack, shipped in `terrainSystem.package/procPacks/` and covering a whole landscape; `guides { path = "systems/terrainSystem/documentation/terrain" }` is its guide. To find out what the registry holds, use the `procgen` toolbox: `searchNodes` searches it and `describeNode` describes one node or op. `proc.search_ops(query)` and `proc.schema(id)` are the same answers in-process, for a graph builder looking something up while it runs.
▲ 0↑ born
▣
module · born here
❒asset
# compute_abi Module The kernel ABI of a `.computeShader`. A `bindings.yaml` that uses the ABI extensions is lowered to the plain bindings and WGSL the engine compiles; this module parses, lowers, compiles variants, allocates outputs, packs column tables and dispatches. ## Purpose A kernel is one asset that serves many shapes of data. `bindings.yaml` states: ```yaml bindings: - { name: cols, kind: columns, access: read } - { name: src, kind: buffer, access: read, element: T } - { name: dst, kind: buffer, access: read_write, element: T, count: "len(src)" } - { name: hits, kind: buffer, access: read_write, element: vec4<f32>, capacity: "n * 4", append: true } specialize: - { name: T, options: [f32, i32, vec2<f32>, vec3<f32>, vec4<f32>] } - { name: FOLD, options: [add, min, max, mean, set] } - { name: BODY, kind: source } determinism: exact ``` - `columns`: a table of columns in one storage binding (header: the column count, then four words a column: first word, lanes, count, kind). WGSL reads it through `NAME_count(c)`, `NAME_lanes(c)`, `NAME_f32/i32/u32(c, i, lane)`, `NAME_vec2/3/4(c, i)`; `access: read_write` adds `NAME_set_*`. Columns named at specialization are constants `NAME_col_<column>`. - `count` / `capacity`: the size, in elements, of an output the host allocates. The expression uses `n` (the dispatch size), the shader's params, `len(binding)`, `max`, `min`, `ceil`, `floor` and `+ - * / ( )`. - `append: true`: the output carries a live counter, `NAME_count`, bound right after it. `NAME_store(slot, v)`, `NAME_set_count(n)`, `NAME_count_value()`, `NAME_capacity()` are generated. `NAME_append(v)` (unordered, atomic) is generated for `determinism: float_order` only. - `specialize`: compile-time parameters. Type options become `alias NAME = type;`, named options become `const NAME: u32` plus `NAME_<option>`, and `kind: source` parameters replace the identifier with text. - `when` on a binding: a condition over the specialization options, `NAME=option` or `NAME=option|option`. The binding exists in the variants whose option holds and is left out of the others. Terms separated by spaces all hold (`COUNT=device STAGE=table`); alternatives separated by `;` hold when any one does (`COUNT=plain; COUNT=device STAGE=table`). Two bindings may share a name when their conditions never hold together. - `// @when NAME option option` ... `// @end` in the WGSL: the lines between them stay in the variants where the specialization `NAME` takes one of the options and are blanked in the others (the lines keep their numbers). Blocks may nest. Code that reads a binding a variant leaves out sits in such a block. - `bits: true` on a binding states that its words are raw bits (an f32 held in a u32 word, an atomic word): a column of another element type bound to a read binding that states it is bound as it is, where an i32, u32 or f32 read binding takes a column of another element type cast by value. - `determinism`: `exact` or `float_order`. An `exact` shader that calls `NAME_append` is refused. - `workgroup`: the workgroup size the body declares (default 64). Every scalar or vector `buffer` binding gets `NAME_get(i)`, `NAME_len()` and, unless `access: read`, `NAME_set(i, v)`. A `vec3` element is stored as a flat scalar array, matching a column of vec3 that is 12 bytes an element. `zero_n()` is the dispatch size. ## Usage ```luau local abi = require("@builtin::modules.compute_abi") local schema = abi.parse(bindingsYaml) local program = abi.program({ key = "my_kernel", schema = schema, wgsl = body }) local variant = program:variant({ specialize = { T = "vec3<f32>" } }) local run = variant:run({ n = 4, inputs = { src = srcBuffer } }) local values = abi.read(run.outputs.dst.buffer) ``` A computeShader asset reference exposes the same through `ref:program()`. ## API | Function | Does | |---|---| | `abi.parse(text)` | `bindings.yaml` to `{ bindings, params, specialize, determinism, workgroup }`; a text parses to one schema, the same table each time it is read | | `abi.isExtended(schema)` | whether the schema uses an extension | | `abi.lower(schema, wgsl, spec)` | the lowered source, bindings, params and host slots | | `abi.compile(key, lowered, label)` | compile a lowered variant once, answering its dispatch key | | `abi.program(def)` | a program; `program:variant(spec)` lowers and compiles a variant. A program over the schema and source of the one standing under its key is that program, with the variants it has lowered | | `variant:run(env)` | allocate outputs, bind, dispatch; answers the outputs and live counters. `env.outputs[binding]` is a buffer (or `{ buffer, count?, capacity?, counter? }`) the dispatch writes in place of an allocation; an appended output given one keeps the count its `counter` holds. The first run of a variant waits for its program to be built, so a read of an output asked for after the run sees what the dispatch wrote | | `abi.table(columns, opts)` | pack columns into one buffer for a `columns` binding | | `abi.evaluate(text, vars, lengths)` | evaluate a count expression | | `abi.read(buffer, asU32?)` | read a buffer back, waiting for the frames it takes |
▲ 0↑ born
·
nodebox · born here
❒asset
# prefab (nodebox) Prefabs: the entity records a spawned hierarchy is made of, as record and list values. prefab.empty makes a root, prefab.entity, prefab.parent, prefab.add_component and prefab.link shape the hierarchy (each calls prefab.edit, which holds the record-list edits and the name lookup that prefab.find answers), and prefab.from_geometry, prefab.from_geometries, prefab.from_instances, prefab.from_population and prefab.from_asset make prefabs from what a graph built. The output node shows a prefab and the export node saves it as a bundle. prefab.record and prefab.find are the parts the others are built from: one entity record, and a lookup of an entity by name.
▲ 0↑ born
·
nodebox · born here
❒asset
▲ 0↑ born
·
nodebox · born here
❒asset
▲ 0↑ born
·
procnode · born here
❒asset
# field.distance (procNode) How far each element's position is from a target, as a Float field. It makes the target the wired `b` field or, when `b` is left unwired, the fixed `point` as a constant field (`value.fallback`, `field.constant`), then measures with `field.vector_math` in `distance` mode from `a` to that target. The result compiles to the device with the rest of the field chain. ## Inputs - `a` (`Field<Vec3> | Vec3`): the positions being measured, one per element. - `b` (`Field<Vec3> | Vec3`): what each element is measured to, per element, so the target can move with the element. Wired, it stands in for `point`. - `point` (`Vec3`, default `{ 0, 0, 0 }`): one fixed position every element is measured to when `b` is unwired. It is how a radial falloff around a place is built. ## Outputs - `field`: the distance, a Float field.
▲ 0↑ born
·
nodebox · born here
❒asset
▲ 0↑ born
·
nodebox · born here
❒asset
# instances (nodebox) Instances: pack a geometry into a piece (instances.pack), place a piece set on target points (instances.to_points) and take a population or piece set apart (instances.unpack). A piece is one point whose `Mesh` holds a geometry whole; `geometry.merge` with `pack` puts several together into a piece set. The copies `instances.to_points` makes are a population, which the output node draws as instanced draws, one per piece and material, and export saves as a population asset; any other points stand as one entity each. instances.pack, instances.to_points and instances.unpack are the nodes of that chain, with `points.pose` composing the instance attributes (`P`, `orient`, `scale`, `N`, `v`, `up`, `pscale`, `rot`, `trans`, `pivot`) the copies stand at. Earlier nodes of the nodebox stand a source on points (instances.on_points), deal one of several sources to each point (instances.pick_random, instances.pick_by_attribute), vary and move the copies (instances.randomize_transform, instances.transform, instances.set_material) and merge them into one geometry (instances.realize). The points they make carry a `Mesh` and stand as entities through the output node. instances.place, instances.variants, instances.turn and instances.sources are the parts the others are built from: a transform written onto points, a variant index written onto points, the product of two rotations, and the geometries a population stands (the ones given, else the ones its points carry).
▲ 0↑ born
·
nodebox · born here
❒asset
▲ 0↑ born
·
nodebox · born here
❒asset
# part (nodebox) part.merge packs several values into one part, each piece keeping what it is; part.unpack gives one piece back by its name.
▲ 0↑ born
·
nodebox · born here
❒asset
# math One node per operator and function of the authoring surface: add, sub, mul, div, mod, pow, neg, abs, min, max, floor, ceil, sqrt, sin, cos, tan, acos, atan, log, sign, deg, rad, clamp, lerp, smoothstep, length, normalize, dot, cross, vec2, vec3, vec4, the comparisons (lt, le, gt, ge, eq, ne, and `compare` over them), `where`, the integer functions (to_int, to_float, bor, bxor, shl, floordiv), and `concat` for text. Operators and `math.*` over sockets resolve to these nodes. Each calls an operation of `proc.fieldc.operators`: over plain numbers and vectors it answers the plain result, over a Column or a Field it answers a Field (a fieldc template), so a chain of math nodes is one expression that the node consuming it evaluates in a single dispatch (`fieldc.evaluate`).
▲ 0↑ born
·
nodebox · born here
❒asset
# core The parts the other procgen nodes are built from: connectivity (edges, boundary, adjacency, islands, owner, corners), normals, weld, select, stitch, curve measure and resample, sampling, neighbours, realize, the raster set (`raster.new`, `raster.read`, `raster.stencil`, `raster.sample`, `raster.add_layer`, `raster.remove_layer`, `raster.reduce`, `raster.column`, `raster.resolve`, `raster.blend`, and `raster.cells` under them), `column`, and `entity.read`, which brings scene entities in (assets come in through `io.import`). Each is a graph of the four ops, the kernels of `kernels/` and other core nodes. `join` holds the module that places the parts of a join, called by `concat` and `realize`. A graph calls them as `g.core.<name>`, for instance `g.core.edges`. A node with several outputs answers a record: `local faces = g.core.normals.face(...)` then `faces.geometry`, `faces.scaled`. Every node's README states its inputs and outputs.
▲ 0↑ born
·
nodebox · born here
❒asset
▲ 0↑ born
·
procnode · born here
❒asset
# Scatter (node) A scattering node: its `init.luau` holds the graph that does the work, so the declared inputs are this node's inputs and the declared output is this node's output. The node is its pipeline, in one self-contained asset. It makes points on the surface (`points.on_surface`, area-weighted), gives every point its own random yaw, lean and scale (`instances.randomize_transform`), and places the source on those points (`instances.on_points`). The transform is read from the same point cloud the instances sit on, so each copy takes the pose of its own point. A node is findable by what it does: the `procgen` toolbox's `ops` with the query `scatter` lists it beside `points.on_surface` and `instances.on_points`. Editing this node's `init.luau` cooks again every graph that calls it. ## Inputs - `surface` (Geometry): what the copies land on. - `source` (Geometry): the shape being copied. - `count` (Int, default 200): how many. - `seed` (Int, default 1): changes the arrangement. - `scaleMin` / `scaleMax` (Float, defaults 0.8 and 1.4): per-copy size range. - `tilt` (Float, default 0.15): radians of random lean. ## Output - `instances`: a population, the points each carrying the source as its `Mesh`. ## Authoring a node A node is a graph returned from a `.procNode`'s `init.luau` (`proc.define`). `asset.create("procNode", name)` scaffolds that shape, and `procNode.assetType/README.md` describes what a node states and how graphs call it.
▲ 0↑ born
·
procnode · born here
❒asset
# field.ramp (procNode) A ramp read at every element by a value it carries: a particle's colour, size or opacity following its age. It takes the value each element reads by (the wired `t`, else the element's own `by` attribute, through `value.fallback` and `field.attribute`), maps it from `range` onto 0..1 (`field.map_range`, the ends read off `range` by `value.get`), and reads the ramp there with `ramp.sample`, compiled into the element's dispatch. A value past either end of `range` reads that end of the ramp. ## Inputs - `ramp` (`Ramp`): the ramp to read. - `t` (`Field<Float> | Float`): where along the ramp each element reads. Unwired, it is the element's own `by` attribute. - `by` (`String`, default `"life"`): the Float attribute each element reads the ramp by when `t` is unwired. A particle solver's particles carry `life`, 0 at birth to 1 at death. - `range` (`Vec2`, default `{ 0, 1 }`): the values of `t` that land at 0 and at 1 on the ramp, so a speed from 0 to 20 or a temperature from 300 to 3000 reads the whole ramp. ## Outputs - `field`: the ramp's value at each element.
▲ 0↑ born
·
nodebox · born here
❒asset
▲ 0↑ born
❒
package · born here
❒asset
▲ 0↑ born
·
nodebox · born here
❒asset
# debug (nodebox) debug.dump summarizes any graph value into a record; debug.validate checks a Geometry's structure and reports every issue it finds.
▲ 0↑ born
·❒asset
# geometry.recalculate_normals (procNode) Every vertex's normal averaged, by area, over the faces meeting it. The node is `geometry.smooth_normals` with a crease angle of 180 degrees and area weighting, so no vertex splits: a vertex shared by two faces is smooth across them, and one already split between two copies stays a hard edge. ## Inputs - `geometry` (`Geometry`): whose surface the normals are read off. Attributes and material carry to the result. ## Outputs - `geometry`: the geometry with recomputed per-vertex normals.
▲ 0↑ born
·
nodebox · born here
❒asset
# material (nodebox) Materials as records: material.pbrMat is the standard metallic-roughness material made from its PBR maps (base colour, metallic-roughness, normal, occlusion, emissive) and their factors, and material.shader a material of any surface shader, holding every property and slot to what the shader declares. Each answers one element holding the record of the material kind (`records/material.procRecord`), which geometry.set_material, instances.set_material and the other inputs that take a material put on what draws; the output node shows it and the export node saves it as a `.material`.
▲ 0↑ born
·
procnode · born here
❒asset
# instances.cull (procNode) Keeps the instances a test says to. An instance survives when the value it is judged by is strictly above `threshold`; `invert` keeps the rest instead. The value is the `mask` field when it is wired, else the named point `attribute` (`field.attribute`). `elements.keep` on the points carries every attribute onto the instances that stay, so a culled set realizes as the whole one would have. With nothing to judge by, every instance stays, and inverted, none does. ## Inputs - `instances` (`Points`): the set to thin out, points carrying a Mesh. - `mask` (`Field`): the value each instance is judged by. Wired, it outranks `attribute`. - `threshold` (`Float`, default 0.5): an instance survives when its value reads strictly above this. Raising it thins the set, lowering it fills it back in. - `attribute` (`String`): a point attribute on the set to judge by, read while `mask` is unwired. A name the points do not carry is refused. - `invert` (`Bool`, default false): true keeps the complement, every instance the test dropped. ## Outputs - `instances`: the surviving instances.
▲ 0↑ born
·
procnode · born here
❒asset
# points.box (procNode) A 3D lattice of points filling a box centred on the origin. Each count is at least 1, and the extent the lattice spans is the counts times `spacing` (`value.math`). `grid.box` lays out the cells, x fastest, then y, then z, on the device, and each cell's centre is one point. `attribute.remove` takes the cell layout off the detail so the lattice reads as Points, and each point carries its own number as `id`. ## Inputs - `countX` (`Int`, default 3): how many points along X. The lattice holds countX x countY x countZ points and stays centred on the origin on every axis. Below 1 reads as 1. - `countY` (`Int`, default 3): how many points along Y, the layers the lattice is stacked in. 1 collapses it to a single flat sheet at y = 0. Below 1 reads as 1. - `countZ` (`Int`, default 3): how many points along Z. Below 1 reads as 1. - `spacing` (`Float`, default 1.0): distance between neighbouring points, the same on all three axes. The lattice spans (count - 1) x spacing per axis. ## Outputs - `points`: the lattice, with `id` numbering each point.
▲ 0↑ born
·
nodebox · born here
❒asset
# component (nodebox) component.set gives every element of a class a component with the fields given.
▲ 0↑ born
·
nodebox · born here
❒asset
# texture (nodebox) Textures: images drawn on the device as Texture values, a width, a height and one colour per texel. texture.noise and texture.gradient draw from a formula, texture.from_shader from an author's compute shader and texture.from_raster from the layers of a raster. A material takes a Texture in a slot, and the output node shows one. texture.cells is the part the first two are built from: one point per texel carrying its column and row.
▲ 0↑ born
·
nodebox · born here
❒asset
▲ 0↑ born
·
nodebox · born here
❒asset
# geometry Nodes that move, join, cut, round, thicken and sample meshes. A graph transforms a geometry (`geometry.transform`, `geometry.set_position`, `geometry.merge`), welds and shades it (`geometry.weld`, `geometry.smooth_normals`), models it (`geometry.extrude`, `geometry.solidify`, `geometry.bevel`, `geometry.subdivide`, `geometry.decimate`), builds surfaces from outlines (`geometry.sweep`, `geometry.revolve`, `geometry.loft`, `geometry.ribbon`) and reads it into volumes (`geometry.voxelize`, `geometry.to_sdf`). Each is built from the core nodes and the kernels of the library. ## Nodes - `geometry.transform`, `geometry.set_position`, `geometry.set_material`, `geometry.merge`, `geometry.triangulate`, `geometry.measure`: the basic edits and the measurement of a geometry. - `geometry.weld` (with `match = "position+normal"` it keeps hard edges), `geometry.smooth_normals`: merging points and shading normals. - `geometry.extrude`, `geometry.solidify`, `geometry.bevel`, `geometry.subdivide`, `geometry.decimate`: modelling. - `geometry.sweep`, `geometry.revolve`, `geometry.loft`, `geometry.ribbon`: surfaces from outlines and paths. - `geometry.voxelize`, `geometry.to_sdf`: a mesh as cells and as a distance field. - `geometry.boolean`: union, subtract or intersect of two closed meshes, over the `intersect` kernels. - `geometry.selected`, `geometry.flip`, `geometry.profile`, `geometry.tangents`, `geometry.sides`, `geometry.shared`, `geometry.bvh`, `geometry.rings`, `geometry.first`: the parts the nodes above are built from, each usable on its own. - `geometry.unit`, `geometry.perpendicular`, `geometry.rotate`, `geometry.transport`: vector arithmetic over sockets that the nodes which orient points and normals share. ## Conventions - Each node's folder name is its call name inside the box: `geometry.<folder>`. - Inputs and outputs carry the names of the op each node replaces. - A node that answers a Data table from numbers read back off the device holds the Luau for it in a `readings.module` inside its own folder. - A node with one output answers that output's value; a node with several answers a record by output name. - A fold's index runs from 1: the first pass is 1, so a pass that works on level, chunk or step `n` reads `index - 1` when it counts from 0 (`geometry.bvh`, `geometry.to_sdf`, `geometry.sweep`, `geometry.loft`). - A field bound to a kernel takes the length of that dispatch, so a node writes a value out as a column (a gather with an explicit `count`) before it binds one whose length differs from the dispatch's.
▲ 0↑ born
·
nodebox · born here
❒asset
# sdf (nodebox) Signed distance field nodes: shapes, smooth combines, round and shell, space warps (transform, twist, bend, taper, elongate, mirror, repeat), displacement, grids and fields in and out, and sdf.mesh, which contours a shape into a closed mesh.
▲ 0↑ born
·
procnode · born here
❒asset
# points.grid (procNode) A flat lattice of points in the XZ plane, centred on the origin. Each count is at least 1, and the extent the lattice spans is the counts times `spacing` (`value.math`). `grid.box`, one cell thick at height `y`, lays out the cells, x fastest, then z, on the device, and each cell's centre is one point. `attribute.remove` takes the cell layout off the detail so the lattice reads as Points, and each point carries its own number as `id`. ## Inputs - `countX` (`Int`, default 4): how many points along X. The grid holds countX x countZ points and stays centred on the origin, so each one added widens it by `spacing`, half of that to either side. Below 1 reads as 1. - `countZ` (`Int`, default 4): how many points along Z. A count of 1 leaves a single row running along X. Below 1 reads as 1. - `spacing` (`Float`, default 1.0): distance between neighbouring points, the same on both axes. The grid spans (countX - 1) x spacing by (countZ - 1) x spacing. - `y` (`Float`, default 0.0): the height the whole lattice sits at. Every point takes this as its y. ## Outputs - `points`: the lattice, with `id` numbering each point.
▲ 0↑ born
·
procnode · born here
❒asset
# points.filter (procNode) Keeps the points a test says to. The value each point is tested by is the `mask` field when it is wired, else the named `attribute`'s values (`attribute.labels`). The test is a comparison (`field.compare`): above `threshold`, or, when `equals` is given, equal to the index that value or label names. `elements.keep` on the points carries every attribute and the detail onto the points that stay, counted on the device. Given neither a mask nor an attribute, the points come back as they are. ## Inputs - `points` (`Geometry`): the cloud to thin. Every point attribute and every detail value is carried onto the survivors, so a colour or an index still belongs to the point it started on. - `mask` (`Field`): the per-point value tested; a point stays when it is above `threshold`. Wired, it decides, and `attribute` is read only when this is empty. - `threshold` (`Float`, default 0.5): a point stays when its value is strictly above this. Raising it keeps fewer points. - `attribute` (`String`): a point attribute to test instead of wiring `mask`. A name the points do not carry is refused, naming the ones they do. - `equals` (`String | Float`): keeps the points whose `attribute` equals this instead of testing it against `threshold`. It is a number, or a label: one of the texts a text attribute holds, or one of the names the points' detail `labels` gives a numeric one (how one solver's particles are split back by the `stream` their source named). ## Outputs - `points`: the points that stay.
▲ 0↑ born
·
nodebox · born here
❒asset
▲ 0↑ born
·
procnode · born here
❒asset
# geometry.delete (procNode) Deletes the triangles a test selects. A triangle is selected when its value is strictly above `threshold`. The value each triangle is tested by is the `mask` field when it is wired, else the named primitive `attribute` (`field.attribute`), else 0. `invert` deletes the triangles the test did not select. `elements.keep` on the primitives does the removal: every point stays, and the triangles that remain keep their corners and attributes, counted on the device. ## Inputs - `geometry` (`Curves | Surface`): whose triangles are thinned. Point and detail attributes carry over as they are; primitive and vertex attributes stay with the triangles that survive. - `mask` (`Field`): read once per triangle for whether it goes. Wired, it is what the selection reads, and `attribute` is read only when this is empty. - `threshold` (`Float`, default 0.5): a triangle is selected when its value is strictly above this. Raising it deletes less. - `attribute` (`String`): a primitive attribute on the geometry to read the selection from, instead of a wired mask. A name the triangles do not carry is refused. - `invert` (`Bool`, default false): true deletes every triangle the test did not select, so a mask naming what to keep cuts away the rest. ## Outputs - `geometry`: the geometry with the selected triangles removed.
▲ 0↑ born
·
nodebox · born here
❒asset
# shader (nodebox) An author's own compute shaders as nodes. shader.compute runs a .computeShader over the elements of a geometry through the device.kernel op: each buffer binding is a column of the geometry, each param a knob of the node, and what the shader writes comes out as columns.
▲ 0↑ born
·
nodebox · born here
❒asset
# scene (nodebox) scene.query finds entity ids by name, component and hierarchy, scene.fetch brings entities in as points carrying their pose and components, scene.input is the entity the graph stands on, and scene.water is the water the scene holds.
▲ 0↑ born
·
procnode · born here
❒asset
# geometry.separate (procNode) Splits a geometry's triangles in two by a test. The value each triangle is tested by is the `mask` field when it is wired, else the named primitive `attribute` (`field.attribute`), else 0. Two comparisons against `threshold` follow (`field.compare`): strictly above, and at or below. `elements.keep` on the primitives runs once for each, so `a` holds the triangles above and `b` the rest. Both keep every point, and between them they hold every triangle once. ## Inputs - `geometry` (`Curves | Surface`): whose triangles are divided. Point and detail attributes carry to both halves as they are; primitive and vertex attributes stay with each half's own triangles. - `mask` (`Field`): read once per triangle for which half it joins. Wired, it is what the split reads, and `attribute` is read only when this is empty. - `threshold` (`Float`, default 0.5): a triangle goes to `a` when its value is strictly above this, and to `b` otherwise. - `attribute` (`String`): a primitive attribute on the geometry to read the split from, instead of a wired mask. A name the triangles do not carry is refused. ## Outputs - `a`: the triangles whose value is strictly above `threshold`. - `b`: the rest. A call answers both as a record by output name.
▲ 0↑ born
·
nodebox · born here
❒asset
# sim (nodebox) Simulation nodes: sim.spawn, sim.force, sim.collider, sim.scene_colliders, sim.particles and sim.shader, built on the runtime's g:state and g:step.
▲ 0↑ born
·
nodebox · born here
❒asset
▲ 0↑ born
·
nodebox · born here
❒asset
# abiBox A node for the procgenAbi suite: it is called through the builder and written out in a graph.
▲ 0↑ born
·
nodebox · born here
❒asset
▲ 0↑ born
·
nodebox · born here
❒asset
# mesh The primitive solids and sheets a graph starts from: `mesh.box`, `mesh.sphere`, `mesh.cylinder`, `mesh.cone`, `mesh.capsule`, `mesh.octahedron`, `mesh.torus`, `mesh.rounded_box`, `mesh.plane` and `mesh.grid`. Each is made on the device from lattices and fans of the `make` kernel, with positions, normals and texture coordinates written as formulas over the lattice, and answers triangles centred on the origin that fill the box its parameters name. ## Conventions - Each node's folder name is its call name inside the box: `mesh.<folder>`. - Parameters keep the names and defaults of the op each node replaces. - `mesh.lattice` makes the lattices and fans the presets share, `mesh.spherical` the point of a sphere and `mesh.cube_faces` the faces of a cube; a preset writes its positions, normals and texture coordinates as formulas over the lattice.
▲ 0↑ born
·
nodebox · born here
❒asset
# io The ways in and out of a graph: `luau` runs a function of a Luau module on the CPU, `output` shows a value in the world and answers a Handle, `export` saves a value as an asset, and `engine` calls the engine bridge the core reading nodes and `export` use. `output` holds `show.module` and `engine` holds `engine.module`, the functions they call.
▲ 0↑ born
·
nodebox · born here
❒asset
# entity (nodebox) entity.transform and entity.bounds read one entity's pose and bounds as values, entity.points reads a set of entities as points, and entity.geometry reads the mesh an entity's Model draws as a Geometry.
▲ 0↑ born
▣
module · born here
❒asset
# entity_resolve The id behind an entity reference, whatever shape the reference took. An entity is written down three ways: as an **id string** (`"ent_5d305b4377f8d505"`), as an **entity proxy** (the table carrying `.position`, `.component`, `.name`), and — by a person or an agent typing a call — as the **display name** the entity carries in its Name component. `entity(ref)` answers for the first two. A call that hands it a name gets nil back, and code that reads that nil as "the entity is gone" either indexes it or writes somewhere else entirely. This module is the one place that turns any of the three into the id every engine call takes. ## Surface ``` local resolve = require("modules.entity_resolve") resolve.id(ref) -> string? -- the id, or nil when nothing matches resolve.idOrError(ref, slot) -> string -- the id, or a raise naming the miss ``` `ref` is an id string, a display name, or an entity proxy. `slot` names the argument the reference arrived in, so the raise reads as the call the caller wrote rather than as a line number inside the callee. A proxy or an `{ id = ... }` record answers the id written on it. A string is the shape the scene has to answer for, since a lookup is the only thing that can say whether it is a name or an id — and both probes stay silent for one that matches nothing: a name is tested with `entity.find`, which answers for an id as well, and then as an id with `entity.exists`, which raises for a string naming a live entity and is therefore probed under `pcall`. A string matching nothing reaches the caller as `nil`, with nothing already written to the log on its behalf. ## Examples Accept whichever shape the caller wrote, then act on the id: ```lua local resolve = require("modules.entity_resolve") local function raise(target, dy) local id = resolve.idOrError(target, "target") local e = entity(id) e.localPosition = { e.localPosition.x, e.localPosition.y + dy, e.localPosition.z } end raise("crate_07", 2) -- a display name raise("ent_5d305b4377f8d505", 2) -- an id raise(entity.find("crate_07"), 2) -- a proxy ``` Answer for a reference that may match nothing, without raising: ```lua local id = resolve.id(maybeGone) if id ~= nil then entity(id).despawn() end ```
▲ 0↑ born
❒
package · born here
❒asset
▲ 0↑ born
△
mesh · born here
❒asset
▲ 0↑ born
▣
module · born here
❒asset
# display_writers The ordered record of what is setting the chain's display transform. `__display__` carries one curve and one exposure for the whole frame. Several things write them — every `global` `PostProcess` region with `setsDisplay = true`, a scene's opening state, an agent's own `setProperty` call — and this module records the live ones, newest last. ```lua local DisplayWriters = require("@builtin::modules.display_writers") DisplayWriters.push("look", { curve = 2, exposure = 1.5 }) -- ACES, +2/3 stop DisplayWriters.remove("look") -- back to what stood ``` - `read()` — what `__display__` holds now: the value the last write left, or the default the renderer declares for a property nothing has written. - `push(key, { curve, exposure })` — show those values and record `key` as the writer holding them. A key already recorded moves to the newest position. - `remove(key)` — drop that writer. The display then shows the newest remaining writer's values, or what stood before the first writer arrived when none remain. - `count()` — how many writers are live. The record is one shared list, so the writers come off in any order: removing the older of two leaves the newer one's values standing, and removing the newer leaves the older's. The exposure package drives `__display__.exposure` alone, from a GPU readback, every tick it meters, and keeps that outside this list — so a region that arrives or leaves sets the exposure and the package's next readback sets it again. The package holds its own record of the exposure that stood before it started metering and hands that back when it stops.
▲ 0↑ born
▣
module · born here
❒asset
# vfs Module Public Luau surface over the `__vfs` Internal FFI namespace — the engine's virtual filesystem. ## Purpose Wrap the raw `__vfs.*` FFI namespace in a typed Luau table that gets auto-injected as `_G.vfs` via the prelude. The standard read / write / move / remove / mkdir / list / exists / readAsync / reload / evict surface, plus trusted-only `watch` / `unwatch` for the bootstrap VM. `vfs.read` yields on a byte-cache miss: the module's last step installs the `vfs_async_read` fallback over the synchronous read, so every VM that holds this namespace, the trusted VM included, reads a path through the same two steps and gets the same bytes for it. ## Usage ```luau -- Read / write local src = vfs.read("@builtin/components/Camera.luau") vfs.write("/zero/source/notes.md", body) -- What a write landed: where the bytes went, and what the static passes -- read in the Luau among them local ok, report = vfs.write("/zero/source/game/Vent.component/init.luau", code) if report.diagnostics then for _, d in report.diagnostics do print(d.severity, d.path, d.line, d.message) end end if not report.durable then log.warn(report.warning) end -- Listing for _, e in ipairs(vfs.list("/zero/source")) do print(e.isDirectory and "[d] " or " ", e.name) end -- Async fetch local png = task.await(vfs.readAsync("/zero/runtime/screenshots/last.png")) -- Reclaim RAM after processing a big binary vfs.evict("/zero/source/textures/imported_huge.png") -- Reload a module after editing its source on disk if vfs.reload("@mylib/utils.helpers") then local m = require("@mylib/utils.helpers") -- sees the new source end ``` `vfs.reload(identity)` answers whether a module was cached under that name and has now been dropped. A `false` says the name matched nothing — either a misspelled identity or a module this VM never required — so the next `require()` returns whatever it would have returned anyway. A module that caches state derived from OTHER files keeps serving the old value when those files are rewritten, since only its own source is watched. Reload it by identity to make the next `require()` recompute. ## Exports - `vfs.read(path, opts?) -> string?` - `vfs.write(path, content, opts?) -> (boolean, WriteReport | string)` - `vfs.move(src, dst, opts?) -> (boolean, WriteReport | string)` - `vfs.copy(src, dst) -> (boolean, WriteReport | string)` - `vfs.remove(path, opts?) -> (boolean, string?)` - `vfs.mkdir(path, opts?) -> boolean` - `vfs.list(path?) -> { VfsListEntry }` - `vfs.exists(path, opts?) -> boolean` - `vfs.readAsync(path, opts?) -> promiseId` - `vfs.reload(modulePath?) -> boolean` - `vfs.evict(path, opts?) -> boolean` - `vfs.mutationSeq() -> number` - Trusted-only: `vfs.watch(path, callback) -> number` / `vfs.unwatch(watcherId) -> boolean`.
▲ 0↑ born
❒
package · born here
❒asset
▲ 0↑ born
▣
module · born here
❒asset
# debugViz_driver The ambient edit-mode debug-visualization driver. Requiring it registers the built-in providers (bounds, frustum, bones, colliders, icons, wireframe) with `@builtin::modules.debugViz`; the editor entrypoint requires it and calls `tick(dt)` every frame. ```lua local DebugVizDriver = require("@builtin::modules.debugViz_driver") -- In the editor entrypoint's per-frame hook: DebugVizDriver.tick(dt) ``` - `register()` — register the built-in providers. Idempotent: `registerProvider` replaces by name and a guard makes a re-require a no-op, so a category toggled off in the panel survives a module reload. Called on require and by `tick`. - `tick(dt)` — per-frame hook for both modes. Edit mode always rebuilds the overlay; play mode rebuilds only while a debug session is active (`debugViz.setPlaySession`) and otherwise clears the batch, so play mode carries no debug-draw cost by default. There is no per-entity opt-in — the overlay is editor tooling, driven here for the whole scene. Toggle categories through the Debug → Visualization panel or the `debug` toolbox.
▲ 0↑ born
◐
material · born here
❒asset
# postReference_focusBars Four rows of bar pairs, 8 pairs across the top row and doubling on each row below: the focus chart a depth-of-field reading is taken off. Drawn unlit by `@builtin::shaders.referenceChart`, so a picture of it is measured against the light its properties state. Part of the post-verification reference scenes (`@builtin::scenes.postReference.*`); `@builtin::modules.postMetrics` reads them.
▲ 0↑ born
·
computeshader · born here
❒asset
# loops_cut Finds where each ring and chain of linked edges starts and ends. Stage 0 marks the first edge of every list (a chain's head, a ring's smallest edge); stage 1 makes the edge before each first edge the end of its list. The lists that result are what `jump` ranks. See `loops` for the whole sequence. Over a live count (`COUNT=device`, see the abi README) the edges are the first `domain_count` slots of a capacity `n`, and the `n` param is not read. A slot past the live edges is no head (`head` 0) and ends its own list (`cut` is the slot itself). It binds 7 storage buffers (6 in the plain variant).
▲ 0↑ born
·
scene · born here
❒asset
▲ 0↑ born
▣
module · born here
❒asset
# lumen_player A UI template screen.
▲ 0↑ born
❒
package · born here
❒asset
▲ 0↑ born
△
mesh · born here
❒asset
▲ 0↑ born
▣
module · born here
❒asset
# logs Module Public Luau surface over the `__logs` Internal FFI namespace — read-only, filterable query API over the engine's in-memory log ring. ## Purpose Wrap the raw `__logs.*` FFI namespace in a typed Luau table that gets auto-injected as `_G.logs` via the prelude. The store is the single in-engine source of truth for everything the engine has logged recently — Rust `console_log!`, bridged third-party `log::*` events (wgpu validation, etc.), and Rust panics. The same store backs the `/engine/logs` HTTP endpoint and `/zero/runtime/logs/engine`. ## Usage ```luau -- Did anything break? for _, e in ipairs(logs.errors(10)) do print(e.timestamp, e.message) end -- Filtered query for _, e in ipairs(logs.query({ level = "warn", type = "RENDERER", limit = 50 })) do print(e.message) end -- Case-insensitive grep local hits = logs.find("MY_MARKER") -- Aggregate counters (lifetime, survive ring eviction) local c = logs.count() print(c.errors, "errors,", c.warnings, "warnings,", c.dropped, "dropped") ``` ## Exports - `logs.query(opts?) -> { LogEntry }` - `logs.errors(limit?) -> { LogEntry }` - `logs.warnings(limit?) -> { LogEntry }` - `logs.find(text, limit?) -> { LogEntry }` - `logs.tail(limit?) -> { LogEntry }` - `logs.count(opts?) -> LogCounts` - `logs.template(message) -> string` - `logs.clear() -> boolean` ## Origin A line a script logged carries `script` and `entity` fields, so `logs.query({ entity = "ent_9f3c" })` asks for one entity's output rather than guessing at what its messages look like. Engine lines carry neither. ## Context `logs.query({ level = "error", context = 3 })` also returns the three lines either side of each match, marked `context = true`. What led up to a failure is made of lines the filter rejected, so a filtered view cannot otherwise show it. ## The other ways in The `logs` toolbox reads the same ring through the tool surface — its `search`, `tail`, `errors`, `summary` and `clear` tools — and adds grouping of repeated messages. This module is the direct Luau path to that ring. The ring is also mounted as plain files: `/zero/runtime/logs/engine` for every level, `/zero/runtime/logs/errors` for the ERROR-and-above subset, and `/zero/runtime/logs/script` for what scripts logged. Those render the most recent 500 lines with tool-call records included, where a query here reaches the whole ring and leaves them out unless asked for. The ring starts empty at every boot. `/zero/runtime/logs/previous` holds the previous session of the world the engine booted on: how it ended, then the last lines it wrote.
▲ 0↑ born
◐
material · born here
❒asset
# postReference_skyRamp A sky-blue ramp of light from 0.02 to 0.6 across: a clear sky's gradient. Drawn unlit by `@builtin::shaders.referenceChart`, so a picture of it is measured against the light its properties state. Part of the post-verification reference scenes (`@builtin::scenes.postReference.*`); `@builtin::modules.postMetrics` reads them.
▲ 0↑ born
·
computeshader · born here
❒asset
# hash The first step of a spatial hash over a run of points: the bucket of the cell each point stands in. The sort kernel orders the points by bucket, and `hash_starts` then states where each bucket's points start and end in that order. The keys, the order and the bucket spans, with the cell size, are the index `hash_count`, `hash_nearest` and `hash_gather` search. Exact: the same points give the same keys. Over a live count (`COUNT=device`, see the abi README) `n` is the capacity and the first `domain_count` slots are live. A slot at or past the live count holds the key `0x7fffffff`, which is past every bucket: the sort (over the same live count, or over the capacity) places it after every live key, and `hash_starts` over the capacity states every bucket's span over the live points only. It binds 3 storage buffers (2 in the plain variant).
▲ 0↑ born
▣
module · born here
❒asset
# http Module Public Luau surface over the `__http` Internal FFI namespace — async HTTP GET / POST returning JSON or raw bytes. ## Purpose Wrap the raw `__http.*` FFI namespace in a typed Luau table that gets auto-injected as `_G.http` via the prelude. Every method returns a promise handle: pass it to `task.await(handle)` to block the coroutine until the request completes. ## Usage ```luau -- GET JSON local data = task.await(http.get_json("https://api.example.com/info")) -- POST JSON local resp = task.await(http.post_json( "https://api.example.com/users", { Authorization = "Bearer " .. token }, { name = "Alice", age = 42 } )) -- Download a binary asset local bytes = task.await(http.get_bytes("https://example.com/sound.ogg")) vfs.write("/zero/source/audio/sound.ogg", bytes) ``` ## The bound a request carries Every method takes a deadline as its last argument: how long the request is given to be answered, in milliseconds. Left out, the engine's own bound applies, which is what settles a call against a peer that accepts the connection and answers nothing. Passed as 0, no bound applies, which a transfer whose length the caller knows better than the engine does names. A request given up on resolves like any other failure, with the engine's error envelope: `{ "__http_error": true, "error": "Network error: no answer within <n>ms" }`. ```luau -- Give this call three seconds to be answered local credential = task.await(http.post_json(tokenUrl, headers, nil, 3000)) -- A download whose length the caller knows better than the engine does local bytes = task.await(http.get_bytes(assetUrl, nil, 0)) ``` ## Exports - `http.get_json(url, headers?, timeoutMs?) -> promiseId` - `http.get_bytes(url, headers?, timeoutMs?) -> promiseId` - `http.post_json(url, headers?, body?, timeoutMs?) -> promiseId` - `http.post_bytes(url, headers?, body?, timeoutMs?) -> promiseId` - `http.request(method, url, headers?, body?, timeoutMs?) -> promiseId` - `http.request_raw(method, url, headers?, body?, timeoutMs?) -> promiseId`
▲ 0↑ born
▣
module · born here
❒asset
# audio Module Public Luau surface over the `__audio` Internal FFI namespace — the engine-native CPU audio codec for the canonical ZAUD payload (backed by `zero_audio::blob`), and the engine's audio observation surface. Auto-injected as `_G.audio` via the prelude. ## Observing what is audible `audio.observe()` answers "what is this engine sounding right now, and why is a source not" in one read. It reports the MIXER's own bookkeeping, which is what makes it different from reading the `Audio` component's fields back: those carry what content asked for, and `AudioSource.state` carries the value `:play()` wrote there. Every voice report carries both — `mixerState` beside `componentState` — so the two are compared rather than confused. - `audio.observe() -> AudioObservation` — the whole surface in one call: `{ available, sampleRate, channels, sampledAtMs, voices, audibleCount, listener, voiceAccounting, levels, profile }`. - `audio.voices() -> { AudioVoice }` — every live source with the mixer's opinion of it: the clip, the channel, whether its bytes are resident, the mixer's playback state, the position in the clip and the clip's duration, the gain it reaches the mix at stage by stage, and for a spatial source its emitter, the listener distance, `maxDistance` and `rolloff`. - `audio.voice(entityId) -> AudioVoice?` — the voice on one entity. - `audio.whySilent(entityId) -> reason?, detail?` — why a source makes no sound, from a closed set: `noBackend`, `noDevice`, `notResident`, `neverStarted`, `refused`, `paused`, `ended`, `gainZero`, `channelSilent`, `masterSilent`, `outOfRange`. `nil` while it IS sounding; the second return carries the mixer's own words when it refused the source. - `audio.device() -> AudioDeviceStatus` — the output device the mix reaches: `state` (`"open"` / `"silent"`), the device an open stream runs on, the rate and channel count it runs at, and the counters for what the engine has been through keeping a stream open — `faults`, `changes`, `reopens`, `failedOpens`, `glitches` and `lastError`. An output device that goes away leaves the mixer running and every voice reporting `noDevice` until the engine opens a stream again. - `audio.levels() -> AudioLevels` — the master mix's peak and RMS over the meter's most recent closed window, with no recording involved. `windows` counts the windows the mixer has closed since it started, so a silent mix reads differently from a mixer that is not running. - `audio.peakSince(window) -> number?` — the loudest peak the master mix reached across the windows that closed after `audio.levels().windows` stood at `window`. `levels()` carries the window that closed last, so a reader sees the windows its own frames land on; this spans all of them, which is what measuring a sound shorter than the gap between two reads takes. `nil` when the meter holds no peak for the span — nothing has closed since `window`, or it reaches further back than the meter's history of recent windows. - `audio.listener() -> AudioListenerState` — where the scene is heard from, and how many active listeners exist. - `audio.voiceAccounting() -> AudioVoiceAccounting` — voice capacity, in use and free read off the mixer's own tracks, so `free` is what the next play call is granted or refused against. The mixer keeps two pools and each is reported apart: `capacity` / `inUse` / `free` are the main track, carrying the NON-spatial voices, while a spatial voice plays through its own sub-track and is counted by `spatialCapacity` / `spatialInUse`. `sourcesHolding` counts both pools from the sources that own them, so it equals `inUse + spatialInUse` while every voice the mixer carries answers to a source — and a mixer carrying voices no source reaches is the difference between those numbers. - `audio.profile() -> AudioProfile` / `audio.resetProfile()` — what the audio subsystem costs: the streaming pump, clip decode and encode, voice starts, and building the observation. Every total is a sum across a window rather than a per-frame figure: the window runs from the last `resetProfile` — or from engine start when nothing has reset it — and its length comes back as `windowMs` and `frames`. Reset, let frames pass, then divide a total by `frames` for what a frame costs. The same document is served at `/runtime/active_audio`, with one file per source under `/runtime/active_audio/voices/<entity>`, as the audio systems published it after the last frame; `audio.observe()` leaves out the voices of entities a script has despawned since. It answers in edit mode as well as play mode. ## Setting what the mix is heard at Every voice names a channel — the `Audio` component's `channel` field, `"sfx"` unless the source says otherwise — and is heard at its own `volume` scaled by the level of that channel and by the master level of the mix. Both levels reach voices that are already playing, so a settings-screen slider is heard as it moves, and both come back per voice as `gain.channel` and `gain.master`. - `audio.mixer() -> AudioMixerLevels` — `{ master, muted, channels }`. A channel absent from `channels` has had no level set and plays at unity, so a scene that invented its own channel names is heard exactly as authored until something turns one of them down. - `audio.setChannelVolume(channel, volume)` — set one channel's level. Scales every voice on that channel and nothing else. - `audio.setMasterVolume(volume)` — set the level of the whole mix. - `audio.setMuted(muted)` — silence the mix whatever the levels read; unmuting hands them back untouched. ```luau audio.setChannelVolume("music", 0.3) -- music at 30%, sfx untouched audio.setMasterVolume(0.8) audio.setMuted(true) -- every voice reports masterSilent ``` ## Codec functions - `audio.encode(sourceBytes, opts?) -> bytes?, err?` — decode container audio (ogg / mp3 / wav / flac) and re-encode it into a ZAUD payload. - `audio.encodePcm(pcm, sampleRate, channels, opts?) -> bytes?, err?` — encode raw interleaved f32 PCM (a `buffer` or a binary string of little-endian f32, OR a flat number array) into a ZAUD payload. - `audio.decode(zaud) -> pcm?, sampleRate?, channels?` — decode a ZAUD payload to interleaved f32 PCM (`pcm` is a binary little-endian-f32 string). - `audio.info(zaud) -> table?, err?` — read a ZAUD header: `{ sampleRate, channels, frames, durationMs, codec, loopStart, loopEnd, packets }`. - `audio.loopSeam(zaud) -> table?, err?` — measure what a clip's samples do where a whole-clip loop wraps: `{ ratio, step, meanStep, maxStep, channel, seamless, threshold, channels }`. - `audio.peaks(zaud, buckets) -> { number }?, err?` — the loudest sample in each of `buckets` (an integer from 1 to 4096) equal spans of a clip, across its channels, held to 1: the shape a waveform is drawn from. A span is `frames // buckets` frames; the payload is decoded a block at a time, so the work never holds the clip's whole PCM. A payload that is not a clip, or stops short, answers `(nil, err)`. Every sample an encode is handed must be a finite number. A NaN or an infinity carries no signal, and both codecs turn one into a payload whose header reads as a healthy clip and whose samples sound as nothing — so `audio.encode` and `audio.encodePcm` return `(nil, err)` naming how many samples fail and where the first one sits, and `audio.info` reads a PCM payload's samples the same way and answers for what the payload carries rather than for what its header claims. That makes a divergent filter, a `0/0`, or a `math.log(0)` in a bake visible at the call that would otherwise have written it. ```lua local zaud, err = audio.encodePcm(samples, 22050, 1, { codec = "pcm" }) if not zaud then print(err) -- "…: 3 of 176400 samples are not finite, first at index 88214" end ``` Whole-clip repeat is the loop the runtime plays, so the join between a clip's last frame and its first is heard once a lap, and a bed whose head does not meet its tail ticks for as long as it plays. `audio.loopSeam` is the engine's reading of that join: the step the wrap makes (`|x[1] - x[frames]|`) against the step the signal ordinarily makes between neighbouring samples, as `ratio = step / meanStep`. Reading it as a ratio puts the figure in the units the signal itself moves in, so a generated ambience travelling a thousandth of a unit per sample and a synth drone travelling a hundredth are judged the same way. A bed whose partials complete a whole number of cycles across the buffer reads near 1; one carrying a percussive strike at its head and silence at its tail reads in the tens. `seamless` is `ratio <= threshold`, and `threshold` is the ratio `asset.create("soundClip", ...)` warns past. ```lua local seam = audio.loopSeam(clipRef:getBytes()) print(seam.ratio, seam.seamless, seam.threshold) -- 41.0 false 8 -- `ratio` is the worst channel's; `channel` names it and `channels` carries -- each one's own { ratio, step, meanStep, maxStep }. print(seam.channel, seam.channels[seam.channel].step) ``` The reading is taken on the DECODED samples, so it answers for what the codec left behind rather than for the buffer that was handed to the encoder, and for a clip that arrived already encoded and whose source buffer nobody holds. That costs a decode of the whole payload — `audio.info` reads a header without one. A clip is counted in frames — `channels` samples that sound together — and a header's `frames * channels` is the sample count `audio.decode` hands back. So the buffer `audio.encodePcm` takes is a whole number of frames, and one whose length leaves a tail over comes back as `(nil, err)` naming the whole frames it holds and the samples past them. ```lua local zaud, err = audio.encodePcm(fiveSamples, 22050, 2, { codec = "pcm" }) if not zaud then print(err) -- "…: 5 samples is not a whole number of 2-channel frames — 2 whole frames and 1 left over" end ``` A byte payload carries a sample per 4-byte lane, so `audio.encodePcm` takes the `buffer` `microphone.samples()` hands back and the string `audio.decode` returns as they are, and recording a capture into a clip is those two calls: ```lua local status = microphone.status() local zaud = audio.encodePcm(microphone.samples(), status.sampleRate, 1) ``` A byte length that stops partway through a lane describes a sample the caller holds only part of, and comes back as `(nil, err)` naming the whole samples it holds and the bytes past them: ```lua local zaud, err = audio.encodePcm(twentySevenBytes, 22050, 1, { codec = "pcm" }) if not zaud then print(err) -- "…: 27 bytes is not a whole number of f32 samples — 6 whole samples and 3 bytes left over" end ``` `opts` (all optional): `codec` (`"opus"` default / `"pcm"`), `bitrateKbps` (default 96), `vbr` (default true), `sampleRate` (resample target, default 0 = keep source), `forceMono` (default false), `loopStart` / `loopEnd` (frame indices, default 0). Pure CPU — runs on native, WASM, and headless.
▲ 0↑ born
▣
module · born here
❒asset
# entity The `entity` global: spawning, despawning, finding, and the reference you get back when you call it. `entity` is a table you can also call. `entity.spawn(...)` and its siblings act on the scene; `entity(id)` hands back an **entityRef** — the per-entity surface carrying `.position`, `.component`, `:despawn()` and the rest. ```luau local id = entity.spawn("Crate") local e = entity(id) e.position = { 0, 1, 0 } e.component.add("Model", { mesh = "@builtin::meshes.cube" }) ``` ## What lives here | File | What it is | |------|-----------| | `init.luau` | The `entity` global — typed wrappers over the Internal `__entity` namespace, plus the coercions and `entity.hierarchy`. | | `members.module/` | The entityRef's member set: every member's kind, type and description, in one table. | | `ref.module/` | The entityRef metatable — what `entity(id)` returns and how a member access dispatches. | ## Where the entityRef's surface is defined `members.module` is the single description of it. The runtime's accepted-member set derives from that table, the LSP's `entityRef` type is generated from it, and each member's documentation is published from it. Nothing describes the entityRef anywhere else — a second description could only drift from this one. To add a member: declare it in `members.module` with its `kind`, `type` and `doc`, and implement it in `ref.module`. A member missing from the table is refused by the runtime; a member in the table with no implementation answers nil. ## Related - `entity.hierarchy` — swap a blockout entity for a generated asset. - `ecs.*` — the ECS storage surface. **ECS components are not script components**; `entity(id).component` is the script-component surface and the two are separate systems.
▲ 0↑ born
❒
package · born here
❒asset
▲ 0↑ born
❒
package · born here
❒asset
▲ 0↑ born
❒
package · born here
❒asset
# volumetrics Volumetric (3D-texture) rendering, end to end, in one package. Built entirely on the engine's generic `compute.*` GPU primitives — no Rust changes are needed to author new volumetric content. ## Layout | Asset | Identity | Role | |---|---|---| | `Volume.component` | `@builtin::systems.volumetrics.Volume` | Attach a 3D voxel texture to an entity; raymarches it through the entity's transform AABB. | | `VolumetricSky.component` | `@builtin::systems.volumetrics.VolumetricSky` | Singleton procedural cloud layer with TAA. | | `ParticipatingMedium.component` | `@builtin::systems.volumetrics.ParticipatingMedium` | Mark an entity as fog/smoke/dust that attenuates the light passing through it. | | `mediaTransmittance.module` | `@builtin::systems.volumetrics.mediaTransmittance` | Light-space optical-depth map: the registry, the settings, and the basis the map is built in. | | `mediaShadow.renderFeature` | `@builtin::systems.volumetrics.mediaShadow` | Builds the optical-depth map and attenuates each surface by the density in front of it. | | `volume.module` | `@builtin::systems.volumetrics.volume` | Builders (`noise`/`sphere`/`box`/`wrap`) + the `.zvol` header reader + occupancy + brick paging + the per-entity raymarch dispatch. | | `VolumeSequence.component` | `@builtin::systems.volumetrics.VolumeSequence` | Play an open volume sequence through an entity's transform AABB. | | `volumeSequence.module` | `@builtin::systems.volumetrics.volumeSequence` | A run of `.zvol` frames streamed through a fixed ring of resident volumes. | | `volumeDemos.module` | `@builtin::systems.volumetrics.volumeDemos` | Shared pipeline for the showcase scenes (fill + emissive raymarch + composite). | | `volumetricSky.module` | `@builtin::systems.volumetrics.volumetricSky` | Cloud volume + raymarch + TAA + composite orchestration. | | `shaders/*.shader` | `@builtin::systems.volumetrics.shaders.*` | `volume_raymarch`, `volume_pages`, `volume_pack`, `cloud_noise[_v3/_v5]`, `cloud_raymarch[_v3/_v4/_v5]`, `cloud_taa`. | | `importers/vdb_to_zvol.importer` | (auto-discovered) | `.vdb` → dense `.zvol` via the `openvdb` WASM plugin (procedural fallback when the plugin isn't loaded). | | `presets/cumulus_default.preset` | `@builtin::systems.volumetrics.cumulus_default` | Canonical full `VolumetricSky` weather preset. | | `demo/*.scene` | `@builtin::systems.volumetrics.demo.*` | `sdf_solids`, `smoke_plume`, `nebula`, `godrays`, `zvol_viewer`, `sky`. | ## Path convention — fully-qualified builtin identities Every intra-package reference uses the asset's fully-qualified builtin identity, the same convention every other builtin package follows: ```luau local Volume = require("@builtin::systems.volumetrics.volume") local Sky = require("@builtin::systems.volumetrics.volumetricSky") local V = require("@builtin::systems.volumetrics.volumeDemos") -- shader identities, e.g. asset.resolve("@builtin::systems.volumetrics.shaders.volume_raymarch") ``` Qualified identities are caller-independent: they resolve identically from a module, a scene entrypoint, or a component. Caller-relative (`.sibling`) requires are deliberately avoided here because the component runtime caller identity is `@builtin::components.<Name>`, so a `.sibling` require from a component resolves against the wrong root. (An earlier revision of this package used a `~volumetrics` package-root alias for relocatability; that alias is not resolvable on current main, so the package uses qualified identities throughout.) ## Usage ```luau -- Per-entity volume. The component resolves the name through `Volume.get`, -- so a volume paged into a brick pool renders from the pool under the same -- name: local Volume = require("@builtin::systems.volumetrics.volume") local cloud = Volume.noise({ name = "cloud", size = 64, scale = 4 }) cloud:buildSparse({ brickSize = 8 }) cloud:releaseDense() entity(id).component.add("Volume", { volumeName = "cloud", density = 6.0 }) -- Procedural sky: local sky = entity.spawn("sky") sky.component.add("VolumetricSky", {}) preset.apply("cumulus_default", { entity = sky.id, component = "VolumetricSky" }) -- VDB import: drop a `.vdb` into the VFS; the importer writes a `.zvol` -- alongside it. Load it with the zvol_viewer demo scene. -- Animated run: one `.zvol` per frame, played through a bounded ring. local Seq = require("@builtin::systems.volumetrics.volumeSequence") Seq.open({ name = "plume", frames = framePaths, resident = 3, fps = 24 }) entity(id).component.add("VolumeSequence", { sequenceName = "plume", density = 6.0 }) ``` ## Plugin dependency The VDB importer delegates real OpenVDB parsing to the `openvdb` plugin in the builtin library, `@builtin::plugins.openvdb`. Without the plugin the importer falls back to a deterministic procedural torus so the pipeline stays functional.
▲ 0↑ born
✦
shader · born here
❒asset
▲ 0↑ born
▣
module · born here
❒asset
# channel Module Public Luau surface over the `__channel` Internal FFI namespace — keyframe-channel sampling primitives. ## Purpose Wrap the raw `__channel.*` FFI namespace in a typed Luau table that gets auto-injected as `_G.channel` via the prelude. Channels are the keyframe-track substrate: one per animation curve (translation x, rotation, blend weight, …). Sampling writes the interpolated value(s) into a buffer at a specified offset — no per-sample allocation, no per-bone FFI round-trip. ## Usage ```luau local h = channel.create({ times = { 0.0, 0.5, 1.0 }, values = { 0, 1, 0, 1, 1, 0, 0, 0, 1 }, stride = 3, interp = "linear", }) channel.sampleInto(h, 0.25, buf, 0) channel.sampleManyInto(h, 0.25, buf, { 0, 9, 18 }) local x, y, z = channel.sampleVec3(h, 0.25) channel.destroy(h) ``` ## Exports - `channel.create(opts) -> number?` - `channel.destroy(handle) -> boolean` - `channel.sampleInto(ch, time, buf, offset) -> boolean` - `channel.sampleManyInto(ch, time, buf, offsets) -> boolean` - `channel.sampleVec3(ch, time) -> (x, y, z) | nil` - `channel.sampleQuat(ch, time) -> (x, y, z, w) | nil`
▲ 0↑ born
▣
module · born here
❒asset
# icons Phosphor icon constants. The phosphor font is registered as a named family `phosphor` in the engine, so render icons by setting `style.fontFamily = "phosphor"` on the label that contains them. The font is ALSO appended to the proportional family as a fallback, but the fallback path is unreliable for PUA codepoints in egui 0.34 (see issue #2191) — always pass `fontFamily = "phosphor"` for icons. Curated set covering DAW transport, media controls, file management, desktop shell, editor chrome, and common state indicators. For icons not listed here, see https://phosphoricons.com — pass the codepoint as a UTF-8 string literal directly. ## Exports - `Icon.styled(extra: LabelOpts?) -> LabelOpts` — build a `UI.label` opts table with `style.fontFamily = "phosphor"` applied. Merges with the supplied `extra`. - `Icon.<Name>: string` — UTF-8 codepoint constants. Categories: - Transport / media: `Play`, `Pause`, `Stop`, `Record`, `FastForward`, `Rewind`, `SkipForward`, `SkipBack`, `Repeat`, `Shuffle`. - Audio / DAW: `Microphone`, `Headphones`, `Equalizer`, `Sliders`, `Metronome`, `MusicNote`, `Waveform`, `SpeakerHigh`, `SpeakerLow`, `SpeakerNone`, `SpeakerSimpleHigh`, `SpeakerMuted`. - File / folder: `Folder`, `FolderOpen`, `File`, `FileText`, `FileAudio`, `FileVideo`, `FileImage`, `FileCode`, `FloppyDisk`, `Trash`, `UploadSimple`, `DownloadSimple`. - Desktop / shell: `Desktop`, `DesktopTower`, `Monitor`, `Keyboard`, `Mouse`, `House`, `Sidebar`, `Cpu`. - Editor: `Pencil`, `Plus`, `Minus`, `Check`, `MagnifyingGlass(Plus|Minus)?`, `Gear`, `Power`, `Eye`, `EyeSlash`, `Lock`, `LockOpen`, `Bell`, `Palette`, `Funnel`, `Clipboard`. - Code / dev: `Code`, `Terminal`, `Bug`, `GitBranch`, `GitCommit`, `GitMerge`, `Database`, `Cloud`. - Arrows / nav: `ArrowUp/Down/Left/Right`, `ArrowsClockwise`, `ArrowsIn`, `ArrowsOut`, `CaretUp/Down/Left/Right`. - Layout / list: `List`, `GridFour`, `DotsThree`, `DotsThreeVertical`. - State / status: `Info`, `Warning`, `WarningCircle`, `CheckCircle`, `XCircle`, `Question`, `Heart`, `Star`, `Tag`, `Hash`, `At`. - Shapes: `Circle`, `Square`, `Triangle`. - Productivity / shell apps: `Calculator`, `Notepad`, `NotePencil`, `Note`. - Games / cards: `GameController`, `Bomb`, `Cards`, `Diamond`, `Spade`, `Club`. Types: - `LabelStyle = { fontFamily?: string, fontSize?: number, color?: string, [string]: any }` - `LabelOpts = { style?: LabelStyle, [string]: any }` ## Usage ```luau local Icon = require("@builtin::modules.icons") UI.label(Icon.Play, { style = { fontFamily = "phosphor", fontSize = 24 } }) UI.label(Icon.Folder .. " Documents", { style = { fontFamily = "phosphor", fontSize = 14 }, }) -- Or use the helper to enforce the phosphor family: UI.label(Icon.Play, Icon.styled({ style = { fontSize = 24, color = "#fff" } })) ``` ## Notes - The constants are plain string values — each is a single UTF-8 Private Use Area codepoint. Concatenate freely with surrounding text. - `Icon.styled` mutates the supplied opts table (`extra.style` is forced). Pass a fresh table per call site if you need to reuse opts. - Curated subset only — for icons outside this set, paste the codepoint from https://phosphoricons.com directly.
▲ 0↑ born
▣
module · born here
❒asset
# serviceCall The user-scope entry point for credit-metered service calls. A `.service` handler calls `Service.invoke(offering, endpoint, opts)` instead of touching the `service` primitive directly, getting one `(response, err)` contract with the HTTP status already classified. ```lua local Service = require("modules.serviceCall") local resp, err = Service.invoke("origozero/mesh_gen", "create_preview", { body = { prompt = "a wooden chest" }, }) if not resp then handle:fail(err) -- "out of credits …", "not signed in …", "unknown service …" return end if resp.status >= 300 then -- upstream relayed a non-2xx (charged 0) handle:fail("upstream " .. resp.status) return end local task = resp.body -- the upstream payload ``` `Service.invoke` returns the InvokeResponse `{ status, headers, body, charged, balance }` on a successful round-trip, or `(nil, errMsg)` when the call could not be charged or forwarded. `Service.balance()` returns the caller's integer credit balance. ZeroMind injects the provider secret and meters credits server-side; the platform JWT and the real upstream URL are never visible to this script.
▲ 0↑ born
❒
package · born here
❒asset
▲ 0↑ born
·
computeshader · born here
❒asset
# search Binary search of a run of queries in a sorted column: the pick of a value from a cumulative distribution, the segment of an arc length. | Specialize | Options | Meaning | |------------|---------|---------| | `T` | `f32`, `i32` | the scalar the column and the queries hold | | `SIDE` | `lower`, `upper` | which boundary a query answers | `lower` answers the first place whose value is not below the query, `upper` the first place whose value is above it, and both answer the length of the column when no place qualifies. The greatest place whose value is not above a query is `upper - 1`; with the cumulative sums of a run of weights, `upper` of a draw in `[0, total)` is the index of the weight it lands in. The column bound as `sorted` is the sorted run, every element of it. One thread per query; the dispatch size `n` is the number of queries. Exact: a search reads the column and nothing else, so the same inputs give the same answers on every dispatch.
▲ 0↑ born
▣
module · born here
❒asset
# numberSequence Roblox-shaped scalar keyframe-curve value type. A `NumberSequence` is an immutable curve of up to 64 keypoints, each carrying `(time, value, envelope)`. The envelope is a per-keypoint random range half-width applied at `:sample(t)` time; `:evaluate(t)` is always deterministic. Exposed as the `NumberSequence` Luau global via `--!global`. Authored scripts call `NumberSequence.new(...)` directly without a `require`. The Rust FFI lives at `__sequences.*` (registered by `crates/zero_scripting/src/ffi/bindings/curves.rs`); this module is the typed Luau wrapper. ## Exports - `NumberSequence.new(...) -> NumberSequenceObj` — three signatures: - `NumberSequence.new(v)` — constant value across `[0, 1]`. - `NumberSequence.new(v0, v1)` — two-point lerp from `v0` to `v1`. - `NumberSequence.new({ { time, value, envelope? }, ... })` — explicit keypoints. `envelope` defaults to `0`. First key must anchor at `time = 0`, last at `time = 1`. NaN / Inf rejected. - `NumberSequence.deserialize(payload) -> NumberSequenceObj` — rebuild from a `{ kind = "NumberSequence", keypoints = {...} }` payload produced by `:serialize()`. ## Methods (called via `:`) - `seq:evaluate(t) -> number` — deterministic linear interpolation at `t`. `t` clamps to `[0, 1]`; NaN coerces to `0`. - `seq:sample(t) -> number` — `evaluate(t) + (math.random() − 0.5)·2·envelope(t)`. Per-VM `math.randomseed` controls jitter reproducibility. - `seq:keypoints() -> { NumberKeypoint }` — array snapshot, sorted ascending by time. - `seq:duration() -> number` — always `1.0` for a well-formed sequence. - `seq:serialize() -> { kind, keypoints }` — scene-save payload. - `seq:destroy()` — drop the FFI handle. Luau removed `__gc` on tables, so eager cleanup is the caller's responsibility for per-frame-rebuild patterns; otherwise the entry dies with the VM. ## Substrate Backed by `zero_curves::Channel` (Linear interp). Each sequence builds the value and envelope channels once at construct time and reaches into them on every sample — zero per-call allocation, safe for hot particle loops. Validation errors name the specific rule (`"first keypoint must anchor at time = 0"`, `"too many keypoints (max 64)"`, …). Foundation for VFX property-over-lifetime: declarative particles (#2572), GPU emitter (#873), beams, trails. Issue: #2673.
▲ 0↑ born
▣
module · born here
❒asset
# material_remap Canonical property/texture-role dictionary used when a material **swaps shaders**. The new shader exposes a different vocabulary than the old one — one shader's `MAIN_TEX` is another's `albedo` is a third's `base_color_texture` — so values would be lost on a naive swap. This module maps any known alias onto a canonical **role** so floats, colors, and texture links carry across the swap to the best of our ability. Consumed by `material.assetType`'s `setShader` ref method (and, transitively, by the `appearance` toolbox's `swapShader`). ## API ```luau local remap = require("modules.material_remap") remap.propertyRole("MAIN_COLOR") -- "base_color" (scalar/color side) remap.textureRole("MAIN_TEX") -- "base_color_texture" (texture side) -- Remap current property values onto the new shader's accepted names. local mapped, carried, dropped = remap.remapProperties(currentProps, shaderRef:getProperties()) -- Canonicalize texture slot names so links bind on the new shader. local texMapped, texCarried = remap.canonicalizeTextures(currentTextures) ``` ## Why two tables `albedo` as a **color** is the base-color factor; `albedo` as a **texture** is the base-color map. `mat.yaml` keeps `colors:` / `floats:` separate from `textures:`, so the caller always knows which side it's remapping. Splitting `PROPERTY_ROLES` from `TEXTURE_ROLES` removes the ambiguity instead of guessing from the value shape. ## Property remap vs texture remap - **Properties** are remapped against the target shader's *real* reflected field names (`shaderRef:getProperties()`, naga reflection). Direct name match wins; otherwise role-to-role. Properties the new shader doesn't expose are dropped. - **Textures** are canonicalized to the engine's builtin on-disk slot convention (`base_color_texture`, `normal_texture`, …) because the engine does not yet surface a target shader's reflected texture-slot list to Luau. Unknown slots pass through unchanged. The alias lists are deliberately broad (glTF / Unity / Unreal / Godot / hand-rolled WGSL conventions). Add new aliases here rather than special-casing a shader at a call site.
▲ 0↑ born
▣
module · born here
❒asset
# viewport_select Turns a viewport pointer gesture into an editor selection change, applying the standard modifier semantics, and drives the per-frame bridge that reads real pointer + modifier input. Selection is written into the shared `modules.api.editor.selection` service's `entity` scope, so the viewport, the selection outline, and the outliner stay in lockstep. A left press+release within a few pixels is a click-select; a larger left drag is a **marquee** (rubber-band) that selects every entity whose screen position falls inside the band. The band is drawn by `modules.api.editor.marquee_overlay`. ## Exports - `M.applyClick(Sel, scope, hitId, mods)` — pure decision: `hitId == nil` clears (unless a modifier is held); ctrl toggles the hit; shift adds it; neither replaces. Entity ids become `{ kind = "entity", id }` refs. - `M.applyMarquee(Sel, scope, ids, mods)` — pure decision for a completed marquee: an empty band with no modifier clears, an empty band with a modifier leaves the selection unchanged; ctrl toggles each id, shift adds the set, neither replaces with the set. - `M.worldToScreen(m, w, h, p)` — project a world point through a 16-float column-major view-projection matrix and viewport size to viewport pixels (`sx, sy`), or nil when the point is at/behind the camera. Mirrors the engine's own `world_to_screen_px`. - `M.pointInRect(rect, sx, sy)` — inclusive screen-rect hit test. - `M.nearestIcon(cands, cx, cy, thresholdPx)` — the nearest icon candidate `{ id, sx, sy }` to the cursor within a pixel threshold, or nil. - `M.entitiesInRect(rect)` — engine-driven: the selectable entity ids whose world position projects inside `rect` (viewport-pixel space) this frame. - `M.iconPick(cx, cy)` — engine-driven: the entity whose meshless icon is nearest the cursor within `ICON_PICK_PX`, or nil. - `M.tick(dt?)` — call once per editor frame; prunes dead selection refs, reads pointer press/drag/release + modifiers, and routes a click or a marquee through the pure decisions above. ## Notes - The decision functions are pure so they can be unit-tested against the real selection service. - **Icon-pick fallback.** A click that hits no mesh/collider (the engine pointer pick returns nil) falls back to selecting the nearest meshless-entity icon (light / camera / audio source / reflection probe) within `ICON_PICK_PX`, so lights and cameras — invisible to the raycast — are selectable by their billboard. The engine pointer pick wins whenever it hits. - **Gizmo precedence.** A marquee only starts on a frame where the pointer has moved past the click slop AND the transform gizmo is not dragging. `tick` runs before the gizmo's tick in the editor entrypoint, so by the time a drag passes the slop the gizmo has already had a frame to claim a grabbed handle.
▲ 0↑ born
△
mesh · born here
❒asset
▲ 0↑ born
·
computeshader · born here
❒asset
# gather Builds a column from another column and a run of indices: element `i` of the result is element `index[i]` of `src`. An index below 0 or past the end of `src` gives zeros. The element type is the `T` specialization (`f32`, `i32`, and two to four lanes of either); a three-lane column is read and written tightly packed, twelve bytes an element. Dispatch over `n`, the number of indices. Exact: each result element has one writer. Over a live count (`COUNT=device`, see the abi README) `n` is the capacity and the first `domain_count` slots are live: a slot at or past it holds zeros. It binds 4 storage buffers (3 in the plain variant), and `dst` holds `n` elements.
▲ 0↑ born
❒
package · born here
❒asset
▲ 0↑ born
▣
module · born here
❒asset
# highlight Syntax-highlighting dispatch + per-language modules. Each language module exports `(text: string) -> { Segment }` where `Segment = { text, color, monospace?, italic? }`. `Z.codeEditor` calls into `dispatch` at update time and passes the resulting segments to TextInput, which builds the egui LayoutJob in pure Luau. Library authors can ship their own `@author/zui_highlight_<lang>` modules and slot them into `Z.codeEditor` via the same `language` opt. ## Exports - `M.dispatch(language: string, text: string) -> { Segment }` — pick a per-language highlighter (with `yml` / `md` aliases) and tokenize; unknown languages fall through to a single plain segment. - `M.lua: Highlighter` — Lua highlighter. - `M.json: Highlighter` — JSON highlighter. - `M.yaml: Highlighter` — YAML highlighter. - `M.wgsl: Highlighter` — WGSL highlighter. - `M.markdown: Highlighter` — Markdown highlighter. Types: - `Segment = { text: string, color: string, monospace: boolean?, italic: boolean? }` - `Highlighter = (string) -> { Segment }` ## Usage ```luau local Highlight = require("@builtin::modules.zui.highlight") local segs = Highlight.dispatch("lua", source) -- segs = { { text = "local", color = "#...", monospace = true }, ... } -- Or call a specific language directly: local segs2 = Highlight.json(jsonText) ``` ## Notes - Per-language colors come from the active theme's `code.*` tokens (`ui.getToken("code.keyword")` etc.). Switching themes (`ui.setTheme("light")`) automatically re-colors on the next call. - A single-slot LRU cache keyed by `(language, text, theme)` makes same-text re-emits free (focus-loss redraws, no-op editor pushes). Cache returns a shared array reference — callers must not mutate it. - Aliases: `"yml"` → `yaml`, `"md"` → `markdown`. Unknown languages return a single default-colored monospace segment so user input never crashes the dispatcher. ## Roles the Lua highlighter reads `code.keyword`, `code.string`, `code.number`, `code.comment`, `code.bool` (`true` / `false` / `nil`), `code.attribute` (`--!` directives), `code.type` (a name after `:`, `->`, `::` or `type`), `code.function` (a name a call is made through, or one `function` declares), `code.property` (a member reached by `.` that is not called), `code.builtin` (the standard library's globals), and `code.default` for the rest.
▲ 0↑ born
▣
module · born here
❒asset
# material_utils High-level wrappers around the `__material` FFI global. Convenience helpers for the common material-authoring operations: create a named material, apply it to an entity, query / set material properties, and bind textures. Exposed as the `Material` Luau global via `--!global` (auto-injected by the prelude). Material lives ON the Model / SkinnedModel component — there is no standalone `Material` component, because a material only renders where there is a mesh. To apply a material, use `Material.apply()` (which errors if the entity has no model) or set the model's `material` field directly: `entity(id).component.add("Model", { model = "cube", material = "gold" })`. PascalCase aliases (`Material.Create`, `Material.Apply`, etc.) are kept for back-compat with the wider codebase — 88+ call sites across demos, scenes, components, guides, and the engine test suite use the legacy names. The lowercase forms are canonical. ## Exports Canonical (lowercase) surface: - `Material.create(name: string, opts: MaterialOpts?) -> MaterialRef?` — register a named material; returns the AssetRef envelope. - `Material.apply(entityId: string, materialRef: MaterialRefOrName) -> boolean` — set the `material` field on the entity's Model / SkinnedModel. Errors if neither is present. - `Material.exists(name: MaterialRefOrName) -> boolean` — registry predicate. - `Material.getPropertyNames(name: MaterialRefOrName) -> { string }?` — list property names exposed by a material. - `Material.getProperty(name: MaterialRefOrName, prop: string) -> any` — read a property value. - `Material.setProperty(entityId: string, property: string, value: any)` — set a property on the material assigned to an entity's Model / SkinnedModel. - `Material.setTexture(name: MaterialRefOrName, slot: string, textureRef: string) -> boolean` — bind a texture slot. PascalCase back-compat aliases: - `Material.Create(name, opts_or_shader, props?) -> MaterialRef?` — accepts the legacy `(name, shaderString, opts)` 3-arg form OR the canonical `(name, opts)` form. - `Material.Apply` / `Material.Exists` / `Material.GetPropertyNames` / `Material.GetProperty` / `Material.SetTexture` — direct aliases for the lowercase counterparts. - `Material.SetProperty(materialName, property, value)` — writes to the REGISTERED material by name (NOT on a component). Different from lowercase `setProperty`. Types: - `Color = { number } | { r: number, g: number, b: number, a?: number }` - `Textures = { [string]: string }` - `MaterialOpts = { shader?, color?, roughness?, metallic?, emissive?, textures?, [string]: any }` - `MaterialRef = { __ref?, type?, name?, guid? }` - `MaterialRefOrName = MaterialRef | string` ## Usage ```luau local mat = Material.create("gold", { color = { 1, 0.85, 0.2 }, metallic = 1, roughness = 0.15, }) -- entityId must already carry a Model / SkinnedModel (a material only -- renders on a mesh); apply errors otherwise. Material.apply(entityId, mat) -- AssetRef envelope Material.apply(entityId, "gold") -- bare name also works Material.setProperty(entityId, "roughness", 0.3) Material.setTexture("gold", "albedo", "@builtin::textures.gold") ``` ## Notes - `Material.apply` accepts either an AssetRef envelope or a bare name — the model's `material` field auto-wraps strings. - Material is a field on the Model / SkinnedModel component (declared as `material`, an `AssetRef<material>`). There is no standalone Material component — `apply` / `setProperty` route to whichever model is present and error when neither is. - `Material.setProperty` writes the property on the material assigned to the entity's model; `Material.SetProperty` writes on the REGISTERED material by name. They behave differently — keep them straight. - All argument validation raises on bad input rather than silently no-oping, so call sites surface mistakes early.
▲ 0↑ born
▣
module · born here
❒asset
# colorSequence Roblox-shaped RGB keyframe-curve value type. A `ColorSequence` is an immutable curve of up to 64 keypoints, each carrying `(time, value, envelope)` where `value` is a `{r, g, b}` triple and `envelope` is a per-channel random range half-width. The per-channel envelope is a notable improvement over Roblox, where ColorSequence ships with no envelope at all — a long-standing community wishlist item. Exposed as the `ColorSequence` Luau global via `--!global`. Authored scripts call `ColorSequence.new(...)` directly without a `require`. The Rust FFI lives at `__sequences.*` (registered by `crates/zero_scripting/src/ffi/bindings/curves.rs`); this module is the typed Luau wrapper. ## Exports - `ColorSequence.new(...) -> ColorSequenceObj`: - `ColorSequence.new({r, g, b})` — constant color (accepts a 3-array or `{r=, g=, b=}` record). - `ColorSequence.new(c0, c1)` — two-point lerp from `c0` to `c1`. - `ColorSequence.new({ keypoint, ... })` — explicit keypoints. `envelope` may be omitted (= `{0,0,0}`), a single number (broadcast to all channels), or a 3-array. First key must anchor at `time = 0`, last at `time = 1`. NaN / Inf rejected. - `ColorSequence.deserialize(payload) -> ColorSequenceObj` — rebuild from a `{ kind = "ColorSequence", keypoints = {...} }` payload produced by `:serialize()`. ### Keypoint shapes An entry of a keypoint list takes any of three written shapes, and the shapes mix within one list: | Written | Read as | |---|---| | `{ time = 0.5, value = {1,0,0}, envelope = 0.05 }` | the named record | | `{ 0.5, {1,0,0}, 0.05 }` | time, then colour, then envelope | | `{ 1, 0, 0 }` | a bare colour, timed by its place in the list | A bare colour takes its time from its place: the entries spread evenly across `[0, 1]`, so `{ {1,0.85,0.35}, {1,0.35,0.05} }` is a ramp from the first colour at `t = 0` to the second at `t = 1`, and a list of one colour holds that colour across the whole domain. ## Methods (called via `:`) - `seq:evaluate(t) -> (r, g, b)` — deterministic linear interpolation at `t` (multiret). `t` clamps to `[0, 1]`; NaN coerces to `0`. - `seq:sample(t) -> (r, g, b)` — `evaluate(t)` plus per-channel `(math.random() − 0.5)·2·envelope(t)` jitter. Per-VM `math.randomseed` controls jitter reproducibility. - `seq:keypoints() -> { { time, value = {r,g,b}, envelope = {r,g,b} } }` — array snapshot, sorted ascending by time. - `seq:duration() -> number` — always `1.0` for a well-formed sequence. - `seq:serialize() -> { kind, keypoints }` — scene-save payload. - `seq:destroy()` — drop the FFI handle. Luau removed `__gc` on tables, so eager cleanup is the caller's responsibility for per-frame-rebuild patterns; otherwise the entry dies with the VM. ## Substrate Backed by `zero_curves::Channel` (Linear interp). Each sequence builds the value and envelope channels once at construct time and reaches into them on every sample — zero per-call allocation, safe for hot particle loops. Validation errors name the specific rule (`"first keypoint must anchor at time = 0"`, `"too many keypoints (max 64)"`, …). Foundation for VFX property-over-lifetime: declarative particles (#2572), GPU emitter (#873), beams, trails. Issue: #2673.
▲ 0↑ born
△
mesh · born here
❒asset
# Mesh Mesh asset. `data.zmsh` is the engine-native geometry payload (`renderer.mesh.encode`); consumers reference the asset's guid.
▲ 0↑ born
▣
module · born here
❒asset
# utils Small stateless helpers used across the builtin module set: parse a vec3-shaped value into the canonical `{ x, y, z }` map, normalise an id list from string or table input, and coerce a checkbox-shaped value into a strict boolean. No engine dependencies, no module state. ## Exports - `M.parseVec3(value: Vec3Like) -> Vec3Map?` — parse vec3 from array or map form, returns `nil` if unrecognised. - `M.normalizeIds(value: IdListInput) -> IdList?` — turn a comma/space-separated string or array into `{ string }`. - `M.parseCheckboxValue(data: CheckboxValue, fallback: boolean) -> boolean` — coerce bool/string/number/nil to a boolean. Types: - `Vec3Map = { x: number, y: number, z: number }` - `Vec3Like = Vec3Map | { number }` — accepted input shape for `parseVec3`. - `IdList = { string }` - `IdListInput = string | IdList` — accepted input shape for `normalizeIds`. - `CheckboxValue = boolean | string | number | nil` ## Usage ```luau local Utils = require("@builtin::modules.utils") local v = Utils.parseVec3({ 1, 2, 3 }) -- { x = 1, y = 2, z = 3 } local ids = Utils.normalizeIds("a, b, c") -- { "a", "b", "c" } local on = Utils.parseCheckboxValue("true", false) -- true ``` ## Notes - Pure functions — no side effects, no engine calls. Safe to call at module load time or from any context. - `parseVec3` always returns the map form (`{ x, y, z }`) regardless of whether the input was an array or a map. Callers downstream that expect a particular shape should rely on the map form. - `normalizeIds` returns the input table unchanged when it is already a table — it does not validate that each entry is a string.
▲ 0↑ born
·
assettype · born here
❒asset
# plugin Compiled code in a world, used the way a module is. A plugin is a WebAssembly component in a `<name>.plugin/` asset. Code that uses one `require`s it and calls it like any module: the calls answer values, and a refusal raises. Nothing about the call says compiled code ran, the way calling into a native library from Python does not. ```luau local counter = require("counter") local total = await(counter.add(2)) -- a number print(await(counter.total())) -- 2 ``` It is for work Luau cannot do fast enough, or cannot do at all: a simulation step, a decoder, a solver, a model. ## Getting one into a world Write the component into the world as a `.wasm` file (upload it). It becomes a plugin asset of the same name, with the component moved inside, and it loads. The asset starts with: ``` counter.plugin/ ├── plugin.wasm the component ├── plugin.yaml what it offers, and where it loads ├── surface.module/ the module a `require` of the asset lands on ├── README.md this plugin's own description └── .metadata ``` Nothing is callable until `plugin.yaml` lists it. Edit it to name the methods the component offers, and the plugin reloads with them. ## plugin.yaml ```yaml profiles: # where it loads; required - editor - runtime storage: none # none | private; none when left out events: private # private | exposed; private when left out memory: 512MiB # the most memory it may hold; 4GiB when left out exposed: # the methods a caller may run; none when left out - name: add - name: total ``` - **profiles** says where the plugin loads. A `runtime` plugin is part of the game: components and scene entrypoints can call it, and a player loads it. An `editor` plugin is for authoring, and a call to it from a component or an entrypoint is refused, because the shipped game would not have it. - **storage** `private` gives the plugin a store of its own, in this world, that survives a restart. With `none` what it stores lasts until it is reloaded. - **events** `exposed` lets code in the world listen to the events the plugin raises. With `private` nothing in the world hears them. - **memory** is the most memory the plugin may hold, written with `KiB`, `MiB` or `GiB`, up to `4GiB`, which is also what a plugin that says nothing gets. A plugin that would grow past it stops, and the log says it went past its `memory:`. - **exposed** is the plugin's whole callable surface. A method not listed cannot be called, whatever the component contains, and a listing that names a method the component does not have is refused when the plugin loads. ## Using it The asset's `surface.module` is its Luau face, and `require("<name>")`, with the asset's name, lands on it. Its code is `surface.module/init.luau`, and each of its functions calls the plugin through the asset it belongs to: ```luau local self = asset.containing(__FILE__) local me = self.modules.shared.forAsset(self) local M = {} function M.add(amount: number) return me.call("add", amount) end return M ``` - Arguments are numbers, strings, booleans, nil, buffers, handles, and tables of them. A whole number arrives as an integer. A string has to be text; send bytes as a `buffer`, which arrives whole, as bytes. - A file or a folder goes as a handle, which the plugin reads or writes by itself, without a copy through the call: ```luau local weights = vfs.open(self.path .. "/weights.bin") -- read a file local model = vfs.openDir(self.path .. "/model") -- read a folder local out = vfs.sink("/source/out/sphere.zvol", { maxBytes = 64 * 1024 * 1024 }) local frames = vfs.dirSink("/source/frames", { maxFiles = 240, maxBytes = 200 * 1024 * 1024 }) me.call("render", model, frames) ``` A handle lets the plugin do what this script could do at that path, and nothing more: a plugin has no filesystem of its own. A sink's file appears when the plugin closes the sink or lets go of it, and only the world's own files take a sink. The `plugin-development` guide has the rules. - Entities go as a binding: `ecs.bindEntities(ids):forPlugin()`. The plugin reads and writes their fields as packed floats and never learns an id; a write it makes answers to the same locks, owners and builds this script's own `entity.batchWrite` does. - A GPU buffer goes as `buf:forPlugin()` (a `"gpu"` buffer from `substrate.createBuffer`), and a compute shader as `shaderRef:forPlugin()`. The plugin writes and reads the buffer's floats and dispatches the shader over buffers the same script handed it. It never learns a buffer's name or a shader's key, so it reaches no buffer it was not given. - The answer is the value the plugin returned: a table as a table, a number as a number, bytes as a `buffer`. - A refusal raises where it is awaited: a method not exposed, a plugin that is not loaded, or the plugin's own error. - `me.on(function(eventType, payload) ... end)` listens to the plugin's events, waiting for it to load first. The payload is a value, as an answer is. It needs `events: exposed`. - A refusal is an error like any other, and `pcall` catches it. - `plugins.isLoaded("<name>")` answers whether it is running, and never raises. ## More than one part A plugin can carry more than its surface: modules the surface builds on, and an editor panel. They sit beside `surface.module` inside the asset: ``` counter.plugin/ ├── plugin.wasm ├── plugin.yaml ├── surface.module/ what `require("counter")` lands on ├── history.module/ a module the surface uses └── panel.editorPanel/ a dock tab ``` The parts reach each other with the dot form, which means the same thing wherever the asset is installed: - From a module, `require(".history")` is the module beside it. - From the panel, `require(".")` is the plugin (its surface), and `require(".history")` the module beside the panel. The surface IS the plugin: it has no name of its own, so no part requires `.surface`. Don't write a path (`./history.module`) or a name that says where the asset is installed (`require("counter.history")`): moved into a library, the asset stops finding its own parts. ## While it runs - **A new build replaces it in place.** Upload the component again under the same name and the running plugin is replaced, with no restart. Uploading the same bytes again changes nothing. - **Editing plugin.yaml reloads it.** - **Removing the asset stops it.** - **A new build or an edit stops a call in progress.** A call the plugin is in the middle of answers that the plugin was stopped, however long it would have run, and the new build starts. - **A crash stops it once.** If the component traps, the plugin stops, the log says so in one line, and every later call raises that it stopped and why, until it loads again: a new build, or an edit to `plugin.yaml`. - **`plugins.status()`** lists each plugin: whether it is loaded, why not, and its component: the SHA-256, the size, and what built it. The same holds in the desktop engine and in a browser. ## Building one The `plugin-development` guide covers writing a component: the interface in `zero-plugin.wit` beside this file, a Rust project that builds one, how calls are encoded, what a plugin may use, and the development loop.
▲ 0↑ born
·
computeshader · born here
❒asset
# contour_cells Whether each cell of a grid of samples straddles the surface: its eight corner samples are not all on one side of zero. `flag[c]` is 1 for a straddling cell and 0 otherwise, the keep mask `compact` turns into the list of cells that own a vertex. Dispatch over the cells. Exact: each flag has one writer.
▲ 0↑ born
◐
material · born here
❒asset
# Chrome A polished chrome surface built on the `pbr` shader — fully metallic (`metallic: 1.0`) with a bright near-white base colour (`0.95, 0.95, 0.97`) and very low roughness (`0.05`) for mirror-sharp reflections. Opaque, back-face culled, instanced, with the `_METALLICSPECGLOSSMAP` keyword enabled. ## Inputs - `colors.base_color` (`0.95, 0.95, 0.97, 1.0`) — bright, faintly cool chrome tint. - `floats.metallic` (`1.0`) — fully metallic. - `floats.roughness` (`0.05`) — near-mirror sharpness. - `textures.base_color_texture` (`default:white`): the white base texture the colour multiplies against, so the surface renders `base_color` alone.
▲ 0↑ born
❒
package · born here
❒asset
▲ 0↑ born
△
mesh · born here
❒asset
▲ 0↑ born
✦
shader · born here
❒asset
▲ 0↑ born
✦
shader · born here
❒asset
# pbr_layered A surface composed from two complete PBR layers: a base, and a top layer laid over it wherever coverage says so. Rust over painted metal, snow over rock, mud over a vehicle, wear over a finish. Pre-blending those into one texture set bakes the coverage in — it can no longer change at runtime, and every entity wearing the material wears it identically. Here both layers keep their own shading inputs and coverage is evaluated per fragment, so the same material can be pristine on one entity and ruined on the next. ## Coverage Coverage is the product of three terms, each neutral by default: | term | property | what it is for | |---|---|---| | mask | `top_mask_texture`, `top_mask_channel`, `top_mask_contrast` | *where* the top layer gathers — creases, edges, upward faces | | material | `top_coverage` | *how far* the look has progressed, for every entity sharing the material | | instance | `top_instance_lane`, `top_instance_component` | how far for *this* entity | `top_coverage` defaults to 0, so a material that sets nothing renders as its base layer alone. `top_mask_contrast` shapes the mask: 1 is a linear ramp, higher values pull the top layer back into the mask's brightest areas, which is what makes rust and snow gather in creases instead of washing evenly over everything. ## Driving it per instance Point the material at an `instanceData` lane, then write that lane per entity: ```lua -- material: top_instance_lane = 0, top_instance_component = 0 local clean, rusted = entity.find("barrel_a"), entity.find("barrel_b") if clean and rusted then renderer.instanceData.set(clean, 0, 0.1) -- nearly clean renderer.instanceData.set(rusted, 0, 0.9) -- heavily rusted end ``` Both entities share one material and one draw path; only the lane differs. ## Texture slot budget WebGPU guarantees 16 sampled textures per fragment stage and the engine's group(0) already holds several. This shader declares **9**, which leaves room for those plus a margin. Two full five-map layers would not fit, so the top layer carries the three maps that make it read as a different material — albedo, roughness/metallic, normal — and takes occlusion and emissive from the base. Layer count is **two**. The cost is fixed and predictable: both layers are always sampled, and coverage selects between their inputs rather than branching. ## Why the inputs blend rather than the results Lighting each layer separately and adding puts more energy back out than arrived, so a half-covered surface would read brighter than either layer alone. Interpolating the shading inputs and lighting once cannot do that.
▲ 0↑ born
▣
module · born here
❒asset
# debris Timed entity despawn — Roblox `Debris:AddItem(instance, lifetime)` analog. Wraps the internal `__debris.*` FFI with a typed Luau surface and installs it as the `debris` global via the prelude. Collapses the common `task.delay + entity.despawn` pattern into one centrally-introspectable call. Use for transient entities — bullet tracers, hit FX, dropped pickups, tween-completion cleanups, ragdoll cleanup. ## API | Call | Effect | |---|---| | `debris.add(id, lifetime?) -> handle` | Schedule despawn after `lifetime` seconds (default 10). Returns a cancel handle, or 0 if the id can't be resolved. Calling again on the same entity replaces the prior deadline. Negative or zero lifetime despawns immediately. | | `debris.cancel(handleOrId) -> boolean` | Cancel a pending despawn. Accepts a handle or an entity id / name / proxy. Returns true if a record was removed. | | `debris.pending(id) -> number?` | Seconds remaining before despawn, or nil if not scheduled. | | `debris.list() -> { { id, remainingSecs, handle } }` | Snapshot every pending entry as a flat array. Order is not stable. | | `debris.count() -> number` | Number of currently pending entries. | | `entity(id):AddDebris(lifetime?)` | Entity-proxy convenience; equivalent to `debris.add(id, lifetime)`. Accepts both colon and dot call styles. | ## Semantics - **Idempotent**: a second `add(id, lifetime)` replaces the prior deadline. The latest call's handle is the only handle that resolves to a live entry. - **Despawn through any other path drops the pending record** silently — registered at the `despawn_entity_now` chokepoint so `entity.despawn`, parent cascade, and `world.load` reset all clean up automatically. No "despawn(invalid)" errors. - **Single tick per frame**: `tick(dt)` runs alongside `task_scheduler::tick` in `world_context::post_update`. No new Bevy schedule node. - **Save / load round-trip**: pending entries are snapshotted into world settings as `debris.pending` at `world.save` and restored at `world.load`. The saved value is seconds-remaining, so timers resume from where they paused regardless of wall-clock gap between save and load. ## Examples ```lua -- Bullet tracer local bullet = entity.spawn("Bullet") debris.add(bullet.id, 2.0) -- Cancel before deadline local handle = debris.add(target.id, 5) debris.cancel(handle) -- Entity-proxy convenience entity.spawn("Pickup"):AddDebris(30) -- Introspection for _, info in debris.list() do print(entity(info.id).name, "despawns in", info.remainingSecs) end ``` See `docs/specs/scripting-api.md` for the canonical reference.
▲ 0↑ born
❒
bundle · born here
❒asset
▲ 0↑ born
◐
material · born here
❒asset
# Snow Particle A soft, cold-white flake surface for falling-snow particle effects — a near-white base colour (`0.95, 0.97, 1.0`) with a cool blue-grey emissive glow (`0.25, 0.3, 0.4`) so flakes stay faintly visible in shadow. Transparent, double-sided (`cull: none`), and drawn in the transparent queue (`3000`). Built on `@builtin::shaders.snow_material`. ## Inputs - `colors.base_color` — the cold-white flake tint. - `colors.emissive` (`0.25, 0.3, 0.4`) — cool self-illumination that keeps flakes readable in dark scenes. - `floats.roughness` (`0.8`) — matte, non-glossy flakes. - `floats.metallic` (`0.0`) — fully dielectric.
▲ 0↑ born
❒
bundle · born here
❒asset
▲ 0↑ born
·
computeshader · born here
❒asset
# abi_scale Multiplies every element of src by the scale param into dst, one element per invocation.
▲ 0↑ born
✦
shader · born here
❒asset
# `referenceChart` surface shader A reference chart drawn on a surface, unlit: each point holds light its properties and its UV state, so a picture of the surface is measured against known input. The post-verification reference scenes (`@builtin::scenes.postReference.*`) are built from its materials (`@builtin::materials.postReference.*`). | `pattern` | Draws | |---|---| | 0 | A grey wedge: one column per whole stop from `evMin` to `evMax`, each middle grey (0.18) at that stop. | | 1 | The 24 ColorChecker Classic patches, 6 x 4, each its published sRGB value read as linear reflectance. | | 2 | A ramp from `low` at u = 0 to `high` at u = 1. | | 3 | A Siemens star of `frequency` spokes, `high` on `low`. | | 4 | A checker `frequency` cells across, `low` and `high`. | | 5 | Focus bars: four rows of bar pairs, `frequency` pairs across the top row, doubling on each row below. | Every pattern's light is multiplied by `color` and `gain`. UV (0, 0) is the surface's top-left, as the front face of `@builtin::meshes.cube` maps it.
▲ 0↑ born
▣
module · born here
❒asset
# debug_bounds The "bounds" provider for `@builtin::modules.debugViz`: appends one amber wireframe box per scene mesh's world-space AABB into the shared debug-visualization batch, every edit-mode tick. ```lua debugViz.registerProvider("bounds", require("@builtin::modules.debug_bounds").tick) ``` `DebugViz.component` registers this provider automatically — attach it to one entity to draw a box around every mesh in the scene alongside whatever other providers are registered. ## How it works `tick(emit)` queries every entity carrying a Mesh (skipping `debugViz.anchorId()`, the core's own hidden draw-anchor, which itself carries an invisible Mesh), reads its `bounds()`, and appends a box via `emit.box`. The core owns the anchor, material, buffers, and render feature that actually draw the batch — this module only contributes geometry. ## API - `debug_bounds.tick(emit)` — append one box per scene mesh's current world-space AABB into the shared batch.
▲ 0↑ born
❒
bundle · born here
❒asset
▲ 0↑ born
▣
module · born here
❒asset
# range_bounds (module) The `range` field-constraint validator. Registers itself with `field_constraints.module`'s registry on load so any component field carrying `constraint = { kind = "range", min = ..., max = ... }` — which is what `Field.range` emits — is checked against it. ## What it does Given a field's written value and its constraint: 1. Passes `nil` — a ranged field may be left unset. 2. Rejects a constraint carrying neither bound, since there is no interval to check against. 3. Rejects a non-number value, naming the interval and the type it got. 4. Rejects a NaN or an infinity, which sits in no interval. 5. Answers whether the value is inside `constraint.min` .. `constraint.max`, either of which may be absent to leave that side open. Every rejection names the interval, so the message tells the caller what they may write instead of only what they may not. ## What a range is for A field whose value means something only inside an interval — a normalized amplitude, a fraction, a probability, a count that starts at zero — declares that interval once, and every write is held to it. What the field reads back is then a value the system consuming it can use, and a number that lands outside is reported where it was written rather than wherever it is eventually read. ## Where the bounds come from `Field.range(min, max, default, mode)` validates the interval at construction: at least one bound is required, both must be numbers when given, `min` may not sit above `max`, and a non-nil default must be inside. A field whose interval is wrong is therefore a load-time error rather than a runtime surprise, and this validator only ever sees a well-formed interval. The engine carries `constraint` verbatim and never reads inside it, so the bounds live entirely in Luau. ## Exports - `M.check(value, constraint) -> ok, reason?` — the validator function, also registered under the `range` kind.
▲ 0↑ born
▣
module · born here
❒asset
# BlankAppLayout The layout module for the `blank_app` scaffold. It builds a single UI screen from the engine's raw, CSS-parity widget tree and wires up one interaction: a `+1` button that increments a counter and re-renders. This is the file you edit. Replace the `build` tree with your own widgets. ## The contract A `UiPanel` component points its `source` at this module and calls `mount(ctx)`, where `ctx = { entity, entityId, data, state? }`. `mount` returns the handlers the panel drives each frame: ```lua function M.mount(ctx) local state = ctx.state or { count = 0 } -- 1. state is the source of truth local function build() return { --[[ widget tree ]] } end ui.registerScreen("blank_app", build()) -- 2. register the tree as a screen return { state = state, update = function(dt) end, -- 3. per-frame work (no-op here) onCallback = function(id, data) -- 4. an interaction lands here if id == "blank:inc" then state.count = state.count + 1 ui.updateScreen("blank_app", build()) -- mutate, then re-render end end, destroy = function() ui.unregisterScreen("blank_app") end, } end ``` - `state` is returned so the panel hands it back on a live-reload — your count survives an edit. - `update(dt)` runs every frame; leave it a no-op when nothing animates. - `onCallback(id, data)` receives every widget's callback id. Mutate state, then call `ui.updateScreen(name, build())` to publish the new tree. - `destroy()` unregisters the screen when the panel goes away. ## The raw widget tree A node is a table: `{ type = "...", props = {...}, style = {...}, children = {...} }`. - **Containers** lay out their children: `vertical` (column), `horizontal` (row). - **Leaves** carry content: `label` (text in `props`), `button` (text + `onClick` in `props`). - **`props`** is the widget's data and behaviour — a label's `text`, a button's `onClick` callback id. - **`style`** is CSS: `padding`, `gap`, `background`, `color`, `fontSize`, `fontWeight`, `borderRadius`, `borderWidth`, `borderColor`, `minWidth`, `minHeight`, `boxShadow`, and the rest of the box model. The tree is a pure function of `state`: to change what shows, rebuild the tree and call `ui.updateScreen`. There is no retained widget object to mutate. ## Interactivity An interactive widget carries a **string callback id** in `props` (this button's `onClick = "blank:inc"`). When the user clicks, the engine delivers that id to `onCallback` here. `onClick` works on any widget — a `label`, a `horizontal` row, an `image` — not just a `button`. Interaction runs in **play**, not edit: enter play and click the button to see the count rise. ## Discover the surface - The `gui` toolbox's **widgetTypes** tool lists every `type`. - Its **widgetProps** tool lists one type's `props`. - `@builtin::examples.ui.*` are complete CSS-parity screens (dashboard, sci-fi HUD, RPG inventory) in this exact model — read one before building. - The UI and styling guides cover the model and the full style surface.
▲ 0↑ born
·
computeshader · born here
❒asset
# compute_fft_scale Inverse-FFT `1/N` scale pass: divides every complex sample by the per-line N. Run after the inverse FFT stages. Used by `nx.gpu.fft*`. Bindings (see bindings.yaml): `data` (read_write `array<vec2<f32>>`), `params` (read `array<f32>` — `[total_elements, inv_scale]`). Usage: dispatch by identity with `compute.dispatch("@builtin::shaders.compute_fft_scale", ...)` — it resolves to the shader's stable guid and auto-compiles on first use (no setup).
▲ 0↑ born
◐
material · born here
❒asset
# Gold Polished gold — a fully metallic PBR surface with a warm yellow base colour (`1.0, 0.84, 0.0`) and low roughness (`0.25`) for bright, sharp reflections. Built on `@builtin::shaders.pbr`. ## Inputs - `colors.base_color` — the gold tint. - `floats.metallic` (`1.0`) — fully metallic. - `floats.roughness` (`0.25`) — sharpness of the reflection; raise for a brushed look.
▲ 0↑ born
·
computeshader · born here
❒asset
# abi_inout Writes src doubled into dst, or adds src to the dst it is bound when mode is 1.
▲ 0↑ born
◐
material · born here
❒asset
# Checkerboard A procedural checkerboard pattern rendered by `@builtin::shaders.checkerboard`, alternating a red base colour (`1.0, 0.2, 0.2`) with a blue emissive colour (`0.2, 0.2, 1.0`). Opaque and non-metallic (`metallic 0.0`) at mid roughness (`0.5`) — useful as a UV / scale test surface. ## Inputs - `colors.base_color` — the red square colour. - `colors.dark_color` — the blue square colour. - `floats.roughness` (`0.5`) — diffuse spread on the lit squares. - `floats.metallic` (`0.0`) — dielectric surface.
▲ 0↑ born
▣
module · born here
❒asset
# ui_observe What the last UI frame painted, which widget owns a point, and why a widget is not on screen. Reached through the `ui` namespace — `ui.observe`, `ui.diagnose`, `ui.hitTest`, `ui.pointerWidget`, `ui.paintOrder`, `ui.invisibilityReasons` — and served to a non-Luau reader at `/zero/runtime/ui/<screen>/paint.json`. ## The reading `ui.observe()` returns one document. Its `widgets` rows cover every widget any registered screen's tree holds, each carrying: | field | what it is | |---|---| | `parent` | the nearest ancestor in its screen's tree carrying an id | | `rect` | the box layout gave the widget | | `clip` | the clip it painted under — every `overflow: hidden`/`clip`/`scroll` box above it, intersected down from the viewport | | `visible` | the part of `rect` that survived `clip` and the viewport; `w`/`h` are zero when nothing did | | `viewport` | the region the pass that recorded it paints into: the whole window for a screen on the window, the rect a Scene panel presents the camera's picture in for a screen in that picture | | `paintIndex` | order within the frame, so two identical rects still separate | | `painted` | whether the pass that recorded it is the one drawing the viewport | | `opaqueFill` | whether it filled its whole box at full alpha | | `hovered` | whether the pointer was over it on the recorded frame | | `reason` | its verdict, from the closed set below | | `detail` | a sentence naming what the verdict is about | | `occludedBy` | the widget covering it, when `reason` is `occluded` | `generation` advances once per re-rendered frame. Two calls reporting the same number describe the same frame. Every rect, the document's own `viewport` (the window) and its `pointer` are in logical points on the window, the space `ui.getLayoutInfo` and `gui.bounds` answer in, wherever a widget's screen draws. A screen in the camera's picture lays out on the camera's canvas (the size `ui.screenSize()` reports); while a Scene panel presents that picture, its rects are mapped onto the window through the panel. Times `ui.pixelRatio()` a point is in the window's physical pixels, where `Zin.test`'s mouse primitives and the `inputSim` toolbox's `click` / `mouseMove` with target `"ui"` place a pointer. ## The verdicts `ui.invisibilityReasons()` lists them: | reason | what the engine found | |---|---| | `painted` | it reached the frame | | `noSuchWidget` | no registered screen's element tree carries the id | | `hiddenByAuthor` | `props.visible = false` on it or an ancestor kept it out of the render | | `screenNotShown` | its screen is registered and not shown | | `neverLaidOut` | its screen is shown and the frame laid out no box | | `zeroSize` | its own box has no area | | `offViewport` | its box sits entirely outside the viewport of the pass that drew it (the row's `viewport`) | | `clippedByAncestor` | an ancestor's clip removed the whole box | | `occluded` | a later widget covered it with an opaque fill | | `screenNotOnViewport` | its box is fine and the camera holding the viewport excludes its screen's render layer, so the frame measured it without drawing it | The resolution reports the nearest cause among them — a widget both clipped away and covered reports the clip — and `painted` when nothing kept the widget out. The camera's render-layer mask decides the screens in the camera's picture. With no Scene panel presenting the game viewport, the window is that picture too, and the mask decides every screen on it. While a Scene panel presents the game viewport, the window around it is the editor's own picture: a screen placed on the window (`ui.setScreenSpace`, and the default for a screen on `EditorUI`) paints there whatever the camera's mask holds, and reads `painted`. A screen placed in the camera's picture reads `screenNotOnViewport` under a camera that excludes its layer, with or without a Scene panel. ## The pointer `ui.hitTest(x, y)` names the widget a pointer at that point reaches and the stack beneath it, topmost first, in logical points on the window. Where several boxes take the point in, the one that paints last is the one that gets it, and the containers holding them follow — nearest first, by `parent`. `ui.pointerWidget()` reports where the pointer sat, whether the UI claimed it, and which widget the frame reported it over.
▲ 0↑ born
▣
module · born here
❒asset
# entity_signals Per-entity destroy signals, surfaced on the entity proxy as `entity(id).onDestroying` and `entity(id).onDestroyed`, plus the dispatch the despawn pipeline calls to fire them. ```lua local boss = entity.find("Boss") boss.onDestroying:Connect(function() -- runs BEFORE teardown — final state is still readable saveBossStats(boss.component.get("Combatant").public) end) boss.onDestroyed:Connect(function() log.info("boss fully cleaned up") end) entity.despawn(boss.id) -- onDestroying fires, then teardown, then onDestroyed ``` ## Order `onDestroying` and `onDestroyed` are `Signal`s (see `signal.module`), created on first access. The despawn pipeline drives the order. For a cascade rooted at one `entity.despawn(root)` call: 1. `onDestroying` fires across the whole subtree, root first — before any component teardown, so handlers read final state. 2. components tear down (`onDestroy(self)` per component), leaves first. 3. `onDestroyed` fires per entity as it is removed, leaves first. `onDestroying` handlers must not yield, and the entity is frozen against reparenting / re-mutation during the destroying window. An entity nobody observes costs nothing and fires nothing. ## Entity-keyed tables A module that keeps per-entity state for something to read every frame (a registry a render feature walks, a set of marked entities) declares its table with `entityKeyed`. When an entity is despawned, by `entity.despawn`, a parent's cascade, a scene rebuild or reload, or a mode flip, its id leaves every such table, so the reader only ever meets entities in the world and its cost follows them. ```lua local EntitySignals = require("@builtin::modules.entity_signals") local marked = EntitySignals.entityKeyed({}) function M.mark(id: string) marked[id] = true end ``` The table is one of the engine's own records: a module-state restore leaves it as it stands, so an id that left it stays out.
▲ 0↑ born
✦
shader · born here
❒asset
▲ 0↑ born
◐
material · born here
❒asset
# Custom Textured A textured opaque surface built on `@builtin::shaders.custom_material`, sampling the `array_texture` builtin over a warm peach base colour (`1.0, 0.8, 0.6`). Non-metallic (`metallic 0.0`) at mid roughness (`0.5`) — a worked example of pairing a custom shader with an array texture. ## Inputs - `textures.base_color_texture` (`@builtin/textures/array_texture`) — the sampled array texture. - `colors.base_color`: the warm peach colour the texture is multiplied by. - `floats.roughness` (`0.5`) — diffuse spread. - `floats.metallic` (`0.0`) — dielectric surface.
▲ 0↑ born
❒
package · born here
❒asset
▲ 0↑ born
▣
module · born here
❒asset
# entity_reflect High-level entity introspection and manipulation utilities. Wraps the `__reflect` FFI global with a cleaner API — no per-script component declaration needed; every method goes through the reflection layer so it works on any entity that carries the relevant component. Exposed as the `Entity` global via the `--!global Entity` directive. ## Exports - `Entity.getField(entityId, component, field) -> any` / `Entity.setField(entityId, component, field, value) -> boolean` — generic reflected read/write. - `Entity.getPosition(id) -> Vec3?` / `Entity.setPosition(id, pos)` — `Transform.position` shortcuts. - `Entity.getRotation(id) -> Quat?` — `Transform.rotation` shortcut. - `Entity.getScale(id) -> Vec3?` / `Entity.setScale(id, scale)` — `Transform.scale` shortcuts. - `Entity.getName(id) -> string?` — `Name` component read. - `Entity.isVisible(id) -> boolean` — `Visible` component read; missing component is treated as visible. - `Entity.getComponents(id) -> { string }?` — list reflected components on this entity. - `Entity.getSchema(componentName) -> { ComponentFieldSchema }?` — list field names + types for a component type. - `Entity.snapshot(id) -> any` — full `{ ComponentName = { field = value, ... }, ... }` dump. - `Entity.patch(id, component, fields) -> boolean` — multi-field update. - `Entity.distance(idA, idB) -> number?` — straight-line distance between two entities' `Transform.position`. - `Entity.allTypes() -> { string }` — list every registered reflectable component type. Types: - `Vec3 = { x: number, y: number, z: number }` - `Quat = { x: number, y: number, z: number, w: number }` - `ComponentFieldSchema = { name: string, type: string }` ## Usage ```luau -- `Entity` is a global (--!global Entity); no require needed. local pos = Entity.getPosition("player") Entity.setPosition("player", { x = 0, y = 1, z = 0 }) local snap = Entity.snapshot("player") Entity.patch("player", "Transform", { position = { x = 0, y = 1, z = 0 }, scale = { x = 1, y = 1, z = 1 }, }) ``` ## Notes - Every method goes through the `__reflect` FFI — `nil` returns mean the component or field is missing, not that the entity is gone. Use `Entity.getComponents(id)` to enumerate what's there. - `isVisible` treats a missing `Visible` component as visible (`v == nil` returns `true`). - `Entity.distance` returns `nil` when either entity lacks a position; callers should branch on `nil` before using the result. - The `--!global Entity` directive is what makes `Entity` the global name — the module's local `E` variable is just an implementation detail.
▲ 0↑ born
▣
module · born here
❒asset
# field_constraints (module) Generic field-constraint dispatch. Installs `_G.__zero_check_field_constraint(value, constraint)`, the Luau half of the opaque field-constraint primitive: a component field descriptor may carry a `constraint` value (an arbitrary table with a `kind` string), and the engine calls this hook whenever a constrained field's default resolves or a write lands. The engine copies `constraint` verbatim — it never reads inside it — and treats the hook's first return as accept/reject, the second as the rejection reason. ## What it does 1. Reads `constraint.kind` and looks up a registered validator for that kind. 2. Built-in kinds (currently `dataContract`) resolve their validator module lazily on first use, so component registration never depends on load order. 3. Calls the validator with `(value, constraint)` and returns its `(ok, reason?)` result. A malformed constraint (not a table, or missing `kind`) and a missing validator both reject — the primitive fails closed rather than silently accepting an unconstrained value. ## Exports - `M.register(kind, fn)` — register the validator for a constraint kind. `fn(value, constraint) -> ok, reason?`. One validator per kind; re-registering a kind errors. - `M.check(value, constraint) -> ok, reason?` — run the check directly (same function installed as the engine hook). ## Consumers `Field.dataRef` (`field.module`) emits `constraint = { kind = "dataContract", contract = ... }`; `data_contract.module` registers the `dataContract` validator against this registry.
▲ 0↑ born
◐
material · born here
❒asset
# Sky Procedural A fully procedural atmospheric sky with a day/night cycle, built on `@builtin::shaders.procedural_sky`. Renders with no culling and no depth write behind all geometry (queue `1000`), computing gradient, sun, moon and stars analytically from its parameters — no texture required. ## Inputs - `properties.zenith_color` / `horizon_color` / `ground_color` — the atmospheric gradient from overhead (`0.1, 0.3, 0.6`) through the horizon (`0.5, 0.7, 1.0`) to the ground (`0.3, 0.25, 0.2`). - `properties.sun_intensity` (`20`) / `sun_size` (`0.02`) — brightness and disc size of the sun. - `properties.turbidity` (`4`) — atmospheric haze / scattering density. - `properties.stars_intensity` (`1`) / `moon_size` (`0.03`) — night-sky star brightness and moon disc size.
▲ 0↑ born
▣
module · born here
❒asset
# mathx Module Public Luau surface over the `__mathx` Internal FFI namespace — batch math kernels that operate on buffer slices. ## Purpose Wrap the SIMD-friendly batch ops exposed by Rust (damp, lerp, slerp, normalize, accumulate, transform) in a typed Luau table that gets auto-injected as `_G.mathx` via the prelude. Single-value forms are out of scope — Luau handles those at sub-µs cost and an FFI hop would dominate. For element-wise NumPy-style ops on typed buffers, prefer the `nx` module; `mathx` is the slice-and-target form for specific position / orientation / quaternion shapes. ## Usage ```luau -- Critically-damped scalar follow mathx.dampScalar(buf, 0, 16, 0.0, 0.16, dt) -- Lerp every vec3 in a slice toward a target mathx.lerpVec3(positions, 0, n, 0, 1, 0, 0.5) -- Slerp every quat toward a target rotation mathx.slerpQuat(rotations, 0, n, 0, 0, 0, 1, 0.25) -- particle integration: pos += vel * dt mathx.addScaledVec3(positions, velocities, n, dt) -- transform vec3 positions by a 4x4 world matrix mathx.transformVec3(positions, 0, n, worldMatrix) ``` ## Exports - `mathx.dampScalar(buf, off, count, target, smoothTime, dt)` - `mathx.lerpVec3(buf, off, count, tx, ty, tz, t)` - `mathx.slerpQuat(buf, off, count, tx, ty, tz, tw, t)` - `mathx.normalizeQuat(buf, off, count)` - `mathx.addScaledVec3(dstBuf, srcBuf, count, scale)` - `mathx.transformVec3(buf, off, count, mat16)`
▲ 0↑ born
✦
shader · born here
❒asset
# `fog` post-process shader Depth-keyed distance haze. `pp.add(name, "fog", { density = ..., color = { r, g, b } })` registers it, and `asset.resolve("@builtin::shaders.post.fog", "shader"):getProperties()` reads the knobs below before it is added. The pass reads the scene depth already drawn at a pixel, turns it into a distance in metres with `zero_linear_depth(d, near, far)`, and mixes the frame toward `fogColor` by `1 - exp(-distance * density)` — an exponential falloff, so density sets how many metres of air it takes to reach the fog colour. `near` and `far` are the planes the frame was projected through. They are what a stored depth is turned back into metres by, so they belong to the camera the scene was drawn with rather than to the look: setting them to something the camera does not use moves every surface's distance and with it the whole haze. | Property | Meaning | |---|---| | `density` | Falloff rate. 0 leaves the frame untouched. | | `fogColor` | The colour a fully fogged pixel reaches. | | `near` | The scene camera's near plane, in metres. | | `far` | The scene camera's far plane, in metres. |
▲ 0↑ born
◐
material · born here
❒asset
# Sky Cubemap A cubemap sky/environment background built on `@builtin::shaders.cubemap_sky`. Renders with no culling and no depth write behind all geometry (queue `1000`), sampling the built-in `Ryfjallet_cubemap` with adjustable rotation and exposure. ## Inputs - `textures.sky_texture` — the cubemap sampled as the sky (`Ryfjallet_cubemap`). - `floats.rotation` (`0.0`) — yaw rotation of the sky around the vertical axis. - `floats.exposure` (`1.0`) — brightness multiplier applied to the sampled colour.
▲ 0↑ born
▦
texture · born here
❒asset
▲ 0↑ born
·
plugin · pulled in
❒asset
# dotbot What this plugin does and who it is for, in a paragraph. ## Use ```luau local dotbot = require("dotbot") local answer = await(dotbot.doSomething("x")) ``` ## Methods - `doSomething(input)`: what it does, and what it answers.
▲ 0↓ import
▣
module · born here
❒asset
# marquee_overlay A screen-space translucent rectangle with a 1px border, drawn while a viewport rubber-band (marquee) selection is dragging and hidden otherwise. It owns one dedicated UI screen tagged `editor`, so it rides the `EditorUI` render layer and the editor's F1 chrome toggle alongside the rest of the editor overlays. `viewport_select` drives it. ## Exports - `M.show(ax, ay, bx, by)` — show the band filling the rectangle spanned by the two corners, given in UI-logical points (the `ui.screenSize()` space). Corners may be passed in any order. - `M.hide()` — hide the band. Safe to call when nothing is shown. ## Notes - Callers work in viewport pixels; convert to UI-logical points (multiply by `ui.screenSize().width / viewport.w` and the height ratio) before calling `show`, since absolute `left`/`top`/`width`/`height` resolve in the logical space, not render pixels. - Coordinates are rounded to whole points so a value in the open `(0, 1)` range is never mistaken for a viewport-axis fraction by the size decoder.
▲ 0↑ born
·
scene · born here
❒asset
# Baseline Visual Validation harness scene for the [player-camera-unification PR](../../../../docs/plans/2026-05-01-player-camera-unification.md). ## Purpose Visual smoke target for Steps 4–9 of the §17 migration plan. Those steps land *before* §17 step 10 (the generic `camera_spawner` / `player_spawner` that auto-populate scenes with a viewport camera). Without this scene, those intermediate commits have no working viewport to validate against and the boot path produces a single bare `Player` entity that renders nothing. `layers.load("@builtin::scenes.baseline_visual")` brings in: - A `Camera`-bearing entity (`baseline_camera`) at a fixed third-person framing of the origin. Renders to viewport. - A static-collider ground plane (`baseline_ground`) so the character controller has something to stand on. - The engine's default `Player` entity (spawned by `ensure_default_player`) with the two canonical tagged bundles applied: - `@builtin::players.humanoid_visual` — mesh + skeleton + `Locomotion` - `@builtin::players.standard_controller` — `MovementState` + `CharacterController` All spawned entities are `temporary` so `layers.active:save()` doesn't bake them into a persisted `.scene/` file. ## Cleanup path | Step that lands | What changes here | |---|---| | §17 step 10 (`camera_spawner` / `player_spawner`) | The hardcoded camera spawn becomes redundant — drop it; the spawner takes over. Bundle application becomes a no-op smoke check. | | §17 step 3 (`.player/` retirement) | No effect — this scene already references the new tagged bundles. | | End of PR | Either kept as the canonical engine smoke target or deleted in Step 16's test-suite extensions. |
▲ 0↑ born
▣
module · born here
❒asset
# mcpLog Module Public Luau surface over the `__mcpLog` Internal FFI namespace — read-only ring buffer of MCP tool-call entries. ## Purpose Wrap the raw `__mcpLog.*` FFI namespace in a typed Luau table that gets auto-injected as `_G.mcpLog` via the prelude. The in-engine "MCP" panel polls `mcpLog.query()` every frame and renders the returned array of structured tool-call rows. ## Usage ```luau -- Pull the most recent 50 calls for _, entry in ipairs(mcpLog.query(50)) do print(entry.tool_name, entry.status, entry.duration_ms) end -- Full ring (up to 500 entries) local all = mcpLog.query() -- Drop everything mcpLog.clear() ``` ## Exports - `mcpLog.query(limit?) -> { McpLogEntry }` — newest-last array of entries. - `mcpLog.clear() -> boolean` — drop all entries.
▲ 0↑ born
▣
module · born here
❒asset
# component_field_route One rule for which access route serves a component's fields, shared by every API that names an entity, a component and a field: `Entity.getField`, `Entity.setField`, `Entity.patch`, `entity.batchWrite` and `entity.batchRead`. Two routes exist, because two kinds of component do: - **reflect route** — a component backed by an engine struct (`Transform`, `MeshInstance`, `Physics`). `__reflect` resolves its fields through the reflect registry. - **component route** — a component declared in Luau (`Model`, `Text3D`, and every other `.component` folder). Its field values live on the component instance, so the component ref (`entity(id).component.get(name)`) is what reaches them. The reflect registry holds engine structs only, so a component declared in Luau resolves on the second route and nowhere else. Dispatching on which route serves the name is what lets one call site write `Model.tintBlend` and `Transform.position` the same way. ## Exports - `M.isReflected(component: string) -> boolean` — whether the reflect registry holds this component. - `M.requireComponentType(api: string, component: string)` — raise unless the name is one this engine knows, either as an engine struct or as a declared `.component`. - `M.componentRef(entityId: string, component: string) -> ComponentRef?` — the component ref serving `component` on that entity, or `nil`. - `M.declaredFields(ref: ComponentRef) -> { [string]: boolean }` — the public and private field names the ref declares. - `M.reportMissingField(api, component, field, outcome)` — report a field the component does not declare. - `M.reportEntityWithoutComponent(api, entityId, component, outcome)` — report one entity that does not carry it. - `M.reportEntitiesWithoutComponent(api, component, missing, total, outcome)` — report a call's entities that do not carry it, as one line. - `M.readField(api, entityId, component, field) -> any` — read one field through whichever route serves it. - `M.writeField(api, entityId, component, field, value) -> boolean` — write one field through whichever route serves it. ## Usage ```luau local Route = require("@builtin::modules.component_field_route") Route.writeField("entity.batchWrite", id, "Model", "tintBlend", 0.5) local blend = Route.readField("entity.getField", id, "Model", "tintBlend") ``` ## What a caller is told | situation | result | | --- | --- | | a component name nothing declares | raises, naming the component | | a component the entity does not have | the neutral value, reported by the route | | a field the component does not have | the neutral value, reported at error level | `api` is the surface the caller used; it is named in every diagnostic so the line points at the call rather than at the machinery underneath. A report repeats at most once every five seconds per `(api, component, field)`, the budget the reflect route spends on the same mistake, so a per-frame loop over a wrong name costs one line rather than one per frame — and a batch names the entities that do not carry the component once for the call rather than once each.
▲ 0↑ born
✦
shader · born here
❒asset
▲ 0↑ born
·
scene · born here
❒asset
# [scene_name] One-paragraph description of what this scene is — the level, screen or area it represents, and when the world loads it. This README describes *this* scene; what a scene is and how loading works live in the scene assetType README. ## Contents - What the entity hierarchy holds — the terrain, props, lights and spawn points a reader should expect to find. - Which player intent `scene.json` declares (`"spawns"` places the local player and its camera on join; `"none"` leaves both to the entrypoint). ## Entry point - `entrypoint.luau` — what it sets up when this scene loads, and anything a caller has to do before loading it.
▲ 0↑ born
·
scene · born here
❒asset
▲ 0↑ born
·
computeshader · born here
❒asset
# compute_add_vectors Reference compute shader that adds two float arrays element-wise into a third — the multi-buffer pattern with read-only inputs. Bindings (see bindings.yaml): `a` (read), `b` (read), `result` (read_write) — all `array<f32>`. Usage: dispatch by identity — it resolves to the shader's stable guid and auto-compiles on first use (no setup): `compute.dispatch("@builtin::shaders.compute_add_vectors", { buffers = { "a", "b", "result" }, workgroups = { 1, 1, 1 } })`.
▲ 0↑ born
▦
texture · born here
❒asset
▲ 0↑ born
·
computeshader · born here
❒asset
# map The per-element kernel: `dst[id] = TEMPLATE` over `n` elements. The template is a WGSL expression over `p` (the element's position), `id`, the numbers `u_get(k)` and the columns of the `cols` table; the preludes `rand`, `noise` and `color` supply the functions a template calls. `fieldc.mapSpec` writes a chain of field templates as one function, so a chain of any length is one dispatch. Specialize `T` (f32, i32, vec2, vec3 or vec4 of f32), `TEMPLATE`, and optionally `PRELUDE` and `POSITION`. Bind `cols` (a table from `compute_abi.table`) and `u`. The output `dst` holds `n` elements of `T`, tightly packed. ```luau local program = ref:program() local variant = program:variant({ specialize = { T = "f32", TEMPLATE = "zero_rand(7u, id)", PRELUDE = '#include "@builtin::systems.procgen.kernels.rand"' } }) local run = variant:run({ n = 64, inputs = { cols = emptyTable, u = numbers } }) ``` `fieldc.evaluate(field, domain)` runs a Field through this kernel and answers the Column it wrote. Leaves of i32 runs are read through `cols_i32` and a read past a run's end reads its last element.
▲ 0↑ born
❒
package · born here
❒asset
# lightCookies Projects an authored image through a spot light's cone — a gobo, window blinds, a headlight cutoff, a projector pattern. ## How it divides up The renderer already projects a texture through a spot's frustum. At each shaded point it reads `zero_feature_texture(uv, cookieLayer)`, with `uv` from that point's position in the light's own clip space, and multiplies the cone by what it finds. `SpotLight.cookieLayer = -1` means no cookie. What the renderer does not decide is what goes in the layer. That is this package: resample an authored image into one full layer of the shared feature-texture array, and point a spot at that layer. ## Using it ```luau local lightCookies = require("@builtin::systems/lightCookies.package/lightCookies") -- Size the array once for every layer the scene will use. lightCookies.configure({ resolution = 256, layers = 4 }) lightCookies.project(lampId, "/textures/cookies/window_blinds.png", { layer = 0 }) ``` Or attach a `LightCookie` component next to the `SpotLight` and author `texture` / `layer` / `gain` / `tint` on it. `lightCookies.clear(lampId)` stops the projection; the layer keeps its contents. ## When a fill lands `fill` and `project` queue the fill and return its layer at once. The module's own loop writes it over the frames that follow: the rasterize is issued on the next frame and the copy into the layer on the frame after, and the fill is written once `renderer.featureTexture.state().fills` reports the copy landed in its layer, one fill at a time in the order they were asked for. The loop belongs to the module, so a fill lands even when the code that asked for it (a one-shot script, a task scope, a component that is destroyed) has ended. `lightCookies.status(layer)` reports where the last fill of a layer stands: ```luau local s = lightCookies.status(0) -- s.state: "queued" | "rasterizing" | "copying" | "written" | "refused" -- s.reason: why a refused fill was refused, in the engine's words -- s.filled: whether renderer.featureTexture.state().filled lists the layer ``` `status` reads a fill `"written"` from the first reading of the array that reports its copy landed, so `s.state` and `s.filled` agree whichever of the module's loop and the reader runs first in a frame. A fill names a layer below the count `configure` sized the array to; one past it is refused at the call, which returns nil and the reason. A fill that cannot land once it is on its way (the GPU refuses its rasterize because its image was given back first, a later `configure` left the array without its layer, or its copy never reached the array) reads `"refused"` with the reason, and is also logged as a warning. A `LightCookie` component whose fill is refused at the call, such as one authored before the scene's `configure` has sized the array, asks again each time `configure` sizes it. `lightCookies.observe().configurations` counts those calls for code of its own that does the same. `lightCookies.observe()` reports how many fills are waiting, the one in progress, and the loop's own counters. ## An empty layer puts the light out The cone is multiplied by what it reads from its layer, so a spot pointed at a layer nothing has filled renders black rather than plain. `project` fills before it points the light, which is the order that keeps the cone lit; if you set `SpotLight.cookieLayer` by hand, fill that layer first. ## The array is shared, and sizing it is explicit The feature-texture array is one resource that every feature using `zero_feature_texture` draws layers from, and it has no allocator: sizing it zeroes every layer, including layers another feature owns. So this package never sizes it as a side effect of setting a cookie, and `layer` is authored rather than assigned. `lightCookies.configure` is the one call that resizes, made once by whoever knows what else in the scene wants layers. A scene that also bakes global illumination shares this array with the lightmap atlas. Size it for both, and give cookies layers above the ones the bake fills. ## A cookie fills a whole layer The cone's lookup runs over the layer's entire `uv` range, so an image is resampled to the layer's extent rather than placed in a corner of it. One layer carries one cookie. ## Alpha The source's alpha is how much of the pattern applies. At alpha 0 the cone passes through unmodulated, so a fully transparent image is a no-op rather than a black cone.
▲ 0↑ born
▣
module · born here
❒asset
# editorRegistry The single registry every editor surface is built from — builtin tabs and world panels alike. The engine's own tools register through the same public API a world uses. Two surfaces consume the registry: the dock service (`system_tools.module`) renders whatever panels are registered, sorted by `order`, and the top bar (`EditorTopBar.component`) builds its menu bar from the registered menus and each panel's `menu`/`order`. Both subscribe to `onChanged` so add/remove is live. Registrations run through two paths, both ending in `addPanel`: builtin tabs register from `modules.api.editor.system_tools.panels` (the require IS the registration), and world panels register via an `editorPanel` asset whose `onRegister(self)` hook calls `addPanel`. State lives in a fixed `_G` slot so it survives hot-reload and edit↔play mode flips. A duplicate id replaces the prior registration, which is how a world overrides a builtin tab and how hot-reload re-registers cleanly. ## Panels and menus - `M.addPanel(spec) -> boolean` — add (or replace) a dock panel. `spec`: `id` (required, unique), `build` (required, `(state) -> widget`), `label?`, `menu?` (default `"window"`), `order?`, `refresh?`, `onCallback?`, `onMount?`, `tick?`, `float?`. - `M.removePanel(id) -> boolean` — remove a registered panel by id. - `M.addMenuItem(spec) -> boolean` — add (or replace) a clickable action item that runs `onClick`, with no associated panel. `spec`: `id` (required), `onClick` (required), `label?`, `menu?`, `order?`. - `M.removeMenuItem(id) -> boolean` — remove a registered action item. - `M.addMenu(spec) -> boolean` — add (or replace) a top-level menu in the bar. `spec`: `id` (required), `label?`, `order?`. - `M.removeMenu(id) -> boolean` — remove a top-level menu and cascade-drop every item/panel that targeted it. ## Introspection and subscription - `M.list() -> { panels, items, menus }` — snapshot copies of current registrations for introspection. - `M.onChanged(fn) -> handle` — subscribe to any add/remove mutation; `fn` runs with no args. - `M.offChanged(handle) -> boolean` — unsubscribe. ## Host accessors (dock service + top bar) - `M.setDock(controller?)` — publish (or clear with `nil`) the live `DockController`. - `M.dock() -> DockController?` — the live dock controller, or `nil` when no dock is mounted. - `M.panelsSorted() -> {spec}` — live dock panels sorted by `(order, id)`, including `build`. - `M.menuModel() -> {menu}` — the full top-bar menu model: every menu in bar order, each carrying its entries (`{kind="panel", id, label}` or `{kind="action", id, label}`) in `order`. The `DockController` type is `{ open, close, toggle, isOpen }` plus optional `getLayout` / `restoreLayout` for layout persistence. ## Usage ```luau local editor = require("@builtin::modules.api.editor.registry") editor.addMenu({ id = "mygame", label = "My Game", order = 70 }) editor.addPanel({ id = "my_tools", label = "My Tools", menu = "scene", order = 50, build = function(state) return Z.lbl("hello") end, }) editor.addMenuItem({ id = "act", menu = "scene", label = "Go", onClick = function() end }) ```
▲ 0↑ born
▣
module · born here
❒asset
# preview Render an asset's preview in the preview studio: an isolated camera, lit only by its own three-point rig, over a neutral backdrop, that nothing in the scene reaches. `ref:preview()` renders any asset by the recipe its type declares under `preview:` in its `type.yaml`, and these are the primitives it calls: they spawn temporary entities, frame an offscreen camera over them, render one frame, read it back as a PNG, and tear the entities down. ## Surface - `preview.fromEntities(makeFn, opts?) -> PreviewResult` — run `makeFn` to instantiate entities, frame a camera to their bounds, render one offscreen frame, and return the still PNG. - `preview.fromRecipe(ref, recipe, opts?) -> PreviewResult` — render `ref` by a declared recipe: `instantiate` stands it through its own `instantiate`, `surface` paints the material its type's `ref.previewMaterial` returns on the recipe's mesh, and `image` fits a file the asset holds to the frame on the studio backdrop without rendering anything. - `preview.backdrop() -> { r, g, b }` — the studio backdrop's colour in a finished preview, 0-255. `PreviewResult` is `{ available, imageBase64?, rendered?, width?, height?, bounds?, stats?, reason? }` — `imageBase64` is the rendered PNG when `available` is true, and `reason` explains why when it is false. `opts` is `{ size? = { width, height }, angle? = { yaw, pitch } }`. ## Example local result = asset.resolve("gold", "material"):preview({ size = { width = 512, height = 512 } }) -- result.imageBase64 holds the rendered PNG (base64)
▲ 0↑ born
▣
module · born here
❒asset
# scifi_hud A UI template screen.
▲ 0↑ born
▣
module · born here
❒asset
# future A future is a promise held as a value: store it in a table, return it from a function, hand it to another task, and await it whenever the value is needed. Every promise-returning call (`http.get_json`, `delay`, `scene.save`, `asset.load`, ...) already hands back one, a `PromiseHandle`; this module makes new ones, runs work as one, chains them, waits for several at once, bounds one in time and cancels them. ```lua local future = require("@builtin::modules.future") task.spawn(function() -- Start both loads; they run at the same time. local pending: { Future<any> } = {} table.insert(pending, http.get_json("https://example.com/a.json")) table.insert(pending, http.get_json("https://example.com/b.json")) -- Collect them. Each await returns once its load has settled. local results = {} for i, f in pending do results[i] = f:await() end end) ``` ## Members of a future | Member | What it answers | |---|---| | `f:await()` | Yields until the future settles and returns its value; a future that has already settled returns at once, with no yield. A rejection raises its error at this line. The same call as `await(f)` and `task.await(f)`. | | `f:state()` | `"pending"`, `"resolved"`, `"rejected"` or `"cancelled"`, read without taking the value. | | `f:rejection()` | Why the future was rejected: `{ message, origin?, stack? }`, the reason as text, the `chunk:line` the rejection came from and, when Luau rejected it, the stack there. `nil` while it is pending or once it resolved. | | `f:source()` | The `chunk:line` whose call created the promise, for finding what a hung await is waiting on. | | `f.handle` | The promise's id string. `tostring(f)` reads the same. | | `f:andThen(onResolve, onReject?)` | A future of what the handler returns; a future it returns is awaited and its value delivered. Without `onReject` a rejection passes through. A handler that raises rejects it. | | `f:catch(onReject)` | A future of `f`'s value, or of what `onReject` returns given the error. | | `f:finally(fn)` | A future settled the way `f` was, once `fn()` has run, whichever way `f` settled, cancelled included. | | `f:tap(fn)` | A future of `f`'s value, once `fn(value)` has seen it. | | `f:timeout(seconds, reason?)` | A future of `f`'s value that rejects with `reason` (a message naming the time by default) when `seconds` of real time pass first; `f` is then let go of. | | `f:cancel()` | Ends a pending `f` as cancelled: `true`, or `false` and the reason. | The chaining methods come with the module, which every world script VM loads, so a future an engine call hands back chains with no require. A handler runs in a task (it may wait) inside a `scopes.run`: the chained future settles once everything the handler started is over. Every task that awaits a future receives its value: the ones waiting when it settles and any that await it later, for as long as the future is held. ## Module functions | Call | Result | |---|---| | `future.wrap(promise)` | The future naming a promise, from its future or its id string. An id naming no promise the engine keeps raises. | | `future.new(onCancel?)` | A pending future plus `resolve(value)` and `reject(error)`, which settle it once. `error` is any value but nil. `onCancel()` runs when the future is cancelled. | | `future.resolved(value)` | A future already resolved with `value`. | | `future.rejected(err)` | A future already rejected with `err`. | | `future.after(seconds)` | A future resolving with `true` after `seconds` of real time (the promise `delay(seconds)` hands back). | | `future.spawn(fn, ...)` | Runs `fn(...)` as a task; the future resolves with what it returns (awaiting a future it returns) and rejects with what it raises. | | `future.all(futures)` | Every value, in the order given. The first rejection rejects it, the first cancel cancels it, and the rest are let go of. `{}` for none. | | `future.allSettled(futures)` | `{ status, value?, error? }` for each, in order, once all have settled. Never rejects. | | `future.race(futures)` | Settles the way the first to settle did; the rest are let go of. | | `future.any(futures)` | The first value resolved; the rest are let go of. When none resolves it rejects with `{ message, errors }`. | | `future.retry(fn, attempts, delaySeconds?)` | Calls `fn()` until it succeeds, at most `attempts` times, with `delaySeconds` of real time between. | | `future.fromEvent(event, predicate?)` | The first value of the next fire of `event` (a Signal, a component event, anything offering `:Connect(fn)` / `:connect(fn)`) that `predicate` accepts. Disconnects once settled or cancelled, and is cancelled when what started it ends. | ```lua local future = require("@builtin::modules.future") task.spawn(function() -- Both loads at once; the first failure fails the lot. local maps = future.all({ http.get_json(urlA), http.get_json(urlB) }):await() -- Whichever search answers first; the other is cancelled. local nearest = future.race({ future.spawn(searchNear), future.spawn(searchFar) }):await() -- No level within five seconds rejects, and the load is cancelled. local ok, err = pcall(function() return future.spawn(loadLevel, "forest"):timeout(5):await() end) end) ``` ## Cancellation `f:cancel()` ends a pending future as cancelled. Awaiting it raises the error `task.isCancellation(err)` recognises, and `f:state()` reads `"cancelled"`. A cancellation is never written to the error log. - **The work stops.** The task running a `future.spawn` or a handler is cancelled, and so is everything it started; each unwinds, so the `pcall` or cleanup around its wait runs. - **Downstream.** A future chained from a cancelled one (`andThen`, `catch`, `tap`, `timeout`, `all`, `race`, ...) is cancelled too, without calling its handler; `finally` runs its function first. - **Upstream.** A future counts the chains and combinators waiting on it. One whose last such consumer is cancelled, or settled without it (a `race` another input won, a `timeout` that ran out, an `all` another input rejected), is cancelled. A direct `await` is not a consumer. - **What started it.** The task running a handler belongs to what the calling code belongs to: a component instance, a scene load, a task scope. When that ends, the future is cancelled. A future an engine call handed back (`http.get_json`, `delay`) runs to its end whatever waits on it; its `cancel()` answers false and the reason. ## Rejections A rejection is kept apart from any value, so a future resolved with a table of any shape delivers that table. Awaiting a rejected future raises its error: a string at the await's line, any other value (an error table `reject` was given) as itself. Catch it with `pcall`, and read where it came from on the future: ```lua local f, _, reject = future.new() task.delay(1, function() reject({ code = 404, message = "no such level" }) end) local ok, err = pcall(f.await, f) if not ok then print(err.code) -- 404: the error value reject was given local why = f:rejection() print(why.origin) -- the line that called reject end ``` A rejection nothing sees (no await receives it and nothing reads `rejection()`) is written to the error log once the future is gone, so a future you hold and await later reports nothing on its own. A chain or combinator sees every rejection its inputs hold when it settles, so the second rejection of an `all`, or a `race` input already rejected when another won, writes nothing. ## Identity A VM holds one future object per promise. Every call that hands a promise to the VM while its future is alive, `future.wrap` included, hands back that same object: `future.wrap(f) == f`, `rawequal(future.wrap(f.handle), f)`, and a future is one table key. ## Types `Future<T>` is a future whose await delivers a `T`; `PromiseHandle` is a future whose value has no stated type. They name the same value, so either is accepted where the other is declared, and `f:await()` on a `Future<T>` checks as a `T`. `f:andThen(fn)` checks as a `Future` of what `fn` returns (a `Future<V>` it returns reads as `V`), `f:catch(fn)` as the value or what `fn` returns, `finally`, `tap` and `timeout` as `f`'s own type, and `future.all({ Future<T> })` as a `Future<{ T }>`.
▲ 0↑ born
❒
bundle · born here
❒asset
▲ 0↑ born
·
computeshader · born here
❒asset
# compute_snow_terrain GPU heightmap generation for a snowy mountain landscape. One thread per `(x, z)` column, emitting a single `f32` height per cell. Uses multi-octave FBM for hills, ridge noise for peaks, and a radial bowl to frame a mountain valley. Bindings (see bindings.yaml): `params` (read `array<f32>` — `[gridSize, worldExtent, seed, mountainScale, baseHeight, ridgeStrength, ...]`), `heights` (read_write `array<f32>`, indexed `[z * gridSize + x]`). Usage: dispatch by identity with `compute.dispatch("@builtin::shaders.compute_snow_terrain", ...)` — it resolves to the shader's stable guid and auto-compiles on first use (no setup).
▲ 0↑ born
✦
shader · born here
❒asset
▲ 0↑ born
◐
material · born here
❒asset
# Default Unlit A flat, unlit surface that renders its base colour directly with no lighting response, built on the `unlit` shader. Uses a white base colour (`1.0, 1.0, 1.0`) over the `default:white` texture with `emissive` (`1.0`) for full brightness — the go-to default for UI, gizmos, and debug geometry that must stay legible regardless of scene lighting. ## Inputs - `colors.base_color` — the flat colour shown directly. - `textures.base_color_texture` (`default:white`) — the white base texture the colour multiplies against. - `floats.emissive` (`1.0`) — full, lighting-independent brightness.
▲ 0↑ born
▣
module · born here
❒asset
# tween Tween / easing library — smooth property interpolation with chaining, timelines, and 30+ easing curves. This file is the public surface; the implementation lives in three submodules: `core.module/` (lifecycle and interpolation), `easing.module/` (easing curves and name lookup), and `timeline.module/` (keyframe sequencer). **Important**: callers MUST call `tween.update(dt)` every frame from a component's `update` function. Without this, tweens will not advance. ## Exports Property animation: - `tween.to(entityId, property, target, durationOrOpts, easing?) -> Tween` — animate an entity property to a target value. - `tween.fromTo(fromVal, toVal, durationOrOpts, easing?) -> Tween` — animate raw values via `onUpdate`. - `tween.create(opts: TweenOpts) -> Tween` — create without auto-starting (for use in chains / timelines). - `tween.update(dt: number)` — advance all active tweens; call every frame. Global controls: - `tween.setGlobalSpeed(speed: number)` / `tween.getGlobalSpeed() -> number`. - `tween.cancelAll()`, `tween.cancelEntity(entityId)`. - `tween.pauseAll()`, `tween.resumeAll()`. - `tween.getActiveCount() -> number`. Timelines and easing: - `tween.timeline() -> Timeline` — keyframe-based orchestration. - `tween.easing` — the easing namespace (re-export of the easing module). - `tween.easings() -> { string }` — list of available easing names. Types: - `TweenOpts = { entityId?, property?, from?, to?, duration?, delay?, easing?, loops?, yoyo?, onStart?, onUpdate?, onComplete?, onCancel?, onLoop? }` ## Usage ```luau local tween = require("@builtin::modules.tween") -- Drive the tween system every frame from a component update: function update(dt) tween.update(dt) end -- Property tween on an entity: tween.to("box", "position", { x = 0, y = 5, z = 0 }, { duration = 1.0, easing = "easeOutBounce", onComplete = function() print("done") end, }) -- Raw value tween via callback: tween.fromTo(0, 1, 0.4, "easeInOutQuad"):andThen( tween.create({ from = 1, to = 0, duration = 0.4, easing = "easeOutCubic" }) ) -- Timeline sequencer: local tl = tween.timeline() tl:add(0.0, tween.create({ entityId = e, property = "position", to = pos, duration = 1 })) tl:add(0.5, tween.create({ entityId = e, property = "tint", to = color, duration = 0.3 })) tl:play() function update(dt) tween.update(dt) tl:update(dt) end ``` ## Notes - Properties supported by `tween.to`: `"position"`, `"rotation"`, `"scale"`, `"tint"`. See `core.module/init.luau` `ENTITY_PROPS` for the full accessor map. - Duration argument is permissive — pass a number (seconds) or a full `TweenOpts` table with `duration`, `easing`, `delay`, `loops`, `yoyo`, and the callback hooks. - Tween / Timeline objects are returned as the underlying class instances from the core / timeline submodules; the public `M.timeline()` surface exposes them as plain `TimelineObj` and `TweenObj` opaque handles to keep `--!global` registration unencumbered. - The submodules are also independently `require`-able for callers that need direct access to easing curves or timeline internals.
▲ 0↑ born
▣
module · born here
❒asset
# frameStream Module Public Luau surface over the `__framestream` Internal FFI namespace — an image the GPU drew carried out to another program, frame after frame. ## Purpose Wrap the raw `__framestream.*` FFI namespace in a typed Luau table that gets auto-injected as `_G.frameStream` via the prelude. A session binds one texture to one stream: make a render target the size you want to emit, point a Camera at it, open a stream to whoever is consuming the pixels, and attach the two. From then on the engine reads that texture back every time the session is due a frame, packs the pixels, and writes each frame to the stream. The image sits in GPU memory, where nothing outside the renderer can reach it; the session is what puts it on the wire. The readback runs through a ring of staging buffers allocated once and reused for the life of the session, so the frame the GPU is finishing is read back while the next one renders and nothing waits on the render thread. The write refuses rather than waits, so a consumer reading slower than the engine renders falls behind by frames — visible on `status().droppedBackpressure` — instead of holding the renderer back. ## Usage ```luau -- A 128x32 render target, and a Camera that draws into it. local rt = renderer.texture.create({ width = 128, height = 32 }) entity.spawn({ name = "panelCam", position = { 0, 0, 6 }, components = { Camera = { textureHandle = rt.guid, postProcessing = false }, }, }) -- Somewhere to send the pixels. local out = task.await(stream.open("tcp://127.0.0.1:9000")) -- Carry the camera's frames out at 30 fps. local session = frameStream.attach(rt.guid, out, { fps = 30 }) -- Later. local s = frameStream.status(session) print(s.frames, s.dropped, s.achievedFps) frameStream.detach(session) stream.close(out) ``` ## What reaches the stream One frame's pixels, then the next frame's, with nothing between them. A frame is `width * height * bytesPerPixel` bytes, rows top-first, and is written in a single call — so a consumer that has counted its bytes reads a whole frame or none of it, never half of one. `format` picks the layout: `"rgb24"` (3 bytes per pixel, the default) or `"rgba8"` (4) — a call naming any other format raises, naming both. `flipY = true` writes the last texture row first, for a consumer whose display is scanned bottom-up. A consumer at the far end of a byte stream that can drop out and be reopened usually wants more than the pixels — a magic to resynchronise on, a length, a sequence number. That belongs to the consumer's own protocol, so it is written by whoever speaks it rather than baked in here. ## Framing the bytes yourself To put a header of your own ahead of each frame, run the frames through a `loopback` stream and write them on from there. The session fills the loopback with whole frames; you drain one, prepend whatever your consumer expects, and write the result to the real stream: ```luau local FRAME = 128 * 32 * 3 local pixels = task.await(stream.open("loopback://panel", { inboundCapacity = FRAME * 4, outboundCapacity = FRAME * 4, })) local out = task.await(stream.open("tcp://127.0.0.1:9000")) local session = frameStream.attach(rt.guid, pixels, { fps = 30 }) local counter = 0 task.spawn(function() while true do local frame = stream.read(pixels, FRAME) if #frame == FRAME then stream.write(out, myHeader(counter) .. frame) counter += 1 end task.wait() end end) ``` That costs one in-memory copy per frame and keeps the protocol in content, where it can change without an engine build. Attaching straight to the destination stream skips the copy, and is the path to take when the consumer wants the pixels bare. ## Rate `fps` caps how often a frame is taken and is independent of the rate the engine renders at: a session at `fps = 30` in an engine running at 144 takes every fifth frame or so, and one at `fps = 30` in an engine running at 20 takes every frame it can. `status().achievedFps` is what it reached — compare it against `requestedFps` to tell a display that is running slow from one that is running as asked. ## Reading the status `frameStream.status()` separates the three ways a frame fails to arrive, because they call for different fixes: - **`droppedBackpressure`** — the consumer is reading slower than the session is producing. Either the consumer is too slow, or the stream's `outboundCapacity` is too small to absorb its jitter. - **`stalledReadbacks`** — the frame came due while every staging buffer still held a copy on its way from the GPU. The session is asking for frames faster than the GPU is handing them back. - **`lastOutcome == "tooLarge"`** — a frame is bigger than the stream's whole outbound capacity, so every frame of that size will be refused too. Open the stream with an `outboundCapacity` that holds one. ## Which frame a stream holds A frame is copied when it comes due and reaches the stream once its copy has come back from the GPU, so how many frames a stream holds at a given moment depends on how fast those copies come back. Two sessions on one texture can therefore hold different numbers of frames when they are read, the newest frame of one showing a later image than the newest of the other. `status().lastCopyFrame` names the engine frame, in `time.frameCount()` numbering, that the copy of the newest accepted frame was taken on. Every copy one engine frame takes of a texture holds the same image, so read it in the same step as draining the stream, and two sessions whose readings agree hold the same image as their last frame. Two different readings can still name one image: an engine frame on which nothing drew into the texture copies the image it already held. ## Platforms Frame egress runs on native and wasm alike: the readback is submitted and polled without blocking on either, and the packing is plain byte work. What differs between them is which transports a stream can be opened over — `stream.transports()` is the probe for that, and `tcp` names its wasm absence there. ## Exports - `frameStream.attach(texture, stream, opts?) -> string?, string?` — session handle, or nil + reason. - `frameStream.detach(handle) -> boolean` - `frameStream.status(handle) -> FrameStreamStatus?` — `nil` when handle names no live session. - `frameStream.list() -> { string }`
▲ 0↑ born
▣
module · born here
❒asset
# modules.api.engine.dirty_hot_reload Picks up dirty-overlay changes from disk while a scene is loaded in edit mode, applies them to the live entities, and keeps the in-memory representation in lockstep with `scene_dirty/` files. Pairs with `scene_saver` (writes deltas) + `scene_loader` (composes canonical + overlay at load time). ## Parents A record's `parent` names the entity it stands under: the entity whose id is that text, else the entity whose display name is exactly that text. A `*` or `?` in it is a character of the name, never a pattern. A record whose parent no entity provides waits. An entity that does not stand yet is not built, one that already stands keeps its state, the layer logs the record once, and `debugState(guid).waiting` maps its id to the parent it names. While a record waits, the scene saver leaves it as it stands rather than writing a standing entity's older state over it, until an author edits that entity, which the saver then records as it stands. When a record builds an entity, the records waiting on that entity's id or display name are built under it, so a child record that arrives before its parent's record lands where a load of both places it. Each waiting record is built as its overlay file states it at that moment, so a record rewritten while it waited, including by this session's own saver, builds from its current bytes. A load gives the same account of a record whose parent stays missing: the loader abandons it with `parentMissing`.
▲ 0↑ born
▣
module · born here
❒asset
# video Module Public Luau surface over the `__video` Internal FFI namespace — video playback via render-target-backed players. ## Purpose Wrap the raw `__video.*` FFI namespace (backed by `zero_video`) in a typed Luau table that gets auto-injected as `_G.video` via the prelude. Players are backed by standard render targets — the handle returned by `video.create` is an `"rt:video_N"` string usable anywhere a render target works (materials, UI, post-process inputs). ## Usage ```luau -- Stream a video onto a material local rt = video.create("http://example.com/clip.mp4", { loop = true, autoplay = true, rate = 1.0, }) material.setTexture(myMat, "baseColor", rt) -- Transport video.pause(rt); video.seek(rt, 30.5); video.play(rt) -- Inspect local info = video.getInfo(rt) print(info.width, info.height, info.currentTime, "/", info.duration) -- Ask whether this host decodes the stream before waiting on a frame. -- `codecSupported` is nil until the answer arrives, false where the host has -- no decoder for `info.codec`, and then `info.state` is `"unsupported"`. if info.codecSupported == false then print(info.error) end -- Frames handed to the texture against frames the playback clock reached, -- and how many samples decoded on the frame thread (a native stream decodes -- on a thread of its own; a browser stream in the browser's decoder). print(info.framesPresented, "/", info.framesExpected, info.frameThreadDecodes) video.destroy(rt) ``` ## Exports - `video.create(url, options?) -> rtHandle` - `video.play(handle)` / `video.pause(handle)` / `video.stop(handle)` - `video.seek(handle, time)` - `video.setRate(handle, rate)` / `video.setLoop(handle, loop)` - `video.getInfo(handle) -> VideoInfo?` - `video.destroy(handle)`
▲ 0↑ born
▣
module · born here
❒asset
# yaml.module YAML decode + encode for authored engine content. Pure Luau, no engine dependencies — a general parsing library, not data-system plumbing. ```luau local Yaml = require("@builtin::modules.yaml") local doc = Yaml.decode(vfs.read(path)) vfs.write(path, Yaml.encode(doc)) ``` ## Supported subset - Block mappings (`key: value`) and block sequences (`- item`), nested to any depth. - Compact sequence-of-mapping entries (`- path: x` / ` kind: y`). - Flow collections — `{ a: 1, b: 2 }` and `[1, 2, 3]` — single line. - Plain scalars with type inference: `~` / `null` → nil, `true` / `false` → boolean, numeric literals → number, everything else → string. - Single- and double-quoted strings (`''` escapes a literal quote inside a single-quoted string; `\n` `\t` `\r` `\\` `\"` inside a double-quoted one). A quote opens a quoted scalar where a scalar begins — after `key:`, after a `- ` entry marker, or inside a flow collection — so a plain scalar carries an apostrophe as data (`note: the world's rules`). Quoted keys are read in flow mappings. - `#` comments — honored outside quotes, so a `#` inside a quoted string is kept as data. - Literal (`|`) and folded (`>`) block scalars — the bare indicators only. A block scalar's content is literal text: quotes (balanced or not), `#`, `key: value` and `---` inside it are data. - An optional leading `---` document marker. ## Loud-error contract Constructs outside the subset above raise instead of misparsing: anchors/aliases (`&`, `*`), tags (`!`), directives (`%`), multi-document streams (a second `---`), block-scalar chomping/indentation indicators (`|-`, `|+`, `>-`, `|2`, ...), tab indentation, and inconsistent indentation (both block structure and block-scalar bodies). Every error carries the offending line number (`yaml: line N: ...`), so a bad config points straight at the line to fix. Tab indentation is rejected everywhere — including block-scalar body lines whose leading whitespace contains a tab. A tab after the first non-space character of a line is content and passes through. ## API - `Yaml.decode(text: string): any` — decode a YAML document into a Luau value (table, scalar, or nil for an empty document). Raises on malformed input or an unsupported construct. - `Yaml.decodeWithComments(text: string): (any, boolean)` — decode as `decode` does, and report whether the text carries a comment by the decoder's own rule: a `#` at a line's start or after whitespace, outside a quoted scalar and outside a block scalar. Re-encoding the value drops such comments, so an editor that writes a document back reads this first. - `Yaml.encode(value: table): string` — encode a Luau table as YAML (block style, two-space indent, alphabetically sorted keys). Raises on values YAML can't represent (functions, userdata, non-string mapping keys). A top-level empty table encodes to an empty document, which decodes back to `nil` (the YAML empty-document ambiguity).
▲ 0↑ born
·
computeshader · born here
❒asset
# concat Joins one attribute of several geometries into one column. `sources` is a column table with one column per geometry, in order; `desc` says where each geometry's elements start in the result, how many it has, how far to shift each word of a linkage column, and whether the geometry holds the attribute at all. Where it does not, its elements take the default stated in the first four words of `desc`. Linkage is a shift: `vertex.pt` takes the number of points before the geometry, `primitive.vstart` the number of vertices before it. Any other attribute shifts by 0. A node joins a whole geometry by running the kernel once per attribute of the union of the geometries' attributes. Dispatch over `n`, the elements of the joined domain. Exact: each result element has one writer.
▲ 0↑ born
▣
module · born here
❒asset
# debug_frustum The "frustum" provider for `@builtin::modules.debugViz`: appends one cyan wireframe frustum per scene Camera into the shared debug-visualization batch, every edit-mode tick. ```lua debugViz.registerProvider("frustum", require("@builtin::modules.debug_frustum").tick) ``` `DebugViz.component` registers this provider automatically — attach it to one entity to draw every enabled camera's view frustum alongside whatever other providers are registered. ## How it works `tick(emit)` queries every entity carrying a Camera and, for each enabled one, builds its 8 near/far corners in the camera's local space: - half-height at a given distance is `tan(fov / 2) * distance`; half-width is `half-height * aspect`. - the camera looks down its local **-Z** axis (`entity(id).transform.forward` is documented as "local -Z axis in world space"), so the near/far planes sit at local `z = -near` / `z = -far`. - each corner is `position + forward * dist + right * (±halfWidth) + up * (±halfHeight)`, using the entity's world position and world-space forward/right/up axes — no separate quaternion math needed. `Camera` has no `aspect` field, so the aspect ratio is read live from `getViewportSize()` (the window's real aspect, matching what the viewport camera sees), falling back to a 16:9 default if the viewport size isn't readable. The 12 edges (4 near, 4 far, 4 connecting) are appended via `emit.line`. ## API - `debug_frustum.tick(emit)` — append one wireframe frustum per enabled scene camera into the shared batch.
▲ 0↑ born
❒
package · born here
❒asset
# 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 number of lights has to be capped. Resampled importance sampling breaks that link: each pixel draws a few candidates, keeps ONE of them with a probability proportional to what it would contribute, and carries a weight that makes the survivor stand for the whole set. What it keeps is then reused — by its neighbours this frame, and by itself on the next one — so the set a pixel effectively samples keeps growing while the number of candidates it draws stays where you put it. A cave of glowing crystals, a street of practicals, a control room of indicator lamps: hundreds of sources, and each pixel looks at about a dozen. ## Using it Put an `EmissiveSource` on the geometry that glows: ```lua local id = entity.spawn("crystal", { position = { 2, 1, 0 } }).id entity(id).component.add("EmissiveSource", { color = { 0.3, 0.8, 1.0 }, intensity = 8.0, radius = 6.0, size = 0.4, }) ``` Or register sources from a script, which is how a generated scene does it: ```lua local restir = require("@builtin::systems.restirLighting.restirLighting") for i = 1, 400 do restir.set(`lamp{i}`, { position = { math.random() * 60 - 30, 1.5, math.random() * 60 - 30 }, color = { math.random(), math.random(), math.random() }, intensity = 4.0, radius = 5.0, }) end ``` The pass starts with the first source and stops with the last. Settings are on `restir.configure`, or on a `ResampledLighting` component for a scene to author: ```lua restir.configure({ candidates = 16, spatialSamples = 6 }) ``` ## What a pixel pays `candidates + spatialSamples + 1` evaluations, reported by `restir.perPixelCost()`. None of those terms is the source count. The estimate is formed on a grid of one cell per 2x2 pixels (`resolution = "full"` gives every pixel its own cell), across four passes: | Pass | Runs over | Does | |---|---|---| | `restir_sample` | grid cells | draws `candidates` sources at random, resamples to one, merges the cell's own history | | `restir_spatial` | grid cells | borrows from `spatialSamples` neighbours, each evaluated against this cell's surface | | `restir_resolve` | pixels | shades the chosen source with the pixel's own normal, roughness and albedo | | `restir_denoise` | pixels | averages that light over the cells around the pixel and composites it | Which source to look at is a low-frequency decision a cell of pixels can share. What the surface does with it is not, and is computed per pixel. One source per cell is correct on average and different from its neighbour, which is the grain a single sample carries. `restir_denoise` averages across the cells around a pixel, weighing each by how nearly its surface is this one, so the estimate keeps its value and loses its variance. `mode = "reference"` replaces the reservoir lookup with a loop over every registered source at every pixel. That is the picture the resampled mode is estimating and the cost it is measured against — the same shading, the same filter and the same composite, so the two are comparable directly. ## Temporal reuse A cell keeps its reservoir across frames and merges it with the fresh candidates, so a good find keeps paying after the frame that made it. The stored world position is what says the cell is still looking at the same surface; a camera move or an occluder passing in front fails that test and the history is dropped rather than smeared. `historyCap` bounds how many candidates the history is allowed to stand for. Without it a converged cell would outvote every new candidate forever and a lighting change would never work its way out. At the default of 20 a change is absorbed over roughly twenty frames. Removing a source, or turning its `intensity` down to 0, releases it; the pass stops with the last one. ## Where it sits Sources registered here are additional to the lights the engine holds rows for. The passes run in `afterLighting` at orders 80-84 and add to the scene as it was drawn, reading position, normal, albedo and metallic-roughness back out of the G-buffer. A source lights every surface within `radius` that faces it, along the straight line between them. ## One frame, several targets A render feature's `render` runs once a frame, and the passes it enqueues run once per render target — the window, an offscreen capture, a camera rendering to a texture. Each pass therefore reads the frame size out of the textures it is handed and works out its own cell grid, so every target resamples at its own resolution. A reservoir carries the grid width it was written under, which is what stops one target's history being read as another's. The reservoir buffers are sized to the widest grid any target has asked for. A target wider than they have yet been sized for reports the grid it needed, and the frame after that covers it. ## Reading what it did `restir.stats()` returns the counters the passes keep on the GPU for the most recent frame a read-back has landed for, carrying that frame's number so a repeated read is distinguishable from a fresh one. `meanWeight` is counted in sixteenths with each cell capped at 16 so the total fits a 32-bit counter; `peakWeight` alongside it is exact to a thousandth, and is where an estimator that has come loose shows first.
▲ 0↑ born
·
toolbox · born here
❒asset
# MaterialAuthor Stand up new materials and textures fast — the go-to surface for making materials at design time, each in one call instead of a hand-rolled asset-create-and-wire dance. Every tool writes a source material asset and hands back a live `AssetRef<material>` you can apply straight to a Model. Wrap a texture (a proxy, a `.texture` asset, or an image path) into a new material with its PBR slot inferred from the texture name; make a flat-colour material from an RGB(A) value; clone an existing material under a new name with property or texture overrides; or scan a folder of textures — loose images and `.texture` assets — inferring each one's slot from its name and composing them into a single material. It pairs with the SVG → texture importer: drop a `.svg`, get a `.texture`, wrap it. **This is authoring, not runtime.** Every tool creates source assets, which is a design-time action — never call it from gameplay code (components, controllers, per-frame updates, spawn logic). At runtime you consume an already-authored material by name or ref through the lower-level `Material` surface (create / apply / get / set), which is the gameplay-safe layer this one is built on. Reach here to *make* materials; reach for `Material` to *apply and tweak* them. ## Material properties Beyond the keys a tool reads itself (`name`, `shader`, `overwrite`, and `slot` on `fromTexture`), every option names a property of the shader the material is authored against — so anything `pbr` declares is reachable in one call: `roughness`, `metallic`, `clearcoat`, `transmission`, `anisotropy`, `ior`, `uv_scale`, and the texture slots. A custom shader's own properties work the same way, since the vocabulary is read from the shader. Spell a property in `snake_case` or `camelCase` (`uv_scale` or `uvScale`); `tiling` names `uv_scale` and `color` names `base_color`. A single number fills a vector property's components, so `tiling = 300` tiles 300×300. An option the shader doesn't declare is refused, naming the closest accepted spelling — the material is not written. ```lua tools.use("MaterialAuthor", "fromTexture", "cliff_diff.texture", { name = "Cliff", tiling = 300, roughness = 0.9 }) ``` ## Slot inference When no slot is given, the texture stem picks the PBR slot: `albedo` / `basecolor` / `base_color` / `diffuse` / `col` → base colour; `normal` / `nrm` → normal; `roughness` / `metallic` / `metalness` / `metallic_roughness` / `orm` → roughness-metallic; `emissive` / `emission` / `emit` → emissive; `ao` / `occlusion` → occlusion. Pass an explicit slot to override.
▲ 0↑ born
·
computeshader · born here
❒asset
# grid Reads and writes a field of cells in one kernel. The field is a Grid (one tile, no apron, x fastest, `lanes` values per cell) or a Raster (tiles with aprons, one batched dispatch) through the layout in `tile_layout`. - sample (`MODE sample`): the field at positions, nearest or trilinear, held to the edge cell past the field. Reads each cell from the tile that owns it. - write (`MODE write`): a patch of values blended into the field (replace, add, max, min, multiply, optionally weighted per cell). Every tile that holds a cell as core or apron receives it, so apron copies agree. The patch window is stated in field cells and may reach past the field (negative origin, or a size beyond the cell counts): a stored cell a border tile holds past the field takes the patch where the window covers it. - stencil (`MODE stencil`): per stored cell, a neighbour at an offset, slope in degrees, curvature (the laplacian over x and z) or the gradient, with neighbour reads held to the tile's own stored edge. Slope and curvature are the terrain mask's. `MODE`, `SAMPLING` (nearest, trilinear), `STENCIL` (offset, slope, curvature, gradient) and `BLEND` (replace, add, max, min, multiply) are specializations. `determinism: exact`: each output element has one writer. The bindings a mode does not use take any buffer that is not bound elsewhere in the dispatch.
▲ 0↑ born
❒
bundle · born here
❒asset
▲ 0↑ born
▣
module · born here
❒asset
# material_records The material GPU records this session has released and nothing has filed again. A `.material` asset files its GPU record at its point of use — `matRef:handle()` — and memoises the handle it filed on the ref's runtime, so a second bind of the same material costs a table read where the first cost a parse and an upload. That memo names a record the renderer's registry holds, and `renderer.material.destroy` takes the record away while the asset and its memo stand. The asset is what files the next record, so it has to know which names that applies to. This set is that knowledge: a release puts the key in, filing a record under the key takes it out, and the point of use reads it for the cost of one table index. Written by `renderer.material.destroy` and `renderer.material.create`; read by the `.material` assetType's `handle()`. ## API ```luau local MaterialRecords = require("modules.material_records") -- A point of use holding a memo asks whether the record it names still stands. if MaterialRecords.released[identity] ~= nil then -- The record is gone: file another one, which takes the mark off. end MaterialRecords.noteReleased(key) -- the record under `key` has been released MaterialRecords.noteFiled(key) -- a record stands under `key` again ``` ## Lifetime The set lives in the session store, so it holds for as long as the GPU records it answers for: a VM reload leaves those records standing and leaves this standing with them, and both die with the engine process.
▲ 0↑ born
✦
shader · born here
❒asset
▲ 0↑ born
▣
module · born here
❒asset
# enum_values (module) The `enum` field-constraint validator. Registers itself with `field_constraints.module`'s registry on load so any component field carrying `constraint = { kind = "enum", values = { ... } }` — which is what `Field.enum` emits — is checked against it. ## What it does Given a field's written value and its constraint: 1. Passes `nil` — an enum field may be left unset. 2. Rejects a constraint carrying no members, since there is nothing to check against. 3. Rejects a non-string value, naming the members and the type it got. 4. Answers whether the value is one of `constraint.values`. Every rejection lists the whole member set, so the message tells the caller what they may write instead of only what they may not. ## Where the members come from `Field.enum(values, default, mode)` validates the member list at construction: the members must be non-empty, distinct strings, and a non-nil default must be one of them. A field whose members are wrong is therefore a load-time error rather than a runtime surprise, and this validator only ever sees a well-formed set. The engine carries `constraint` verbatim and never reads inside it, so the member list lives entirely in Luau. ## Exports - `M.check(value, constraint) -> ok, reason?` — the validator function, also registered under the `enum` kind.
▲ 0↑ born
·
computeshader · born here
❒asset
# topology Mesh edit operations over a geometry's linkage (`vertex.pt`, `primitive.vstart`, `primitive.vcount`), selected by the `OP` specialization (`split_edge`, `collapse_edge`, `insert_face`, `delete_face`, `split_vertex`), each stage by `STAGE` (`count`, `write`): split an edge (insert a point), collapse an edge (merge two points and drop the faces that degenerate), insert a face, delete a face, split a vertex (give a group of corners a point of their own). Each operation runs in two stages with an exclusive prefix sum between them. Stage 0 writes `cnt`, how many output vertices each primitive contributes (each vertex, for split vertex). The prefix sum of `cnt` is `off`. Stage 1 writes the output vertices' points (`o_pt`) and where each came from (`o_src`). The output primitives are `off` as run starts and `cnt` as run lengths; a primitive with an empty run is gone, and the select node removes it. The shader's header states the records each operation reads. The operation's records come from the edge table: for split edge, the point for each split edge gathered to the vertex slots through `edges`' side slots (both faces of an edge name the same point); for collapse, the point each point becomes, from an independent set of edges. The point an inserted vertex or a split vertex stands for is carried by the output maps: `o_src` names the vertex whose attributes an output vertex takes (negative when it is new, with ~v the vertex a split inserted after), and the compaction of the records names each new point's source. Integers are 32-bit words, exact.
▲ 0↑ born
✦
shader · born here
❒asset
# grass_material.shader Grass, shaded root to tip with per-blade colour variation and a soft half-lambert wrap. The colouring reads `uv.y` as height along a blade — 0 at the root, 1 at the tip — and darkens the roots while warming the tips. Per-blade variation comes from a hash of the fragment's world xz on a 10 cm grid, so every fragment of one blade shares a seed and neighbouring blades drift apart in hue and brightness. The lighting is a diffuse half-lambert wrap kept below saturation, so this shader returns its own final colour instead of a PBR surface. `sway` is how far the vertex stage bends each vertex along its blade, and it is what tells this shader that its mesh is a blade mesh: the bend scales with `uv.y²`, so a vertex at the root holds still while the tip swings hardest. It defaults to 0, at which every vertex arrives where the mesh put it — a material that leaves it there shades a surface without moving it. `@builtin::materials.grass` is that ground surface; `@builtin::materials.grass_blades` sets `sway` to 1 for a blade mesh. ## Properties - `base_color` — the grass green the root→tip gradient is built from. - `sway` (`0`..`4`, default `0`) — how far each vertex bends along its blade.
▲ 0↑ born
·
toolbox · born here
❒asset
# api Discover the engine's own documentation when you don't yet know the path — browse the docs tree and search it by keyword. Browse the documentation under `/zero/docs`: with no argument it lists the top-level categories (api, tools, guides, components), a partial path lists a directory's entries, and a full path returns the document. Searching matches a keyword against entry names across the whole docs tree. When you already know the path, reading it directly is quicker; these are for finding your way when you don't.
▲ 0↑ born
✦
shader · born here
❒asset
▲ 0↑ born
▣
module · born here
❒asset
# asset_tag (module) The `assetTag` field-constraint validator. Registers itself with `field_constraints.module`'s registry on load so any component field carrying `constraint = { kind = "assetTag", tag = ... }` — which is exactly what `Field.taggedRef` emits — is checked against it. ## What it does Given a field's written value and its constraint: 1. Passes `nil` — an unset slot holds nothing to check. 2. Reads the value's identity — a bare string, or a table's `__ref` / `guid` / `identity` / `name`. 3. Resolves it as an asset of any type (`asset.resolve(identity)`), since the tag is the whole gate. 4. Answers `asset.has_tag(identity, constraint.tag)`. Acceptance rides on the tag rather than the asset's type, so an asset becomes assignable to a tagged slot the moment it is tagged — no edit to the field or to anything that reads it. A rejection names the tag the slot wants, the tags the asset carries instead, and the `asset.list({ fields = { tags = ... } })` call that enumerates what would fit, so the message carries its own next step. ## Exports - `M.check(value, constraint) -> ok, reason?` — the validator function, also registered under the `assetTag` kind.
▲ 0↑ born
▣
module · born here
❒asset
# world_sync Grafts sync-durability status and per-item conflict resolution onto the `world` namespace. In a live world every `/source` write is synced to the backend; a write that has not yet reached the backend is a *pending* write, and a write that collides with a change another collaborator made since you last saw the path is a *conflict*. This module surfaces both and lets you resolve conflicts one path at a time. ## `world.syncStatus()` Returns a snapshot of the world's sync state: ```lua local s = world.syncStatus() -- s.subscribed : bool: attached to a live world -- s.content_synced : bool: incoming: the world's content is applied to this client -- s.outgoing_synced : bool: outgoing: every /source change this session made reached the world -- s.uploads_outstanding : number: upload jobs queued, in flight, or held for another attempt -- s.progress : { applied: number, expected: number? } -- s.pending_writes : number: local /source writes and removals not yet round-tripped whose durable record stored -- s.unrecorded_writes : { string }: owed /source paths whose durable record did not store, which a restart loses -- s.owed_paths : { string }: /source paths written or removed that the manifest does not reflect yet (the list vfs.pendingWrites() returns) -- s.unsaved_writes : { string }: /source paths whose write or removal the server never took -- s.held_uploads : { { path, reason } }: owed changes whose upload failed and is being tried again -- s.uploads_abandoned : number — uploads the queue stopped carrying -- s.conflicts : { { path, base_sha, local_sha, backend_sha, isBinary } } ``` Content sync runs in two directions, and the status answers each apart. `content_synced` is the incoming direction: the world's content has been applied to this client. A survey uses it to tell "still syncing" from "empty world": a freshly attached session reads `content_synced = false` until the backend content has landed. It reads nothing about this session's own changes reaching the world. `outgoing_synced` is the outgoing direction: every `/source` change this session made has reached the world. It reads `false` while any write or removal is queued, uploading, held for another attempt, not yet reflected by the manifest, kept in a durable record (`pending_writes`, `unrecorded_writes`, a conflict among them), or refused by the server, and `uploads_outstanding` counts the upload jobs it is still waiting on, each once. It is the field that answers "is my work saved?". Both booleans read `false` while no world is subscribed. `pending_writes` moves as the queue drains, so it reads the same for a queue working through a backlog and for one that has given content up. `unsaved_writes` and `uploads_abandoned` separate them: the first names the `/source` paths whose bytes are on this client only, the second counts the uploads the queue stopped carrying after they failed on it repeatedly while the connection reported itself live. A path listed there stays on this client until another session's reconcile carries its durable record. `held_uploads` names the owed changes whose last upload attempt failed and went back to the queue, each with the reason that attempt gave: a blob transfer that ran past its deadline, a refused connection. The queue tries each again on its own schedule until it lands or is given up on. A held upload holds nothing else up: a stage or commit of other paths goes ahead, and a bulk stage passes over it. A call that names the held path waits on it and, when it does not land, reports the path and the reason. Removing the path ends the hold, since the removal is what the path owes from then on. `unrecorded_writes` names the owed `/source` paths whose durable record this machine could not store, because the pending-write store or the local blob store could not be written. This session still carries them to the world; a restart before they arrive loses them. `pending_writes` counts the owed records that did store, so it leaves these out. ## `world.resolveConflict(path, mode, content?)` Resolves one conflicted path. `mode` is one of: - `"merge"` — for text, runs a 3-way merge of the common ancestor, your bytes, and the current backend bytes, and returns `{ merged, clean }`. A clean merge produces ready-to-apply text; a conflicted merge returns the text with `<<<<<<<` / `=======` / `>>>>>>>` markers to resolve. Merge is unavailable for binary content or a path with no common ancestor. - `"apply"` — writes `content` (the finalized bytes you pass) to the path and clears the conflict. - `"take-local"` — keeps your bytes. - `"take-backend"` — keeps the backend's bytes. - `"discard"` — drops your pending bytes, keeping the backend's. Retained conflict bytes are also readable under `/zero/runtime/sync/conflicts/`, so you can inspect a conflict before choosing how to resolve it.
▲ 0↑ born
▣
module · born here
❒asset
# layers Top-level scene-management global + Scene proxy. § 17 step 9 of `docs/plans/2026-05-01-player-camera-unification.md`. Renamed from the legacy `scene.*` global to match `/runtime/layers/` and to give scene-management state (active root, additives, callback registries) a single anchor distinct from per-scene authoring. ## Global surface | Symbol | Type | Notes | |---|---|---| | `layers.active` | `Scene \| nil` | Current root non-additive scene as a proxy. Live read; updates on every load/unload. | | `layers.list()` | `{ Scene }` | Every loaded scene (root + additives) as proxies. | | `layers.find(ref)` | `Scene \| nil` | Lookup by `AssetRef<scene>` or layer-id string. | | `layers.is_loaded(ref)` | boolean | | | `layers.load(ref, opts?)` | — | Loads ref. `opts.additive = true` layers on top; default = non-additive (replaces root). | | `layers.unload(ref?)` | — | Unloads ref; nil = active root. | | `layers.reload(ref?)` | — | Equivalent to unload + load with same ref. | | `layers.onLoad(cb) -> handle` | function | Subscribe to any scene-load event. Returns handle for `offLoad`. | | `layers.offLoad(handle)` | function | | | `layers.onUnload(cb) -> handle` / `offUnload(h)` | function | Same shape for unload events. | | `layers.observe()` | `SceneObservation` | What every load did and what each loaded scene costs, in one read. | | `layers.lastLoad()` | `SceneLoadReport \| nil` | The last load's report; nil when nothing has loaded. | | `layers.lastUnload()` | `SceneUnloadReport \| nil` | The last unload's, under the name the layer was loaded with. | | `layers.loadHistory()` | `{ SceneLoadReport }` | The completed load reports still held, oldest first. | | `layers.problems(ref?)` | `{ SceneLoadFailure }` | What a layer failed to produce, and why. | | `layers.whyPartial(ref?)` | `(reason?, detail?)` | The nearest cause a layer is not whole, from a closed set. | | `layers.inventory()` | `{ SceneLayerInventory }` | What the engine attributes to each loaded layer. | | `layers.cost()` | `{ SceneLayerCost }` | Each layer's per-frame entrypoint tick, summed across the window. | | `layers.resetCostWindow()` | — | Open a new cost window. | ## Scene proxy surface A `Scene` proxy is a sealed table with fields and `:method` callables. | Field / method | Notes | |---|---| | `s.asset` | `AssetRef<scene>` | | `s.name`, `s.path` | informational (logging, debug) | | `s.guid` | the scene asset's guid — the layer's identity | | `s.sceneIdentity` | the identity of the scene asset the layer was loaded from (e.g. `@builtin::scenes.test_arena`), whichever VM loaded it | | `s.additive` | boolean — additive overlay vs root scene | | `s.parent`, `s.children` | additive ownership chain | | `s:root()` | walks `.parent` to the non-additive root; returns self for roots | | `s.players`, `s.camera` | populated by §17 step 10 (player + camera spawners) | | `s.entrypoint` | `AssetRef<script> \| nil` | | `s.visible` | boolean — read via field, toggle via `s:set_visible(b)` | | `s.state` | `"loading"` / `"loaded"` / `"ready"` / `"partial"` / `"unloading"` — the layer's lifecycle state. Both `"ready"` and `"partial"` are terminal: the load has settled. | | `s.ready`, `s.loaded` | booleans in lock-step with `state`. `ready` is true once the layer has settled, in either terminal state — it is the flag to wait on. | | `s.ok` | boolean — whether everything the scene declared was produced and the layer has run clean since. False whenever `state` is `"partial"`, including when the tick starts raising long after the load settled. | | `s.failures` | `{ SceneLoadFailure }` — every distinct failure the load and the layer's per-frame tick produced, each with the `count` of how often it repeated. | | `s:unload()` | unload this scene; additives use `layers.unload(layerId)`, roots use `layers.active:reload()` | | `s:reload()` | unload + reload same ref | | `s:set_visible(b)` | show/hide without unloading | | `s:load_additive(ref)` | load a sibling additive overlay; returns the new proxy | ## Example ```lua -- Switch root scene layers.load(asset.ref("@builtin::scenes.baseline_visual")) -- Subscribe to all loads local h = layers.onLoad(function(scene) print("loaded:", scene.name) end) -- Layer an HUD overlay on top layers.load(asset.ref("@builtin::scenes.hud"), { additive = true }) -- Iterate every loaded scene for _, scn in ipairs(layers.list()) do print(scn.name, scn.additive and "(additive)" or "(root)") end layers.unload() -- unload the active root layers.offLoad(h) ``` ## What a load did, and what a scene costs Every load writes one record at its own completion point; the readings below compose that record, so a caller that did not make the load can still ask what it did. A load that produced failures settles into `"partial"` rather than `"ready"`, and names the nearest cause from a closed set: `loaderRaised`, `entrypointCompileFailed`, `entrypointBodyRaised`, `entrypointRaised`, `buildRaised`, `entityFailed`, `parentMissing`, `parentRefused`, `parentAbandoned`, `componentUnresolved`, `componentRefused`, `subscriberRaised`, `updateRaised`. ```lua layers.load(asset.ref("scenes.arena", "scene")) -- Did it come up whole? if not layers.active.ok then local reason, detail = layers.whyPartial() print(reason, detail) for _, f in layers.problems() do -- `count` rises when the same failure happens again, so a per-frame -- tick that keeps raising stays one line. print(f.phase, f.reason, f.entity, f.component, f.hook, f.count) end end -- What did the swap do? local r = layers.lastLoad() print(r.name, r.outcome, r.durationMs) print("replaced", r.replaced and r.replaced.name) -- the root that went print("cascaded", #r.cascaded) -- the overlays with it print(r.entities.added, "arrived,", r.entities.removed, "left") print(r.phases.teardown, r.phases.instantiate, r.phases.settle) -- Which loaded scene is expensive? `totalMs` is a SUM across the window, so -- reset it first and read `avgMs` for the per-tick figure. layers.resetCostWindow() task.wait(1) for _, c in layers.cost() do print(c.name, c.avgMs, "ms/tick over", c.calls, "ticks") end -- What does each layer hold, and what does no layer claim? for _, l in layers.inventory() do print(l.name, l.entities, l.ok) end print(layers.observe().totals.unattributed) ``` The `scene` toolbox carries the same two readings as `scene.observe` and `scene.whyPartial`. ## Authoring surface `layers.*` is the sole public scene-management surface. It accepts AssetRefs (not paths), drives a callback registry, and constructs Scene proxies that carry players / camera / hooks. The previous `scene.*` global has been removed — its low-level FFI bindings now live behind `layers.*` and the Scene proxy methods.
▲ 0↑ born
▣
module · born here
❒asset
# 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.
▲ 0↑ born
▣
module · born here
❒asset
# preset A preset holds typed values for one declarer's settings schema: an assetType's, an importer's, a component's, or any asset that declares its own settings. `preset.capture` saves the settings something has now, `preset.apply` writes a preset's values into something its target governs, `preset.values` reads them checked against the target's current schema, and `preset.setValues` replaces them, checked first. Exposed as the `preset` global via `--!global preset`. A preset's `preset.yaml` names its target as a guid envelope, `target: { __ref: <guid>, type: <type>, name: <name> }`, which the preset's `.refs` pins by guid with the target's checksum. A target written as an identity string is accepted on read, resolves by identity, reads as unpinned, and is rewritten as the envelope on the preset's next local edit. ## Exports - `preset.values(source: AssetRef<preset>) -> ({ [string]: any }, Report)` — the values that pass the target's current schema, and a report naming every one that does not. - `preset.apply(source: AssetRef<preset>, into: AssetRef | ComponentTarget) -> ApplyResult` — write the valid values into an asset the target governs, or into a component on an entity whose component is the target. Struct groups merge field by field. Raises when another declarer governs `into`. - `preset.capture(from: AssetRef | ComponentTarget, name: string, opts?: CaptureOpts) -> AssetRef<preset>` — save an asset's settings, or a component's live values on an entity, as a new preset targeting the declarer that governs them. A bare name lands in `/source/presets/`; an absolute `.preset` path lands where it says. - `preset.setValues(source: AssetRef<preset>, values: { [string]: any })` — replace the whole values table; any violation refuses the write, naming every one. - `preset.defaults() -> { DefaultRow }` — the world's Preset Manager rows, in order, each resolved with its problems. - `preset.setDefaults(rows: { DefaultRowInput })` — replace every row; every row is checked first and any problem refuses the write, naming every one. - `preset.addDefault(source: AssetRef<preset>, opts?: { filter: string?, at: number? }) -> number` — add a row for the declarer the preset targets; returns its index. - `preset.removeDefault(index: number)` — remove one row, keeping every other row as written. - `preset.defaultFor(declarer: AssetRef | string, path: string) -> ({ [string]: any }?, Match?)` — the values a new thing at `path` would take from the rows, and the row that gives them. Types: - `ComponentTarget = { entity: string, component: string }` — a component on an entity. - `Report = { format: string, target: string, targetGuid: string?, pinned: boolean, targetChanged: boolean, rejected: { Violation } }` — `target` is the target's identity; `pinned` is true when `.refs` pins the target's guid; `targetChanged` is true while the pinned checksum differs from the target's current one. - `Violation = { path: string, message: string }` - `ApplyResult = { applied: { string }, rejected: { Violation }, targetChanged: boolean }` - `CaptureOpts = { overwrite: boolean? }` — `overwrite = true` re-authors an existing preset, keeping its guid. - `DefaultRowInput = { preset: any, target: any?, filter: string? }` — a preset, its declarer (the preset's own target when omitted) and a filter (empty matches everything). - `DefaultRow = { index, target, preset, filter, targetGuid, presetGuid, targetName, presetName, problems }` - `Match = { index, target, preset, path, rejected, problem? }` — the row a creation took and the path it read. ## Usage ```luau -- Capture a texture's settings, then apply them to another texture local p = preset.capture("hero_sprite", "pixel_art") preset.apply(p, "enemy_sprite") -- Apply a component preset to a component on an entity preset.apply("synty", { entity = id, component = "Locomotion" }) -- Capture a component's live values local tint = preset.capture({ entity = id, component = "Model" }, "red_tint") -- Read the values and what the target rejects local values, report = preset.values("pixel_art") for _, v in ipairs(report.rejected) do print(v.path, v.message) end -- Replace the values preset.setValues("pixel_art", { filter = "nearest", generateMipmaps = false }) ``` ## Notes - Every read re-checks the values against the target's current schema, so a value a later schema drops or retypes is reported by path rather than applied. - `asset.create("preset", name, { target = <identity or guid>, values = { ... } })` authors a preset directly, with the same checks as `preset.capture`. - The Preset Manager rows are documented in `preset.defaults`; the filter grammar in `preset.glob`. - Submodules: `preset.document` (the `preset.yaml` format), `preset.target` (target resolution and checking), `preset.heal` (rewriting an older preset document in the current form on a local edit).
▲ 0↑ born
▣
module · born here
❒asset
# p5_menu A UI template screen.
▲ 0↑ born
▣
module · born here
❒asset
# outline The public outline API — mark entities to draw with a coloured silhouette rim. Its registry (entity id → `{ color, thickness }`) is read every frame by the `@builtin::renderFeatures.outline` render feature, which draws a screen-space jump-flood rim hugging each marked entity's silhouette (uniform width, no gaps on hard-edge or multi-part meshes). The feature is ensured live on first use, so `outline.set(entity)` is all a caller needs. An entity's outline lasts as long as the entity: when it is despawned, by `entity.despawn`, a parent's cascade, a scene rebuild or reload, its id leaves the registry, so the feature's per-frame cost follows the outlined entities in the world. A `Model` or `SkinnedModel` drawing an outline from its outline fields clears it when the component is removed. ```luau local outline = require("modules.outline") outline.set(entity, { color = { 1, 0.5, 0, 1 }, thickness = 3 }) outline.clear(entity) outline.clearAll() ```
▲ 0↑ born
·
computeshader · born here
❒asset
# contour_triangles Each kept quad as two triangles: `pt` holds, per quad, the vertex numbers of its corners (`a b c`, `a c d`), and `vstart` the position of each triangle's first corner in `pt`. Slot `n` of the dispatch is the `n`-th quad `edges[n]` names, so the quads are numbered in edge order. Dispatch over the kept quads. Exact: each slot has one writer.
▲ 0↑ born
▣
module · born here
❒asset
# editor_ops Delete and duplicate the current editor selection, each as one undo step. Delete despawns every selected entity and clears the selection; duplicate clones each selected entity via `entity.duplicate` — a full component, attribute, visual, and material clone that includes descendants — and selects the copies. Both wrap the batch in a multiplayer undo operation so a single Ctrl+Z reverts the whole action. The editor scene entrypoint binds these to Delete and Ctrl+D.
▲ 0↑ born
·
toolbox · born here
❒asset
# lighting The canonical scene-lighting toolbox — ambient light, directional sun, sky, clear color, and point / spot / area lights, each with modify-or-spawn semantics: every call leaves the scene in the requested lighting state, spawning the canonical `sun` / `ambient` entities (or switching the renderer-level sky) when the scene has none. Set the whole environment in one call (sun, ambient, sky, and clear color, all optional), or set each independently: the directional sun, the ambient light, or the sky — swapped from a preset word, a project material matched by name, or a material ref. Add a point light as an entity at a position. Update any point, spot, or area light by name — its color, intensity, reach, shadows, and its shape controls (spot cone, area rect). Read the full lighting state back — every light with its values, clear color, authored sky, live sky config — in the same shape the whole-scene setup accepts. These are the interactive authoring surface, for setting a scene's lighting from `execute()`. The behaviour lives in the runtime lighting capability (`modules.api.engine.lighting`); a component, entrypoint, or the scene saver that adjusts lighting at runtime uses that capability directly, not a tool.
▲ 0↑ born
▣
module · born here
❒asset
# toolExec Run a JavaScript script that drives the engine's toolbox catalogue. This is the engine half of `tool_exec`. A harness hands over a script; this runs it on ZeroJS with exactly two globals — `zero.<toolbox>.<tool>(...)` to call a tool, and `tools.toolboxes()` / `list()` / `describe()` / `search()` to find one. No engine globals, no VM. **It is not "execute, but JavaScript."** A raw runtime with the engine's globals is `execute` with different syntax. What a shell command and a tool-calling loop both lack is the ability to hold an intermediate result: you cannot list 200 entities, filter them in-process and act on the 6 that matched without the whole list passing through the model. That is the gap — composition, not capability. Every tool reachable here is reachable from `zero` in bash and `tools.use` in Luau, under the same names. **Every binding is async, and that is not a style choice.** A tool may yield — `zeromind.search` makes an HTTP round trip — and a yield cannot cross the JS→Luau call boundary. The failure is silent: the call returns and its value is lost. So no tool is ever called from inside the interpreter. `__start` spawns a Luau task that may yield freely and hands back a ticket; `__poll` reads the settled result without yielding; the prelude turns the pair into a promise. The script says `await zero.scene.list()` and the yielding happens outside the VM, where it is allowed. The caller's source is wrapped in an async function, so `await` works on the first line — ZeroJS has no top-level await, and an agent should not have to know that. ```js const boxes = await tools.toolboxes(); const camTools = await tools.list("camera"); const spec = await tools.describe("camera.lookAt"); const hits = await tools.search("low poly tree"); const scenes = await zero.scene.list(); return scenes.filter(s => s.name.startsWith("level")).length; ```
▲ 0↑ born
▣
module · born here
❒asset
# toml Pure-Luau TOML parser and emitter. Use whenever a script needs to read or write a TOML file — settings, importer rules, tool configs, or any other authored config the user touches by hand. Auto-injected as the global `toml` by the prelude — no `require` in user code. `vfs.read` returns bytes. JSON has built-in parsing via `json`, but TOML — the format used for `.world_settings`, `pyproject`-style configs, and any human-friendly key=value file — needs a parser. `toml` provides one with no native dependency, so it works the same on native and WASM. ## Exports - `toml.parse(src: string) -> { [string]: any }` — TOML bytes to nested Luau table. Throws with the line number on syntax errors. - `toml.encode(root: { [string]: any }) -> string` — Luau table to canonical TOML bytes (alphabetical section/key order; deterministic output). - `toml.replaceArrayTables(src: string, name: string, rows: { { [string]: any } }) -> string` — replace every `[[name]]` block of a document with `rows` (appended at the end, one block each) and keep every other byte. ## Usage ```luau -- Parse local body = vfs.read("/zero/source/myconfig.toml") local config = toml.parse(body) print(config.render.culling_mode) -- Mutate + write back config.render.culling_mode = "cpu" vfs.write("/zero/source/myconfig.toml", toml.encode(config)) ``` ## Supported TOML - Sections, including dotted (`[a.b.c]`) - Arrays of tables (`[[name]]`, `[[a.b]]`), with sub-tables of a row (`[name.sub]` after `[[name]]`); read as a sequence of tables and written back as one block per element - Key/value pairs with dotted keys (`a.b.c = 1`) - Strings: `"..."` (escaped) and `'...'` (literal); triple-quoted variants for multi-line bodies - Integers (with `_` digit separators) and floats (incl. `inf`, `-inf`, `nan`, exponents) - Booleans (`true` / `false`) - Inline arrays (`[1, 2, 3]`) - Inline tables (`{ a = 1, b = 2 }`) - `#` comments ## Not implemented These are rare in settings/config files; add when a real call site needs them rather than carrying dead code. - Hex / octal / binary integer literals (`0xff`, `0o77`, `0b1010`) - Date / time literals ## Notes - `--!global toml` directive promotes the module's typed functions onto the runtime universe's globals bucket, so `toml.parse` / `toml.encode` are available without any per-source `require`. - The encoder is fully deterministic: sections and keys are sorted alphabetically, integer-shaped numbers are emitted without a decimal point (`2` not `2.0`), and nested tables become dotted section headers (`[a.b]`). - Parser errors carry the line number for fast diagnosis.
▲ 0↑ born
·
toolbox · born here
❒asset
# camera Author and manage the cameras in a scene: create one, list every camera, read its full parameters, change them, aim it, frame a subject, or make it the primary camera. A physical camera on the machine (a webcam, a phone camera) is `cameraInput`, covered by the `topics/camera-input` guide. ## Create & inspect - **create** — spawn a camera entity at a position, with any starting parameters in one call: fov, near/far, priority, render layers, a follow target, a behavior, a `lookAt`, and `target` (render-to-texture — `true` or `{ width, height, name }` renders the camera offscreen instead of to the viewport). Returns the new camera's full parameter record. - **list** — every camera in the scene, enumerated from the live ECS so a just-spawned, hidden, or runtime camera is never missed, sorted by descending priority, each row marking whether it is the on-screen (`active`) camera, whether it is the `player` camera (the one the PlayerPrototype uses), and its main/editor role. - **get** — the full parameter record for one camera (position, rotation, fov, near, far, priority, render layers, debug channel, render target, behavior, follow). Defaults to the active/main camera when you name none. ## Modify - **set** — change any of these camera parameters in one call: `fov`, `near`, `far`, `priority`, `debugPass` (the diagnostic view this camera renders, by name), `debugChannel` (the same as a raw numeric channel), `renderLayers` (the layer-name spec, e.g. `"all !ui"`), `behavior` (a `cameraBehavior`-tagged component), and `follow` (the entity the behavior tracks). Pass no camera to modify the active one. A camera's render target is set when it is created (`create` with `opts.target`). - **lookAt** — aim a camera at a world position or at another entity. - **frame** — position and aim a camera to frame a target entity from a distance and angle in one step. - **bindToPlayer** — make the joining player use a camera in play by binding it to the scene's PlayerPrototype: it re-parents the camera into the prototype (so it is cloned with the player) and sets the prototype's `camera` role to it. Pair it with a static behavior (`set { behavior = "@builtin::controller.menu" }`) for a fixed PS1-style camera the player moves within, or a follow behavior (`orbital_follow`, `third_person_follow`, `first_person`, …) for a tracking one. A player-less scene has no prototype — there the rendering camera is simply the highest-priority one, ordered with `set { priority = N }`. ## Related surfaces - Capturing an image from a specific camera is the `capture` toolbox (`capture` with a camera, or `oneshot`) — it renders any camera's authored pose offscreen without changing what is on screen. - Putting geometry ON a render layer is the `renderLayer` toolbox (`renderLayer.set`); `renderLayer.camera` is the layer-system-side way to set a camera's include filter, the same field `camera.set { renderLayers = ... }` writes. - The scene's primary camera is normally spawned for you from the scene's PlayerPrototype; `camera.create` adds additional cameras (a security view, a cutscene angle, a render-texture feed).
▲ 0↑ born
△
mesh · born here
❒asset
▲ 0↑ born
✦
shader · born here
❒asset
▲ 0↑ born
·
scene · born here
❒asset
▲ 0↑ born
▣
module · born here
❒asset
# kanban_planner A UI template screen.
▲ 0↑ born
▦
texture · born here
❒asset
▲ 0↑ born
△
mesh · born here
❒asset
▲ 0↑ born
◐
material · born here
❒asset
# Emissive White A bright white light-emitting PBR surface built on the `pbr` shader with the `_EMISSION` keyword enabled. A white base colour (`1.0, 1.0, 1.0`) paired with a strong `emissive` (`20.0`) makes it glow and drive bloom — well suited to light fixtures, emitters, and glowing accents. Non-metallic (`metallic 0.0`) at mid roughness (`0.5`). ## Inputs - `floats.emissive` (`20.0`) — high emission strength that produces the glow. - `keywords: _EMISSION` — enables the emissive path in the PBR shader. - `colors.base_color` — the white surface and emission colour. - `floats.roughness` (`0.5`) — diffuse spread on the lit surface.
▲ 0↑ born
❒
package · born here
❒asset
▲ 0↑ born
▣
module · born here
❒asset
# json_utils Safe JSON decoding helpers and a WASM-safe number formatter. Wraps `Json.decode` with `pcall` so engine-provided JSON strings can't propagate parse errors into call sites. Also handles the engine convention of writing the literal string `"null"` when a JSON value is absent. ## Exports - `M.decodeOrNil(jsonStr: string?) -> any` — safe decode; returns `nil` on empty input, `"null"`, or parse failure. - `M.decodeOrEmptyTable(jsonStr: string?) -> table` — safe decode; returns `{}` on any failure or non-table result. - `M.safeDecode(jsonStr: string?) -> any` — historical alias for `decodeOrNil`. - `M.formatNumber(n: number, decimals?: number) -> string` — truncate a number to N decimal places using only string ops. WASM-safe. ## Usage ```luau local JsonUtils = require("@builtin::modules.json_utils") local payload = JsonUtils.decodeOrNil(engineJsonString) local table = JsonUtils.decodeOrEmptyTable(maybeMissing) local label = JsonUtils.formatNumber(3.14159, 2) -- "3.14" ``` ## Notes - `decodeOrNil` short-circuits on `nil`, empty string, and the literal string `"null"`. The Json.decode call itself is wrapped in `pcall`, so malformed input is never observable to callers. - `decodeOrEmptyTable` is intentionally strict: non-table decode results (numbers, strings, booleans, nil) collapse to `{}`. Use `decodeOrNil` if you need to distinguish those. - `formatNumber` uses `tostring` + `string.sub` so it avoids the `string.format` numeric specifiers that don't work reliably under WASM in Luau. Pass `decimals = 0` to drop the fractional part entirely; a count below `0` reads the same as `0`, and a fractional count counts the whole places in it. - `formatNumber` truncates the mantissa of a value `tostring` renders in exponent form (`1.5e-07`) and keeps the exponent, so the result still reads back through `tonumber`.
▲ 0↑ born
❒
package · born here
❒asset
# gpuSort A stable least-significant-digit radix sort, run entirely on the GPU, over a caller-owned array of u32 keys. The three passes here are the body of one radix digit. They read a key array and a permutation and write a new permutation; they never touch whatever the keys describe, so any subsystem with a key per item can drive them. The caller supplies the keys, the identity seed, the per-pass constants and the dispatch sequence. ## One pass | Order | Shader | Dispatch | |-------|--------|----------| | 1 | `radix_hist` | one workgroup of 256 per 256 items | | 2 | `radix_scan` | `{ 1, 1, 1 }` | | 3 | `radix_scatter` | one workgroup of 256 per 256 items | Each pass sorts on one 8-bit digit and keeps the relative order of items sharing that digit, so `ceil(bits / 8)` passes over the low digits upward compose into a total order on `(key, item index)`. ## Driving it The caller owns six buffers: the keys, two permutation buffers the passes ping-pong between, the digit histogram (`ceil(count / 256) * 256` entries) and one constants buffer per pass. A pass reads its constants as | Word | Meaning | |------|---------| | `[0]` | item count | | `[1]` | block count, `ceil(count / 256)` | | `[2]` | this pass's digit shift — `0`, `8`, `16`, `24` | | `[3]` | `1` on the last pass, `0` otherwise | | `[4]` | the key a dropped item carries | Every value is fixed for the life of the sorted array, so the constants are written once rather than re-uploaded per frame. Each pass flips which buffer holds the permutation, so the parity of the pass count decides where the caller seeds the identity for the last scatter to land in the buffer the consumer reads. An item the caller wants dropped takes a key at or past `[4]` — one past the last real key — which parks it in the tail of the permutation; the last pass resolves it to `0xffffffff`, and the consumer reads that as "this slot draws nothing". A fixed dispatch size with a degenerate tail is what lets a consumer issue a constant draw with no read-back and no indirect arguments.
▲ 0↑ born
▣
module · born here
❒asset
# modules.api.engine.player_lifecycle Wires the UserIdentity component's avatar-bind events into the `localPlayerReady` lifecycle hook. Owns the local-avatar-bound latch (fires once after the avatar entity is bound + has settled past the bundle.instantiate deferred-mutation pipeline) and the opt-out path for legacy v6 scenes that declare the avatar slot as `""`. Exposes the `__layers_local_avatar_bound` + `__layers_local_avatar_opt_out` dispatch channels wired in `install()`; the UserIdentity component's lifecycle and the legacy v6 player_spawner route through them.
▲ 0↑ born
▣
module · born here
❒asset
# asset_residency Compute backends for the **lazy** residency properties on every `AssetRef` (`has_backing_asset`, `has_runtime_changes`, `cpu_resident`, `gpu_resident`). The AssetRef metatable's `__index` dispatcher (`assetType.shared.ref`) calls these only when a caller actually reads a flag — nothing is computed at resolve time, so an asset costs nothing for residency until it is observed. The three backing/runtime states: | `has_backing_asset` | `has_runtime_changes` | Meaning | |---|---|---| | true | false | USED, UNMODIFIED | | true | true | USED, MODIFIED (copy-on-write fork) | | false | true | RUNTIME-GENERATED | `cpu_resident` and `gpu_resident` are orthogonal to those three, and to each other. `cpu_resident` asks *"is this asset referenced from a live script-component context, so its bytes are warmed in memory?"* (the `/runtime/assets/` CPU layer); `gpu_resident` asks whether the device holds a texture or mesh under the asset's guid. They are separate pools and an asset can be in one and not the other — `asset.observe()` lists both. See `docs/specs/runtime-asset-copying.md` — "CPU residency vs GPU residency". The backing/runtime flags are derived from where the asset lives — source under `/zero/source/…` (backed), a copy-on-write fork / runtime-generated asset under `/zero/runtime/assets/<identity>.<category>/` (runtime change) — using the canonical `<identity>.<category>/` suffix shape, generic over any category. `cpu_resident` and `gpu_resident` are live queries through `asset.cpuResident` and `asset.gpuResident`.
▲ 0↑ born
·
toolbox · born here
❒asset
# services Generate real content — 3D meshes, textures, audio — from a prompt, by spending credits. This is the whole surface for it: discover what you can make, start a generation, track it, and find what you've made. You never touch the underlying asset type or task plumbing. When a build needs a recognizable thing — a treasure chest, a snowboard, a character, a sound — you **generate** it. You don't assemble it from boxes and spheres. ## The loop Discover what you can make: each service lists its operations, and each operation is self-describing: its inputs, the kind of asset it produces, and its credit cost. Check your credit balance if you want. Start a generation with a service and a prompt; this returns immediately with a handle and runs in the background, because a generation takes minutes and blocking would trip the execute time limit. When the run ends it posts a notice, `generation finished` naming its id and the job's `asset` (the spawnable asset path, which you can drop straight into a blocked-out slot) or `generation failed` naming its id and the error, and the notice reaches you on your next tool call; `services.status` reads a run's progress by its id meanwhile. Alongside the live per-session job view, a durable outputs record survives restarts by reading the provenance stamped on each generated asset. ## Describing your assets `describe` is the one tool here that makes text, not content. It finds the assets in your world that lack the description their type asks for (for the built-in types that ask, a README, or the `about.yaml` texts of a type) and fills them in: `{ mode = "find" }` lists what is missing and what filling it costs, `{ mode = "all" }` fills it, and `{ assets = { ... } }` fills the assets you name. A call costs 1 credit and covers many assets, and `find` reports the number of calls before anything is spent. What it writes is an ordinary file in the asset, there for you to edit.
▲ 0↑ born
·
computeshader · born here
❒asset
# rt_ao_apply Multiplies the denoised occlusion into the scene colour. ## Bindings | name | kind | |------|------| | `scene_color` | `texture2d` | | `ao_clean` | `texture2d` | | `applied` | `storage2d (rgba16f)` |
▲ 0↑ born
·
computeshader · born here
❒asset
# sort A stable radix sort of a column's keys into a permutation: place `s` of `perm` holds the index of the element that sorts `s`-th, so a gather by `perm` puts any column in key order. | Specialize | Options | Meaning | |------------|---------|---------| | `KEY` | `f32`, `i32` | the scalar a key holds | Elements with equal keys keep their run order, in both directions (`params.descending` 1 sorts from the greatest key down), and `-0` and `+0` are one key. The sort works 4 bits of the key at a time from the least significant up, and a pass whose digit is the same in every key is skipped, so a run of keys inside a narrow range takes fewer passes. `work` is the sort's second permutation; its contents after a dispatch are not an answer. The dispatch size `n` is the number of keys. One workgroup of 128 works the whole run; the other workgroups of a larger dispatch return. Exact: the sort is a function of the keys and their order, so the same keys give the same permutation on every dispatch. Over a live count (`COUNT=device`, see the abi README) `n` is the capacity and the first `domain_count` keys are live: they are sorted, and a slot at or past the live count holds its own index in `perm`. A live count of 0 sorts nothing and `perm` is the identity. It binds 4 storage buffers (3 in the plain variant).
▲ 0↑ born
·
computeshader · born here
❒asset
# compute_ocean Ocean wave simulation. Displaces a mesh's vertex buffer in place by a sum of Gerstner waves, recomputing each normal analytically from the wave derivatives. `steepness` sets how far the surface travels along each wave's direction as well as up. At `0` every vertex stays on its rest column and the result is a plain sine height field; raising it sharpens crests and broadens troughs, which is the shape that reads as a wave rather than a ripple. Values above roughly `1 / (frequency * amplitude * wave_count)` make the surface self-intersect. Bindings (see bindings.yaml): `vertices` (read_write `array<u32>` — the mesh vertex buffer, 76 bytes/vertex, bitcast to f32), `params` (read `array<f32>` — `[time, grid_size, amplitude, frequency, wind_x, wind_z, steepness, pad]`). Usage: dispatch by identity with `compute.dispatch("@builtin::shaders.compute_ocean", ...)` each frame — it resolves to the shader's stable guid and auto-compiles on first use (no setup).
▲ 0↑ born
▣
module · born here
❒asset
# water_surfaces The water surfaces a scene holds, gathered from the systems that make them. A system with water — a terrain's lakes, rivers and seas, an ocean — registers a provider here, and whatever stands things on water asks every provider at once. A procedural graph reads them all through `scene.water`. A provider is a table of two functions: - `version()` names what its water is now. A surface that moves every frame answers differently every frame, which is what makes a reader stand its points again. - `stand(request)` puts points on its water on the device. `request` holds: - `run`: the points' GPU positions, in the graph's space; - `count`: how many points there are; - `out`: the `P`, `normal`, `slope` and `wet` GPU columns to write; - `place` / `unplace`: graph to world and back, 16 numbers each; - `offset`: metres above the surface; - `keepY`: whether a point's own height rides on the surface's. It writes the points over its water, leaves the rest as they are, and returns the names of the kernels it dispatched. ```luau local water = require("@builtin::modules.water_surfaces") water.register("myWater", { version = versionNow, stand = standOnMyWater }) for _, p in water.list() do print(p.name, p.provider.version()) end ```
▲ 0↑ born
❒
bundle · born here
❒asset
▲ 0↑ born
✦
shader · born here
❒asset
▲ 0↑ born
·
computeshader · born here
❒asset
# compute_fft_copy Copies a `vec2<f32>` (re, im) buffer element-wise. Used by `nx.gpu.fft*` to seed the ping-pong scratch buffer from the caller's input on stage 0 without an extra ping-pong dispatch. Bindings (see bindings.yaml): `src` (read), `dst` (read_write) — both `array<vec2<f32>>`. Usage: dispatch by identity with `compute.dispatch("@builtin::shaders.compute_fft_copy", ...)` — it resolves to the shader's stable guid and auto-compiles on first use (no setup).
▲ 0↑ born
◐
material · born here
❒asset
# Water An animated water surface with a deep blue base colour (`0.1, 0.3, 0.8`) and low roughness (`0.2`) for glossy, reflective highlights, driven by a scrolling normal map for wave detail. Opaque, back-face culled, instanced, in the opaque queue (`2000`). Built on `@builtin::shaders.water_material`, sampling `@builtin/textures/water_normals`. ## Inputs - `colors.base_color` (`0.1, 0.3, 0.8`) — the water body colour. - `floats.roughness` (`0.2`) — low roughness for sharp specular reflections. - `floats.metallic` (`0.0`) — dielectric surface. - `textures.normal_texture` — the wave normal map (`@builtin/textures/water_normals`).
▲ 0↑ born
·
assettype · born here
❒asset
# Importer (asset type) An importer is a Luau function that converts a source file (GLB, PNG, CSV, custom binary) into one or more derived assets registered in the engine. Importers extend the engine's file-format knowledge — adding a new importer means "the engine now understands `.foo` files". Importers run automatically on VFS writes that match their claimed extensions. ## When to use one - You're bringing a new file format into the engine and want to derive engine-native assets from it (meshes, textures, animations, data tables). - You want one VFS write to fan out into multiple derived assets (e.g. a GLB write derives mesh, texture, and animation assets). - You want format ingestion to be idempotent + hot-reloadable — importers re-run on source-file changes. If you only need to consume an existing engine asset type, you don't need an importer; just resolve it via `asset.resolve`. If you're building an authoring tool that emits assets, that's a `.tool`, not an importer. ## Where it lives - Source: `/zero/source/.../<name>.importer/` - Identity: `<name>` (the `.importer` suffix strips). - Folder shape: - `init.luau` (or `init.lua`) — exports `canImport` and `import`, and `extensions`: the file extensions it claims, lower-case without the dot. An importer that makes one asset beside its source also exports `produces`, the type of that asset (`"texture"` makes `<stem>.texture`). **Required.** - `README.md` — instance documentation. **Required.** - `.metadata` — agent-editable tags + free-form fields. **Required.** - `settings.luau` — the importer's own settings schema, declared with `Field` descriptors. Optional; an importer without one takes no settings. - `settings.yaml` — the importer's settings values. Optional. ## Settings An importer that takes settings declares them in its own `settings.luau` and receives the resolved values as `ctx.settings` in `import(ctx)`. `asset.schema(importerRef)` reports the schema, `asset.settings(importerRef)` the values, and `asset.setSettings(importerRef, patch)` changes them. What an importer stamps onto the assets it creates is typically one of these: the texture, SVG and model importers each declare `textureSettings = Field.settingsOf("@builtin::assetTypes.texture", NoSync)` — the texture type's own fields — and pass it to `asset.create` as `importerSettings`, the importer layer of each texture's settings. A change to an importer's settings counts as a change to what an import is made from, so an unchanged source is imported again after one. ## How to create one ```luau asset.create("importer", "<name>") -- Creates: /zero/source/<name>.importer/ -- init.luau (canImport / import stubs) -- README.md (instance README template) -- `folder` places it in a subfolder of /source instead of the root: asset.create("importer", "<name>", { folder = "importers" }) ``` ## How it operates 1. **Registration.** Writing `init.luau` into a `.importer/` folder indexes the importer with the importer registry. 2. **Dispatch.** When a file is written to VFS, the engine asks every registered importer `canImport(path, bytes)` (cheap predicate). The first importer that accepts dispatches to its `import` function. 3. **Derivation.** `import(path, bytes)` returns a list of `{ path, kind }` pairs naming the assets to derive. The engine instantiates each derived asset against its registered category. 4. **Idempotency.** `import` MUST be deterministic — importing the same source bytes twice produces identical derived assets. The importer pipeline uses content-hashing to short-circuit re-imports when bytes haven't changed. 5. **Hot reload.** Editing the importer reloads it; subsequent writes use the new logic. Already-imported assets stay imported with the old derivation until their source is imported again — writing the source path re-dispatches the importers, and `tools.use("importers", "run", <path>)` re-imports a target, many targets, or a whole folder on demand. 6. **Play.** A source written while play runs lands on the play shadow and imports there, so the source and its asset are kept or discarded together at play-exit. A source play holds where it stands (written before play, or by a durable write during it; `vfs.playHoldsInPlace`) cannot be moved into a new asset while play runs, so its import is owed: the job reads `owed` (reason `play-lock`), a notice names it, and it imports when play ends or pauses. `tools.use("importers", "queue")` lists owed imports and `importers.cancel` drops one. ## Re-importing over an asset an import made An import keeps its source inside the asset it makes: `art/plate.svg` becomes `art/plate.texture/` holding `plate.svg`. `import(ctx)` reads where to write from `ctx.output` (`<dir>/<stem>.<produces>` on a first import) and keeps the source with `ctx.retainSource(dest, opts?)`, which moves it to `dest` and returns where it is now. `import` returns `{ output, source }`, `source` being the file the asset keeps, which the asset's `import` provenance records as its source. - **The source written again beside the asset.** Writing `art/plate.svg` once more re-imports it into the same `plate.texture`: the new bytes replace the svg the texture retains (which keeps its guid), the written file leaves, and the asset is made again from the new bytes. Its `import` provenance counts the iteration on. - **The same bytes written again.** When the source's checksum and the importer's settings match the import that made the asset, no import runs: the asset already holds those bytes, so the written copy is folded into it, and `writeOutcome` answers `skipped` with reason `unchanged-content`, naming the asset and the file that holds the bytes. - **The asset itself.** `tools.use("importers", "run", <asset>)` re-imports a produced asset in place from the source it retains (`ctx.container` names it), so rewriting `plate.texture/plate.svg` and running it re-bakes the texture. A run that makes nothing says why: each skipped result carries a `reason` (`missing`, `no-source`, `no-claimant`, `unclaimed`). ## Discovery - `asset.list("importer")` — every registered importer. - `asset.inspect("<name>")` — claimed extensions, derivation conventions, source, this type README. - `ls /zero/source/<name>.importer` lists the files the folder holds. - `tools.use("importers", "list")` — every registered importer, with its name and identity. - `tools.use("importers", "explain", <path>)` — which importers claim a given source and which gate would block the import. ## Where a write's bytes end up An importer that claims a written source may move it into the asset it makes (the texture importer turns `shots/a.png` into `shots/a.texture/` holding `a.png`), so the path the write named stops holding the bytes. `require("@builtin::assetTypes.importer.shared").writeOutcome(path, sinceFrame, timeoutSecs)` answers where they went, for a write made on engine frame `sinceFrame` (`time.frameCount()` read before it): nil when no importer claims them, else the import's `state`, the `importer`, the asset made (`output`), the file the bytes are in now (`source`) and a `note` saying all of it. Every write that fires its side effects is offered to the importers by the same rule, whichever route made it, and the MCP `write_file` and `upload_file` answers carry this outcome as `import`. A quiet write (`{ quiet = true }`, and every `capture` save) is offered to no importer, and its bytes stay where they were written; `writeOutcome` answers `not-started` for one an importer would claim. A write whose import the running play session holds back (a fresh import moves its source into the asset it makes, and play holds a source that predates it, or that a durable write made canonical, where it stands) answers `owed` at once, with `reason` `play-lock`: the bytes stay at the written path, and the import runs when play ends or pauses. `writeOutcomes(paths, sinceFrame, timeoutSecs)` answers a run of writes made together (a model, then its buffer and textures) once all of them have landed, within one wait: a folder `write_file` or `upload_file` lands every file without waiting on any import, then answers each this way. `write_file { awaitImport = false }` lands one file of such a run without waiting on its import. ## Authoring conventions - Keep `canImport` cheap — it's called for every VFS write. Match on extension or a quick header probe; defer real parsing to `import`. - Declare the extensions the importer claims as `M.extensions` and have `canImport` claim by that list. The Inspector lists them, and `require("@builtin::assetTypes.importer.shared").extensionsOf(importerRef)` answers them (lower-case, without the dot; nil for an importer that declares none). - Name derived assets predictably (e.g. `imported/<source-stem>/<asset-name>`). Predictable paths let callers reference derived assets directly. - Make `import` pure relative to its inputs. Don't read mutable globals or side-channel state. - Log derivations via `print` / the script log so authors can see what got produced from each source. ## Common pitfalls - **Expensive `canImport`.** It runs on every VFS write; an expensive predicate slows down the entire authoring loop. - **Non-idempotent `import`.** Re-importing produces different output → content hashing falsely caches stale results. Use only deterministic transforms. - **Conflicting importers.** Two importers claiming the same extension causes ambiguous dispatch. First-registered wins, but this is fragile — coordinate ownership of file extensions. - **Derived-asset suffixes.** Make sure derived paths end in the right `.<category>` suffix; otherwise they won't register as recognized assets. ## Related types - `.module` — for plain ingestion helpers that don't need the importer dispatch path. - `.tool` — for one-off conversions an agent invokes manually. - `.service` — if your importer needs persistent state (e.g. a cache); the service owns it, the importer reads from it. ## In the Inspector Imports: the extensions the importer claims (`M.extensions`), each file it imported in this session with the state its job reached, and **Find assets it produced**, which lists the assets in the world whose import provenance names this importer.
▲ 0↑ born
◐
material · born here
❒asset
# postReference_chrome A polished metal: its highlight of a bright light is a sub-pixel specular, the firefly a bloom and a temporal resolve are read on in the post-verification reference scenes.
▲ 0↑ born
backing path · systems/procgen.package/proc.module/eval.module

Problems

Everything affecting this asset right now: its own problems, anything wrong inside it, and problems on its direct dependencies.

These counts cover only the parts this page shows. Problems on the parts it leaves out are not counted here.

1+problem

Inside

1 problem across 1 item
●
dep.unresolved · L274
could not resolve `@builtin::systems.procgen.proc.eval.stamps`
⌬ZeroMind agent review · awaiting first pass
Findings
Reviewer findings (handle · model · tag · quoted note) appear here once the per-pass review log lands. Today only the rolled-up agent_score is exposed.
usability—
did it work as advertised
quality—
authoring polish + cohesion
performance—
frame & memory budget held
agent review score
—
/ 100
awaiting first pass
usability × 0.40
+ quality × 0.35
+ performance × 0.25
± compat factor

Usability ratings

Did the part work as advertised when consumers tried to drop it in. Separate from upvotes: those are taste; this is "did it function".

—%no reports yet
Sign in to report whether this part worked for you.
Discussion

Scoped to this part · feeds back into the world's score.

0comments
Sign in to post.sign in
No comments yet. Be the first.