WebGPU compute#

The Wgpu* classes provide the features only WebGPU has: compute kernels, storage buffers, compute writers and scattered stores into tables, and the GPU radix sort and compaction that SplatRenderer sorts with. Everything here needs the WebGPU backend and throws on WebGL2 (check getGpuCaps(renderer).compute).

Code: src/wgpu/. Kernels are TSL functions, like everything in gpu/, which three compiles to WGSL compute shaders (Shaders).

const { If, uint } = TSL;

const counts = new WgpuBuffer({ renderer, format: "uint32", count: n });
const double = new WgpuCompute({
  body: ({ index }) => {
    If(index.lessThan(uint(n)), () => {
      counts.element(index).assign(index.mul(2));
    });
  },
});

// n threads, in workgroups of 64
double.dispatch(renderer, n);

// A Uint32Array
const values = await counts.read();

Storage arrays#

GpuArrays made with storage: true can be written from compute. They also support everything other arrays do: uploads, readbacks, copies, swaps, readers, and fragment writers.

// Both are typed GpuArrays<S, true>
const arrays = new GpuArrays({
  renderer,
  bundles: { extSplats: Splats.EXT },
  capacity,
  storage: true,
});
const copy = GpuArrays.fromData(renderer, data, { storage: true });

WgpuArrayWriter#

WgpuArrayWriter is the compute version of GpuArrayWriter. It takes the same constructor arguments, runs through the same arrays.write / writeBatch, and gives the same texels for the same indices. Both kinds can be mixed in one batch.

const writer = new WgpuArrayWriter({
  outputs: { extSplats: Splats.EXT },
  write: (index, out) => {
    const splat = reader.loadSplat(index);
    const pbr = reader.loadPbr(index);
    GpuSplats.storeExt(out.extSplats, splat, pbr);
  },
});

arrays.write(writer, { at: 0, count: n, first: 0 });

// A compute and a fragment writer in one batch
arrays.writeBatch([
  { writer, at: 0, count: 1000 },
  { writer: fragmentWriter, at: 4096, count: 500 },
]);
OptionDefaultWhat it does
outputsthe bundles written (names to definitions)
write(index, out)assigns one texel per output channel
workgroupSize64threads per workgroup
labelfrom the channelsthe kernel's name in GPU profilers

WgpuArrayStore#

WgpuArrayStore writes elements at any index from a kernel. It is the writing counterpart of GpuArrayReader.

const store = new WgpuArrayStore(Splats.EXT).bind(arrays, "extSplats");
const scatter = new WgpuCompute({
  body: ({ index }) => {
    If(index.lessThan(n), () => {
      // One element at any index (packing 1)
      store.store("extA", target(index), value(index));
    });
  },
});

scatter.dispatch(renderer, count);
arrays.count = count;
MemberWhat it does
new WgpuArrayStore(def)a store of one bundle definition; the shader code is built from it alone
bind(arrays, name)write bundle name of storage arrays from now on; bind again after the arrays are resized or swapped
store(channel, index, value)writes one element of an unpacked bundle
storeTexel(channel, texel, value)writes a whole texel (of a packed bundle)

The kernel must set arrays.count itself.

WgpuCompute and WgpuComputeBatch#

const kernel = new WgpuCompute({
  workgroupSize: [64],
  label: "myKernel",
  body: ({ index, globalId, localId, workgroupId, numWorkgroups }) => {
    // ...
  },
});

// A thread count, [x, y, z] workgroups, or an indirect buffer
kernel.dispatch(renderer, threads);

// Two kernels in one compute pass
const batch = new WgpuComputeBatch(renderer);
batch.add(a, n);
batch.add(b, n);
batch.submit();
MemberWhat it does
new WgpuCompute({ body, workgroupSize?, label? })a kernel built once from a TSL body. workgroupSize is 1 to 3 numbers (default [64])
dispatch(renderer, size)runs it now. size (DispatchSize): a thread count (rounded up to whole workgroups), workgroups per dimension, or an IndirectStorageBufferAttribute the GPU wrote (3 x uint32)
dispose()frees the kernel
ComputeIdsthe body's argument: globalId, localId, workgroupId, numWorkgroups, and index (the linear thread index)
new WgpuComputeBatch(renderer), add(kernel, size), submit({ split? })dispatches recorded into as few compute passes as possible; split: true dispatches one by one, so profilers time each kernel

