renderable = {
const AGENT_COUNT = 1_000_000
const { device, context } = await gpu.init(S, S)
invalidation.then(() => {
interaction.draw()
device?.destroy()
})
const DISPATCH_SIZE = [Math.ceil(S / 16), Math.ceil(S / 16), 1]
const externalInputSize = { width: interaction.context.canvas.width, height: interaction.context.canvas.height }
const externalInput = device.createTexture({
size: externalInputSize,
format: 'rgba8unorm',
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT
})
device.queue.copyExternalImageToTexture(
{ source: interaction.context.canvas },
{ texture: externalInput },
externalInputSize
)
const texInput = gpu.canvasTexture(device, sample.canvas, { usage: gpu.Usage.StorageTexture })
const texOutput = device.createTexture({
size: [S, S],
format: 'rgba8unorm',
usage:
GPUTextureUsage.STORAGE_BINDING |
GPUTextureUsage.COPY_SRC |
GPUTextureUsage.TEXTURE_BINDING
})
const uniforms = new Float32Array([0, ...Object.values(config)]);
const uniformBuffer = gpu.uniformBuffer(device, uniforms);
// Agents
const agent = new Float32Array(
util.flatArr(AGENT_COUNT, i => {
const species = [0, 0, 0, 0]
species[util.randi(species.length)] = 1
return [
// S / 2 + 20 * Math.cos(i / AGENT_COUNT * util.TWO_PI),
// S / 2 + 20 * Math.sin(i / AGENT_COUNT * util.TWO_PI),
// 0, // padding
// i / AGENT_COUNT * util.TWO_PI,
i / AGENT_COUNT * S,
S / 2,
0, // padding
util.rand(util.TWO_PI),
...species,
]
}),
);
const agentBufferPing = gpu.storageBuffer(device, agent);
const agentBufferPong = gpu.storageBuffer(device, agent);
// Trails
const trailCountsBuffer = gpu.storageBuffer(device, new Uint32Array(S * S));
const trailTexturePing = texInput;
const trailTexturePong = texOutput;
const bindGroupLayout = device.createBindGroupLayout({
label: 'everything else bind group layout',
entries: [
{
binding: 0,
visibility: GPUShaderStage.FRAGMENT | GPUShaderStage.COMPUTE,
buffer: {},
},
// agents in
{
binding: 1,
visibility: GPUShaderStage.VERTEX | GPUShaderStage.COMPUTE,
buffer: { type: 'read-only-storage' },
},
// agents out
{
binding: 2,
visibility: GPUShaderStage.COMPUTE,
buffer: { type: 'storage' },
},
// trail counts
{
binding: 3,
visibility: GPUShaderStage.COMPUTE,
buffer: { type: 'storage' },
},
// trail texture IN
{
binding: 4,
visibility: GPUShaderStage.VERTEX | GPUShaderStage.COMPUTE,
texture: {},
},
// trail texture OUT (blur + decay + counts)
{
binding: 5,
visibility: GPUShaderStage.COMPUTE,
storageTexture: { format: 'rgba8unorm' },
},
{
binding: 6,
visibility: GPUShaderStage.COMPUTE,
texture: {},
},
],
});
const agentBindGroupPing = device.createBindGroup({
label: 'agent bind group ping',
layout: bindGroupLayout,
entries: [
{ binding: 0, resource: { buffer: uniformBuffer.buffer } },
{ binding: 1, resource: { buffer: agentBufferPing.buffer } },
{ binding: 2, resource: { buffer: agentBufferPong.buffer } },
{ binding: 3, resource: { buffer: trailCountsBuffer.buffer } },
{ binding: 4, resource: trailTexturePing.createView() },
{ binding: 5, resource: trailTexturePong.createView() },
{ binding: 6, resource: externalInput.createView() },
],
});
const agentBindGroupPong = device.createBindGroup({
label: 'agent bind group pong',
layout: bindGroupLayout,
entries: [
{ binding: 0, resource: { buffer: uniformBuffer.buffer } },
{ binding: 1, resource: { buffer: agentBufferPong.buffer } },
{ binding: 2, resource: { buffer: agentBufferPing.buffer } },
{ binding: 3, resource: { buffer: trailCountsBuffer.buffer } },
{ binding: 4, resource: trailTexturePong.createView() },
{ binding: 5, resource: trailTexturePing.createView() },
{ binding: 6, resource: externalInput.createView() },
],
});
const pipelineLayout = device.createPipelineLayout({
label: 'pipeline layout',
bindGroupLayouts: [bindGroupLayout],
});
const agentPipeline = device.createComputePipeline({
label: 'agent pipeline',
layout: pipelineLayout,
compute: {
module: device.createShaderModule({ code: shaders.agent }),
entryPoint: 'cs',
},
});
const trailCombinePipeline = device.createComputePipeline({
label: 'trail combine pipeline',
layout: pipelineLayout,
compute: {
module: device.createShaderModule({ code: shaders.trailCombine }),
entryPoint: 'cs',
},
});
const debug = new TextureRenderer(device, texOutput, {
// colorScheme: gpu.canvasTexture(device, colorScheme),
style: { background: '#000' },
})
// event handlers
{
const boxSize = 50
let bbox
let pressing = false
const start = e => {
bbox = debug.context.canvas.getBoundingClientRect()
pressing = true
interaction.context.globalAlpha = 0.2
}
const end = e => {
pressing = false
interaction.context.globalAlpha = 1.0
}
const drag = e => {
if (!pressing) return;
const x = e.clientX - bbox.left - window.scrollX
const y = e.clientY - bbox.top - window.scrollY
interaction.context.beginPath()
interaction.context.arc(x, y, util.randi(20) + 5, 0, util.TWO_PI)
interaction.context.fill()
updateExternalTexture()
}
// observable-specific cleanup (remove event listeners):
debug.canvas.removeEventListener('pointerdown', start)
debug.canvas.removeEventListener('pointerup', end)
debug.canvas.removeEventListener('pointermove', drag)
// end
debug.canvas.addEventListener('pointerdown', start)
debug.canvas.addEventListener('pointerup', end)
debug.canvas.addEventListener('pointermove', drag)
}
function updateExternalTexture() {
device.queue.copyExternalImageToTexture(
{ source: interaction.context.canvas, flipY: true },
{ texture: externalInput },
{ width: S, height: S }
);
}
updateExternalTexture()
function render() {
// TIME
// let p = performance.now()
// let now = p - Math.floor(p);
const now = Date.now();
uniforms.set([now], 0);
uniformBuffer.write();
const encoder = device.createCommandEncoder();
const agentPass = encoder.beginComputePass();
agentPass.setPipeline(agentPipeline);
agentPass.setBindGroup(0, agentBindGroupPing);
agentPass.dispatchWorkgroups(Math.ceil(AGENT_COUNT / 64), 1, 1);
agentPass.setBindGroup(0, agentBindGroupPong);
agentPass.dispatchWorkgroups(Math.ceil(AGENT_COUNT / 64), 1, 1);
agentPass.end();
const trailCombinePass = encoder.beginComputePass();
trailCombinePass.setPipeline(trailCombinePipeline);
// for (let i = 0; i < 2; i++) {
trailCombinePass.setBindGroup(0, agentBindGroupPing);
trailCombinePass.dispatchWorkgroups(...DISPATCH_SIZE);
trailCombinePass.setBindGroup(0, agentBindGroupPong);
trailCombinePass.dispatchWorkgroups(...DISPATCH_SIZE);
// }
trailCombinePass.end();
debug.render()
device.queue.submit([encoder.finish()]);
}
render();
return { render, display: debug.canvas, updateExternalTexture }
}