Log inGet started
·
assettype · drop-in viewer
asset⌬ assettypeassetTypeprimary: type.yaml·originates fromworld 07158574-5…

computeShader.assetType

A `.computeShader` is a GPU compute (GPGPU) program with a **zero-scaffolding** authoring contract: you write only `@compute fn main(...)` plus a declarative `bindings.yaml`, and the engine GENERATES every `@group/@binding` declaration — storage buffers, textures, samplers, stora…

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

Compute shader (asset type)

A .computeShader is a GPU compute (GPGPU) program with a zero-scaffolding authoring contract: you write only @compute fn main(...) plus a declarative bindings.yaml, and the engine GENERATES every @group/@binding declaration — storage buffers, textures, samplers, storage textures, and a params: uniform — expands #includes, naga-validates the result, and registers it. The single source of truth for the binding layout is bindings.yaml; there are no hand-written @binding lines to keep in sync.

New to how content lives across the CPU and GPU? Read core/resource-model first: it covers the asset / CPU / GPU forms every resource takes, the upload path, and how compute fits the performance ladder (core/performance).

Folder shape

A .computeShader is a folder asset:

<name>.computeShader/
  shader.wgsl        # the WGSL body — only `@compute fn main` + helpers (required)
  bindings.yaml      # the binding + params schema (required)
  README.md          # a one-paragraph description of THIS shader (required)
  .metadata          # asset metadata

Each file carries a committed .meta sidecar pinning its stable guid. The identity is <name> (the .computeShader suffix strips).

Resolve the shader once, then dispatch through the reference:

local carve = asset.resolve("carve", "computeShader")
carve:dispatch({ buffers = { heights }, workgroups = { 64 } })

asset.resolve is what records your script's dependency on the shader, so the shader travels with the world your script lives in — packed, pulled, or published. It compiles on first dispatch; there is no setup step.

bindings.yaml

One ordered bindings: list — the binding index is the list position (0, 1, 2, …) — plus an optional params: scalar-uniform block. kind defaults to buffer when omitted.

bindings:
  - { name: clouds,    kind: storage3d, format: rgba16f }   # texture_storage_3d<rgba16float, write>
  - { name: occupancy, kind: texture3d }                    # texture_3d<f32>
  - { name: smp,       kind: sampler }                      # sampler
  - { name: out_tex,   kind: storage2d, format: rgba16f }   # texture_storage_2d<rgba16float, write>
  - { name: U,         kind: buffer, access: read, element: f32 }   # var<storage, read> U: array<f32>
  - { name: Uniforms,  kind: buffer, access: read, element: VolumeUniforms, array: false }  # var<storage, read> Uniforms: VolumeUniforms
params:
  - { name: scale, type: float, default: 2.0 }
  - { name: count, type: int,   default: 0 }
