Some checks failed
Check & deploy / build (pull_request) Failing after 1m16s
144 lines
5.2 KiB
TypeScript
144 lines
5.2 KiB
TypeScript
import { appConfig } from '../config';
|
|
import { AgentPipeline } from '../pipelines/agents/agent-pipeline';
|
|
import { BrushPipeline } from '../pipelines/brush/brush-pipeline';
|
|
import { DiffusionPipeline } from '../pipelines/diffusion/diffusion-pipeline';
|
|
import { EraserAgentPipeline } from '../pipelines/eraser/eraser-agent-pipeline';
|
|
import { EraserTexturePipeline } from '../pipelines/eraser/eraser-texture-pipeline';
|
|
import { RenderPipeline } from '../pipelines/render/render-pipeline';
|
|
import { settings } from '../settings';
|
|
import { CanvasReadbackRequest } from './game-loop-types';
|
|
import { GpuProfiler } from './gpu-profiler';
|
|
import { SimulationTextures } from './simulation-textures';
|
|
|
|
interface SimulationFramePipelines {
|
|
agentPipeline: AgentPipeline;
|
|
brushPipeline: BrushPipeline;
|
|
eraserAgentPipeline: EraserAgentPipeline;
|
|
eraserTexturePipeline: EraserTexturePipeline;
|
|
diffusionPipeline: DiffusionPipeline;
|
|
renderPipeline: RenderPipeline;
|
|
}
|
|
|
|
export class SimulationFrameRenderer {
|
|
private sourceActiveFramesRemaining = 0;
|
|
private sourceMapsCleared = true;
|
|
|
|
public constructor(
|
|
private readonly device: GPUDevice,
|
|
private readonly textures: SimulationTextures,
|
|
private readonly pipelines: SimulationFramePipelines,
|
|
private readonly gpuProfiler: GpuProfiler | null = null
|
|
) {}
|
|
|
|
public resetSourceMapActivity(): void {
|
|
this.sourceActiveFramesRemaining = 0;
|
|
this.sourceMapsCleared = true;
|
|
}
|
|
|
|
public get isSourceMapActive(): boolean {
|
|
return this.sourceActiveFramesRemaining > 0;
|
|
}
|
|
|
|
public execute(
|
|
isErasing: boolean,
|
|
canvasReadbackRequest?: CanvasReadbackRequest | null
|
|
): void {
|
|
const commandEncoder = this.device.createCommandEncoder();
|
|
this.gpuProfiler?.beginFrame();
|
|
|
|
// Clear the deposit map up-front so agents write fresh deposits each frame
|
|
// and diffuse sees only this frame's contributions added to trailMapA.
|
|
this.textures.clearDepositMap(commandEncoder);
|
|
let wroteSourceMap = false;
|
|
if (isErasing) {
|
|
if (this.pipelines.eraserAgentPipeline.hasActiveMask()) {
|
|
const eraserMask = this.textures.eraserMask.getTextureView();
|
|
// Erase trailMapA directly — it's what agent and diffuse will read.
|
|
this.pipelines.eraserTexturePipeline.executeCombined(
|
|
commandEncoder,
|
|
eraserMask,
|
|
this.textures.sourceMapA.getTextureView(),
|
|
this.textures.trailMapA.getTextureView(),
|
|
this.gpuProfiler?.timestampWrites('eraserTexture')
|
|
);
|
|
this.pipelines.eraserAgentPipeline.execute(
|
|
commandEncoder,
|
|
eraserMask,
|
|
this.gpuProfiler?.timestampWrites('eraserAgent')
|
|
);
|
|
}
|
|
} else {
|
|
wroteSourceMap = this.pipelines.brushPipeline.executeSource(
|
|
commandEncoder,
|
|
this.textures.sourceMapA.getTextureView(),
|
|
this.gpuProfiler?.timestampWrites('brush')
|
|
);
|
|
}
|
|
|
|
if (wroteSourceMap) {
|
|
this.sourceActiveFramesRemaining = getSourceActiveFrameCount();
|
|
this.sourceMapsCleared = false;
|
|
}
|
|
|
|
const useSourceMap = this.isSourceMapActive;
|
|
if (!useSourceMap && !this.sourceMapsCleared) {
|
|
this.textures.clearSourceMaps(commandEncoder);
|
|
this.sourceMapsCleared = true;
|
|
}
|
|
|
|
this.pipelines.agentPipeline.execute(
|
|
commandEncoder,
|
|
this.textures.trailMapA.getTextureView(),
|
|
this.textures.depositMap.getTextureView(),
|
|
this.gpuProfiler?.timestampWrites('agent')
|
|
);
|
|
this.pipelines.diffusionPipeline.execute(
|
|
commandEncoder,
|
|
this.textures.trailMapA.getTextureView(),
|
|
this.textures.trailMapB.getTextureView(),
|
|
this.textures.trailMapA.getSize(),
|
|
this.textures.depositMap.getTextureView(),
|
|
this.gpuProfiler?.timestampWrites('trailDiffusion')
|
|
);
|
|
const canvasTexture = this.pipelines.renderPipeline.execute(
|
|
commandEncoder,
|
|
this.textures.trailMapB.getTextureView(),
|
|
this.textures.sourceMapA.getTextureView(),
|
|
useSourceMap,
|
|
this.gpuProfiler?.timestampWrites('render')
|
|
);
|
|
canvasReadbackRequest?.encode(commandEncoder, canvasTexture);
|
|
|
|
if (useSourceMap) {
|
|
this.pipelines.diffusionPipeline.execute(
|
|
commandEncoder,
|
|
this.textures.sourceMapA.getTextureView(),
|
|
this.textures.sourceMapB.getTextureView(),
|
|
this.textures.sourceMapB.getSize(),
|
|
null,
|
|
this.gpuProfiler?.timestampWrites('sourceDiffusion')
|
|
);
|
|
}
|
|
const afterGpuProfileSubmit = this.gpuProfiler?.resolve(commandEncoder);
|
|
this.device.queue.submit([commandEncoder.finish()]);
|
|
afterGpuProfileSubmit?.();
|
|
canvasReadbackRequest?.afterSubmit();
|
|
// After this frame's diffuse, trailMapB holds the fresh trail; swap so
|
|
// trailMapA is "current trail" again for the next frame and any external
|
|
// readers (e.g. export snapshot).
|
|
this.textures.swapTrailMaps();
|
|
if (useSourceMap) {
|
|
this.textures.swapSourceMaps();
|
|
this.sourceActiveFramesRemaining -= 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
const getSourceActiveFrameCount = (): number => {
|
|
const frameCount =
|
|
settings.brushEffectDuration * appConfig.simulation.brushEffectFramesPerSecond;
|
|
if (Number.isFinite(frameCount) && frameCount > 0) {
|
|
return Math.ceil(frameCount);
|
|
}
|
|
return Math.max(1, appConfig.simulation.sourceActiveFramesAfterWrite);
|
|
};
|