refactor: redesign render graph nodes
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:
+156
-36
@@ -1,13 +1,26 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { addNodeItems, moveAddNodeSelection, searchAddNodeItems } from "../static/render-graph/add-node-menu.js";
|
||||
import { createNodeIdAllocator, spawnRequestedNode } from "../static/render-graph/node-spawn.js";
|
||||
import {
|
||||
addNodeItems,
|
||||
moveAddNodeSelection,
|
||||
searchAddNodeItems,
|
||||
} from "../static/render-graph/add-node-menu.js";
|
||||
import {
|
||||
createNodeIdAllocator,
|
||||
spawnRequestedNode,
|
||||
} from "../static/render-graph/node-spawn.js";
|
||||
|
||||
test("add-node model contains all 17 catalog types in application groups", () => {
|
||||
assert.equal(addNodeItems.length, 17);
|
||||
assert.deepEqual([...new Set(addNodeItems.map((item) => item.group))], ["Source", "Compute", "Render / post", "Present"]);
|
||||
assert.equal(new Set(addNodeItems.map((item) => item.typeId)).size, 17);
|
||||
assert.deepEqual(searchAddNodeItems("tone render").map((item) => item.typeId), ["tone_map"]);
|
||||
test("add-node model contains all 13 catalog types in application groups", () => {
|
||||
assert.equal(addNodeItems.length, 13);
|
||||
assert.deepEqual(
|
||||
[...new Set(addNodeItems.map((item) => item.group))],
|
||||
["Source", "Compute", "CPU preparation", "Render / post", "Frame"],
|
||||
);
|
||||
assert.equal(new Set(addNodeItems.map((item) => item.typeId)).size, 13);
|
||||
assert.deepEqual(
|
||||
searchAddNodeItems("tone render").map((item) => item.typeId),
|
||||
["tone_map"],
|
||||
);
|
||||
assert.deepEqual(searchAddNodeItems("no such node"), []);
|
||||
});
|
||||
|
||||
@@ -21,13 +34,19 @@ test("allocator avoids existing and session-reserved IDs and is bounded", () =>
|
||||
const values = ["a-a", "a-a", "b-b"];
|
||||
const allocate = createNodeIdAllocator(() => values.shift());
|
||||
assert.equal(allocate(["node_aa"]), "node_bb");
|
||||
assert.throws(() => createNodeIdAllocator(() => "bad id")([]), /Unable to allocate/);
|
||||
assert.throws(
|
||||
() => createNodeIdAllocator(() => "bad id")([]),
|
||||
/Unable to allocate/,
|
||||
);
|
||||
});
|
||||
|
||||
test("all 17 types spawn with exact position, current version and generated ID", async () => {
|
||||
let revision = 5, expectedType;
|
||||
test("all 13 types spawn with exact position, current version and generated ID", async () => {
|
||||
let revision = 5,
|
||||
expectedType;
|
||||
const request = { compositionRevision: 5, viewPosition: { x: 12.25, y: -4 } };
|
||||
const root = { getState: async () => ({ version: 91, nodes: [{ id: "existing" }] }) };
|
||||
const root = {
|
||||
getState: async () => ({ version: 91, nodes: [{ id: "existing" }] }),
|
||||
};
|
||||
const view = {
|
||||
getHostSnapshot: () => ({ compositionRevision: revision }),
|
||||
addNode: async (params, options) => {
|
||||
@@ -38,41 +57,89 @@ test("all 17 types spawn with exact position, current version and generated ID",
|
||||
},
|
||||
};
|
||||
let id = 0;
|
||||
const allocate = createNodeIdAllocator(() => `00000000-0000-0000-0000-${String(++id).padStart(12, "0")}`);
|
||||
const allocate = createNodeIdAllocator(
|
||||
() => `00000000-0000-0000-0000-${String(++id).padStart(12, "0")}`,
|
||||
);
|
||||
for (const item of addNodeItems) {
|
||||
expectedType = item.typeId;
|
||||
assert.equal(await spawnRequestedNode(root, view, request, item.typeId, allocate), true);
|
||||
assert.equal(
|
||||
await spawnRequestedNode(root, view, request, item.typeId, allocate),
|
||||
true,
|
||||
);
|
||||
}
|
||||
revision = 6;
|
||||
assert.equal(await spawnRequestedNode(root, view, request, "tone_map", allocate), false);
|
||||
assert.equal(
|
||||
await spawnRequestedNode(root, view, request, "tone_map", allocate),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("spawn rechecks composition after getState and propagates add errors", async () => {
|
||||
let revision = 2;
|
||||
const request = { compositionRevision: 2, viewPosition: { x: 1, y: 2 } };
|
||||
const root = { getState: async () => { revision++; return { version: 3, nodes: [] }; } };
|
||||
const root = {
|
||||
getState: async () => {
|
||||
revision++;
|
||||
return { version: 3, nodes: [] };
|
||||
},
|
||||
};
|
||||
const allocate = createNodeIdAllocator(() => "a");
|
||||
const view = { getHostSnapshot: () => ({ compositionRevision: revision }), addNode: async () => { throw Error("must not add"); } };
|
||||
assert.equal(await spawnRequestedNode(root, view, request, "tone_map", allocate), false);
|
||||
const view = {
|
||||
getHostSnapshot: () => ({ compositionRevision: revision }),
|
||||
addNode: async () => {
|
||||
throw Error("must not add");
|
||||
},
|
||||
};
|
||||
assert.equal(
|
||||
await spawnRequestedNode(root, view, request, "tone_map", allocate),
|
||||
false,
|
||||
);
|
||||
revision = 2;
|
||||
root.getState = async () => ({ version: 3, nodes: [] });
|
||||
await assert.rejects(spawnRequestedNode(root, view, request, "tone_map", allocate), /must not add/);
|
||||
await assert.rejects(
|
||||
spawnRequestedNode(root, view, request, "tone_map", allocate),
|
||||
/must not add/,
|
||||
);
|
||||
});
|
||||
|
||||
test("spawn cancels when a pending getState becomes mutated or dead", async () => {
|
||||
const request = { compositionRevision: 2, viewPosition: { x: 1, y: 2 } };
|
||||
let resolveState, revision = 2, alive = true, adds = 0;
|
||||
const root = { getState: () => new Promise((resolve) => { resolveState = resolve; }) };
|
||||
let resolveState,
|
||||
revision = 2,
|
||||
alive = true,
|
||||
adds = 0;
|
||||
const root = {
|
||||
getState: () =>
|
||||
new Promise((resolve) => {
|
||||
resolveState = resolve;
|
||||
}),
|
||||
};
|
||||
const view = {
|
||||
getHostSnapshot: () => ({ compositionRevision: revision }),
|
||||
addNode: async () => { adds++; },
|
||||
addNode: async () => {
|
||||
adds++;
|
||||
},
|
||||
};
|
||||
const pendingMutation = spawnRequestedNode(root, view, request, "tone_map", () => "node_a", () => alive);
|
||||
const pendingMutation = spawnRequestedNode(
|
||||
root,
|
||||
view,
|
||||
request,
|
||||
"tone_map",
|
||||
() => "node_a",
|
||||
() => alive,
|
||||
);
|
||||
revision++;
|
||||
resolveState({ version: 1, nodes: [] });
|
||||
assert.equal(await pendingMutation, false);
|
||||
revision = 2;
|
||||
const pendingDestroy = spawnRequestedNode(root, view, request, "tone_map", () => "node_b", () => alive);
|
||||
const pendingDestroy = spawnRequestedNode(
|
||||
root,
|
||||
view,
|
||||
request,
|
||||
"tone_map",
|
||||
() => "node_b",
|
||||
() => alive,
|
||||
);
|
||||
alive = false;
|
||||
resolveState({ version: 1, nodes: [] });
|
||||
assert.equal(await pendingDestroy, false);
|
||||
@@ -80,14 +147,27 @@ test("spawn cancels when a pending getState becomes mutated or dead", async () =
|
||||
});
|
||||
|
||||
test("spawn has a final liveness guard after ID allocation", async () => {
|
||||
let alive = true, adds = 0;
|
||||
let alive = true,
|
||||
adds = 0;
|
||||
const request = { compositionRevision: 1, viewPosition: { x: 0, y: 0 } };
|
||||
const root = { getState: async () => ({ version: 1, nodes: [] }) };
|
||||
const view = { getHostSnapshot: () => ({ compositionRevision: 1 }), addNode: async () => { adds++; } };
|
||||
const result = await spawnRequestedNode(root, view, request, "tone_map", () => {
|
||||
alive = false;
|
||||
return "node_reserved";
|
||||
}, () => alive);
|
||||
const view = {
|
||||
getHostSnapshot: () => ({ compositionRevision: 1 }),
|
||||
addNode: async () => {
|
||||
adds++;
|
||||
},
|
||||
};
|
||||
const result = await spawnRequestedNode(
|
||||
root,
|
||||
view,
|
||||
request,
|
||||
"tone_map",
|
||||
() => {
|
||||
alive = false;
|
||||
return "node_reserved";
|
||||
},
|
||||
() => alive,
|
||||
);
|
||||
assert.equal(result, false);
|
||||
assert.equal(adds, 0);
|
||||
});
|
||||
@@ -95,19 +175,59 @@ test("spawn has a final liveness guard after ID allocation", async () => {
|
||||
test("spawn suppresses teardown RPC rejections but propagates genuine live add errors", async () => {
|
||||
const request = { compositionRevision: 1, viewPosition: { x: 0, y: 0 } };
|
||||
let alive = true;
|
||||
const view = { getHostSnapshot: () => ({ compositionRevision: 1 }), addNode: async () => {} };
|
||||
const root = { getState: async () => { alive = false; throw Error("detached state"); } };
|
||||
assert.equal(await spawnRequestedNode(root, view, request, "tone_map", () => "node_a", () => alive), false);
|
||||
const view = {
|
||||
getHostSnapshot: () => ({ compositionRevision: 1 }),
|
||||
addNode: async () => {},
|
||||
};
|
||||
const root = {
|
||||
getState: async () => {
|
||||
alive = false;
|
||||
throw Error("detached state");
|
||||
},
|
||||
};
|
||||
assert.equal(
|
||||
await spawnRequestedNode(
|
||||
root,
|
||||
view,
|
||||
request,
|
||||
"tone_map",
|
||||
() => "node_a",
|
||||
() => alive,
|
||||
),
|
||||
false,
|
||||
);
|
||||
|
||||
alive = true;
|
||||
root.getState = async () => ({ version: 1, nodes: [] });
|
||||
view.addNode = async () => { alive = false; throw Error("detached add"); };
|
||||
assert.equal(await spawnRequestedNode(root, view, request, "tone_map", () => "node_b", () => alive), false);
|
||||
view.addNode = async () => {
|
||||
alive = false;
|
||||
throw Error("detached add");
|
||||
};
|
||||
assert.equal(
|
||||
await spawnRequestedNode(
|
||||
root,
|
||||
view,
|
||||
request,
|
||||
"tone_map",
|
||||
() => "node_b",
|
||||
() => alive,
|
||||
),
|
||||
false,
|
||||
);
|
||||
|
||||
alive = true;
|
||||
view.addNode = async () => { throw Error("live add failure"); };
|
||||
view.addNode = async () => {
|
||||
throw Error("live add failure");
|
||||
};
|
||||
await assert.rejects(
|
||||
spawnRequestedNode(root, view, request, "tone_map", () => "node_c", () => alive),
|
||||
spawnRequestedNode(
|
||||
root,
|
||||
view,
|
||||
request,
|
||||
"tone_map",
|
||||
() => "node_c",
|
||||
() => alive,
|
||||
),
|
||||
/live add failure/,
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user