Files
feuerwerk/src/viewport/ShaderLibrary.ts
T
adminandClaude Opus 5.5 a526e1b40f
game-maker / gate (push) Successful in 3m16s
game-maker / suite (push) Failing after 14m37s
Use a shader graph as a node in other shaders (#303)
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>
2026-10-03 17:09:53 +02:00

78 lines
3.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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();