kindextra keysgenerated WGSL declaration
buffer (default)access: read | read_write (default read_write), element: <wgsl type> (default f32), array: true | false (default true)var<storage, ACCESS> NAME: array<ELEMENT>; (or … NAME: ELEMENT; when array: false)
texture3dvar NAME: texture_3d<f32>;
texture2dsample: float | uint | sint (default float)var NAME: texture_2d<f32>;, or <u32> / <i32> for the other classes
texture_depthvar NAME: texture_depth_2d; — the one sampled shape with no sample: of its own; a depth-aspect channel (@scene.depth) binds here, not through texture2d
storage3dformat, access: write | read | read_write (default write)var NAME: texture_storage_3d<FMT, ACCESS>;
storage2dformat, access: write | read | read_write (default write)var NAME: texture_storage_2d<FMT, ACCESS>;
samplervar NAME: sampler;
textureCubeArrayvar NAME: texture_cube_array<f32>; — the environment cube array, bound to @scene.environment by a render feature's pass
verticesaccess: read | read_write (default read_write)var<storage, ACCESS> NAME: array<u32>; plus the mesh's layout uniform and the zeroVertex* accessors
acceleration_structurevar NAME: acceleration_structure; (hardware ray tracing)
  • Eight storage buffers per shader — the WebGPU guarantee, and what the engine asks every device for. buffer and vertices bindings spend that budget; textures, samplers, storage textures and params: do not. An acceleration_structure brings engine-owned buffers with it — three where the device has hardware ray query (the tables zeroHitSurface / zeroMaterial read) and five on the software backend (those, plus the scene triangles and the hierarchy over them) — so a traced pass has three slots of its own on the backend web users get. Over budget, the engine refuses the shader and logs which buffers it declared.
  • A buffer defaults to array<ELEMENT>. Set array: false for a single NON-array storage buffer — a struct binding (e.g. element: VolumeUniforms, array: falsevar<storage, read> NAME: VolumeUniforms;). The struct definition itself lives in shader.wgsl, not bindings.yaml.
  • A sampled texture binds under the class its FORMAT puts it in, so sample states that class and the generated texture_2d<..> matches it. An integer channel — @scene.material, which is Rgba8Uint — is uint; everything else is the float default. Bind an integer channel to a float slot and the engine refuses the pass and names both classes, because the device would otherwise reject the bind group and invalidate the pipeline.
  • A storage texture's access decides both the generated declaration and the layout the engine registers, so the two always agree. It defaults to write, the access every target grants every storage-compatible format. read_write is granted on r32f alone; declaring it on any other format is refused at the compile, with the reason on compute.programState. A pass that needs to read a wider target reads it through a separate texture3d / texture2d binding, or through a second storage binding declared read.
  • format tags (storage textures): r8, r16f, r32f, rgba8, rgba16f, rgba32f. Storage textures only allow storage-compatible formats (r32f / rgba8 / rgba16f / rgba32f).
  • An acceleration_structure binding resolves to the scene acceleration structure when the shader runs as a render-pass with ray tracing enabled (renderer.setRaytrace(true)). Declaring one makes the engine emit the helper functions the body can call: zeroTraceClosest(origin, dir, t_min, t_max) -> ZeroHit (struct: hit, t, instance, primitive, bary, attr, where instance is the hit object's render slot), zeroTraceAny(origin, dir, t_min, t_max) -> bool (terminate-on-first-hit, for shadow/occlusion rays), and zeroHitSurface(hit) -> ZeroSurface (normal, uv, material) + zeroMaterial(index) -> ZeroMaterial (base_color, emissive, metallic, roughness) to shade what a hit landed on. The same surface works on both backends — hardware ray query and the software/compute path — so the shader doesn't branch on renderer.raytraceCapability(). Bind it from a render-pass, which is where the scene acceleration structure exists.

params: declares scalar uniforms (type: float | int). When non-empty the engine appends a ZeroComputeParams uniform as the trailing binding (read it in WGSL as params.<name>) and tunes its values at runtime with shaderRef:setParam(prop, value).

Writing a mesh's vertices — kind: vertices

A mesh's vertices are raw bytes on the GPU, and the engine stores a mesh in one of two layouts: 92 bytes a vertex with every attribute a f32, or 32 bytes a vertex with normals and tangents quantized to bytes and UVs to shorts. Which one a mesh got depends on the mesh.

Declare kind: vertices and the shader is handed both the buffer and the layout that mesh is in, plus accessors that address it:

bindings:
  - { name: verts,   kind: vertices }                              # the mesh, writable
  - { name: heights, kind: buffer, access: read, element: f32 }
params:
  - { name: lift, type: float, default: 1.0 }
@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) id: vec3<u32>) {
    if (id.x >= zeroVertexCount()) { return; }
    var p = zeroVertexPosition(id.x);
    p.y = p.y + heights[id.x] * params.lift;
    zeroSetVertexPosition(id.x, p);
    zeroSetVertexNormal(id.x, vec3<f32>(0.0, 1.0, 0.0));
}
carve:dispatchOnVertices({ model = meshRef:handle().guid, buffers = { heights }, workgroups = { 64 } })

The accessors, over a binding declared read_write:

readwrite
zeroVertexCount() -> u32
zeroVertexPosition(i) -> vec3<f32>zeroSetVertexPosition(i, vec3<f32>)
zeroVertexNormal(i) -> vec3<f32>zeroSetVertexNormal(i, vec3<f32>)
zeroVertexTangent(i) -> vec4<f32>zeroSetVertexTangent(i, vec4<f32>)
zeroVertexUv(i) -> vec2<f32>zeroSetVertexUv(i, vec2<f32>)
zeroVertexColor(i) -> vec4<f32>zeroSetVertexColor(i, vec4<f32>)
zeroVertexNodeIndex(i) -> u32
zeroVertexWord(i, offset) -> u32

