Allow more agents & dynamic setting of agent count
This commit is contained in:
parent
0f5dd1f820
commit
09e672442b
12 changed files with 145 additions and 50 deletions
|
|
@ -1,4 +1,5 @@
|
|||
export interface GameLoopSettings {
|
||||
maxAgentCountUpperLimit: number;
|
||||
agentCount: number;
|
||||
renderSpeed: number;
|
||||
simulatedDelayMs: number;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
36
src/index.ts
36
src/index.ts
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
11
src/utils/format-number.ts
Normal file
11
src/utils/format-number.ts
Normal 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}`;
|
||||
};
|
||||
28
src/utils/graphics/get-workgroup-counts.ts
Normal file
28
src/utils/graphics/get-workgroup-counts.ts
Normal 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];
|
||||
};
|
||||
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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>) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue