Repository navigation
Compute nodes
materials/compute_nodes.mojo runs compute programs over storage buffers, on the host and on the GPU. It ports three.js's compute nodes from src/nodes/gpgpu/: ComputeNode, AtomicFunctionNode, BarrierNode, WorkgroupInfoNode and SubgroupFunctionNode. It also ports the storage buffers and textures that they read and write. render/gpgpu_sort.mojo ports BitonicSort and CountingSort from examples/jsm/gpgpu/.

examples/particles.mojo draws this picture.
var store = StorageBufferStore()
var positions = store.instanced_array(count, NODE_VEC3)
var velocities = store.instanced_array(count, NODE_VEC3)
var kernel = ComputeKernel()
var i = kernel.instance_index()
kernel.add_assign(positions, i, kernel.element(velocities, i))
var update = kernel.compute(count)
var host = HostCompute()
host.compute(store, update) # or GpuCompute().compute(store, update)
var moved = store.array(positions)This is three.js's Fn( () => { position.addAssign( velocity ) } )().compute( count ) and renderer.compute( update ).
A StorageBufferStore owns the buffers. A StorageBufferNode names one buffer, its element type and its count.
| Call | three.js |
|---|---|
store.instanced_array(count, type) |
instancedArray( count, type ) |
store.instanced_array(floats, type) |
instancedArray( typedArray, type ) |
store.storage(StorageBuffer(...)) |
storage( attribute, type, count ) |
store.storage_texture(width, height) |
new StorageTexture( width, height ) |
store.array(node) |
attribute.array |
store.set_array(node, floats) |
attribute.array.set( floats ) |
store.texture(storage_texture) |
The storage texture as a material reads it |
An element is a float or a vector of two, three or four floats. A uint element holds a whole number as a float. It is exact up to 2 ** 24.
A material reads a buffer after a run. Copy store.array(node) into a geometry attribute, or read store.texture(texture) as a float texture.
A ComputeKernel holds a NodeGraph, kernel.graph, and the statements built on it. Build values with the graph's nodes. Read the invocation and the buffers through the kernel.
| Call | three.js |
|---|---|
instance_index() |
instanceIndex |
invocation_local_index() |
invocationLocalIndex |
global_id(), local_id()
|
globalId, localId
|
workgroup_id(), num_workgroups()
|
workgroupId, numWorkgroups
|
subgroup_size(), subgroup_index(), invocation_subgroup_index()
|
subgroupSize, subgroupIndex, invocationSubgroupIndex
|
element(buffer, index) |
buffer.element( index ) |
assign(buffer, index, value) |
buffer.element( index ).assign( value ) |
add_assign(buffer, index, value) |
buffer.element( index ).addAssign( value ) |
texture_store(texture, coord, value) |
textureStore( texture, coord, value ) |
texture_load(texture, coord) |
textureLoad( texture, coord ) |
to_const(value) |
value.toConst() |
workgroup_array(type, count) |
workgroupArray( type, count ) |
workgroup_barrier(), storage_barrier(), texture_barrier()
|
The three barriers |
compute(count, workgroup_size) |
.compute( count, [ workgroupSize ] ) |
compute compiles the kernel to a ComputeNode. The workgroup size is 64 unless you set it. A node runs count invocations in workgroups along x. Change the count with set_count. Set a uniform with node.program.set_uniform.
A statement is a store, an atomic function or a held value. The invocation runs its statements in order. Each statement's index and value are roots of the graph, and NodeGraph.compile_roots lays them out.
A statement inside a graph.If is kept only where its branch runs. A statement cannot be made inside a graph.Loop, because End copies the loop body and not the statements. Use a Mojo loop to repeat a statement. The loop count is fixed in both cases.
to_const(value) computes a value once and keeps it for the invocation. Later statements read the kept value, also after a barrier.
A barrier ends a step. Every invocation runs a step before any invocation runs the next step.
In a step, a read sees the buffer as the step began. It also sees the stores of the same invocation that come before it. A read never sees the store of another invocation in the same step. So the host and the device agree.
Two invocations that store to one element in one step race, as on a GPU. The host keeps the store of the last invocation.
An atomic function changes one element of a float buffer at once, and returns the value that the element held before.
| Call | three.js |
|---|---|
atomic_load(buffer, index) |
atomicLoad |
atomic_store(buffer, index, value) |
atomicStore |
atomic_add, atomic_sub
|
atomicAdd, atomicSub
|
atomic_max, atomic_min
|
atomicMax, atomicMin
|
atomic_and, atomic_or, atomic_xor
|
atomicAnd, atomicOr, atomicXor
|
atomic_func(op, buffer, index, value) |
atomicFunc( method, pointer, value ) |
The host runs the invocations in order. The device runs them at the same time. So the old values can come in a different order on the device, but the sums agree. The bit functions read whole numbers of 32 bits, as the node bit operations do.
A workgroup array has one copy for each workgroup. Each copy starts as zeros at the start of a run. An invocation reads and writes the copy of its own workgroup.
A subgroup is one invocation. subgroup_size() is one. Each subgroup function gives what one invocation gives it: subgroup_add(x) is x, subgroup_exclusive_add(x) is zero, and subgroup_ballot(p) sets one bit.
BitonicSort(store, data, workgroup_size) sorts a buffer of floats in place, the smallest value first. The count must be a power of two. sort.compute(runner, store) runs every step. sort.compute_step(runner, store) runs one step.
CountingSort(store, count, bin_count, workgroup_size) orders the places 0 to count - 1 by a bin. Set the bin with set_bin_node(bin_node). The bin node is a function that builds the bin from the kernel. sort.order holds the sorted places after compute. compute_cpu(store, bins) gives the same order without a compute program.
The prefix pass of a counting sort is one invocation with two statements for each bin. The default of 4096 bins takes a few seconds to build.
GpuCompute in render/gpu.mojo runs a node on the device. Each step is one launch, with one block for each workgroup. Both backends call run_statements on a ComputeSource, so the buffers agree to the float. tests/test_gpu.mojo checks a kernel and both sorts.
- A barrier ends a step for every invocation of the dispatch. In WebGPU,
workgroupBarrierwaits only for the workgroup. - A dispatch is along x only.
globalId.yandglobalId.zare zero. - Every value is a float. A
uintorintelement is a float that holds a whole number. - An atomic function works on a buffer of floats. WebGPU atomics work on
u32andi32. - A read of an element that is not in the buffer returns zeros. A store to such an element does nothing.
- The quad functions
quadSwapX,quadSwapY,quadSwapDiagonalandquadBroadcast. They need a quad of four invocations, and a subgroup here is one invocation. - A material that reads a storage buffer or a storage texture directly, such as
positionBuffer.toAttribute(). - A 3D storage texture,
StorageTexture3DNode. - A dispatch size in two or three dimensions, and
computeKernelwith a workgroup size in more than one dimension. - Struct elements,
structtypes ininstancedArray.
Tutorials
How-to
Reference
- Scene graph
- Rotations
- Cameras
- Geometry
- Geometry addons
- Lofts and clipping groups
- Sculptor
- Procedural generators
- Meshes and assets
- Lines
- Points and sprites
- Helpers
- Scene objects
- Animated objects
- Raycasting
- Curves and paths
- Animation
- Skinning
- Materials
- Node materials
- TSL functions
- Lights
- Lighting addons
- Fog
- Textures
- Renderer
- Renderer hooks and material flags
- Rasterization
- GPU backend
- Render target
- Post-processing
- GPU computation
- Compute nodes
- Progressive light map
- Gaussian splats
- Voxel global illumination
- SVG renderer
- Image files
- Image decode queue
- Model files
- More model files
- Exporters
- Scene JSON
- Windowing and controls
- Math
- Norm consumers
- Math addons
- Units
- Coverage tool
- Commands
- Examples
- Benchmarks
- Extensions
- Shared physics
- Physics query snapshots
- Continuous collision
- Tick force restoration
- Water
- Streamline
- Animals
- Buildings
- CARLA
- Femur
- Tibia
- Fibula
- Patella
- Knee
- Muscles
- Vessels
- Lymph
- Nerves
- Integument
- Leg
- Foot
- Pelvis
- Torso
- Arm
- Hand
- Head
- Genome
- Game humanoid
- Humanoid fidelity
- Audio-aligned game faces
- Converted assets
- Mesh quality
- Segment inertia
Explanation
- Software rasterizer
- Scene graph as an array
- Linear color
- Normals are not colors
- Perspective correction
- Fixed-point coverage
- Per-fragment shading
- Mipmaps
- Sorted transparency
- Types and checks
- Render hooks as a trait
- Shared CPU and GPU code
- Bands and threads
- PNG structure
- Extensions beside the port
- CUDA teardown hang
- Mojo compiler hang