Allow more agents & dynamic setting of agent count

This commit is contained in:
Andras Schmelczer 2023-05-27 13:07:39 +01:00
parent 0f5dd1f820
commit 09e672442b
No known key found for this signature in database
GPG key ID: FC8F2C3D3D1A718C
12 changed files with 145 additions and 50 deletions

View file

@ -1,4 +1,5 @@
export interface GameLoopSettings {
maxAgentCountUpperLimit: number;
agentCount: number;
renderSpeed: number;
simulatedDelayMs: number;

View file

@ -59,7 +59,7 @@ export default class GameLoop {
this.agentGenerationPipeline = new AgentGenerationPipeline(
this.device,
this.commonState,
settings.agentCount
settings.maxAgentCountUpperLimit
);
this.agentGenerationPipeline.spawnFirstGeneration();
@ -117,7 +117,9 @@ export default class GameLoop {
if (this.hasFinished) {
return;
}
const generationCounts = await this.agentGenerationPipeline.countAgents();
const generationCounts = await this.agentGenerationPipeline.countAgents(
settings.agentCount
);
this.gameRules.updateGenerationCounts(generationCounts);
requestAnimationFrame(this.updateCounts.bind(this));
}
@ -168,7 +170,7 @@ export default class GameLoop {
);
document.documentElement.style.setProperty(
'--accent-color',
`rgb(${accentColor.map((v) => v * 255).join(',')})`
`rgb(${accentColor.map((v: number) => v * 255).join(',')})`
);
const deltaTime = this.deltaTimeCalculator.calculateDeltaTimeInSeconds(time);

View file

@ -2,12 +2,14 @@ import '../assets/icons/info.svg';
import GameLoop from './game-loop/game-loop';
import { GameRules } from './game-loop/game-rules';
import './index.scss';
import { CollapsiblePanelAnimator } from './page/collapsible-panel-animator';
import { FullScreenHandler } from './page/full-screen-handler';
import { InfoPageHandler } from './page/info-page-handler';
import { MenuHider } from './page/menu-hider';
import { setUpSettingsPage } from './page/set-up-settings-page';
import { applyArrayPlugins } from './utils/array';
import { DeltaTimeCalculator } from './utils/delta-time-calculator';
import { ErrorHandler, Severity } from './utils/error-handler';
import { formatNumber } from './utils/format-number';
import { initializeGpu } from './utils/graphics/initialize-gpu';
declare global {
@ -30,13 +32,15 @@ declare global {
const getElements = () => ({
aside: document.querySelector('aside') as HTMLDivElement,
infoButton: document.querySelector('button.info') as HTMLButtonElement,
infoElement: document.querySelector('.pages') as HTMLDivElement,
infoElement: document.querySelector('.info-page') as HTMLDivElement,
settingsPage: document.querySelector('.settings-page') as HTMLDivElement,
minimizeFullScreenButton: document.querySelector(
'button.minimize-full-screen'
) as HTMLButtonElement,
maximizeFullScreenButton: document.querySelector(
'button.maximize-full-screen'
) as HTMLButtonElement,
settingsButton: document.querySelector('button.settings') as HTMLButtonElement,
restartButton: document.querySelector('button.restart') as HTMLButtonElement,
canvas: document.querySelector('canvas') as HTMLCanvasElement,
canvasContainer: document.querySelector('main.canvas-container') as HTMLCanvasElement,
@ -61,7 +65,17 @@ const main = async () => {
try {
applyArrayPlugins();
new InfoPageHandler(elements.infoButton, elements.infoElement);
const infoPageHandler = new CollapsiblePanelAnimator(
elements.infoButton,
elements.infoElement
);
const settingsPageHandler = new CollapsiblePanelAnimator(
elements.settingsButton,
elements.settingsPage
);
settingsPageHandler.onOpen = infoPageHandler.close.bind(infoPageHandler);
infoPageHandler.onOpen = settingsPageHandler.close.bind(settingsPageHandler);
new MenuHider(elements.aside, FullScreenHandler.isInFullScreenMode);
new FullScreenHandler(
elements.minimizeFullScreenButton,
@ -75,22 +89,30 @@ const main = async () => {
const deltaTimeCalculator = new DeltaTimeCalculator();
const gameRules = new GameRules(performance.now() / 1000);
let isSettingsPageSetUp = false;
console.log(gameRules.nextGenerationId);
const updateCounters = () => {
elements.counters.innerHTML = `FPS: ${deltaTimeCalculator.fps.toFixed(2)}
current gen: ${game?.aliveAgentCounts.currentGenerationCount ?? 0}
next gen: ${game?.aliveAgentCounts.nextGenerationCount ?? 0}`;
current gen: ${formatNumber(game?.aliveAgentCounts.currentGenerationCount ?? 0)}
next gen: ${formatNumber(game?.aliveAgentCounts.nextGenerationCount ?? 0)}`;
window.requestAnimationFrame(updateCounters);
};
updateCounters();
while (!shouldStop) {
game = new GameLoop(elements.canvas, gpu, deltaTimeCalculator, gameRules);
if (!isSettingsPageSetUp) {
isSettingsPageSetUp = true;
setUpSettingsPage(elements.settingsPage, game.maxAgentCount, () =>
game?.destroy()
);
}
await game.start();
}
} catch (e) {
ErrorHandler.addError(Severity.ERROR, e.message);
ErrorHandler.addError(Severity.ERROR, e.stack);
console.error(e);
}
};

View file

@ -1,15 +1,25 @@
struct Settings {
agentCount: u32 // might be smaller than the length of the agents array
};
@group(1) @binding(0) var<uniform> settings: Settings;
struct Counters {
evenGenerationAlive: atomic<i32>,
oddGenerationAlive: atomic<i32>,
evenGenerationAlive: atomic<u32>,
oddGenerationAlive: 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>) {
let id = global_id.x;
if id >= arrayLength(&agents) {
@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;
}
@ -18,4 +28,7 @@ fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
} else {
atomicAdd(&counters.oddGenerationAlive, 1);
}
// atomicStore(&counters.evenGenerationAlive, settings.agentCount);
// atomicStore(&counters.oddGenerationAlive, workgroup_count.y);
}

View file

@ -1,6 +1,9 @@
@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
let id = global_id.x;
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 >= arrayLength(&agents) {
return;

View file

@ -1,3 +1,4 @@
import { getWorkgroupCounts } from '../../../utils/graphics/get-workgroup-counts';
import random from '../../../utils/graphics/random.wgsl';
import { smartCompile } from '../../../utils/graphics/smart-compile';
import { CommonState } from '../../common-state/common-state';
@ -9,7 +10,7 @@ import { GenerationCounts } from './generation-counts';
export class AgentGenerationPipeline {
private static readonly WORKGROUP_SIZE = 64;
private static readonly UNIFORM_COUNT = 4;
private static readonly UNIFORM_COUNT = 1;
private static readonly COUNTER_COUNT = 3;
private readonly bindGroupLayout: GPUBindGroupLayout;
@ -60,17 +61,17 @@ export class AgentGenerationPipeline {
});
this.countersBuffer = this.device.createBuffer({
size: AgentGenerationPipeline.COUNTER_COUNT * Int32Array.BYTES_PER_ELEMENT,
size: AgentGenerationPipeline.COUNTER_COUNT * Uint32Array.BYTES_PER_ELEMENT,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC,
});
this.countersStagingBuffer = this.device.createBuffer({
size: AgentGenerationPipeline.COUNTER_COUNT * Int32Array.BYTES_PER_ELEMENT,
size: AgentGenerationPipeline.COUNTER_COUNT * Uint32Array.BYTES_PER_ELEMENT,
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
});
this.uniforms = this.device.createBuffer({
size: AgentGenerationPipeline.UNIFORM_COUNT * Float32Array.BYTES_PER_ELEMENT,
size: AgentGenerationPipeline.UNIFORM_COUNT * Uint32Array.BYTES_PER_ELEMENT,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
@ -134,7 +135,8 @@ export class AgentGenerationPipeline {
public get maxAgentCount(): number {
return Math.min(
this.maxAgentCountUpperLimit,
Math.floor(this.device.limits.maxBufferSize / AGENT_SIZE_IN_BYTES)
Math.floor(this.device.limits.maxBufferSize / AGENT_SIZE_IN_BYTES),
this.device.limits.maxComputeWorkgroupsPerDimension ** 3
);
}
@ -146,15 +148,20 @@ export class AgentGenerationPipeline {
passEncoder.setPipeline(this.firstGenerationPipeline);
passEncoder.setBindGroup(1, this.bindGroup);
passEncoder.dispatchWorkgroups(
Math.ceil(this.maxAgentCount / AgentGenerationPipeline.WORKGROUP_SIZE)
...getWorkgroupCounts(
this.device,
this.maxAgentCount,
AgentGenerationPipeline.WORKGROUP_SIZE
)
);
passEncoder.end();
this.device.queue.submit([commandEncoder.finish()]);
}
public async countAgents(): Promise<GenerationCounts> {
this.device.queue.writeBuffer(this.countersBuffer, 0, new Int32Array([0, 0]));
public async countAgents(agentCount: number): Promise<GenerationCounts> {
this.device.queue.writeBuffer(this.countersBuffer, 0, new Uint32Array([0, 0]));
this.device.queue.writeBuffer(this.uniforms, 0, new Uint32Array([agentCount]));
const commandEncoder = this.device.createCommandEncoder();
@ -163,7 +170,11 @@ export class AgentGenerationPipeline {
this.commonState.execute(passEncoder);
passEncoder.setBindGroup(1, this.bindGroup);
passEncoder.dispatchWorkgroups(
Math.ceil(this.maxAgentCount / AgentGenerationPipeline.WORKGROUP_SIZE)
...getWorkgroupCounts(
this.device,
agentCount,
AgentGenerationPipeline.WORKGROUP_SIZE
)
);
passEncoder.end();
@ -172,14 +183,14 @@ export class AgentGenerationPipeline {
0,
this.countersStagingBuffer,
0,
AgentGenerationPipeline.COUNTER_COUNT * Int32Array.BYTES_PER_ELEMENT
AgentGenerationPipeline.COUNTER_COUNT * Uint32Array.BYTES_PER_ELEMENT
);
this.device.queue.submit([commandEncoder.finish()]);
await this.countersStagingBuffer.mapAsync(GPUMapMode.READ);
const data = new Int32Array(this.countersStagingBuffer.getMappedRange().slice(0));
const data = new Uint32Array(this.countersStagingBuffer.getMappedRange().slice(0));
this.countersStagingBuffer.unmap();
return {
evenGenerationCount: data[0],

View file

@ -1,7 +1,7 @@
import { getWorkgroupCounts } from '../../utils/graphics/get-workgroup-counts';
import random from '../../utils/graphics/random.wgsl';
import { smartCompile } from '../../utils/graphics/smart-compile';
import { CommonState } from '../common-state/common-state';
import { AGENT_SIZE_IN_BYTES } from './agent-generation/agent';
import agentSchme from './agent-generation/agent-schema.wgsl';
import { AgentSettings } from './agent-settings';
import shader from './agent.wgsl';
@ -10,7 +10,7 @@ import { vec2 } from 'gl-matrix';
export class AgentPipeline {
private static readonly WORKGROUP_SIZE = 64;
private static readonly UNIFORM_COUNT = 16;
private static readonly UNIFORM_COUNT = 17;
private readonly bindGroupLayout: GPUBindGroupLayout;
private readonly pipeline: GPUComputePipeline;
@ -19,6 +19,8 @@ export class AgentPipeline {
private previousTrailMapIn?: GPUTextureView;
private previousTrailMapOut?: GPUTextureView;
private agentCount = 0;
public constructor(
private readonly device: GPUDevice,
private readonly commonState: CommonState,
@ -57,13 +59,16 @@ export class AgentPipeline {
turnWhenLost,
individualTrailWeight,
deinfectionProbability,
agentCount,
}: AgentSettings & {
evenGenerationAggression: number;
oddGenerationAggression: number;
nextGenerationId: number;
center: vec2;
radius: number;
agentCount: number;
}) {
this.agentCount = agentCount;
this.device.queue.writeBuffer(
this.uniforms,
0,
@ -82,6 +87,7 @@ export class AgentPipeline {
turnWhenLost,
individualTrailWeight,
deinfectionProbability,
agentCount,
])
);
}
@ -98,9 +104,7 @@ export class AgentPipeline {
this.commonState.execute(passEncoder);
passEncoder.setBindGroup(1, this.bindGroup);
passEncoder.dispatchWorkgroups(
Math.ceil(
this.agentsBuffer.size / AGENT_SIZE_IN_BYTES / AgentPipeline.WORKGROUP_SIZE
)
...getWorkgroupCounts(this.device, this.agentCount, AgentPipeline.WORKGROUP_SIZE)
);
passEncoder.end();
}

View file

@ -17,7 +17,9 @@ struct Settings {
turnWhenGoingInTheRightDirection: f32,
turnWhenLost: f32,
individualTrailWeight: f32,
deinfectionProbability: f32
deinfectionProbability: f32,
agentCount: f32 // might be smaller than the length of the agents array
};
@ -31,10 +33,13 @@ struct Settings {
@group(1) @binding(3) var trailMapOut: texture_storage_2d<rgba16float, write>;
@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
let id = global_id.x;
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 >= arrayLength(&agents)) {
if id >= u32(settings.agentCount) {
return;
}

View file

@ -0,0 +1,11 @@
export const formatNumber = (value: number, unit = ''): string => {
if (value >= 1e6) {
return `${(value / 1e6).toFixed(1)} million ${unit}`;
}
if (value >= 1e3) {
return `${(value / 1e3).toFixed(1)} thousand ${unit}`;
}
return `${value === Math.floor(value) ? value : value.toFixed(2)}${unit}`;
};

View file

@ -0,0 +1,28 @@
export const getWorkgroupCounts = (
device: GPUDevice,
invocationCount: number,
workgroupSize: number
): [number, number, number] => {
const workgroupCount = Math.ceil(invocationCount / workgroupSize);
const workgroupCountX = Math.min(
device.limits.maxComputeWorkgroupsPerDimension,
workgroupCount
);
const workgroupCountY = Math.min(
device.limits.maxComputeWorkgroupsPerDimension,
Math.ceil(workgroupCount / workgroupCountX)
);
const workgroupCountZ = Math.min(
device.limits.maxComputeWorkgroupsPerDimension,
Math.ceil(workgroupCount / workgroupCountX / workgroupCountY)
);
if (workgroupCountX * workgroupCountY * workgroupCountZ < workgroupCount) {
throw new Error('Cannot have this many invocations');
}
return [workgroupCountX, workgroupCountY, workgroupCountZ];
};

View file

@ -22,10 +22,7 @@ export const initializeGpu = async (): Promise<GPUDevice> => {
requiredLimits: {
maxBufferSize: adapter.limits.maxBufferSize,
maxStorageBufferBindingSize: adapter.limits.maxStorageBufferBindingSize,
maxComputeInvocationsPerWorkgroup: adapter.limits.maxComputeInvocationsPerWorkgroup,
maxComputeWorkgroupSizeX: adapter.limits.maxComputeWorkgroupSizeX,
maxComputeWorkgroupSizeY: adapter.limits.maxComputeWorkgroupSizeY,
maxComputeWorkgroupSizeZ: adapter.limits.maxComputeWorkgroupSizeZ,
maxComputeWorkgroupsPerDimension: adapter.limits.maxComputeWorkgroupsPerDimension,
},
});

View file

@ -1,3 +1,5 @@
import { formatNumber } from './format-number';
export interface SliderConfiguration {
min: number;
max: number;
@ -72,17 +74,13 @@ export class SettingsSlider<T extends Record<string, number>> {
);
}
private get formattedValue(): string {
const value = this.settings[this.settingName];
const unit = this.config.unit ?? '';
return `${value === Math.floor(value) ? value : value.toFixed(2)}${unit}`;
}
private onChange() {
this.settings[this.settingName] = Number(this.slider.value) as any;
this.config.onChangeCallback?.(this.settings[this.settingName]);
this.valueDisplay.innerText = this.formattedValue;
this.valueDisplay.innerText = formatNumber(
this.settings[this.settingName],
this.config.unit
);
}
public updateConfig(config: Partial<SliderConfiguration>) {