lgtm
This commit is contained in:
parent
ce383ce34c
commit
d2da0d1617
25 changed files with 531 additions and 1036 deletions
|
|
@ -1,6 +1,11 @@
|
|||
import { vec2 } from 'gl-matrix';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { appConfig } from '../config';
|
||||
import { AgentGenerationPipeline } from '../pipelines/agents/agent-generation/agent-generation-pipeline';
|
||||
import { settings } from '../settings';
|
||||
import { AgentPopulation } from './agent-population';
|
||||
|
||||
vi.hoisted(() => {
|
||||
Object.defineProperty(globalThis, 'localStorage', {
|
||||
configurable: true,
|
||||
|
|
@ -11,11 +16,6 @@ vi.hoisted(() => {
|
|||
});
|
||||
});
|
||||
|
||||
import { appConfig } from '../config';
|
||||
import { AgentGenerationPipeline } from '../pipelines/agents/agent-generation/agent-generation-pipeline';
|
||||
import { settings } from '../settings';
|
||||
import { AgentPopulation } from './agent-population';
|
||||
|
||||
const originalAgentBudgetMax = settings.agentBudgetMax;
|
||||
const originalBrushSize = settings.brushSize;
|
||||
const originalSelectedColorIndex = settings.selectedColorIndex;
|
||||
|
|
@ -63,10 +63,35 @@ describe('AgentPopulation adaptive budget', () => {
|
|||
expect(settings.agentBudgetMax).toBeGreaterThan(1_000_000);
|
||||
expect(population.activeAgentCount).toBeGreaterThan(1_000_000);
|
||||
expect(settings.agentBudgetMax).toBeLessThanOrEqual(
|
||||
appConfig.simulation.globalAgentCap
|
||||
appConfig.simulation.budget.adaptiveCapMax
|
||||
);
|
||||
});
|
||||
|
||||
it('does not grow the cap above the adaptive max agent count', () => {
|
||||
const population = createPopulation();
|
||||
const maxAgentCount = appConfig.simulation.budget.adaptiveCapMax;
|
||||
settings.agentBudgetMax = maxAgentCount - 1;
|
||||
setPopulationActiveCount(population, maxAgentCount - 1);
|
||||
|
||||
population.growBudget(1 / 60, 60, 60);
|
||||
population.spawnStrokeAgents(vec2.fromValues(0, 0), vec2.fromValues(16, 0));
|
||||
|
||||
expect(settings.agentBudgetMax).toBe(maxAgentCount);
|
||||
expect(population.activeAgentCount).toBe(maxAgentCount);
|
||||
});
|
||||
|
||||
it('clamps a manually raised cap before adding agents', () => {
|
||||
const population = createPopulation();
|
||||
const maxAgentCount = appConfig.simulation.budget.adaptiveCapMax;
|
||||
settings.agentBudgetMax = maxAgentCount + 1_000;
|
||||
setPopulationActiveCount(population, maxAgentCount);
|
||||
|
||||
population.spawnStrokeAgents(vec2.fromValues(0, 0), vec2.fromValues(16, 0));
|
||||
|
||||
expect(settings.agentBudgetMax).toBe(maxAgentCount);
|
||||
expect(population.activeAgentCount).toBe(maxAgentCount);
|
||||
});
|
||||
|
||||
it('decreases the cap and active count slowly when FPS falls below the threshold', () => {
|
||||
const population = createPopulation();
|
||||
setPopulationActiveCount(population, 1_000_000);
|
||||
|
|
@ -74,8 +99,6 @@ describe('AgentPopulation adaptive budget', () => {
|
|||
population.growBudget(10, 50, 60);
|
||||
|
||||
expect(settings.agentBudgetMax).toBe(appConfig.simulation.budget.adaptiveCapMin);
|
||||
expect(population.activeAgentCount).toBe(
|
||||
appConfig.simulation.budget.adaptiveCapMin
|
||||
);
|
||||
expect(population.activeAgentCount).toBe(appConfig.simulation.budget.adaptiveCapMin);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ const INITIAL_AGENT_COUNT = appConfig.simulation.initialAgentCount;
|
|||
const MIN_STROKE_AGENT_COUNT = appConfig.simulation.stroke.minAgentCount;
|
||||
const MAX_STROKE_AGENT_COUNT = appConfig.simulation.stroke.maxAgentCount;
|
||||
const STROKE_AGENT_DENSITY_MULTIPLIER = appConfig.simulation.stroke.densityMultiplier;
|
||||
const ADAPTIVE_CAP_MAX = appConfig.simulation.budget.adaptiveCapMax;
|
||||
const ADAPTIVE_CAP_MIN = appConfig.simulation.budget.adaptiveCapMin;
|
||||
const ADAPTIVE_CAP_DECREASE_AGENTS_PER_SECOND =
|
||||
appConfig.simulation.budget.adaptiveCapDecreaseAgentsPerSecond;
|
||||
|
|
@ -129,6 +130,7 @@ export class AgentPopulation {
|
|||
}
|
||||
|
||||
const count = data.length / AGENT_FLOAT_COUNT;
|
||||
settings.agentBudgetMax = this.clampAdaptiveCap(settings.agentBudgetMax);
|
||||
this.expandAdaptiveCapForPendingAgents(count);
|
||||
|
||||
const available = Math.max(0, settings.agentBudgetMax - this.activeCount);
|
||||
|
|
@ -214,8 +216,9 @@ export class AgentPopulation {
|
|||
|
||||
private clampAdaptiveCap(value: number): number {
|
||||
const pipelineCap = Math.max(0, Math.floor(this.pipeline.maxAgentCount));
|
||||
const minCap = Math.min(ADAPTIVE_CAP_MIN, pipelineCap);
|
||||
const maxCap = Math.min(ADAPTIVE_CAP_MAX, pipelineCap);
|
||||
const minCap = Math.min(ADAPTIVE_CAP_MIN, maxCap);
|
||||
const finiteValue = Number.isFinite(value) ? value : minCap;
|
||||
return Math.min(pipelineCap, Math.max(minCap, Math.round(finiteValue)));
|
||||
return Math.min(maxCap, Math.max(minCap, Math.round(finiteValue)));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,5 @@
|
|||
import { appConfig } from '../config';
|
||||
|
||||
interface TelemetrySnapshot {
|
||||
frameCpuStartedAt: number;
|
||||
encodeCpuMs: number;
|
||||
activeAgentCount: number;
|
||||
agentBudgetMax: number;
|
||||
canvas: HTMLCanvasElement;
|
||||
devicePixelRatio: number;
|
||||
}
|
||||
|
||||
const COMMON_DISPLAY_REFRESH_RATES = [
|
||||
50, 60, 72, 75, 90, 100, 120, 144, 165, 180, 240,
|
||||
] as const;
|
||||
|
|
@ -22,20 +13,11 @@ export class FramePerformance {
|
|||
public displayRefreshFps = 60;
|
||||
public readonly refreshTargetFps = 60;
|
||||
|
||||
private lastTelemetryAt = 0;
|
||||
private previousFrameTime: DOMHighResTimeStamp | null = null;
|
||||
private hasConfirmedDisplayRefreshFps = false;
|
||||
private pendingDisplayRefreshFps = 0;
|
||||
private pendingDisplayRefreshFrameCount = 0;
|
||||
|
||||
public markCpuStart(): number {
|
||||
return appConfig.telemetry.enabled ? performance.now() : 0;
|
||||
}
|
||||
|
||||
public measureSince(startedAt: number): number {
|
||||
return appConfig.telemetry.enabled ? performance.now() - startedAt : 0;
|
||||
}
|
||||
|
||||
public update(time: DOMHighResTimeStamp): void {
|
||||
const previous = this.previousFrameTime;
|
||||
this.previousFrameTime = time;
|
||||
|
|
@ -56,39 +38,6 @@ export class FramePerformance {
|
|||
fps * appConfig.simulation.budget.fpsSmoothingNew;
|
||||
}
|
||||
|
||||
public renderTelemetry({
|
||||
frameCpuStartedAt,
|
||||
encodeCpuMs,
|
||||
activeAgentCount,
|
||||
agentBudgetMax,
|
||||
canvas,
|
||||
devicePixelRatio,
|
||||
}: TelemetrySnapshot): void {
|
||||
if (!appConfig.telemetry.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const now = performance.now();
|
||||
if (now - this.lastTelemetryAt < appConfig.telemetry.intervalMs) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastTelemetryAt = now;
|
||||
console.debug('Fleeting Garden telemetry', {
|
||||
fps: Math.round(this.latestFps),
|
||||
smoothedFps: Math.round(this.smoothedFps),
|
||||
refreshTargetFps: Math.round(this.refreshTargetFps),
|
||||
displayRefreshFps: Math.round(this.displayRefreshFps),
|
||||
activeAgentCount,
|
||||
agentBudgetMax,
|
||||
canvasWidth: canvas.width,
|
||||
canvasHeight: canvas.height,
|
||||
dpr: devicePixelRatio,
|
||||
frameCpuMs: now - frameCpuStartedAt,
|
||||
encodeCpuMs,
|
||||
});
|
||||
}
|
||||
|
||||
private updateDisplayRefreshEstimate(fps: number): void {
|
||||
const displayRefreshFps = this.snapDisplayRefreshRate(fps);
|
||||
if (displayRefreshFps === null) {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,4 @@ export interface GameLoopSettings {
|
|||
simulatedDelayMs: number;
|
||||
selectedColorIndex: number;
|
||||
spawnPerPixel: number;
|
||||
|
||||
startColorHue: number;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,11 +23,7 @@ export default class GameLoop {
|
|||
private static readonly DEV_STATS_INTERVAL_MS = 250;
|
||||
|
||||
private readonly resources: GameLoopResources;
|
||||
private readonly audio = new GardenAudio(
|
||||
gardenAudioConfig,
|
||||
appConfig.audioEngine,
|
||||
appConfig.simulation.maxMirrorSegmentCount
|
||||
);
|
||||
private readonly audio = new GardenAudio(gardenAudioConfig);
|
||||
private readonly renderInputs = new RenderInputCache();
|
||||
private readonly introPrompt: IntroPrompt;
|
||||
private readonly eraserPreview: EraserPreview;
|
||||
|
|
@ -68,7 +64,6 @@ export default class GameLoop {
|
|||
eraserAgentPipeline: this.resources.eraserAgentPipeline,
|
||||
eraserTexturePipeline: this.resources.eraserTexturePipeline,
|
||||
eraserPreview: this.eraserPreview,
|
||||
getCanvasSize: () => this.canvasSize,
|
||||
getDevicePixelRatio: () => this.devicePixelRatio,
|
||||
getMirrorSegmentCount: () => this.mirrorSegmentCount,
|
||||
onStartDrawing: () => this.introPrompt.markStartedDrawing(),
|
||||
|
|
@ -167,7 +162,6 @@ export default class GameLoop {
|
|||
return;
|
||||
}
|
||||
|
||||
const frameCpuStartedAt = this.framePerformance.markCpuStart();
|
||||
const deltaTime = this.deltaTimeCalculator.calculateDeltaTimeInSeconds(time);
|
||||
this.framePerformance.update(time);
|
||||
this.agentPopulation.growBudget(
|
||||
|
|
@ -194,7 +188,6 @@ export default class GameLoop {
|
|||
vibe: activeVibe,
|
||||
selectedColorIndex: settings.selectedColorIndex,
|
||||
isErasing,
|
||||
mirrorSegmentCount: this.mirrorSegmentCount,
|
||||
});
|
||||
|
||||
this.resources.setFrameParameters({
|
||||
|
|
@ -212,24 +205,14 @@ export default class GameLoop {
|
|||
eraserPixelSize,
|
||||
});
|
||||
|
||||
const encodeCpuStartedAt = this.framePerformance.markCpuStart();
|
||||
this.resources.executeFrame(
|
||||
isErasing,
|
||||
this.toolbarContrastMonitor.takeReadbackRequest(time)
|
||||
);
|
||||
const encodeCpuMs = this.framePerformance.measureSince(encodeCpuStartedAt);
|
||||
|
||||
this.pointerInput.clearSwipesIfIdle();
|
||||
await this.agentPopulation.compactAfterErase(this.pointerInput.isSwipeActive);
|
||||
|
||||
this.framePerformance.renderTelemetry({
|
||||
frameCpuStartedAt,
|
||||
encodeCpuMs,
|
||||
activeAgentCount: this.agentPopulation.activeAgentCount,
|
||||
agentBudgetMax: settings.agentBudgetMax,
|
||||
canvas: this.canvas,
|
||||
devicePixelRatio: this.devicePixelRatio,
|
||||
});
|
||||
this.updateDevStats(time);
|
||||
|
||||
if (settings.simulatedDelayMs > 0) {
|
||||
|
|
|
|||
|
|
@ -118,7 +118,6 @@ const createPointerInput = async () => {
|
|||
eraserAgentPipeline,
|
||||
eraserPreview,
|
||||
eraserTexturePipeline,
|
||||
getCanvasSize: () => [canvas.width, canvas.height],
|
||||
getDevicePixelRatio: () => 1,
|
||||
getMirrorSegmentCount: () => 1,
|
||||
onEraseGestureEnded,
|
||||
|
|
@ -185,9 +184,7 @@ describe('GardenPointerInput drawing startup', () => {
|
|||
expect(audio.beginGesture).toHaveBeenCalledTimes(1);
|
||||
expect(audio.touchDown).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
canvasSize: [300, 200],
|
||||
colorIndex: 0,
|
||||
position: expect.any(Float32Array),
|
||||
})
|
||||
);
|
||||
expect(audio.stroke).not.toHaveBeenCalled();
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ interface GardenPointerInputOptions {
|
|||
eraserAgentPipeline: EraserAgentPipeline;
|
||||
eraserTexturePipeline: EraserTexturePipeline;
|
||||
eraserPreview: EraserPreview;
|
||||
getCanvasSize: () => vec2;
|
||||
getDevicePixelRatio: () => number;
|
||||
getMirrorSegmentCount: () => number;
|
||||
onStartDrawing: () => void;
|
||||
|
|
@ -32,7 +31,6 @@ export class GardenPointerInput {
|
|||
private activePointerId: number | null = null;
|
||||
private lastPointerPosition: vec2 | null = null;
|
||||
private lastPointerEventTimeMs: number | null = null;
|
||||
private lastPointerPressure = 0.5;
|
||||
private smoothedStrokePoints: Array<vec2> = [];
|
||||
private lastSmoothedBrushPosition: vec2 | null = null;
|
||||
private isErasing = false;
|
||||
|
|
@ -109,18 +107,12 @@ export class GardenPointerInput {
|
|||
return;
|
||||
}
|
||||
|
||||
const position = this.getCanvasPointerPosition(event);
|
||||
if (event.pointerType !== 'touch') {
|
||||
this.options.audio.start(activeVibe, { userGesture: true });
|
||||
}
|
||||
this.options.audio.beginGesture();
|
||||
this.options.audio.touchDown({
|
||||
vibe: activeVibe,
|
||||
colorIndex: settings.selectedColorIndex,
|
||||
position,
|
||||
canvasSize: this.options.getCanvasSize(),
|
||||
mirrorSegmentCount: this.options.getMirrorSegmentCount(),
|
||||
pressure: this.getPointerPressure(event),
|
||||
});
|
||||
this.options.onStartDrawing();
|
||||
this.activePointerId = event.pointerId;
|
||||
|
|
@ -131,7 +123,6 @@ export class GardenPointerInput {
|
|||
this.lastPointerPosition = null;
|
||||
this.lastPointerEventTimeMs = null;
|
||||
this.clearSmoothedStroke();
|
||||
this.lastPointerPressure = this.getPointerPressure(event);
|
||||
this.addSwipeAt(event, { emitAudio: false });
|
||||
};
|
||||
|
||||
|
|
@ -178,7 +169,6 @@ export class GardenPointerInput {
|
|||
};
|
||||
|
||||
private addSwipeAt(event: PointerEvent, options: { emitAudio?: boolean } = {}): void {
|
||||
const devicePixelRatio = this.options.getDevicePixelRatio();
|
||||
const position = this.getCanvasPointerPosition(event);
|
||||
const previousPosition = this.lastPointerPosition ?? position;
|
||||
const previousTimeMs = this.lastPointerEventTimeMs ?? event.timeStamp;
|
||||
|
|
@ -186,10 +176,6 @@ export class GardenPointerInput {
|
|||
appConfig.deltaTime.minDeltaTimeSeconds,
|
||||
(event.timeStamp - previousTimeMs) / 1000
|
||||
);
|
||||
const distancePixels = vec2.distance(previousPosition, position);
|
||||
const velocityPixelsPerSecond = distancePixels / elapsedSeconds;
|
||||
const pressure = this.getPointerPressure(event);
|
||||
this.lastPointerPressure = pressure > 0 ? pressure : this.lastPointerPressure;
|
||||
|
||||
const segments = this.isErasing
|
||||
? [{ from: previousPosition, to: position }]
|
||||
|
|
@ -214,14 +200,9 @@ export class GardenPointerInput {
|
|||
vibe: activeVibe,
|
||||
from: previousPosition,
|
||||
to: position,
|
||||
canvasSize: this.options.getCanvasSize(),
|
||||
colorIndex: settings.selectedColorIndex,
|
||||
isErasing: this.isErasing,
|
||||
pressure: pressure > 0 ? pressure : this.lastPointerPressure,
|
||||
velocityPixelsPerSecond,
|
||||
elapsedSeconds,
|
||||
eraserSizePixels: settings.eraserSize * devicePixelRatio,
|
||||
mirrorSegmentCount: this.options.getMirrorSegmentCount(),
|
||||
});
|
||||
}
|
||||
this.lastPointerPosition = position;
|
||||
|
|
@ -363,14 +344,6 @@ export class GardenPointerInput {
|
|||
|
||||
return segments;
|
||||
}
|
||||
|
||||
private getPointerPressure(event: PointerEvent): number {
|
||||
if (Number.isFinite(event.pressure) && event.pressure > 0) {
|
||||
return Math.min(1, Math.max(0, event.pressure));
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
const rotatePointAround = (point: vec2, center: vec2, angle: number): vec2 => {
|
||||
|
|
@ -409,5 +382,4 @@ const getBrushCurveResolution = (): number => {
|
|||
const isSamePointerSample = (left: PointerEvent, right: PointerEvent): boolean =>
|
||||
left.clientX === right.clientX &&
|
||||
left.clientY === right.clientY &&
|
||||
left.pressure === right.pressure &&
|
||||
left.buttons === right.buttons;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue