A calculation needed in several shaders (a greyscale, a noise, an outline) had to be built again in every one of them. A shader function keeps it as an asset of its own, called like a behavior is since #16. - New asset type `shaderfunction` ("Shader function"), edited in the Shader tab. The panel beside the graph shows no picture, the readout counts its inputs and outputs. - Two new nodes: `Function input` and `Function output`. Their declared ports, numbers or colours only, are the ports of every call. One of each per function, like Start and End. - The call is node `ShaderCall`, named after the function. The palette lists every function, it can be dragged in from the Assets panel, and a double-click opens it. - The compiler sets the function in where it is called (`inlineFunctions`). It renames the copied nodes `<call>/<node>` and turns an unconnected port into a constant. Passes, Time, the Edge cut and the corner walk then see a plain graph. - A problem inside a function is reported at the call, in the function's words. The function's own tab checks it on its own (`checkShaderFunction`). - `ShaderLibrary` keys a shader on its graph and on every function it calls, however deep, so a change reaches every caller. - The port commands mirror a new, renamed or removed port into every call, as for a behavior's Start and End. `NodeDef.portKinds` holds the two ends to Number and Color. - As agreed in the issue: no `Property`, `Color property`, Output or Vertex output in a function, and `Time` is the caller's clock. - Own decision: a `Pass` may stand in a function. It becomes a pass of the calling shader, so a wide blur can be a function. - A function calling itself, also through another, says so at the node and is not set in. The palette does not offer such a call. - The Shader tab showed only the compiler's problems. A call nothing reaches was never compiled and looked healthy, so a call now also shows its own problem there, as in a behavior. Tests: - Gate: 228 files, 3510 tests, CHECK EXIT 0 (+13: 11 in tests/core/shader-functions.test.ts, 2 for the new icon) - e2e 28: 100 checks, all passed (+3 in 28l-viii). Called and in place both #606060/#a2a2a2, halved in the function #303030/#515151. - Counter-check: a fingerprint without the called functions fails 1 unit test, and in the browser the change check (#606060 stays). - The first e2e runs found two faults, both fixed: a double-click only selected the function, and an unreached call of itself showed no problem. - e2e total 1728 is computed (1725 + 3). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
78 lines
3.1 KiB
TypeScript
78 lines
3.1 KiB
TypeScript
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<string, { fingerprint: string; build: ShaderBuild }>();
|
||
private readonly checks = new Map<string, { fingerprint: string; check: FunctionCheck }>();
|
||
/** 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<string, ShaderBuild | null>();
|
||
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();
|