access: read gives you the readers only — a var<storage, read> binding cannot be assigned to, so the writers are left out rather than handed to you broken.

Once a pass has written a mesh's vertices, the engine bounds that mesh by what the pass wrote: it reduces the vertices to an AABB every frame and culls against that, so geometry a pass moves across the scene needs no oversized placeholder to survive culling. renderer.mesh.boundsSource(guid) reports "compute" for such a mesh and "geometry" for one still bounded by what it was created with.

A shader declares at most one vertices binding; the accessors are generated for that mesh. It can be declared anywhere in the list — the mesh is bound by the dispatch, and buffers fills the other storage slots in declaration order. The tangent's w carries handedness, and quantized normals/tangents come back in -1..1 while quantized UVs and colours come back in 0..1, so the values are the same ones a surface shader sees whichever layout the mesh is in.

shader.wgsl

Write only @compute fn main (and any helpers / #includes). The names declared in bindings.yaml are in scope — data, out_tex, params.scale, … — with no @group/@binding lines of your own.

// @domain: compute
// Bindings are declared in bindings.yaml — do NOT write @group/@binding here.
@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) id: vec3<u32>) {
    if (id.x < arrayLength(&data)) {
        data[id.x] = data[id.x] * 2.0;
    }
}

Dispatch & runtime

Everything a compute shader does, it does through its reference:

methodwhat it does
ref:dispatch(opts){ buffers, workgroups } — bind one buffer per declared storage binding, in order
ref:dispatchOnVertices(opts){ model, buffers?, workgroups } — bind the mesh model names to the vertices binding, buffers filling the rest
ref:dispatchEx(opts){ resources, workgroups } — for shaders mixing textures, samplers and storage textures: one { kind, name } per declared binding, in order
ref:setParam(prop, value)write one params: scalar uniform
ref:createBuffer(name, opts){ type, len, usage? } — a GPU buffer owned by this shader
ref:buffer(name)the buffer this shader last made under name
ref:getBindings()the parsed bindings.yaml schema
ref:getSource() / ref:setSource(wgsl)read / overwrite shader.wgsl
ref:compile()compile now instead of on the first dispatch
ref:status()what the engine did with this shader's dispatches, one record per target

The params: uniform is engine-owned and takes no entry in buffers or resources, and neither does a vertices binding's layout.

workgroups is { x, y, z }, { x }, or a bare x, and every dimension it leaves out is 1. A zero in any dimension is refused: the dispatch is dropped and recorded as a failure, so it surfaces in ref:status() and compute.failing() with the reason. A count derived from how much data there is passes through math.max first, so an empty set dispatches one workgroup rather than none:

