Optimise
All checks were successful
Check & deploy / build (pull_request) Successful in 1m51s

This commit is contained in:
Andras Schmelczer 2026-05-21 20:33:49 +01:00
commit ed5a4379db
76 changed files with 1418 additions and 988 deletions

View file

@ -2,14 +2,16 @@ import { vec2 } from 'gl-matrix';
import { createBindGroupCache } from '../../../utils/graphics/bind-group-cache';
import { smartCompile } from '../../../utils/graphics/smart-compile';
import { dispatchAgentWorkgroups } from '../agent-dispatch';
import {
dispatchAgentWorkgroups,
getAgentWorkgroupSize,
substituteAgentWorkgroupSize,
} from '../agent-dispatch';
import { AGENT_SIZE_IN_BYTES, getMaxSupportedAgentCount } from '../agent-limits';
import compactionShader from './agent-compaction.wgsl?raw';
import resizeShader from './agent-resize.wgsl?raw';
import agentSchema from './agent-schema.wgsl?raw';
export { AGENT_FLOAT_COUNT } from '../agent-limits';
export class AgentGenerationPipeline {
private static readonly UNIFORM_COUNT = 4;
private static readonly COUNTER_COUNT = 1;
@ -34,6 +36,7 @@ export class AgentGenerationPipeline {
private readonly resizePipeline: GPUComputePipeline;
private readonly compactionPipeline: GPUComputePipeline;
private readonly clearCompactedTailPipeline: GPUComputePipeline;
private readonly workgroupSize: number;
private activeAgentsBuffer: GPUBuffer;
private inactiveAgentsBuffer: GPUBuffer;
@ -90,7 +93,7 @@ export class AgentGenerationPipeline {
});
this.activeAgentsBuffer = this.createAgentsBuffer();
this.inactiveAgentsBuffer = this.createInactivePlaceholderBuffer();
this.inactiveAgentsBuffer = this.createAgentsBuffer();
this.countersBuffer = this.device.createBuffer({
size: AgentGenerationPipeline.COUNTER_COUNT * Uint32Array.BYTES_PER_ELEMENT,
@ -107,17 +110,20 @@ export class AgentGenerationPipeline {
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
this.workgroupSize = getAgentWorkgroupSize(device);
const sizedSchema = substituteAgentWorkgroupSize(device, agentSchema);
this.resizePipeline = device.createComputePipeline({
layout: device.createPipelineLayout({
bindGroupLayouts: [emptyBindGroupLayout, this.bindGroupLayout],
}),
compute: {
module: smartCompile(device, agentSchema, resizeShader),
module: smartCompile(device, sizedSchema, resizeShader),
entryPoint: 'main',
},
});
const compactionModule = smartCompile(device, agentSchema, compactionShader);
const compactionModule = smartCompile(device, sizedSchema, compactionShader);
this.compactionPipeline = device.createComputePipeline({
layout: device.createPipelineLayout({
@ -151,16 +157,6 @@ export class AgentGenerationPipeline {
});
}
// The inactive slot only needs a real allocation during compaction. The rest of
// the time we keep a one-agent placeholder so the bind group at binding 3 stays
// valid for resize without holding a second N-agent buffer in GPU memory.
private createInactivePlaceholderBuffer(): GPUBuffer {
return this.device.createBuffer({
size: AGENT_SIZE_IN_BYTES,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC,
});
}
public get maxAgentCount(): number {
return this.allocatedMaxAgentCount;
}
@ -187,9 +183,11 @@ export class AgentGenerationPipeline {
)
);
const previousActiveAgentsBuffer = this.activeAgentsBuffer;
const previousInactiveAgentsBuffer = this.inactiveAgentsBuffer;
const previousMaxAgentCount = this.allocatedMaxAgentCount;
this.allocatedMaxAgentCount = nextMaxAgentCount;
this.activeAgentsBuffer = this.createAgentsBuffer();
this.inactiveAgentsBuffer = this.createAgentsBuffer();
const copyAgentCount = Math.min(
Math.max(0, Math.floor(activeAgentCount)),
@ -209,10 +207,9 @@ export class AgentGenerationPipeline {
}
// GPUBuffer.destroy() defers actual freeing until pending submissions
// finish, so calling it synchronously after submit is safe and avoids the
// transient 4-buffers-live spike that pushes iOS Safari past its per-tab
// memory ceiling.
// finish, so calling it synchronously after submit is safe.
previousActiveAgentsBuffer.destroy();
previousInactiveAgentsBuffer.destroy();
return this.allocatedMaxAgentCount;
}
@ -251,7 +248,7 @@ export class AgentGenerationPipeline {
const passEncoder = commandEncoder.beginComputePass();
passEncoder.setPipeline(this.resizePipeline);
passEncoder.setBindGroup(1, this.getBindGroup());
dispatchAgentWorkgroups(passEncoder, agentCount);
dispatchAgentWorkgroups(passEncoder, this.workgroupSize, agentCount);
passEncoder.end();
this.device.queue.submit([commandEncoder.finish()]);
@ -262,12 +259,6 @@ export class AgentGenerationPipeline {
return 0;
}
// Stash the placeholder, swap in a real N-agent destination buffer just
// for this compaction so the rest of the time we only carry one full
// agent buffer in memory.
const placeholder = this.inactiveAgentsBuffer;
this.inactiveAgentsBuffer = this.createAgentsBuffer();
this.agentCountUniformValues[0] = agentCount;
this.device.queue.writeBuffer(this.uniforms, 0, this.agentCountUniformValues);
@ -276,10 +267,11 @@ export class AgentGenerationPipeline {
const passEncoder = commandEncoder.beginComputePass();
passEncoder.setPipeline(this.compactionPipeline);
passEncoder.setBindGroup(1, this.getBindGroup());
dispatchAgentWorkgroups(passEncoder, agentCount);
dispatchAgentWorkgroups(passEncoder, this.workgroupSize, agentCount);
passEncoder.setPipeline(this.clearCompactedTailPipeline);
dispatchAgentWorkgroups(
passEncoder,
this.workgroupSize,
Math.ceil(agentCount / AgentGenerationPipeline.CLEAR_COMPACTED_TAIL_STRIDE)
);
passEncoder.end();
@ -295,13 +287,6 @@ export class AgentGenerationPipeline {
this.device.queue.submit([commandEncoder.finish()]);
this.swapAgentBuffers();
// After swap, inactive is the previous active (full size). Destroy it and
// restore the placeholder; the destroy is deferred by WebGPU until the
// submitted compaction work has finished.
const previousActiveAgentsBuffer = this.inactiveAgentsBuffer;
this.inactiveAgentsBuffer = placeholder;
previousActiveAgentsBuffer.destroy();
await this.countersStagingBuffer.mapAsync(GPUMapMode.READ);
const compactedCount = new Uint32Array(
this.countersStagingBuffer.getMappedRange(),