Log inGet started
World

Builtin Library

Branchmain
Branchesmainpublish
Commitdd348ac6c3

README.md

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.

What a play stands in the world goes over to a present all at once. A present opens a stage (eval.openStage): every show its cooks make (an io.output's show) is prepared and set aside, and once the present's pulls are cooked the stage commits (eval.commitStage) and every show stands in that frame. A value that stood and that the present supersedes (a node's Handle cooked again, an output node no plan holds any more) is set aside as well (opts.standingRetired) and let go of right after the commit, each release on a task of its own (eval.releaseStanding), so the world never holds part of one present beside part of another. A present that raises stands none of its shows, drops the nodes that made them (eval.forgetValues), and leaves what stood standing for the next present to replace. A value that stands is never kept per key on a timeline with an end: a key it held before is cooked again, and its show made again, when it comes back. A present cooks with the inputs the play holds when it starts: inputs setInputs gives it while it runs are the next present's.

A show of an output reconciles onto the entities the last show of that output stood under the same place, by element id. The place is the entity the graph stands on (the scene's entity); for a graph that stands on no entity it is the play's own (play.showPlace), which a play names on its thread while it presents or moves on the device (eval.standOn, read by eval.standingPlace) and a play started with inherit takes over. Two plays that stand on no entity show beside each other, and releasing one leaves the other standing on its own runs.

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. Each node cooks at opts.clock: an animated value it reads, handed straight to an op or through a node asset's input into a field op (fieldc.pcallSampling), holds its value at that time; one that becomes a field stays a live field, rewritten for a later time without a cook.

proc.historyKey and proc.liveDigest of a call-form graph are queries of the same plan: the plan is made, read and keyed exactly as a cook makes, reads and keys it (what it waits on is cooked, its leaves are keyed), and only the final read of the flat graph answers the key or the digest. So a key asked before an eval and after it is the same, and a plan a query made is kept keyed for the cooks after it.

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 graph's hash

proc.depHash(graph) (eval.depHash) is the hash of two things: the document without its nodes (id, version, metadata, timeline, inputs, outputs, composites, scopes, states, surface, plain values; graph.documentHeader) and the sum of the nodes' digests, mod 2^52. A node's digest is the hash of its id and of the node as the document states it (builder.documentNode: the node, with the place a build gave it when that is not the one its table holds). The sum does not depend on the order of the nodes, so two documents of the same nodes hash equal however their nodes are held, and a document read back from JSON hashes as the graph it was written from.

A graph keeps its sum with the count of edits it was stated at (eval.depSum). A graph built against its base (_diff, see the builder README) states it as the base's sum less the digests of the base's nodes that the build changed, removed or moved, plus the digests of those nodes in the new graph; the nodes it filed from the base are not read. A graph with no base, one edited since the build, and a document state it from every node (eval.depHashWhole is that sum, the definition the patched sum equals). eval.depHashCounts() answers the sums patched (patched), worked out from every node (whole) and found kept (held), and the digests made. eval.setDepHashFromNodes(false) returns to the hash of the serialized document folded in one pass; the two give different numbers for the same graph, so a key built from a hash (a composite's version, the disk cache) misses once when the setting moves.

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.
  • The body of a node asset is planned once for each structure its calls give it and kept (expand.bodyCounts): a plan made again reads the kept body, and the nodes it writes out are the ones it wrote before. A kept body stands while its asset's source is as it was and every definition it was written out from (the asset's own, and each op and node asset it calls, at any depth) is the one the registry holds. A node registered again lets go of the bodies that reach it and no other, so an edit of a node cooks again what the node reaches and nothing beside it. A call is compared with a kept body by what its leaves hold, without reading them: a plan reads the inputs of the bodies it uses, at any depth of calls, and none that only a body it compared and did not use reads.
  • A node the plan cooks while it is made is keyed over the flat graph as it stands then. The passes after emission rewrite nodes of that cone (an expression folded, a node read by none taken out), so a made plan lets go of the keys its flat graph holds (eval.forgetHeldKeys): the keys held for what the flat graph held while it was made go, and the next eval derives them from what it holds.
  • A Field that states what it computes (proc.fieldc's _expr) is keyed by that statement and the operands it names, not by the closure it samples with: a field written out again keys as it did.
  • 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 plan made against its base, which it took the tables of, takes the base's leaf list, leaf index and the cone of each node (expand.keyWatches(plan, dynamic, base, reach); expand.setWatchesCarried(false) walks every cone). The cones of the nodes the edit reached (reach, from eval.moveKeys) and of the nodes it let go of are dropped, and only the watches whose roots hold no cone are walked; the rest keep the places they stood on. The places keep their numbers: a leaf that no cone and no watch holds any more is a tombstone (kind = "dead", key 0) that no list names, and when the tombstones are at least 64 and outnumber the leaves the next plan walks whole. A registry that moved since the base was keyed, a base that is not keyed and a plan that wrote its own tables are walked whole. expand.counts() answers the plans carried and walked whole (with the reasons), the watches kept and walked, the nodes walked, the tombstones and the compactions.
  • 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 (made, reused), the milliseconds each took (madeMs, reusedMs), 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. What the answer reaches takes in the values a geometry's columns carry (a packed piece's Mesh record and the geometry its mesh holds): a value names what it carries and does not own through __proc_carried. A value a node of the cook made that the answer reaches only through what it carries is handed to the value carrying it (substrate.holdCarried) before the answer is copied or let go of: it stands while any holder of it stands (a copy of a holder is a holder) and goes with the last of them. A column that owns its values (a detail value) owns the geometries and columns in it and lets go of them with itself, so what it carries is what they carry. A node that lists what its passes made (eval.ownedBy, a fold or a for over geometries) lists the carried values its answer reaches that the values it was handed do not, and none a column owns. A store keeps what its entries carry from the release of a cook (it is among what the store holds), and a store shared by plays counts, among the values an entry lets go of with the last entry holding them, only the values the entry owns: a carried value is its maker's.
  • 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 (every transfer.readback, whatever the traffic of its thread counted, and a drained cpu.call) is recorded as a read-back row: its inputs, whether they were drained, and a digest of its plain outputs. A node that holds nodes of its own is asked again by their rows. 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. A graph that reads a figure that moves with its inputs (the bounds of a mesh, a volume) answers another value at every cook; one that reads the whole number or the verdict that figure comes to (a power of two, 0 for a solid) answers the same.
  • A count the device holds is not a read-back row: a node states the room it is allocated (device.bound) and the list runs the count on the device inside that room. A count the cook states no room for is allocated twice what the cook measured, in powers of two, and a room is never smaller than an earlier recording of the entry allocated (Run:room). The room a count of a number takes is a decision of the cook: the list is run again for every number that takes the same room, and for a number that takes another the recording is let go of and made again.
  • A recording holds the guards its nodes state (Run:guard, device.guard, device.bound without a room): words of the device the list writes, which are 0 while the cook's conditions hold. After a replay the words are asked for without waiting (substrate.deferredReads, counted apart from readbacks), and the cook of the entry that follows reads the answers that have landed (at most eight replays ahead; past that the latest words are read at once). A word other than 0 lets the recording go, so that cook is made node by node and recorded again: it raises the refusal the condition states, or allocates a count that outgrew its room four times the room (counts().guarded).
  • 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 list that dispatched a kernel declaring determinism: float_order (a float sum whose threads arrive in any order) is held to f32 columns that agree to the rounding of a sum taken in another order.
  • A buffer a cook lets go of while it is recorded stays until the recording goes (substrate.deferReleases): the list names it, and a replay writes it.
  • 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, a write and a release belong to the recording of the thread that made it.
  • The first replay answers a copy word for word onto runs of its own (cookrun.deviceCopy) and is held to the cook. Every replay after it answers a share of the runs the list wrote (cookrun.handOut; counts().shared): a Geometry clone, the columns of which share the runs, costs the caller nothing to make. The recording names every holder of those runs (a clone of a share is named too); before the list runs again it moves the holders still out onto a copy of the runs, so an answer a caller holds across cooks keeps the values it was handed (counts().detached). The held answer of a plan is always a copy.

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.

A fold whose state is made of numbers and Fields alone, and whose first pass answers Fields stating what they compute without working on the device, is one loop inside the chain that reads its state (fieldc.loop): the pass is cooked once more over Fields standing for the state and for the place of the pass, and the count is one of the chain's numbers. A fold cooked a pass at a time nests each pass's chain in the next, so its kernel grows with every pass; run as a loop, the chain's text and its kernel are the same whatever the count. foldrun.counts().looped counts the folds run that way. A pass that reads its state at another point (inside field.substitute) is cooked a pass at a time: the loop holds the state at the element it runs for.

The shape of a state names each column it holds; a column carried as values (a record, a text) and a detail value stand by the evaluator's key of what they hold (eval.keyOf), under which a geometry or a column a record holds stands as the runs it reads and the writes they have had.

How a step runs

A step solver's definition (construct.step.<id>) is registered under the content of its body, and every plan that writes that body runs it. It holds the body's nodes, inputs, outputs and exports; the flat graph the body was written in is read by the plan that writes it, while that plan is made. A step solver plans its body once (proc.eval.stepplan), when the state takes its first step.

A body reads the nodes outside it through exports: each ref to the output of a node is exported once, under the node and the output it names, and every later read of it in the body is the same export. Any other ref is exported under the key eval.keyOf makes of it.

  • 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.lt, le, gt, ge, eq or ne node of its operator, 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.expr is the arithmetic of a graph as one node. The plan writes each operator (+ - * /, math.*, vector.*, g.compare) as a cpu.call of a function of fieldc.operators and the construct.item that reads its result, and after the flat graph is written out it gathers every connected set of them, with the x, y, z and w reads of their results and the g.where choices between them that a condition per element makes, into one construct.expr whose tape lists the steps ({ fn, args }, { fn = "where", choice = true, args } or { lane, args }, each operand a leaf, an earlier step or a const) and whose leaves are the values the steps read. A condition is per element when it stands on an element field or on a column of the elements of a class; the cook makes both alternatives of such a choice and chooses between them element by element, so the plan decides nothing for it. A construct.select whose condition is one value the cook answers, or whose alternatives are whole values, stays a node. Each call step runs through callrun.invoke, the call cpu.call makes, and the node answers the last step. flat.folds[id] lists the flat id of the call or choice each step stands for, and describeFailure reads a failing step at that call. A node the plan, a watch or an output names, and a member written for another alternative of a choice than its reader, stays a cpu.call and a construct.item; a member a decision names is the root of an expression of its own. An expression holds members a cook derives a number through (a replay of a recorded list works those out again for other values of the graph's inputs) or members it does not, never both. docs/specs/procgen-system.md section 13.1 has the rules. The expressions whose tapes state the same steps hold one frozen tape table, filed by a text that states the tape exactly (a number by its bits, a vector by its three components, a string with its length), and each node holds its own { kind = "const", value } reference to it (expand.setTapesShared).

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.

A present evaluates the pulls whose keys can have moved and leaves the others standing with the Handles they hold. A pull stands when the plans made since it was evaluated reached none of the nodes its key stands on (moveKeys files them), it was evaluated at this clock, with these inputs, at this registry revision and before the play last moved on the device, no module or asset a cook read was edited, and its cone holds no solver, no graph used as a node, no live read and no CPU registration. A pull whose cone reads the scene stands while the digest of the scene state the plan reads (play:liveDigest()) is the one it was evaluated at. A play that takes over the node cache of the play it replaces takes the Handles with it.

The device shape of a plan (eval.timeOnDevice with keptWith: which nodes keep the play's time from reaching it through live values alone) is the sum of the share each node of its flat graph holds. A plan made against its base states it from the base's: the nodes the plan wrote anew are taken again and every other node keeps its share (eval.deviceShapePatches() counts these, eval.deviceShapeWalks() the walks of every node). The causes are listed in node id order.

What procgen keeps (proc.memory)

Every play, whoever started it, and each module-level cache that holds procgen state (the kept bodies, the answers kept plans hold, the frames timelines keep, the world store the plays share) is a keeper. proc.memory.report() answers { total, luau, budget, tiers, plays, keepers, byPart, commits, dropped, pending }: the bytes each keeper holds, a play by part (graph, plan.flat, plan.inlined, plan.origins, plan.reuse, plan.cones, plan, nodeCache.values, nodeCache.pulled, nodeCache.entries, nodeCache.solvers, play.coneMemo, play.stats, play) with the tier of the state, and the bytes the Luau heap holds (luau). A part states its bytes from counts kept where its state is filed and let go of: a plan's flat node count and rows, a node cache's entries, solvers and the entries whose values were let go of (eval.cacheCounts(cache), kept by fileEntry, dropEntry and the store's writes), the kept bodies' flat nodes (expand.bodyCounts().kept). The bytes one unit costs are proc.memory.PER, fitted to the heap that letting each part go gave back. The tier of a part is what letting it go costs: 1 a table made again when read (the table of origins the flat graph and the profile file by flat id, the cook times, the answers kept plans hold, the frames timelines keep), 2 a node cache value or kept body cooked or planned again where an edit reaches it, 3 the play's edit state (the plan's reuse record and cones and the answers kept for the pulls: the next plan is made without its base), 4 the cache as a whole, 5 (proc.memory.STANDS) what a present stands on, which no budget lets go of.

With a budget (proc.memory.setBudget(bytes); none by default) the ledger reads its total at the commit points: after Play.replan has made its plan the play's, after a present settles, and on a pressure signal (proc.memory.signal). Over the budget it lets go of tier 1, then 2, 3 and 4, the module-level keepers first and then the plays used longest ago first, until the total is at 80% of the budget. A tier can be switched off (proc.memory.setTier(tier, false)). A play whose present or plan is running is left whole, and when one runs besides the play that commits, the check waits for the next commit point. report().dropped lists the commit points that let go of state: the budget, the total before and after, the bytes by tier and the keepers and plays that gave them.

What each tier lets go of, and why a play answers what it answered: tier 1 is made from the plan when read (expand.originsOf, profile.originsOf). Tier 2 is eval.letGoValues: an entry that no pull answers and nothing stands on in the world is left a trimmed one (no key, no values), as trimToBound leaves one, and the values it held are released unless another entry, the world store, a value a handle stands on or a registration the device rewrites reaches them (registration.reads lists what a rewrite reads and lands in), and a value that holds a live run is left to its registration; the next cook that reaches the node derives its key from its inputs and makes the values again. A kept body let go of is planned again where a call needs it, as with expand.setBodiesEnabled(false), whether or not a play holds a reuse record: a plan answers the same plan, keys and reads from kept bodies and from bodies planned afresh. Tier 3 is expand.dropReuse and expand.dropCones and a play without its answers (play.coneMemo): the next plan has no base, which is a plan made as a first one, and its dirty set names the ledger (withoutBase); the cones are keyed whole and the plan's watches are read by cooking them, as with expand.setWatchesCarried(false); the device shape is stated from every node (eval.deviceShapeWalks). Tier 4 is tier 2 for every entry, the pulls' too, while the handles the play shows stand.