Advanced compute

Structured advanced compute

TinyFX keeps ordinary parallel for as the race-free default and exposes a separate resource model for algorithms that need retained in-place state, integer atomics, dynamic output allocation, or several dependent dispatches. Statements execute in source order. There is no phase or top-level compute construct.

Resource types

The Tier 1 element set is int, uint, float, their numeric vectors, and enums. Atomic storage is restricted to scalar int or uint.

let scratch = workspace<float4>(65_536);
let bins = atomics<uint>(256);
let selected = appendList<uint>(65_536);
let cursor = counter(target: scratch);
  • workspace<T> is retained, in-place GPU storage.
  • atomics<int|uint> is accessed only through atomic methods.
  • appendList<T> owns fixed-capacity data, a count, and an overflow flag.
  • counter(target:) produces reservations valid only for its target workspace.

Advanced storage is affine: assigning the same resource to a second local is rejected. This gives the checker one resource identity on which to record the dispatch access role and GPU-count provenance.

Workspace access roles

Each workspace has one checked role per dispatch:

RoleAllowed access
storage readarbitrary reads and no writes
owned writewrites to the exact loop index or a target-bound slot and no reads
owned read/writereads and writes of the exact loop index only

The exact index rule includes component and non-repeating swizzle writes. A same-dispatch gather plus write is rejected; split it into two source-ordered statements.

parallel for i in 0..scratch.len() {
  scratch[i] = scratch[i] + 1.0; // owned read/modify/write
}

Atomics and unordered

Void commutative updates are deterministic when one atomic resource uses one compatible family in a dispatch: add/subtract, min, max, and, or, or xor. Their integer arithmetic wraps at 32 bits.

Operations that expose arbitration order require the contextual prefix unordered parallel for: fetchAdd, exchange, compare/exchange, a load that races an update, reservation claims, and append.

counts.clear(0);
parallel for i in 0..values.len() {
  counts.add(bucket(values[i]), 1); // exact final histogram
}
 
selected.reset();
unordered parallel for i in 0..values.len() {
  if (values[i] > 0.0) { selected.append(uint(i)); }
}

An append past capacity is dropped, sets overflowed, and never writes out of bounds. selected.overflowed is a synchronized snapshot observation.

Target-bound slots

let storage = workspace<uint>(128);
let cursor = counter(target: storage);
cursor.reset();
 
unordered parallel for i in 0..items.len() {
  const slot = storage.claim(cursor);
  if (slot.valid) { storage.write(slot, uint(i)); }
}

A slot cannot be mutable, copied, returned, captured, reused, or passed to a different workspace. Its type is an opaque capability, not an integer index.

Deterministic operations

The operations layer currently provides sequential reference semantics in the VM and generated TypeScript core:

let counts = values.histogram(bins: 256, key: (value: float) => bucket(value));
let starts = counts.exclusiveScan();
let alive = values.compact(keep: (value: float) => value > 0.0);
let grouped = categories.groupBy(groups: 12, key: (value: int) => value);
let ordered = priorities.sortBy(key: (value: int) => value);
let total = values.reduce(initial: 0, combine: (a: int, b: int) => a + b);

Integer scans, histograms, stable compact/group/sort, and integer reductions are exact. Compact preserves input order. Group-by emits groups in ascending group order and preserves input order within each group. Sort-by is stable. The reference reduction is a left-to-right fold. A GPU-resident physical operation planner, including its floating-point order/tolerance contract, is still required before these methods can be used as a no-readback simulation pipeline.

GPU-resident counts

appendList.count and operation result counts are opaque views tied to one producer allocation and its known maximum. They can be used where a count belongs:

parallel for i in 0..alive.count { consume(alive[i]); }
drawAdvanced(shader: particles, mesh: quad, instances: alive.count);

The shared Rust wgpu core writes private 12-byte dispatch or 16/20-byte draw argument buffers with a one-invocation fixup and calls WebGPU indirect execution. The main range kernel reads the producer count itself to guard the final partial workgroup. Zero count therefore produces zero work without mapping the producer. Raw indirect buffers and arbitrary atomic-to-count casts are not source-visible.

Execution-target status

SurfaceTier 1 primitivesOperationsGPU-count consumption
Rust VM referencesequentialsequentialdirect logical count
shared Rust wgpu (native and wasm)GPUsequential host path today; GPU-owned wasm input is rejectedindirect draw/dispatch
generated TypeScript referencesequential callbacksequentialdirect logical count
generated TypeScript WebGPUnot yet represented by a physical schemanot yet representednot yet represented

Generated WebGPU execution fails directly when an advanced command has no physical blueprint; it does not silently select a browser CPU fallback. The remaining generated-WebGPU and operation-planner work is tracked as an acceptance blocker in SPRINT_4.md.

Inspection and compatibility

tfx build --explain-passes reports advanced access roles, logical operation subpasses, retained candidates, and indirect plans. Native tfx run --trace-passes reports the selected workgroup and whether the observed range or draw used GPU-count indirect lowering.

Advanced programs use .tfxb revision 14. Readers require an exact revision; revision 13 artifacts fail with the normal rebuild guidance. Existing builtin wire-key and instruction identities remain unchanged, with new tags appended.

Tier 1 does not expose workgroup-local memory, barriers, subgroups, bind groups, raw WGSL, or a global GPU barrier.