Builtin Library
README.md
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:
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 throughNAME_count(c),NAME_lanes(c),NAME_f32/i32/u32(c, i, lane),NAME_vec2/3/4(c, i);access: read_writeaddsNAME_set_*. Columns named at specialization are constantsNAME_col_<column>. -
count/capacity: the size, in elements, of an output the host allocates. The expression usesn(the dispatch size), the shader's params,len(binding),max,min,ceil,floorand+ - * / ( ). -
live: true: on an output ofcount: "n", the output carries the live count its dispatch ran over (the count thedomain_countbinding was handed), so a reader of it takes the live elements only. -
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 fordeterminism: float_orderonly. -
specialize: compile-time parameters. Type options becomealias NAME = type;, named options becomeconst NAME: u32plusNAME_<option>, andkind: sourceparameters replace the identifier with text. -
whenon a binding: a condition over the specialization options,NAME=optionorNAME=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...// @endin the WGSL: the lines between them stay in the variants where the specializationNAMEtakes 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: trueon 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:exactorfloat_order. Anexactshader that callsNAME_appendis refused. -
workgroup: the workgroup size the body declares (default 64). -
page/column: a kernel binds each arena page once and reaches the runs standing in it by offset.- { name: PA, kind: page, access: read_write, atomic: false }is one storage binding of u32 words (atomic: true:atomic<u32>words);- { name: pos, kind: column, page: PA, element: vec3<f32>, access: read }is a run of the page, with no binding of its own. A kernel of thirty columns over three pages has three bindings, whatever the runs' offsets are. A column getsNAME_len(),NAME_get(i),NAME_word(w)and, unlessaccess: read,NAME_set(i, v)andNAME_set_word(w, v); an index pastNAME_len()reaches the last element. On anatomic: truepage a one-lane column also getsNAME_atomic_exchange,NAME_atomic_minandNAME_atomic_max, the integer onesNAME_atomic_add,_and,_or,_xor, and an f32 columnNAME_atomic_add(compare-and-swap loops where the hardware has no atomic). Each column adds the paramscol_base_NAME(the word its run starts at in the page) andcol_len_NAME(its elements).abi.pageBinding(lowered, { [column] = handle | name })answers the page each page binding takes and those params, fromsubstrate.placement, and raises when the columns of one page binding stand in different pages. Runs made insideproc.substrate.withPageGroup(key, fn)stand in pages the group owns, in the order they were made, so work that asks for the same shapes in the same order binds the same pages every play. A buffer of its own (not an arena run) is a page of one column. At most eight pages bind in one kernel (the storage-buffer cap).
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
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 |