This tutorial follows programs/b04-particles.ts. The program defines host data, GPU bindings, a kernel, and one dispatch.
Particle uses two Vec3f fields. The layout generator gives each value the required C and WGSL layout.
@CStruct
class Particle {
pos: Vec3f;
vel: Vec3f;
constructor(pos: Vec3f, vel: Vec3f) {
this.pos = pos;
this.vel = vel;
}
}SimParams stores the time step and the active particle count.
@CStruct
class SimParams {
dt: f32;
count: u32;
constructor(dt: f32, count: u32) {
this.dt = dt;
this.count = count;
}
}The layout class gives the uniform and storage bindings their script types.
class ParticleLayout {
params!: Uniform<SimParams>;
particles!: MutStorage<Particle>;
}The helper has a real script body. The generator lowers the same typed operations to WGSL.
function integrate(particle: Particle, dt: f32): Particle {
const speed: f32 = particle.vel.length();
if (speed > 0.0) {
const pos: Vec3f = particle.pos.add(particle.vel.scale(dt));
return new Particle(pos, particle.vel);
}
return particle;
}The kernel reads the uniform value and writes one storage element.
function particleKernel(res: ParticleLayout, ctx: ComputeInvocation): void {
const settings: SimParams = res.params.$;
const i: u32 = ctx.globalId.x;
if (i < settings.count) {
res.particles[i] = integrate(res.particles[i], settings.dt);
}
}simulateCompute calls this kernel over host wrapper storage. Its generated host-runnable constant prevents sequential simulation of kernels that require GPU synchronization.
The declaration connects the layout type, the kernel function, and the workgroup size.
export const particles: ComputePipelineSpec = computePipeline<ParticleLayout>(particleKernel, {
name: "particles",
workgroupSize: [64, 1, 1],
});The program uses generated byte sizes for both buffers.
using params = device.createBuffer({
label: "b04-params",
size: SimParams_SIZE as u64,
usage: GPUBufferUsage.UNIFORM + GPUBufferUsage.COPY_DST,
});The generated WGSL text, entry name, layout, and workgroup constants create the runtime pipeline.
using pipeline = createComputePipeline(
device,
particles_WGSL,
particles_ENTRY,
[particles_LAYOUT0],
[particles_WORKGROUP_X, particles_WORKGROUP_Y, particles_WORKGROUP_Z],
);The typed factory joins each named resource to its layout binding. It returns the bindGroup value for the dispatch.
const resources: ParticleLayoutResources = createParticleLayoutResources(
params,
particlesBuffer,
);
using bindGroup = createParticlesBindGroup0(device, pipeline, resources);The typed pipeline converts the thread count into workgroup counts for all three axes.
pipeline.dispatchThreads(encoder, [bindGroup], count, 1, 1);The command buffer enters the device queue after the encoder finishes.
using command = encoder.finishDefault();
device.queue.submit([command]);