carve:dispatch({ buffers = { heights }, workgroups = { math.max(1, math.ceil(#points / 64)) } })

Buffers and readback (the CPU side)

A buffer is created through the shader that owns it and comes back as a handle. Pass the handle to a dispatch — including another shader's — to bind it there:

local double = asset.resolve("@builtin::shaders.compute_double", "computeShader")

-- `type` is the element the shader declares, `len` how many of them.
local data = double:createBuffer("data", { type = "f32", len = 4, usage = { "readback" } })
data:write({ 1.0, 2.0, 3.0, 4.0 })   -- :writeU32 for integer words, :writeBytes for packed bytes
double:dispatch({ buffers = { data }, workgroups = { 1, 1, 1 } })

local pending = data:read()          -- starts an async GPU->CPU readback
-- a frame or two later, drain it (nil until it has arrived):
local out = pending:result()         -- :resultU32 / :resultBytes read the same bytes another way
data:destroy()                       -- free it when done

type is one of "f32", "vec3", "vec4", "quat", "mat4", and usage adds "readback", "vertex", "index" or "indirect" on top of the storage a buffer always has. An "indirect" buffer holds a draw's arguments where the GPU reads them, so a shader that compacts geometry writes the drawn count into the same dispatch that produced it and a render feature's kind = "draw" pass submits exactly that — the renderFeature asset type's README documents that pass and the storage.indirect name it reads. The handle's key is the shader's guid plus the name you gave it and a serial, so two shaders that both want a buffer called data get one each — and so do two callers asking this same shader for one. ref:buffer(name) returns the last one made under that name, which is how two callers share one on purpose.

A buffer that no shader computes — a vertex or index run that is only ever drawn — is allocated straight from the primitive instead: substrate.createBuffer({ name = ..., type = ..., len = ..., kind = "gpu", usage = { "vertex" } }).

The first dispatch of a shader compiles it, so the very first readback can still reflect the un-run shader; dispatch again once it is compiled, or ref:compile() to pre-warm. This create/write/dispatch/readback loop is the CPU side of the resource model in core/resource-model.

Is it compiled?

ref:compile() queues the compile; the pipeline is built on the render side frames later, so that call cannot say whether it produced a program. compute.programState(ref) is where that lands:

if compute.programState(carve) == "ready" then carve:dispatch(opts) end

local state, reason = compute.programState(carve)
if state == "failed" then print(reason) end
  • "absent" — the engine holds nothing under this key and nothing is in flight: never asked for, or released.
  • "pending" — a compile was asked for and has not reached the device. A recompile of a resident program reads pending too, because what it produces is a different program from the one bound now — which is what makes this the precondition to wait on after an edit.
  • "ready" — compiled and resident, so a dispatch naming it binds it.
  • "failed" — the most recent compile produced no program, and the second return says why.

A dispatch issued while the program is "pending" is held behind the compile and dropped if it never lands, so a readback taken before "ready" reports the buffer as it was. Wait for "ready", not for a count of frames.

Did it run?

A dispatch is recorded into a command encoder frames after the call that asked for it returned, so that call's true means "queued", not "ran". ref:status() is where the outcome lands — one record per target, cumulative:

for _, d in ipairs(carve:status()) do
    print(d.target, d.ok, d.dispatches, d.failures, d.lastError)
end

target is the mesh guid for a dispatchOnVertices and the buffers it bound for a dispatch that writes only those, so two systems driving one shader over their own buffers read as the two passes they are. ok is the most recent outcome; lastError says why the last failure failed and is kept after a recovery. An empty result means nothing has dispatched this shader at all. compute.failing() answers the same question across every shader at once.

A failing dispatch also posts a notice, so a pass that stops running reports itself rather than waiting to be found.

Running one every frame

A kind = "compute" substrate job queues one dispatch per frame:

jobs.register({
    phase = "main",
    executor = {
        kind = "compute",
        shader = asset.resolve("carve", "computeShader"),
        buffers = { "heights" },
        workgroups = { 64 },
    },
})

Registration & hot reload

Writing shader.wgsl or bindings.yaml fires the .computeShader assetType's onChange, which calls __compute.compile — that generates the binding declarations, expands #includes, naga-validates, and registers. This happens on create and on every edit, so saving recompiles with no world reload. A shader with invalid WGSL (a type mismatch, an undefined name) fails to compile with the reason logged ([compute] Compile of '<name>' failed: …) and is not registered. You don't need to register a shader before dispatching it — the first dispatch auto-compiles it on use. ref:compile() remains as an optional explicit pre-warm, only to avoid the one-frame first-dispatch warm-up in a latency-critical spot.

What compute is holding, and why a key names nothing

Every buffer, volume, storage target, history pair and sampler a dispatch can bind is filed under a key, and compute.observe() lists all of them with what each costs:

local r = compute.observe()
print(r.totals.count, r.totals.bytes)
for _, res in ipairs(r.resources) do
    print(res.key, res.kind, res.bytes, res.owner.name)
end

A row carries the key verbatim — the same string a dispatch names it by and a dispatch-failure message prints — plus its kind, its bytes, its extent and format where it has them, the usage bits it was created with, and the frame it was created on. owner is read back out of the key (<shader-guid>::<name>#<serial>), so a resource nothing has ever dispatched over still names the shader that made it. compute.resources(ref) narrows to one shader's — including the <shader-guid>::__params uniform the compile created for a params: block, which the shader owns like any other resource.

r.totals.bytes is the compute figure of renderer.gpuMemory() — the two are read off the same registries, so a listing accounts for the category exactly. The same reading is served at /zero/runtime/compute.

When a key names nothing, compute.diagnose(key) says which of the states the inventory distinguishes it is in, from the closed set compute.absentReasons() enumerates:

reasonwhat the inventory read
residenta resource is filed under exactly this key; the row is on resource
replacedthe owner holds the same name at a different serial — current names the live key
nameUnusedthe owner holds resources, none under this name
ownerAbsentno live resource carries this key's owner
unownedthe key carries no owner segment, and nothing is filed under it
notPublishedno renderer has published an inventory, so no key can be answered for

So a key out of [compute] dispatch '…' failed resolves against what exists, and a typo reports which part of it the engine could not match rather than coming back empty.

A reason is the inventory at the moment the call read it. The reading is republished on a frame where a registry gained or lost an entry, so a resource created and diagnosed inside one script sees the reading from before it was made; wait a frame and ask again.

Discovery

  • asset.list("computeShader") — every registered compute shader.
  • asset.inspect("<name>") — status, source, parse errors, this README.
  • computeRef:getBindings() — the parsed bindings.yaml schema ({ bindings, params }).
  • compute.programState(ref) — whether the program is compiled, still compiling, or failed, and why.
  • compute.observe() — every GPU resource compute holds, and what it costs.
  • compute.diagnose(key) — whether a key names a live resource, and why not.
  • profiler.gpuFrame() — what each dispatch of this shader spends on the GPU. A dispatch is timed under compute.<identity> — the same identity asset.resolve takes, so a timing row names the shader that produced it.

Common pitfalls

  • Compute shaders don't render — verify output via a buffer readback or a debug pass, not a screenshot.
  • ref:compile() returns before the program exists — the pipeline is built on the render side; compute.programState(ref) says when it is "ready", and a dispatch issued before that is dropped.
  • A dispatch call returning true means queued — ask ref:status() whether it ran.
  • A destroyed buffer is refused where it is bound — a handle outlives its storage, and buf:alive() reports which.
  • Binding order is the contractbuffers and resources bind in bindings: list order; reordering the list reorders the bindings.
  • Don't write @group/@binding in shader.wgsl — the engine owns them; re-declaring is the fastest way to a layout mismatch.
  • Don't infer a mesh's vertex stride — declare kind: vertices and use the accessors. A wrong stride writes positions correctly and normals into the wrong word, which renders as flat lighting rather than as broken geometry.
  • Storage-texture format must be storage-compatibler32f / rgba8 / rgba16f / rgba32f; others are rejected at compile.
  • A key that comes back empty is not the same as one that failedcompute.diagnose(key) separates a name never used from a generation that was replaced, and names the live key in the second case.

Related types

  • .shader — render-domain shaders (surface, sky, post-process, screen) and the legacy @domain: compute fallback.

Interface

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

conforms to

zero/asset-type/v1
⌬ Spec
suffix.computeShadercontainernoprimary aliasesshader.wgslplural dircomputeShadersrequired filesshader.wgsl, bindings.yaml, README.md, .metadata
Exposed API
⌬ Instance methods

getSource(self: ?) → string

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

argtypedescription
self?

examples

local src = computeRef:getSource()

setSource(self: ?, src: string) → boolean

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

argtypedescription
self?
srcstringNew WGSL source (only `@compute fn main` + helpers).

examples

computeRef:setSource(myWgsl)

getBindings(self: ?)

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

argtypedescription
self?

examples

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

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

Create a GPU buffer this compute shader owns. It is a substrate buffer — the same one every other part of the engine deals in — filed under this shader's guid, `name`, and a serial, so two callers asking this shader for a `params` each get their own. Pass the handle to any shader's dispatch, including another shader's, to bind it there. The shader remembers what it made: `shaderRef:buffer(name)` returns the last buffer created under that name, which is how two callers share one instead. `"vec4"`, `"quat"`, `"mat4"`), `len` is how many of them, and `usage` adds what the buffer is used for beyond the storage it always has: `"readback"` to read it on the CPU, `"vertex"` to draw it as geometry, `"index"` to draw it as an index run, `"indirect"` for a draw to read its arguments out of. A buffer can carry several. There is no integer element: a buffer is a block of 32-bit words, so a binding the shader declares as `u32` or `atomic<u32>` in `bindings.yaml` is created as `"f32"` here and written with `buf:writeU32`. The element type sets the STRIDE; what the words mean is the shader's to say.

argtypedescription
self?
namestringWhat this buffer is for, e.g. `"params"` or `"verts"`.
opts{ [string]: any }`{ type, len, usage? }` — `type` is the element (`"f32"`, `"vec3"`,

examples

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

buffer(self: ?, name: string) → any

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

argtypedescription
self?
namestringThe name the buffer was created under.

examples

local params = shaderRef:buffer("params") or shaderRef:createBuffer("params", { type = "vec4", len = 3 })

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

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

argtypedescription
self?
namestringTexture name, unique within this shader.
opts{ [string]: any }`{ width, height, depth, format?, storage? }`.

examples

local density = shaderRef:createTexture3D("density", { width = 64, height = 64, depth = 64, format = "r16f", storage = true })

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

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

argtypedescription
self?
namestringTexture name, unique within this shader.
opts{ [string]: any }`{ width, height, format? }`.

examples

local out = shaderRef:createStorageTexture2D("out", { width = 1920, height = 1080, format = "rgba16f" })

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

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

argtypedescription
self?
namestringHistory name, unique within this shader.
opts{ [string]: any }`{ width, height, format? }`.

examples

local history = shaderRef:createTextureHistory("taa", { width = 1920, height = 1080, format = "rgba16f" })

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

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

argtypedescription
self?
namestringSampler name, unique within this shader.
opts{ [string]: any }?Sampler options — filtering and addressing.

examples

local smp = shaderRef:createSampler("linear", { filter = true, clamp = true })

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

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

argtypedescription
self?
sourceanyThe buffer holding tightly-packed rows in the format's texel layout.
namestringTexture name, unique within this shader.
widthnumberTexture width in texels.
heightnumberTexture height in texels.
formatstring?Texel format: `"rgba16f"` (default), `"rgba32f"`, `"rgba8"`.

examples

local tex = shaderRef:copyBufferToTexture(packed, "foam", 256, 256, "rgba16f")

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

Dispatch this compute shader with named buffers bound to its declared storage bindings, in order. Compiles on first use. A zero in any workgroup dimension is refused and recorded as a dispatch failure, so a count derived from how much data there is passes through `math.max(1, ...)` first. declared storage binding; `workgroups` is `{ x, y, z }`, `{ x }`, or `x`. in `shaderRef:status()` and `compute.failing()`.

argtypedescription
self?
opts{ [string]: any }`{ buffers, workgroups }` — `buffers` names one compute buffer per

examples

shaderRef:dispatch({ buffers = { "positions" }, workgroups = { 64 } })

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

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

argtypedescription
self?
opts{ [string]: any }`{ model, buffers?, workgroups }` — `model` is the mesh guid whose

examples

shaderRef:dispatchOnVertices({ model = meshHandle.guid, workgroups = { 64 } })

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

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

argtypedescription
self?
opts{ [string]: any }`{ resources, workgroups }` — each resource is `{ kind, name }`.

examples

shaderRef:dispatchEx({ resources = { { kind = "storage_2d", name = "target" } }, workgroups = { 8, 8 } })

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

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

argtypedescription
self?
propstringParameter name as declared in `bindings.yaml`.
valuenumberNew scalar value.

examples

shaderRef:setParam("scale", 4.0)

compile(self: ?)

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

argtypedescription
self?

examples

shaderRef:compile()

status(self: ?)

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

argtypedescription
self?

examples

for _, d in ipairs(shaderRef:status()) do print(d.target, d.ok, d.lastError) end
⌬ Hooks

compileByName(ref: string) → void

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

argtypedescription
refstringA compute-shader asset reference (identity / guid) resolvable by `asset.resolve`.

examples

require("@builtin::assetTypes.assetType.shared.ref").loadTypeModule("computeShader").compileByName("@builtin::shaders.compute_double")

dispatchKey(self: ?) → string

Compile this compute shader through the engine: read the WGSL body + the declared binding schema and hand both to `compute.program.compile` primitive, which generates the `@group/@binding` declarations, expands `#include`s, naga-validates, and registers the compiled program. Convergent — `compute.program.compile` registers into the shader registry (it does not write the VFS), so it cannot re-trigger `onChange`. The key the compiled program is registered under, and the key every dispatch resolves by: the shader's stable GUID — canonical, location-independent and collision-safe. Falls back to the identity only when the asset somehow has no guid (a transient / in-memory asset).

argtypedescription
self?

onChange(ref: ?, change: ?) → void

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

argtypedescription
ref?
change?

Sub-parts

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

3items
This part has no composite children. See the Files segment for its leaf payloads.

Problems

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

0problems
No problems reported. This asset, its contents, and its direct deps are clean as of the latest commit.
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.