Workgroup scans for your own kernels are static methods of WgpuCompute: scanTier(renderer, tier?) ("subgroup" where the device has subgroups, else "portable": a ScanTier), threadRank(tier, localId) (each thread's rank in scan order), and workgroupExclusiveScan(tier, scratch, rank, value) (an exclusive prefix sum of one uint per thread, plus the total), over SCAN_WORKGROUP (256) threads.

WgpuBuffer#

WgpuBuffer is a typed, resizable storage buffer of a 32-bit format with 1, 2, or 4 components.

const buffer = new WgpuBuffer({
  renderer,
  format: "uint32",
  count,
  atomic: false,
  indirect: false,
});

// In TSL
buffer.element(i);

// From JavaScript
buffer.upload(values);
await buffer.read();
buffer.clear();
buffer.copyFrom(other);
buffer.ensureCapacity(n, { keep: true });
MemberWhat it does
new WgpuBuffer({ renderer, format, count, atomic?, indirect?, label? })atomic: the elements are atomic<u32> or atomic<i32>; indirect: dispatch or draw arguments; label: the GPU buffer's name in WebGPU errors and debuggers
element(i)element i in TSL
upload(data, offset?), read(offset?, count?), clear(offset?, count?)CPU writes, readbacks, and zeroing
copyFrom(src, { srcStart?, dstStart?, count? })a GPU copy from a buffer of the same format
setCapacity(n, { keep? }), ensureCapacity(n, { keep? })resize; keep copies the contents. ensureCapacity doubles, but never past the binding limit
swap(other)trades GPU buffers (and their labels) with a buffer of the same format and kind
node, attribute, capacity, byteLength, components, gpuBuffer()the TSL node, three's attribute, the size, and the GPUBuffer
dispose()frees the buffer

WgpuRadixSort and WgpuCompact#

These are general building blocks: the sort takes any uint32 keys and values, and the compaction takes any TSL function that returns a key.

// keys and values are WgpuBuffer<"uint32">, sorted in place
const sort = new WgpuRadixSort({ renderer, keys, values });

// Or sortIndirect(countBuffer, { bits })
sort.sort(count, { bits: 24 });

const compact = new WgpuCompact({ renderer, key: (i) => keyOf(i) });

// Keys equal to this are dropped
compact.invalid.value = 0xffffff;

// Outputs compact.keys, compact.values (indices) and compact.count (element 0)
compact.compact(n);
MemberWhat it does
new WgpuRadixSort({ renderer, keys, values, tier?, keysPerThread? })a stable LSD sort with 8-bit digits. tier: "auto" (default), "portable", or "subgroup". keysPerThread: 8 (default) or 16 (RadixKeysPerThread)
sort(count, { bits? })sorts the first count keys and their values; bits is 16, 24, or 32 (default 32): 2, 3, or 4 passes
sortIndirect(countBuffer, { bits? })the count from a buffer the GPU wrote (after compaction); every dispatch is sized on the GPU
new WgpuCompact({ renderer, key, tier? })stream compaction of key(i) for the first n indices
compact(n), invalid, keys, values, countrun it; the dropped key (a uint uniform, default all ones); the outputs
dispose()frees the kernels and buffers

WgpuSplatSorter#

SplatRenderer sorts with this class in sortMode: "gpu". Compaction drops the invalid keys of a projection's sortKeys bundle, sortIndirect orders the rest back to front, and a one-thread kernel writes the drawIndexedIndirect arguments. Nothing is read back.

MemberWhat it does
new WgpuSplatSorter(renderer)the sorter's kernels and buffers
sort(projected, count, bits)sorts the first count splats of arrays with a sortKeys bundle (SortKeys.KEYS)
valuesthe sorted splat indices: values.element(i) is the i-th splat drawn
countthe visible count, as a TSL node
indirectthe draw arguments: [6 x 1024, ceil(visible / 1024), 0, 0, 0]
readCount()the visible count, read back
WgpuSplatSorter.QUADS_PER_INSTANCE1024: splats per instance of the draw
dispose()frees it

More on how the renderer uses it: Sorting.

Source: docs/wgpu.md