feat: add render graph driven renderer architecture

Amp-Thread-ID: https://ampcode.com/threads/T-019f9d91-77c1-7206-a60f-ed6554ce92ab

Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
This commit is contained in:
Amp
2026-07-27 02:53:44 +00:00
co-authored by heaust
parent 8a8369706b
commit d4e8634f67
290 changed files with 48804 additions and 1995 deletions
@@ -0,0 +1,129 @@
import { expect, test } from "@playwright/test";
const expectedNodeTypes = [
"fxnode.common.frame",
"fxnode.common.reroute",
"fxnode.common.group-input",
"fxnode.common.group-output",
"fxnode.shader.value",
"fxnode.shader.color",
"fxnode.shader.math",
"fxnode.shader.vector-math",
"fxnode.shader.mix",
"fxnode.shader.color-ramp",
"fxnode.shader.texture-coordinate",
"fxnode.shader.noise-texture",
"fxnode.shader.image-texture",
"fxnode.shader.principled-bsdf",
"fxnode.shader.material-output",
"fxnode.geometry.position",
"fxnode.geometry.mesh-cube",
"fxnode.geometry.set-position",
"fxnode.geometry.transform-geometry",
"fxnode.geometry.join-geometry",
"fxnode.compositor.image",
"fxnode.compositor.color-balance",
] as const;
test("example installs every startup node through the live worker API", async ({ page }) => {
await page.addInitScript(() => {
const sent: unknown[] = [],
received: unknown[] = [];
(
window as unknown as { applicationStartupMessages: { sent: unknown[]; received: unknown[] } }
).applicationStartupMessages = { sent, received };
const NativeWorker = Worker;
class StartupWorker extends NativeWorker {
constructor(url: string | URL, options?: WorkerOptions) {
super(url, options);
super.addEventListener("message", (event) => received.push(structuredClone(event.data)));
}
override postMessage(message: unknown, options?: StructuredSerializeOptions | Transferable[]) {
sent.push(structuredClone(message));
super.postMessage(message, options as StructuredSerializeOptions);
}
}
Object.defineProperty(window, "Worker", { configurable: true, writable: true, value: StartupWorker });
});
await page.goto("/examples/blender/");
await page.evaluate(() => window.fxnodeExample.ready);
const result = await page.evaluate(async () => {
const messages = (window as unknown as { applicationStartupMessages: { sent: any[]; received: any[] } })
.applicationStartupMessages,
init = messages.sent.find((message) => message.type === "init"),
updates = messages.sent.filter((message) => message.type === "composition.update"),
stateSet = messages.sent.find((message) => message.type === "state.set"),
api = window.fxnodeExample.root!;
const updateIds = new Set(updates.map((message) => message.id)),
compositionReceipts = messages.received
.filter((message) => message.type === "response" && updateIds.has(message.id))
.map((message) => message.value),
stateSetIndex = messages.sent.indexOf(stateSet),
lastUpdateIndex = Math.max(...updates.map((message) => messages.sent.indexOf(message))),
snapshot = await api.getState(),
undo = await api.undo({ expectedVersion: 1 }),
empty = await api.getState(),
redo = await api.redo({ expectedVersion: 2 }),
restored = await api.getState();
return {
initKeys: Object.keys(init).sort(),
updates: updates.map((message) => ({
kind: message.update.kind,
id: message.update.id,
expected: message.expected,
})),
compositionReceipts,
stateSet,
stateSetIndex,
lastUpdateIndex,
snapshot,
undo,
empty,
redo,
restored,
saved: await api.save(),
initialLayout: await (await fetch("/examples/blender/initialLayout.json")).json(),
};
});
expect(result.initKeys).toEqual([
"applicationId",
"applicationVersion",
"historyLimit",
"id",
"protocol",
"resources",
"type",
]);
expect(result.updates).toHaveLength(31);
expect(result.updates.slice(0, 9).map((update) => update.kind)).toEqual([
"theme.set",
"header-styles.set",
...Array(6).fill("socket.compose"),
"compatibility.set",
]);
expect(result.updates.slice(9).map((update) => update.kind)).toEqual(Array(22).fill("node.compose"));
expect(result.updates.slice(9).map((update) => update.id)).toEqual(expectedNodeTypes);
expect(result.updates.map((update) => update.expected)).toEqual(Array(31).fill({ kind: "current" }));
expect(result.compositionReceipts).toHaveLength(31);
expect(
result.compositionReceipts.every((receipt) => receipt.graphVersion === 0 && receipt.graphChanged === false),
).toBe(true);
expect(result.stateSetIndex).toBeGreaterThan(result.lastUpdateIndex);
expect(result.stateSet.expected).toEqual({ kind: "current" });
expect(result.stateSet.state.schemaVersion).toBeUndefined();
expect(result.stateSet.state.version).toBeUndefined();
expect(result.stateSet.state.nodes.every((node: any) => node.known === true)).toBe(true);
expect(result.snapshot.version).toBe(1);
expect(result.snapshot.nodes.length).toBe(result.initialLayout.nodes.length);
expect(result.snapshot.nodes.every((node: any) => node.known)).toBe(true);
expect(result.undo).toEqual({ status: "committed", version: 2 });
expect(result.empty).toMatchObject({ version: 2, nodes: [], links: [] });
expect(result.redo).toEqual({ status: "committed", version: 3 });
expect(result.restored.nodes).toEqual(result.snapshot.nodes);
expect(result.restored.links).toEqual(result.snapshot.links);
expect(result.saved).toEqual({
schemaVersion: 2,
...result.initialLayout,
nodes: result.initialLayout.nodes.map(({ known: _known, ...node }: any) => node),
});
});