import type { ShaderAsset, ShaderFunctionAsset } from '../core/model/Asset'; import { shaderFunctionsOf } from '../core/model/graphLookup'; import { findAssetOf, type Project } from '../core/model/Project'; import { graphFingerprint } from '../core/graph/Graph'; import { checkShaderFunction, compileShader, type FunctionCheck, type ShaderBuild, } from '../core/shader/compile'; import { functionsFingerprint } from '../core/shader/functions'; import { gameClock } from '../core/time'; import type { ShaderSource } from './shaders'; /** * Compiled shader graphs, one per asset, recompiled when the graph's content changes – or that of * a function it calls (#303). A graph is edited in place, so the cache keys on a fingerprint of what * the fragment depends on. */ class ShaderLibrary { private readonly builds = new Map(); private readonly checks = new Map(); /** How often a graph went through the compiler – what the checks read to prove a move did not. */ compiles = 0; /** Set from the test hook: the clock stands at this second until released. */ private frozenAt: number | null = null; /** What a Time node reads: the global clock, so a pause or a time scale reaches the shaders. */ now(): number { return this.frozenAt ?? gameClock.now; } freezeTime(at: number | null): void { this.frozenAt = at; } buildFor(asset: ShaderAsset, project: Project): ShaderBuild { const functions = shaderFunctionsOf(project); const fingerprint = `${graphFingerprint(asset.graph)}\n${functionsFingerprint(asset.graph, functions)}`; const cached = this.builds.get(asset.id); if (cached && cached.fingerprint === fingerprint) return cached.build; this.compiles += 1; const build = compileShader(asset.graph, functions); this.builds.set(asset.id, { fingerprint, build }); return build; } /** A function on its own: what its tab and its nodes report. */ checkFor(asset: ShaderFunctionAsset, project: Project): FunctionCheck { const functions = shaderFunctionsOf(project); const fingerprint = `${graphFingerprint(asset.graph)}\n${functionsFingerprint(asset.graph, functions)}`; const cached = this.checks.get(asset.id); if (cached && cached.fingerprint === fingerprint) return cached.check; const check = checkShaderFunction(asset.graph, functions, asset.id); this.checks.set(asset.id, { fingerprint, check }); return check; } /** For one redraw: each asset is looked up and fingerprinted once, however many sprites wear it. */ source(project: Project): ShaderSource { const seen = new Map(); return { now: this.now(), compiled: (assetId) => { let build = seen.get(assetId); if (build === undefined) { const asset = findAssetOf(project, assetId, 'shader'); build = asset ? this.buildFor(asset, project) : null; seen.set(assetId, build); } return build?.compiled ?? null; }, }; } } export const shaderLibrary = new ShaderLibrary();