Log inGet started
World

Builtin Library

Branchmain
Branchesmainpublish
Commit61a33237e0

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 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 + - * / ( ).

  • live: true: on an output of count: "n", the output carries the live count its dispatch ran over (the count the domain_count binding 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 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).

  • 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 gets NAME_len(), NAME_get(i), NAME_word(w) and, unless access: read, NAME_set(i, v) and NAME_set_word(w, v); an index past NAME_len() reaches the last element. On an atomic: true page a one-lane column also gets NAME_atomic_exchange, NAME_atomic_min and NAME_atomic_max, the integer ones NAME_atomic_add, _and, _or, _xor, and an f32 column NAME_atomic_add (compare-and-swap loops where the hardware has no atomic). Each column adds the params col_base_NAME (the word its run starts at in the page) and col_len_NAME (its elements). abi.pageBinding(lowered, { [column] = handle | name }) answers the page each page binding takes and those params, from substrate.placement, and raises when the columns of one page binding stand in different pages. Runs made inside proc.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

FunctionDoes
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