Rebuild core around render graph AST and shared memory

Amp-Thread-ID: https://ampcode.com/threads/T-01a01380-b478-77d0-84a0-102880a5c5ae
Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
This commit is contained in:
Amp
2026-08-18 11:58:31 +00:00
co-authored by heaust
parent a23ef37b4d
commit 0e44917f9e
101 changed files with 4409 additions and 2610 deletions
+7
View File
@@ -0,0 +1,7 @@
{
"name": "@yawn/render-graph-ast",
"version": "0.1.0",
"description": "Canonical Yawn render-graph AST and S-expression codec",
"type": "module",
"exports": "./src/index.js"
}
+189
View File
@@ -0,0 +1,189 @@
/** Canonical DAG AST shared by every Yawn render-graph frontend. */
const AST_KIND = "yawn-render-graph";
const AST_VERSION = 1;
const IDENTIFIER = /^[A-Za-z][A-Za-z0-9_.-]*$/;
export class GraphAstError extends TypeError {
constructor(code, message = code) {
super(message);
this.name = "GraphAstError";
this.code = code;
}
}
const fail = (code, message) => {
throw new GraphAstError(code, message);
};
const object = (value) =>
value !== null && typeof value === "object" && !Array.isArray(value);
const identifier = (value) =>
typeof value === "string" &&
IDENTIFIER.test(value) &&
new TextEncoder().encode(value).length <= 64;
const finiteData = (value) =>
value === null ||
typeof value === "string" ||
typeof value === "boolean" ||
(typeof value === "number" && Number.isFinite(value)) ||
(Array.isArray(value) && value.every(finiteData)) ||
(object(value) && Object.values(value).every(finiteData));
const clone = (value) => structuredClone(value);
const freeze = (value) => {
if (value && typeof value === "object" && !Object.isFrozen(value)) {
Object.freeze(value);
Object.values(value).forEach(freeze);
}
return value;
};
const u32 = (value, name) => {
if (!Number.isInteger(value) || value < 0 || value > 0xffffffff)
fail("AST_U32", `${name} must be a uint32`);
return value;
};
function normalizePipelines(raw = {}) {
if (!object(raw)) fail("AST_PIPELINES", "pipelines must be an object");
const render = (raw.render ?? []).map((pipeline) => {
if (!object(pipeline)) fail("AST_PIPELINE", "render pipeline must be an object");
const result = {
name: pipeline.name,
shader: pipeline.shader,
vertexEntry: pipeline.vertexEntry ?? "vs_main",
fragmentEntry: pipeline.fragmentEntry ?? "fs_main",
doubleSided: pipeline.doubleSided ?? false,
};
if (
!identifier(result.name) ||
!identifier(result.vertexEntry) ||
!identifier(result.fragmentEntry) ||
typeof result.shader !== "string" ||
typeof result.doubleSided !== "boolean"
)
fail("AST_PIPELINE", "invalid render pipeline declaration");
return result;
});
const compute = (raw.compute ?? []).map((pipeline) => {
if (!object(pipeline)) fail("AST_PIPELINE", "compute pipeline must be an object");
const result = {
name: pipeline.name,
shader: pipeline.shader,
entry: pipeline.entry ?? "main",
dispatch: Array.from(pipeline.dispatch ?? []),
};
if (
!identifier(result.name) ||
!identifier(result.entry) ||
typeof result.shader !== "string" ||
result.dispatch.length !== 3 ||
result.dispatch.some((value) => u32(value, "dispatch") === 0)
)
fail("AST_PIPELINE", "invalid compute pipeline declaration");
return result;
});
const names = new Set();
for (const pipeline of [...render, ...compute]) {
if (names.has(pipeline.name))
fail("AST_PIPELINE_DUPLICATE", `duplicate pipeline '${pipeline.name}'`);
names.add(pipeline.name);
}
return { render, compute };
}
function normalizeNode(raw) {
if (!object(raw) || !identifier(raw.id) || !object(raw.executor))
fail("AST_NODE", "invalid node");
if (raw.state !== "enabled" && raw.state !== "muted")
fail("AST_NODE", "node state must be enabled or muted");
if (!identifier(raw.executor.key)) fail("AST_NODE", "invalid executor key");
const parameters = clone(raw.parameters ?? {});
if (!finiteData(parameters)) fail("AST_DATA", "parameters must be finite data");
if (!object(raw.inputs ?? {})) fail("AST_NODE", "inputs must be an object");
const inputs = {};
for (const name of Object.keys(raw.inputs ?? {}).sort()) {
if (!identifier(name) || !Array.isArray(raw.inputs[name]))
fail("AST_INPUT", "invalid node input");
inputs[name] = raw.inputs[name].map((reference) => {
if (!object(reference) || !identifier(reference.node) || !identifier(reference.socket))
fail("AST_REFERENCE", "invalid DAG reference");
return { node: reference.node, socket: reference.socket };
});
}
return {
id: raw.id,
state: raw.state,
executor: { key: raw.executor.key, version: u32(raw.executor.version, "executor version") },
parameters,
inputs,
};
}
/** Creates the canonical in-memory graph AST shared by all authoring frontends. */
export function createGraphAst({ kind, version, id, revision, pipelines = {}, nodes }) {
if (kind !== undefined && kind !== AST_KIND) fail("AST_KIND", "invalid AST kind");
if (version !== undefined && version !== AST_VERSION) fail("AST_VERSION", "unsupported AST version");
if (!identifier(id)) fail("AST_ID", "invalid graph id");
u32(revision, "revision");
if (revision === 0) fail("AST_REVISION", "revision must be nonzero");
if (!Array.isArray(nodes)) fail("AST_NODES", "nodes must be an array");
const normalized = nodes.map(normalizeNode);
const ids = new Set();
for (const node of normalized) {
if (ids.has(node.id)) fail("AST_NODE_DUPLICATE", `duplicate node '${node.id}'`);
ids.add(node.id);
}
return freeze({
kind: AST_KIND,
version: AST_VERSION,
id,
revision,
pipelines: normalizePipelines(pipelines),
nodes: normalized,
});
}
export const reference = (node, socket) => {
if (!identifier(node) || !identifier(socket)) fail("AST_REFERENCE", "invalid DAG reference");
return Object.freeze({ node, socket });
};
function data(value) {
if (value === null) return "null";
if (typeof value === "boolean") return String(value);
if (typeof value === "number") {
if (!Number.isFinite(value)) fail("AST_DATA", "numbers must be finite");
return JSON.stringify(value);
}
if (typeof value === "string") return JSON.stringify(value);
if (Array.isArray(value)) return `(array${value.map((item) => ` ${data(item)}`).join("")})`;
if (object(value))
return `(object${Object.keys(value)
.sort()
.map((key) => ` (field ${JSON.stringify(key)} ${data(value[key])})`)
.join("")})`;
fail("AST_DATA", "unsupported AST data value");
}
/** Serializes a graph AST to the only graph wire format accepted by Yawn core. */
export function serializeGraphAst(raw) {
const graph = createGraphAst(raw);
const nodes = graph.nodes
.map((node) => {
const inputs = Object.entries(node.inputs)
.map(
([name, references]) =>
`\n (input ${JSON.stringify(name)}${references
.map(
(reference) =>
` (ref ${JSON.stringify(reference.node)} ${JSON.stringify(reference.socket)})`,
)
.join("")})`,
)
.join("");
return `\n (node ${JSON.stringify(node.id)} ${node.state}\n (executor ${JSON.stringify(node.executor.key)} ${node.executor.version})\n (params ${data(node.parameters)})\n (inputs${inputs})\n )`;
})
.join("");
return `(yawn-graph ${AST_VERSION}\n (id ${JSON.stringify(graph.id)})\n (revision ${graph.revision})\n (pipelines ${data(graph.pipelines)})\n (nodes${nodes}))\n`;
}
export const GRAPH_AST_KIND = AST_KIND;
export const GRAPH_AST_VERSION = AST_VERSION;