Shader library: fingerprint without node positions, a drag no longer recompiles (#65)
game-maker / suite (push) Successful in 9m5s
game-maker / suite (push) Successful in 9m5s
shaderFingerprint(graph) covers node id/type/params/ports and edge endpoints only; x/y, collapsed, folded and edge ids stay out. ShaderLibrary keys on it and counts compiles (__gm.shaderCompiles()). Measured before: one compile per preview step of a node drag (12 for a harness drag); after: 0, a param change exactly +1. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
016e83c9b5
commit
102a649a35
6 files changed
+82
-6
No files matched your search
@@ -37,7 +37,7 @@ npm run build # writes dist/ and regenerates index.html; the folder is wh
|
|||||||
The browser suite and the three example games need a machine with a GPU:
|
The browser suite and the three example games need a machine with a GPU:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
node tools/seed-e2e.mjs && node tools/e2e.mjs # the editor, 758 checks (GM_STEPS=20 for one step)
|
node tools/seed-e2e.mjs && node tools/e2e.mjs # the editor, 760 checks (GM_STEPS=20 for one step)
|
||||||
node tools/seed-jumprun.mjs && node tools/smoke-jumprun.mjs
|
node tools/seed-jumprun.mjs && node tools/smoke-jumprun.mjs
|
||||||
node tools/seed-tetris.mjs && node tools/smoke-tetris.mjs
|
node tools/seed-tetris.mjs && node tools/smoke-tetris.mjs
|
||||||
node tools/seed-actor.mjs && node tools/smoke-actor.mjs
|
node tools/seed-actor.mjs && node tools/smoke-actor.mjs
|
||||||
|
|||||||
@@ -47,6 +47,18 @@ export interface ShaderBuild {
|
|||||||
* becomes one local per output, in dependency order, and every Property a float uniform. The texel
|
* becomes one local per output, in dependency order, and every Property a float uniform. The texel
|
||||||
* arrives premultiplied, so the prelude unpremultiplies and the epilogue multiplies back.
|
* arrives premultiplied, so the prelude unpremultiplies and the epilogue multiplies back.
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* What the fragment depends on and nothing else: node ids, types, params and declared ports, and
|
||||||
|
* which port feeds which. Positions, collapsing and folding are the editor's; they stay out so a
|
||||||
|
* moved node does not compile again (#65).
|
||||||
|
*/
|
||||||
|
export function shaderFingerprint(graph: Graph): string {
|
||||||
|
return JSON.stringify({
|
||||||
|
nodes: graph.nodes.map((n) => [n.id, n.type, n.params, n.ports ?? null]),
|
||||||
|
edges: graph.edges.map((e) => [e.from.node, e.from.port, e.to.node, e.to.port]),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function compileShader(graph: Graph): ShaderBuild {
|
export function compileShader(graph: Graph): ShaderBuild {
|
||||||
const problems: Record<string, string> = {};
|
const problems: Record<string, string> = {};
|
||||||
const problem = (node: GraphNode, text: string) => {
|
const problem = (node: GraphNode, text: string) => {
|
||||||
|
|||||||
@@ -415,6 +415,8 @@ export function installDebugHook(app: Application, state: EditorState, editor: E
|
|||||||
},
|
},
|
||||||
/** Holds the shader clock at `at` seconds, so a Time shader's pixels can be measured; null releases it. */
|
/** Holds the shader clock at `at` seconds, so a Time shader's pixels can be measured; null releases it. */
|
||||||
shaderTime: (at: number | null) => shaderLibrary.freezeTime(at),
|
shaderTime: (at: number | null) => shaderLibrary.freezeTime(at),
|
||||||
|
/** Graphs compiled so far; a moved node must not add one (#65). */
|
||||||
|
shaderCompiles: () => shaderLibrary.compiles,
|
||||||
/** Which views redraw every tick because a Time shader is on screen. */
|
/** Which views redraw every tick because a Time shader is on screen. */
|
||||||
animates: () => ({ scene: viewport.animates, game: gameView.animates }),
|
animates: () => ({ scene: viewport.animates, game: gameView.animates }),
|
||||||
lowLatency: () =>
|
lowLatency: () =>
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import type { ShaderAsset } from '../core/model/Asset';
|
import type { ShaderAsset } from '../core/model/Asset';
|
||||||
import { findAssetOf, type Project } from '../core/model/Project';
|
import { findAssetOf, type Project } from '../core/model/Project';
|
||||||
import { compileShader, type ShaderBuild } from '../core/shader/compile';
|
import { compileShader, shaderFingerprint, type ShaderBuild } from '../core/shader/compile';
|
||||||
import type { ShaderSource } from './shaders';
|
import type { ShaderSource } from './shaders';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compiled shader graphs, one per asset, recompiled when the graph's content changes. A graph is
|
* Compiled shader graphs, one per asset, recompiled when the graph's content changes. A graph is
|
||||||
* edited in place, so the cache keys on its serialized form rather than on its identity.
|
* edited in place, so the cache keys on a fingerprint of what the fragment depends on.
|
||||||
*/
|
*/
|
||||||
export class ShaderLibrary {
|
export class ShaderLibrary {
|
||||||
private readonly builds = new Map<string, { fingerprint: string; build: ShaderBuild }>();
|
private readonly builds = new Map<string, { fingerprint: string; build: ShaderBuild }>();
|
||||||
|
/** How often a graph went through the compiler – what the checks read to prove a move did not. */
|
||||||
|
compiles = 0;
|
||||||
private readonly started = performance.now();
|
private readonly started = performance.now();
|
||||||
/** Set from the test hook: the clock stands at this second until released. */
|
/** Set from the test hook: the clock stands at this second until released. */
|
||||||
private frozenAt: number | null = null;
|
private frozenAt: number | null = null;
|
||||||
@@ -23,9 +25,10 @@ export class ShaderLibrary {
|
|||||||
}
|
}
|
||||||
|
|
||||||
buildFor(asset: ShaderAsset): ShaderBuild {
|
buildFor(asset: ShaderAsset): ShaderBuild {
|
||||||
const fingerprint = JSON.stringify(asset.graph);
|
const fingerprint = shaderFingerprint(asset.graph);
|
||||||
const cached = this.builds.get(asset.id);
|
const cached = this.builds.get(asset.id);
|
||||||
if (cached && cached.fingerprint === fingerprint) return cached.build;
|
if (cached && cached.fingerprint === fingerprint) return cached.build;
|
||||||
|
this.compiles += 1;
|
||||||
const build = compileShader(asset.graph);
|
const build = compileShader(asset.graph);
|
||||||
this.builds.set(asset.id, { fingerprint, build });
|
this.builds.set(asset.id, { fingerprint, build });
|
||||||
return build;
|
return build;
|
||||||
|
|||||||
@@ -5,7 +5,14 @@
|
|||||||
*/
|
*/
|
||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import { CreateAssetCommand, SetAssetPropertyCommand } from '../../src/core/commands/assetCommands';
|
import { CreateAssetCommand, SetAssetPropertyCommand } from '../../src/core/commands/assetCommands';
|
||||||
import { AddNodeCommand, ConnectCommand } from '../../src/core/commands/graphCommands';
|
import {
|
||||||
|
AddNodeCommand,
|
||||||
|
CollapseNodesCommand,
|
||||||
|
ConnectCommand,
|
||||||
|
FoldEdgeCommand,
|
||||||
|
MoveNodeCommand,
|
||||||
|
SetNodeParamCommand,
|
||||||
|
} from '../../src/core/commands/graphCommands';
|
||||||
import { SetOverrideCommand } from '../../src/core/commands/sceneObjectCommands';
|
import { SetOverrideCommand } from '../../src/core/commands/sceneObjectCommands';
|
||||||
import type { Graph } from '../../src/core/graph';
|
import type { Graph } from '../../src/core/graph';
|
||||||
import { BUILT_IN_NODES } from '../../src/core/graph/nodes';
|
import { BUILT_IN_NODES } from '../../src/core/graph/nodes';
|
||||||
@@ -27,7 +34,7 @@ import {
|
|||||||
shaderParamDefs,
|
shaderParamDefs,
|
||||||
} from '../../src/core/properties/shaders';
|
} from '../../src/core/properties/shaders';
|
||||||
import { inlineInputs } from '../../src/core/graph/NodeRegistry';
|
import { inlineInputs } from '../../src/core/graph/NodeRegistry';
|
||||||
import { compileShader } from '../../src/core/shader/compile';
|
import { compileShader, shaderFingerprint } from '../../src/core/shader/compile';
|
||||||
import {
|
import {
|
||||||
SHADER_NODES,
|
SHADER_NODES,
|
||||||
SHADER_NUMBER,
|
SHADER_NUMBER,
|
||||||
@@ -567,3 +574,22 @@ describe('a sprite wearing a shader', () => {
|
|||||||
expect(shader.graph.nodes.map((n) => n.id)).toEqual(['o']);
|
expect(shader.graph.nodes.map((n) => n.id)).toEqual(['o']);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('the fingerprint the shader library caches on', () => {
|
||||||
|
it('ignores a move, a collapse and a fold, and changes with a param or an edge', () => {
|
||||||
|
const f = makeProject();
|
||||||
|
const s = shaderEditor(f);
|
||||||
|
const n = buildAdjustColors(s);
|
||||||
|
const before = shaderFingerprint(s.asset().graph);
|
||||||
|
f.stack.execute(new MoveNodeCommand(s.id, n.bright, 300, 200));
|
||||||
|
f.stack.execute(new CollapseNodesCommand(s.id, [n.bright, n.luma], true));
|
||||||
|
f.stack.execute(new FoldEdgeCommand(s.id, s.asset().graph.edges[0]!.id, true));
|
||||||
|
expect(s.asset().graph.nodes.find((x) => x.id === n.bright)).toMatchObject({ x: 300, collapsed: true });
|
||||||
|
expect(shaderFingerprint(s.asset().graph)).toBe(before);
|
||||||
|
f.stack.execute(new SetNodeParamCommand(s.id, n.shifted, 'b', 0.25));
|
||||||
|
const withParam = shaderFingerprint(s.asset().graph);
|
||||||
|
expect(withParam).not.toBe(before);
|
||||||
|
s.link(n.brightness, 'value', n.output, 'alpha');
|
||||||
|
expect(shaderFingerprint(s.asset().graph)).not.toBe(withParam);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -18,6 +18,7 @@ export async function shader(t) {
|
|||||||
json,
|
json,
|
||||||
evaluate,
|
evaluate,
|
||||||
mutate,
|
mutate,
|
||||||
|
drag,
|
||||||
click,
|
click,
|
||||||
clickText,
|
clickText,
|
||||||
locate,
|
locate,
|
||||||
@@ -162,6 +163,38 @@ export async function shader(t) {
|
|||||||
a.graph.nodes.find((n) => n.id === ${JSON.stringify(propertyNode?.id)}).params.name = 'glow';`);
|
a.graph.nodes.find((n) => n.id === ${JSON.stringify(propertyNode?.id)}).params.name = 'glow';`);
|
||||||
await sleep(300);
|
await sleep(300);
|
||||||
|
|
||||||
|
// 28c-ii. Where a node sits is the editor's business: dropping it somewhere else compiles nothing
|
||||||
|
// again, a changed param does – once (#65).
|
||||||
|
const nodeRef = JSON.stringify(propertyNode?.id);
|
||||||
|
const compiledBefore = await json('__gm.shaderCompiles()');
|
||||||
|
const grab = await json(`__gm.shader.nodeAt(${nodeRef})`);
|
||||||
|
await drag(grab.x, grab.y, grab.x + 60, grab.y + 40);
|
||||||
|
await sleep(300);
|
||||||
|
const dropped = await json(`__gm.shader.nodeAt(${nodeRef})`);
|
||||||
|
const compiledAfterMove = await json('__gm.shaderCompiles()');
|
||||||
|
check(
|
||||||
|
'dropping a node somewhere else moves it without compiling the graph again',
|
||||||
|
dropped.x > grab.x + 30 && compiledAfterMove === compiledBefore,
|
||||||
|
JSON.stringify({ grab, dropped, compiledBefore, compiledAfterMove }),
|
||||||
|
);
|
||||||
|
const defaultBefore = await json(
|
||||||
|
`__gm.state.project.assets.find((x) => x.id === ${JSON.stringify(made.id)}).graph.nodes.find((n) => n.id === ${nodeRef}).params.default`,
|
||||||
|
);
|
||||||
|
const setDefault = (value) =>
|
||||||
|
mutate(`
|
||||||
|
const a = __gm.state.project.assets.find((x) => x.id === ${JSON.stringify(made.id)});
|
||||||
|
a.graph.nodes.find((n) => n.id === ${nodeRef}).params.default = ${JSON.stringify(value)};`);
|
||||||
|
await setDefault(2);
|
||||||
|
await sleep(300);
|
||||||
|
const compiledAfterParam = await json('__gm.shaderCompiles()');
|
||||||
|
check(
|
||||||
|
'changing a param compiles it again, once',
|
||||||
|
compiledAfterParam === compiledAfterMove + 1,
|
||||||
|
JSON.stringify({ compiledAfterMove, compiledAfterParam }),
|
||||||
|
);
|
||||||
|
await setDefault(defaultBefore);
|
||||||
|
await sleep(300);
|
||||||
|
|
||||||
// 28d. A flat sprite on the camera, drawn on top of everything; its Shader dropdown lists the assets.
|
// 28d. A flat sprite on the camera, drawn on top of everything; its Shader dropdown lists the assets.
|
||||||
await mutate(`
|
await mutate(`
|
||||||
const project = __gm.state.project;
|
const project = __gm.state.project;
|
||||||
|
|||||||
Reference in new issue
Block a user