more cleaning up

This commit is contained in:
Andras Schmelczer 2026-05-16 20:45:42 +01:00
commit 560398fefb
110 changed files with 931 additions and 2645 deletions

View file

@ -7,8 +7,6 @@ struct Settings {
struct Counters {
aliveAgentCount: atomic<u32>,
padding0: atomic<u32>,
padding1: atomic<u32>,
};
@group(1) @binding(0) var<uniform> settings: Settings;
@ -22,10 +20,9 @@ var<workgroup> workgroupCopyCount: u32;
@compute @workgroup_size(64)
fn main(
@builtin(global_invocation_id) global_id: vec3<u32>,
@builtin(local_invocation_id) local_id: vec3<u32>,
@builtin(num_workgroups) workgroup_count: vec3<u32>
@builtin(local_invocation_id) local_id: vec3<u32>
) {
let id = get_id(global_id, workgroup_count);
let id = get_id(global_id);
if local_id.x == 0u {
atomicStore(&workgroupAliveCount, 0u);
@ -65,10 +62,9 @@ fn main(
@compute @workgroup_size(64)
fn copyCompactedAgents(
@builtin(global_invocation_id) global_id: vec3<u32>,
@builtin(local_invocation_id) local_id: vec3<u32>,
@builtin(num_workgroups) workgroup_count: vec3<u32>
@builtin(local_invocation_id) local_id: vec3<u32>
) {
let id = get_id(global_id, workgroup_count);
let id = get_id(global_id);
if local_id.x == 0u {
workgroupCopyCount = atomicLoad(&counters.aliveAgentCount);

View file

@ -1,35 +0,0 @@
struct Settings {
agentCount: u32 // might be smaller than the length of the agents array
};
@group(1) @binding(0) var<uniform> settings: Settings;
struct Counters {
redAgentsAlive: atomic<u32>,
greenAgentsAlive: atomic<u32>,
};
@group(1) @binding(2) var<storage, read_write> counters: Counters;
@compute @workgroup_size(64)
fn main(
@builtin(global_invocation_id) global_id: vec3<u32>,
@builtin(num_workgroups) workgroup_count: vec3<u32>
) {
let id = global_id.x + global_id.y * (workgroup_count.x * 64) + global_id.z * (workgroup_count.x * workgroup_count.y * 64);
if id >= settings.agentCount {
return;
}
if agents[id].colorIndex < 0.0 {
return;
}
if agents[id].colorIndex < 0.5 {
atomicAdd(&counters.redAgentsAlive, 1);
} else {
atomicAdd(&counters.greenAgentsAlive, 1);
}
}

View file

@ -1,37 +0,0 @@
@compute @workgroup_size(64)
fn main(
@builtin(global_invocation_id) global_id: vec3<u32>,
@builtin(num_workgroups) workgroup_count: vec3<u32>
) {
let id = get_id(global_id, workgroup_count);
if id >= arrayLength(&agents) {
return;
}
let clusterId = f32(id % 1000);
let random = textureSampleLevel(
noise,
noiseSampler,
vec2(f32(id % 1999) / 2000, f32(id) / 1999 / 2000),
0
);
let randomPosition = textureSampleLevel(
noise,
noiseSampler,
vec2(clusterId / 2000, clusterId / 2000),
0
);
agents[id] = Agent(
randomPosition.xz * state.size,
random.r * 3.14 * 2,
0,
vec2<f32>(-1.0, -1.0),
0.0,
0.0,
);
}

View file

@ -1,6 +1,5 @@
import { describe, expect, it, vi } from 'vitest';
import type { CommonState } from '../../common-state/common-state';
import { AGENT_SIZE_IN_BYTES } from './agent';
import { AgentGenerationPipeline } from './agent-generation-pipeline';
@ -52,7 +51,7 @@ class FakeBuffer {
private readonly mappedRange: ArrayBuffer;
public readonly destroy = vi.fn();
public readonly mapAsync = vi.fn(async (_mode: number) => undefined);
public readonly mapAsync = vi.fn(async () => undefined);
public readonly getMappedRange = vi.fn(() => this.mappedRange);
public readonly unmap = vi.fn();
@ -72,9 +71,7 @@ class FakeComputePass {
public readonly setPipeline = vi.fn((pipeline: GPUComputePipeline) => {
this.pipeline = pipeline as unknown as FakePipeline;
});
public readonly setBindGroup = vi.fn(
(_index: number, _bindGroup: GPUBindGroup) => undefined
);
public readonly setBindGroup = vi.fn(() => undefined);
public readonly dispatchWorkgroups = vi.fn((x: number, y = 1, z = 1) => {
this.device.dispatchCalls.push({
entryPoint: this.pipeline?.entryPoint ?? 'unset',
@ -111,12 +108,8 @@ class FakeCommandEncoder {
}
class FakeQueue {
public readonly writeBuffer = vi.fn(
(_buffer: GPUBuffer, _offset: number, _data: BufferSource) => undefined
);
public readonly submit = vi.fn(
(_commandBuffers: Iterable<GPUCommandBuffer>) => undefined
);
public readonly writeBuffer = vi.fn(() => undefined);
public readonly submit = vi.fn(() => undefined);
}
class FakeShaderModule {
@ -137,9 +130,7 @@ class FakeDevice {
private bufferIndex = 0;
public readonly createBindGroupLayout = vi.fn(
(_descriptor: GPUBindGroupLayoutDescriptor) => ({}) as GPUBindGroupLayout
);
public readonly createBindGroupLayout = vi.fn(() => ({}) as GPUBindGroupLayout);
public readonly createBuffer = vi.fn((descriptor: GPUBufferDescriptor) => {
const label =
['agents', 'compactedAgents', 'counters', 'countersStaging', 'uniforms'][
@ -155,15 +146,10 @@ class FakeDevice {
isMappedReadBuffer ? this.compactedCount : 0
) as unknown as GPUBuffer;
});
public readonly createBindGroup = vi.fn(
(_descriptor: GPUBindGroupDescriptor) => ({}) as GPUBindGroup
);
public readonly createPipelineLayout = vi.fn(
(_descriptor: GPUPipelineLayoutDescriptor) => ({}) as GPUPipelineLayout
);
public readonly createBindGroup = vi.fn(() => ({}) as GPUBindGroup);
public readonly createPipelineLayout = vi.fn(() => ({}) as GPUPipelineLayout);
public readonly createShaderModule = vi.fn(
(_descriptor: GPUShaderModuleDescriptor) =>
new FakeShaderModule() as unknown as GPUShaderModule
() => new FakeShaderModule() as unknown as GPUShaderModule
);
public readonly createComputePipeline = vi.fn(
(descriptor: GPUComputePipelineDescriptor) => {
@ -183,18 +169,10 @@ const createPipeline = (compactedCount: number) => {
installGpuConstants();
const device = new FakeDevice(compactedCount);
const commonState = {
bindGroupLayout: {} as GPUBindGroupLayout,
execute: vi.fn(),
} as unknown as CommonState;
return {
device,
pipeline: new AgentGenerationPipeline(
device as unknown as GPUDevice,
commonState,
1024
),
pipeline: new AgentGenerationPipeline(device as unknown as GPUDevice, 1024),
};
};

View file

@ -1,37 +1,29 @@
import { vec2 } from 'gl-matrix';
import { getWorkgroupCounts } from '../../../utils/graphics/get-workgroup-counts';
import { getWorkgroupCount } from '../../../utils/graphics/get-workgroup-count';
import { smartCompile } from '../../../utils/graphics/smart-compile';
import { CommonState } from '../../common-state/common-state';
import { AGENT_SIZE_IN_BYTES } from './agent';
import compactionShader from './agent-compaction.wgsl?raw';
import countingShader from './agent-counting.wgsl?raw';
import firstGenerationShader from './agent-first-generation.wgsl?raw';
import resizeShader from './agent-resize.wgsl?raw';
import agentSchema from './agent-schema.wgsl?raw';
import { GenerationCounts } from './generation-counts';
export class AgentGenerationPipeline {
private static readonly WORKGROUP_SIZE = 64;
private static readonly UNIFORM_COUNT = 4;
private static readonly COUNTER_COUNT = 3;
private static readonly COUNTER_COUNT = 1;
private readonly bindGroupLayout: GPUBindGroupLayout;
private readonly compactionBindGroupLayout: GPUBindGroupLayout;
private readonly uniforms: GPUBuffer;
private readonly bindGroup: GPUBindGroup;
private readonly compactionBindGroup: GPUBindGroup;
private readonly firstGenerationPipeline: GPUComputePipeline;
private readonly countingPipeline: GPUComputePipeline;
private readonly resizePipeline: GPUComputePipeline;
private readonly compactionPipeline: GPUComputePipeline;
private readonly compactedAgentsCopyPipeline: GPUComputePipeline;
public readonly agentsBuffer: GPUBuffer;
private readonly compactedAgentsBuffer: GPUBuffer;
public readonly countersBuffer: GPUBuffer;
public readonly countersStagingBuffer: GPUBuffer;
private readonly countersBuffer: GPUBuffer;
private readonly countersStagingBuffer: GPUBuffer;
private readonly counterClearValues = new Uint32Array(
AgentGenerationPipeline.COUNTER_COUNT
);
@ -41,36 +33,10 @@ export class AgentGenerationPipeline {
public constructor(
private readonly device: GPUDevice,
private readonly commonState: CommonState,
private readonly maxAgentCountUpperLimit: number
) {
const emptyBindGroupLayout = device.createBindGroupLayout({ entries: [] });
this.bindGroupLayout = device.createBindGroupLayout({
entries: [
{
binding: 0,
visibility: GPUShaderStage.COMPUTE,
buffer: {
type: 'uniform',
},
},
{
binding: 1,
visibility: GPUShaderStage.COMPUTE,
buffer: {
type: 'storage',
},
},
{
binding: 2,
visibility: GPUShaderStage.COMPUTE,
buffer: {
type: 'storage',
},
},
],
});
this.compactionBindGroupLayout = device.createBindGroupLayout({
entries: [
{
binding: 0,
@ -130,30 +96,6 @@ export class AgentGenerationPipeline {
this.bindGroup = this.device.createBindGroup({
layout: this.bindGroupLayout,
entries: [
{
binding: 0,
resource: {
buffer: this.uniforms,
},
},
{
binding: 1,
resource: {
buffer: this.agentsBuffer,
},
},
{
binding: 2,
resource: {
buffer: this.countersBuffer,
},
},
],
});
this.compactionBindGroup = this.device.createBindGroup({
layout: this.compactionBindGroupLayout,
entries: [
{
binding: 0,
@ -182,51 +124,21 @@ export class AgentGenerationPipeline {
],
});
this.firstGenerationPipeline = device.createComputePipeline({
layout: device.createPipelineLayout({
bindGroupLayouts: [commonState.bindGroupLayout, this.bindGroupLayout],
}),
compute: {
module: smartCompile(
device,
CommonState.shaderCode,
agentSchema,
firstGenerationShader
),
entryPoint: 'main',
},
});
this.countingPipeline = device.createComputePipeline({
layout: device.createPipelineLayout({
bindGroupLayouts: [commonState.bindGroupLayout, this.bindGroupLayout],
}),
compute: {
module: smartCompile(device, CommonState.shaderCode, agentSchema, countingShader),
entryPoint: 'main',
},
});
this.resizePipeline = device.createComputePipeline({
layout: device.createPipelineLayout({
bindGroupLayouts: [commonState.bindGroupLayout, this.bindGroupLayout],
bindGroupLayouts: [emptyBindGroupLayout, this.bindGroupLayout],
}),
compute: {
module: smartCompile(device, CommonState.shaderCode, agentSchema, resizeShader),
module: smartCompile(device, agentSchema, resizeShader),
entryPoint: 'main',
},
});
const compactionModule = smartCompile(
device,
CommonState.shaderCode,
agentSchema,
compactionShader
);
const compactionModule = smartCompile(device, agentSchema, compactionShader);
this.compactionPipeline = device.createComputePipeline({
layout: device.createPipelineLayout({
bindGroupLayouts: [commonState.bindGroupLayout, this.compactionBindGroupLayout],
bindGroupLayouts: [emptyBindGroupLayout, this.bindGroupLayout],
}),
compute: {
module: compactionModule,
@ -236,7 +148,7 @@ export class AgentGenerationPipeline {
this.compactedAgentsCopyPipeline = device.createComputePipeline({
layout: device.createPipelineLayout({
bindGroupLayouts: [commonState.bindGroupLayout, this.compactionBindGroupLayout],
bindGroupLayouts: [emptyBindGroupLayout, this.bindGroupLayout],
}),
compute: {
module: compactionModule,
@ -251,7 +163,8 @@ export class AgentGenerationPipeline {
? this.maxAgentCountUpperLimit
: Number.POSITIVE_INFINITY,
Math.floor(this.device.limits.maxBufferSize / AGENT_SIZE_IN_BYTES) - 1,
this.device.limits.maxComputeWorkgroupsPerDimension ** 3
this.device.limits.maxComputeWorkgroupsPerDimension *
AgentGenerationPipeline.WORKGROUP_SIZE
);
}
@ -276,82 +189,16 @@ export class AgentGenerationPipeline {
const commandEncoder = this.device.createCommandEncoder();
const passEncoder = commandEncoder.beginComputePass();
this.commonState.execute(passEncoder);
passEncoder.setPipeline(this.resizePipeline);
passEncoder.setBindGroup(1, this.bindGroup);
passEncoder.dispatchWorkgroups(
...getWorkgroupCounts(
this.device,
agentCount,
AgentGenerationPipeline.WORKGROUP_SIZE
)
getWorkgroupCount(agentCount, AgentGenerationPipeline.WORKGROUP_SIZE)
);
passEncoder.end();
this.device.queue.submit([commandEncoder.finish()]);
}
public spawnFirstGeneration(): void {
const commandEncoder = this.device.createCommandEncoder();
const passEncoder = commandEncoder.beginComputePass();
this.commonState.execute(passEncoder);
passEncoder.setPipeline(this.firstGenerationPipeline);
passEncoder.setBindGroup(1, this.bindGroup);
passEncoder.dispatchWorkgroups(
...getWorkgroupCounts(
this.device,
this.maxAgentCount,
AgentGenerationPipeline.WORKGROUP_SIZE
)
);
passEncoder.end();
this.device.queue.submit([commandEncoder.finish()]);
}
public async countAgents(agentCount: number): Promise<GenerationCounts> {
this.counterClearValues.fill(0);
this.agentCountUniformValues.fill(0);
this.agentCountUniformValues[0] = agentCount;
this.device.queue.writeBuffer(this.countersBuffer, 0, this.counterClearValues);
this.device.queue.writeBuffer(this.uniforms, 0, this.agentCountUniformValues);
const commandEncoder = this.device.createCommandEncoder();
const passEncoder = commandEncoder.beginComputePass();
passEncoder.setPipeline(this.countingPipeline);
this.commonState.execute(passEncoder);
passEncoder.setBindGroup(1, this.bindGroup);
passEncoder.dispatchWorkgroups(
...getWorkgroupCounts(
this.device,
agentCount,
AgentGenerationPipeline.WORKGROUP_SIZE
)
);
passEncoder.end();
commandEncoder.copyBufferToBuffer(
this.countersBuffer,
0,
this.countersStagingBuffer,
0,
AgentGenerationPipeline.COUNTER_COUNT * Uint32Array.BYTES_PER_ELEMENT
);
this.device.queue.submit([commandEncoder.finish()]);
await this.countersStagingBuffer.mapAsync(GPUMapMode.READ);
const data = new Uint32Array(this.countersStagingBuffer.getMappedRange().slice(0));
this.countersStagingBuffer.unmap();
return {
evenGenerationCount: data[0],
oddGenerationCount: data[1],
};
}
public async compactAgents(agentCount: number): Promise<number> {
if (agentCount <= 0) {
return 0;
@ -366,27 +213,17 @@ export class AgentGenerationPipeline {
const commandEncoder = this.device.createCommandEncoder();
const passEncoder = commandEncoder.beginComputePass();
passEncoder.setPipeline(this.compactionPipeline);
this.commonState.execute(passEncoder);
passEncoder.setBindGroup(1, this.compactionBindGroup);
passEncoder.setBindGroup(1, this.bindGroup);
passEncoder.dispatchWorkgroups(
...getWorkgroupCounts(
this.device,
agentCount,
AgentGenerationPipeline.WORKGROUP_SIZE
)
getWorkgroupCount(agentCount, AgentGenerationPipeline.WORKGROUP_SIZE)
);
passEncoder.end();
const copyPassEncoder = commandEncoder.beginComputePass();
copyPassEncoder.setPipeline(this.compactedAgentsCopyPipeline);
this.commonState.execute(copyPassEncoder);
copyPassEncoder.setBindGroup(1, this.compactionBindGroup);
copyPassEncoder.setBindGroup(1, this.bindGroup);
copyPassEncoder.dispatchWorkgroups(
...getWorkgroupCounts(
this.device,
agentCount,
AgentGenerationPipeline.WORKGROUP_SIZE
)
getWorkgroupCount(agentCount, AgentGenerationPipeline.WORKGROUP_SIZE)
);
copyPassEncoder.end();

View file

@ -8,10 +8,9 @@ struct ResizeSettings {
@compute @workgroup_size(64)
fn main(
@builtin(global_invocation_id) global_id: vec3<u32>,
@builtin(num_workgroups) workgroup_count: vec3<u32>
@builtin(global_invocation_id) global_id: vec3<u32>
) {
let id = get_id(global_id, workgroup_count);
let id = get_id(global_id);
if id >= u32(resizeSettings.agentCount) {
return;

View file

@ -2,8 +2,6 @@ import { describe, expect, it } from 'vitest';
import { AGENT_FLOAT_COUNT, AGENT_SIZE_IN_BYTES } from './agent';
import compactionShader from './agent-compaction.wgsl?raw';
import countingShader from './agent-counting.wgsl?raw';
import firstGenerationShader from './agent-first-generation.wgsl?raw';
import resizeShader from './agent-resize.wgsl?raw';
import agentSchema from './agent-schema.wgsl?raw';
@ -60,15 +58,12 @@ describe('Agent TS/WGSL contract', () => {
});
it('keeps generation shader workgroup sizes aligned with agent indexing', () => {
[firstGenerationShader, countingShader, resizeShader, compactionShader].forEach(
(shader) => {
expect(shader).toMatch(/@workgroup_size\(64\)/);
}
);
[resizeShader, compactionShader].forEach((shader) => {
expect(shader).toMatch(/@workgroup_size\(64\)/);
});
expect(agentSchema).toContain('workgroup_count.x * 64');
expect(agentSchema).toContain('workgroup_count.x * workgroup_count.y * 64');
expect(compactionShader).toContain('let id = get_id(global_id, workgroup_count);');
expect(agentSchema).toContain('return global_id.x;');
expect(compactionShader).toContain('let id = get_id(global_id);');
expect(compactionShader).toContain('if id < settings.agentCount');
});

View file

@ -9,6 +9,6 @@ struct Agent {
@group(1) @binding(1) var<storage, read_write> agents: array<Agent>;
fn get_id(global_id: vec3<u32>, workgroup_count: vec3<u32>) -> u32 {
return global_id.x + global_id.y * (workgroup_count.x * 64) + global_id.z * (workgroup_count.x * workgroup_count.y * 64);
}
fn get_id(global_id: vec3<u32>) -> u32 {
return global_id.x;
}

View file

@ -1,4 +0,0 @@
export interface GenerationCounts {
evenGenerationCount: number;
oddGenerationCount: number;
}

View file

@ -2,7 +2,7 @@ import {
createCachedFloat32BufferWrite,
writeFloat32BufferIfChanged,
} from '../../utils/graphics/cached-buffer-write';
import { getWorkgroupCounts } from '../../utils/graphics/get-workgroup-counts';
import { getWorkgroupCount } from '../../utils/graphics/get-workgroup-count';
import { smartCompile } from '../../utils/graphics/smart-compile';
import { CommonState } from '../common-state/common-state';
import agentSchema from './agent-generation/agent-schema.wgsl?raw';
@ -117,7 +117,7 @@ export class AgentPipeline {
this.commonState.execute(passEncoder);
passEncoder.setBindGroup(1, bindGroup);
passEncoder.dispatchWorkgroups(
...getWorkgroupCounts(this.device, this.agentCount, AgentPipeline.WORKGROUP_SIZE)
getWorkgroupCount(this.agentCount, AgentPipeline.WORKGROUP_SIZE)
);
passEncoder.end();
}

View file

@ -25,10 +25,9 @@ struct Settings {
@compute @workgroup_size(64)
fn main(
@builtin(global_invocation_id) global_id: vec3<u32>,
@builtin(num_workgroups) workgroup_count: vec3<u32>
@builtin(global_invocation_id) global_id: vec3<u32>
) {
let id = get_id(global_id, workgroup_count);
let id = get_id(global_id);
if id >= u32(settings.agentCount) {
return;

View file

@ -1,7 +1,6 @@
import { vec2 } from 'gl-matrix';
import { appConfig } from '../../config';
import { clamp } from '../../utils/clamp';
import {
createCachedFloat32BufferWrite,
writeFloat32BufferIfChanged,
@ -25,7 +24,6 @@ export class BrushPipeline {
private readonly bindGroupLayout: GPUBindGroupLayout;
private readonly bindGroup: GPUBindGroup;
private readonly pipeline: GPURenderPipeline;
private readonly multiTargetPipeline: GPURenderPipeline;
private readonly uniforms: GPUBuffer;
private readonly uniformValues = new Float32Array(BrushPipeline.UNIFORM_COUNT);
@ -39,7 +37,6 @@ export class BrushPipeline {
BrushPipeline.ATTRIBUTES_PER_LINE_SEGMENT
);
private linePoints: Array<vec2> = [];
private lineSegments: Array<LineSegment> = [];
private actualSegments: Array<LineSegment> = [];
@ -59,7 +56,6 @@ export class BrushPipeline {
});
const shaderModule = smartCompile(device, CommonState.shaderCode, shader);
this.pipeline = this.createPipeline(shaderModule, 'fragment', 1);
this.multiTargetPipeline = this.createPipeline(shaderModule, 'fragmentMrt', 2);
this.uniforms = this.device.createBuffer({
@ -80,12 +76,6 @@ export class BrushPipeline {
});
}
public addSwipe(position: vec2) {
const previousPosition = this.linePoints[this.linePoints.length - 1] ?? position;
this.addSwipeSegment(previousPosition, position);
this.linePoints.push(vec2.clone(position));
}
public addSwipeSegment(from: vec2, to: vec2) {
this.lineSegments.push({
from: vec2.clone(from),
@ -94,7 +84,6 @@ export class BrushPipeline {
}
public clearSwipes() {
this.linePoints.length = 0;
this.lineSegments.length = 0;
this.actualSegments.length = 0;
}
@ -103,8 +92,7 @@ export class BrushPipeline {
brushSize,
brushSizeVariation,
selectedColorIndex,
isErasing,
}: BrushSettings & { selectedColorIndex: number; isErasing: boolean }) {
}: BrushSettings & { selectedColorIndex: number }) {
const brushRadius = brushSize / 2;
const brushRadiusVariation = Math.floor(brushRadius * brushSizeVariation);
const brushFeather = Math.max(1, brushRadius * BrushPipeline.FEATHER_RADIUS_RATIO);
@ -115,10 +103,10 @@ export class BrushPipeline {
this.uniformValues[1] = brushRadiusVariation;
this.uniformValues[2] = 0;
this.uniformValues[3] = 0;
this.uniformValues[4] = !isErasing && selectedColorIndex === 0 ? 1 : 0;
this.uniformValues[5] = !isErasing && selectedColorIndex === 1 ? 1 : 0;
this.uniformValues[6] = !isErasing && selectedColorIndex === 2 ? 1 : 0;
this.uniformValues[7] = isErasing ? 0 : 1;
this.uniformValues[4] = selectedColorIndex === 0 ? 1 : 0;
this.uniformValues[5] = selectedColorIndex === 1 ? 1 : 0;
this.uniformValues[6] = selectedColorIndex === 2 ? 1 : 0;
this.uniformValues[7] = 1;
writeFloat32BufferIfChanged(
this.device,
this.uniforms,
@ -264,10 +252,6 @@ export class BrushPipeline {
return offset;
}
public execute(commandEncoder: GPUCommandEncoder, trailMapOut: GPUTextureView): void {
this.executeWithPipeline(commandEncoder, this.pipeline, [trailMapOut]);
}
public executeMultiTarget(
commandEncoder: GPUCommandEncoder,
sourceMapOut: GPUTextureView,
@ -392,6 +376,6 @@ export class BrushPipeline {
}
private get lineCount() {
return clamp(this.actualSegments.length, 0, BrushPipeline.MAX_LINE_COUNT);
return this.actualSegments.length;
}
}

View file

@ -1,7 +1,4 @@
export interface BrushSettings {
brushSize: number;
brushCurveResolution: number;
eraserSize: number;
mirrorSegmentCount: number;
brushSizeVariation: number;
}

View file

@ -31,21 +31,6 @@ fn vertex(
return VertexOutput(vec4(position, 0.0, 1.0), screenPosition, start, end);
}
@fragment
fn fragment(
@location(0) screenPosition: vec2<f32>,
@location(1) start: vec2<f32>,
@location(2) end: vec2<f32>
) -> @location(0) vec4<f32> {
let strength = brushStrength(screenPosition, start, end);
if(strength < 0.02) {
discard;
}
return brushOutput(strength);
}
@fragment
fn fragmentMrt(
@location(0) screenPosition: vec2<f32>,

View file

@ -23,8 +23,8 @@ export class CommonState {
public static readonly shaderCode = /* wgsl */ `
struct State {
size: vec2<f32>,
deltaTime: f32,
time: f32,
padding0: f32,
};
@group(0) @binding(0) var<uniform> state: State;
@ -95,19 +95,11 @@ export class CommonState {
});
}
public setParameters({
canvasSize,
deltaTime,
time,
}: {
canvasSize: vec2;
deltaTime: number;
time: number;
}) {
public setParameters({ canvasSize, time }: { canvasSize: vec2; time: number }) {
this.uniformValues[0] = canvasSize[0];
this.uniformValues[1] = canvasSize[1];
this.uniformValues[2] = deltaTime;
this.uniformValues[3] = time;
this.uniformValues[2] = time;
this.uniformValues[3] = 0;
writeFloat32BufferIfChanged(
this.device,
this.uniforms,

View file

@ -53,12 +53,13 @@ export class CopyPipeline {
new Float32Array(this.vertexBuffer.getMappedRange()).set(vertexData);
this.vertexBuffer.unmap();
const shaderModule = smartCompile(device, shader);
this.pipeline = device.createRenderPipeline({
layout: device.createPipelineLayout({
bindGroupLayouts: [this.bindGroupLayout],
}),
vertex: {
module: smartCompile(device, shader),
module: shaderModule,
entryPoint: 'vertex',
buffers: [
{
@ -75,7 +76,7 @@ export class CopyPipeline {
],
},
fragment: {
module: smartCompile(device, shader),
module: shaderModule,
entryPoint: 'fragment',
targets: [
{

View file

@ -6,8 +6,8 @@ struct Settings {
};
@group(1) @binding(0) var<uniform> settings: Settings;
@group(1) @binding(2) var trailMap: texture_2d<f32>;
@group(0) @binding(0) var<uniform> settings: Settings;
@group(0) @binding(2) var trailMap: texture_2d<f32>;
@fragment

View file

@ -34,4 +34,10 @@ describe('diffusion pipeline parameters', () => {
expect(shader).not.toContain('pow(');
expect(shader).not.toContain('noise');
});
it('keeps shader resource groups aligned with the simplified pipeline layout', () => {
expect(shader).toContain('@group(0) @binding(0) var<uniform> settings');
expect(shader).toContain('@group(0) @binding(2) var trailMap');
expect(shader).not.toContain('@group(1)');
});
});

View file

@ -5,7 +5,6 @@ import {
} from '../../utils/graphics/cached-buffer-write';
import { setUpFullScreenQuad } from '../../utils/graphics/full-screen-quad';
import { smartCompile } from '../../utils/graphics/smart-compile';
import { CommonState } from '../common-state/common-state';
import shader from './diffuse.wgsl?raw';
import { DiffusionSettings } from './diffusion-settings';
@ -51,10 +50,7 @@ export class DiffusionPipeline {
private readonly bindGroupsByInput = new WeakMap<GPUTextureView, GPUBindGroup>();
public constructor(
private readonly device: GPUDevice,
private readonly commonState: CommonState
) {
public constructor(private readonly device: GPUDevice) {
this.bindGroupLayout = device.createBindGroupLayout(
DiffusionPipeline.bindGroupLayout
);
@ -64,11 +60,11 @@ export class DiffusionPipeline {
this.pipeline = device.createRenderPipeline({
layout: device.createPipelineLayout({
bindGroupLayouts: [commonState.bindGroupLayout, this.bindGroupLayout],
bindGroupLayouts: [this.bindGroupLayout],
}),
vertex,
fragment: {
module: smartCompile(device, CommonState.shaderCode, shader),
module: smartCompile(device, shader),
entryPoint: 'fragment',
targets: [
{
@ -128,8 +124,7 @@ export class DiffusionPipeline {
const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
passEncoder.setPipeline(this.pipeline);
passEncoder.setVertexBuffer(0, this.vertexBuffer);
this.commonState.execute(passEncoder);
passEncoder.setBindGroup(1, bindGroup);
passEncoder.setBindGroup(0, bindGroup);
passEncoder.draw(4, 1);
passEncoder.end();
}

View file

@ -3,5 +3,4 @@ export interface DiffusionSettings {
decayRateTrails: number;
diffusionRateBrush: number;
decayRateBrush: number;
brushEffectDuration: number;
}

View file

@ -1,18 +1,14 @@
import { vec2 } from 'gl-matrix';
import { appConfig } from '../../config';
import {
createCachedFloat32BufferWrite,
writeFloat32BufferIfChanged,
} from '../../utils/graphics/cached-buffer-write';
import { getWorkgroupCounts } from '../../utils/graphics/get-workgroup-counts';
import { getWorkgroupCount } from '../../utils/graphics/get-workgroup-count';
import { smartCompile } from '../../utils/graphics/smart-compile';
import agentSchema from '../agents/agent-generation/agent-schema.wgsl?raw';
import { CommonState } from '../common-state/common-state';
import shader from './eraser-agent.wgsl?raw';
export class EraserAgentPipeline {
private static readonly WORKGROUP_SIZE = appConfig.pipelines.eraser.workgroupSize;
private static readonly WORKGROUP_SIZE = 64;
private static readonly UNIFORM_COUNT = 4;
private readonly bindGroupLayout: GPUBindGroupLayout;
@ -24,16 +20,15 @@ export class EraserAgentPipeline {
);
private readonly bindGroupsByMask = new WeakMap<GPUTextureView, GPUBindGroup>();
private linePoints: Array<vec2> = [];
private pendingSegmentCount = 0;
private activeSegmentCount = 0;
private agentCount = 0;
public constructor(
private readonly device: GPUDevice,
private readonly commonState: CommonState,
private readonly agentsBuffer: GPUBuffer
) {
const emptyBindGroupLayout = device.createBindGroupLayout({ entries: [] });
this.bindGroupLayout = device.createBindGroupLayout({
entries: [
{
@ -67,41 +62,25 @@ export class EraserAgentPipeline {
this.pipeline = device.createComputePipeline({
layout: device.createPipelineLayout({
bindGroupLayouts: [commonState.bindGroupLayout, this.bindGroupLayout],
bindGroupLayouts: [emptyBindGroupLayout, this.bindGroupLayout],
}),
compute: {
module: smartCompile(device, CommonState.shaderCode, agentSchema, shader),
module: smartCompile(device, agentSchema, shader),
entryPoint: 'main',
},
});
}
public addSwipe(position: vec2): void {
const previousPosition = this.linePoints[this.linePoints.length - 1] ?? position;
this.addSwipeSegment(previousPosition, position);
this.linePoints.push(vec2.clone(position));
}
public addSwipeSegment(from: vec2, to: vec2): void {
void from;
void to;
public addSwipeSegment(): void {
this.pendingSegmentCount += 1;
}
public clearSwipes(): void {
this.linePoints.length = 0;
this.pendingSegmentCount = 0;
this.activeSegmentCount = 0;
}
public setParameters({
agentCount,
eraserSize: _eraserSize,
}: {
agentCount: number;
eraserSize: number;
}): void {
void _eraserSize;
public setParameters({ agentCount }: { agentCount: number }): void {
this.agentCount = agentCount;
this.activeSegmentCount = this.pendingSegmentCount;
this.pendingSegmentCount = 0;
@ -129,14 +108,9 @@ export class EraserAgentPipeline {
const passEncoder = commandEncoder.beginComputePass();
passEncoder.setPipeline(this.pipeline);
this.commonState.execute(passEncoder);
passEncoder.setBindGroup(1, this.getBindGroup(eraserMask));
passEncoder.dispatchWorkgroups(
...getWorkgroupCounts(
this.device,
this.agentCount,
EraserAgentPipeline.WORKGROUP_SIZE
)
getWorkgroupCount(this.agentCount, EraserAgentPipeline.WORKGROUP_SIZE)
);
passEncoder.end();
}

View file

@ -10,10 +10,9 @@ struct Settings {
@compute @workgroup_size(64)
fn main(
@builtin(global_invocation_id) global_id: vec3<u32>,
@builtin(num_workgroups) workgroup_count: vec3<u32>
@builtin(global_invocation_id) global_id: vec3<u32>
) {
let id = get_id(global_id, workgroup_count);
let id = get_id(global_id);
if id >= u32(settings.agentCount) {
return;

View file

@ -1,7 +1,6 @@
import { vec2 } from 'gl-matrix';
import { appConfig } from '../../config';
import { clamp } from '../../utils/clamp';
import {
createCachedFloat32BufferWrite,
writeFloat32BufferIfChanged,
@ -37,7 +36,6 @@ export class EraserTexturePipeline {
EraserTexturePipeline.ATTRIBUTES_PER_LINE_SEGMENT
);
private linePoints: Array<vec2> = [];
private lineSegments: Array<LineSegment> = [];
private actualSegments: Array<LineSegment> = [];
@ -88,12 +86,6 @@ export class EraserTexturePipeline {
});
}
public addSwipe(position: vec2): void {
const previousPosition = this.linePoints[this.linePoints.length - 1] ?? position;
this.addSwipeSegment(previousPosition, position);
this.linePoints.push(vec2.clone(position));
}
public addSwipeSegment(from: vec2, to: vec2): void {
this.lineSegments.push({
from: vec2.clone(from),
@ -102,7 +94,6 @@ export class EraserTexturePipeline {
}
public clearSwipes(): void {
this.linePoints.length = 0;
this.lineSegments.length = 0;
this.actualSegments.length = 0;
}
@ -110,8 +101,8 @@ export class EraserTexturePipeline {
public setParameters({ eraserSize }: { eraserSize: number }): void {
const eraserRadius = eraserSize / 2;
this.uniformValues[0] = eraserRadius;
this.uniformValues[1] = eraserRadius * eraserRadius;
this.uniformValues[0] = eraserRadius * eraserRadius;
this.uniformValues[1] = 0;
this.uniformValues[2] = 0;
this.uniformValues[3] = 0;
writeFloat32BufferIfChanged(
@ -356,6 +347,6 @@ export class EraserTexturePipeline {
}
private get lineCount(): number {
return clamp(this.actualSegments.length, 0, EraserTexturePipeline.MAX_LINE_COUNT);
return this.actualSegments.length;
}
}

View file

@ -1,6 +1,6 @@
struct Settings {
eraserRadius: f32,
eraserRadiusSquared: f32,
padding0: f32,
padding1: f32,
padding2: f32,
};

View file

@ -9,11 +9,10 @@ import { RenderSettings } from './render-settings';
import shader from './render.wgsl?raw';
export class RenderPipeline {
private static readonly UNIFORM_COUNT = 20;
private static readonly UNIFORM_COUNT = 16;
private readonly bindGroupLayout: GPUBindGroupLayout;
private readonly canvasPipeline: GPURenderPipeline;
private readonly exportPipeline: GPURenderPipeline;
private readonly pipeline: GPURenderPipeline;
private readonly sampler: GPUSampler;
private readonly uniforms: GPUBuffer;
private readonly uniformValues = new Float32Array(RenderPipeline.UNIFORM_COUNT);
@ -43,8 +42,7 @@ export class RenderPipeline {
});
const format = navigator.gpu.getPreferredCanvasFormat();
this.canvasPipeline = this.createPipeline(format, vertex);
this.exportPipeline = this.createPipeline(format, vertex);
this.pipeline = this.createPipeline(format, vertex);
this.uniforms = this.device.createBuffer({
size: RenderPipeline.UNIFORM_COUNT * Float32Array.BYTES_PER_ELEMENT,
@ -79,14 +77,10 @@ export class RenderPipeline {
public setParameters({
channelColors,
backgroundColor,
cameraCenter,
cameraZoom,
clarity,
}: RenderSettings & {
channelColors: Array<[number, number, number]>;
backgroundColor: [number, number, number];
cameraCenter: [number, number];
cameraZoom: number;
}) {
const [a, b, c] = channelColors;
this.uniformValues[0] = a[0];
@ -105,10 +99,6 @@ export class RenderPipeline {
this.uniformValues[13] = backgroundColor[1];
this.uniformValues[14] = backgroundColor[2];
this.uniformValues[15] = clarity;
this.uniformValues[16] = cameraCenter[0];
this.uniformValues[17] = cameraCenter[1];
this.uniformValues[18] = cameraZoom;
this.uniformValues[19] = 0;
writeFloat32BufferIfChanged(
this.device,
this.uniforms,
@ -136,7 +126,7 @@ export class RenderPipeline {
],
};
const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
passEncoder.setPipeline(this.canvasPipeline);
passEncoder.setPipeline(this.pipeline);
this.commonState.execute(passEncoder);
passEncoder.setVertexBuffer(0, this.vertexBuffer);
passEncoder.setBindGroup(1, bindGroup);
@ -164,7 +154,7 @@ export class RenderPipeline {
},
],
});
passEncoder.setPipeline(this.exportPipeline);
passEncoder.setPipeline(this.pipeline);
this.commonState.execute(passEncoder);
passEncoder.setVertexBuffer(0, this.vertexBuffer);
passEncoder.setBindGroup(1, bindGroup);

View file

@ -7,9 +7,6 @@ struct Settings {
backgroundColorPadding2: f32,
backgroundColor: vec3<f32>,
clarity: f32,
cameraCenter: vec2<f32>,
cameraZoom: f32,
padding0: f32,
};
@group(1) @binding(0) var<uniform> settings: Settings;
@ -19,10 +16,8 @@ struct Settings {
@fragment
fn fragment(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {
let cameraUv = settings.cameraCenter / state.size;
let viewUv = (uv - vec2(0.5)) / settings.cameraZoom + cameraUv;
let traces = textureSample(trailMap, Sampler, viewUv);
let sources = textureSample(sourceMap, Sampler, viewUv);
let traces = textureSample(trailMap, Sampler, uv);
let sources = textureSample(sourceMap, Sampler, uv);
let traceStrengths = vec3(
clarity(traces.r),

View file

@ -1,7 +1,6 @@
import { describe, expect, it } from 'vitest';
import compactionShader from './agents/agent-generation/agent-compaction.wgsl?raw';
import countingShader from './agents/agent-generation/agent-counting.wgsl?raw';
import { AgentGenerationPipeline } from './agents/agent-generation/agent-generation-pipeline';
import resizeShader from './agents/agent-generation/agent-resize.wgsl?raw';
import { AgentPipeline } from './agents/agent-pipeline';
@ -92,7 +91,7 @@ describe('WGSL uniform layout contracts', () => {
pipeline: CommonState,
source: CommonState.shaderCode,
structName: 'State',
fieldNames: ['size', 'deltaTime', 'time'],
fieldNames: ['size', 'time', 'padding0'],
});
});
@ -157,9 +156,6 @@ describe('WGSL uniform layout contracts', () => {
'backgroundColorPadding2',
'backgroundColor',
'clarity',
'cameraCenter',
'cameraZoom',
'padding0',
],
});
});
@ -175,7 +171,7 @@ describe('WGSL uniform layout contracts', () => {
pipeline: EraserTexturePipeline,
source: eraserTextureShader,
structName: 'Settings',
fieldNames: ['eraserRadius', 'eraserRadiusSquared', 'padding1', 'padding2'],
fieldNames: ['eraserRadiusSquared', 'padding0', 'padding1', 'padding2'],
});
});
@ -190,7 +186,6 @@ describe('WGSL uniform layout contracts', () => {
it('keeps agent-generation uniforms large enough for every generation shader', () => {
const generationUniformCounts = [
countUniformScalars(countingShader, 'Settings'),
countUniformScalars(resizeShader, 'ResizeSettings'),
countUniformScalars(compactionShader, 'Settings'),
];