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:
Amp
2026-07-28 13:00:29 +00:00
co-authored by heaust
parent fc8c16daec
commit edc7c8ef29
23 changed files with 5229 additions and 1810 deletions
+267 -41
View File
@@ -56,14 +56,20 @@ const sourceMaps = new WeakMap();
export const getSourceMap = (ir) => sourceMaps.get(ir);
export const mapAuthoringDiagnostic = (ir, diagnostic) => {
const details = diagnostic?.details;
const path = [details?.path, diagnostic?.path, details?.field, diagnostic?.field]
.find((value) => typeof value === "string");
const path = [
details?.path,
diagnostic?.path,
details?.field,
diagnostic?.field,
].find((value) => typeof value === "string");
const map = getSourceMap(ir);
let match;
if (path && map)
for (const key of Object.keys(map))
if (
(path === key || path.startsWith(`${key}.`) || path.startsWith(`${key}[`)) &&
(path === key ||
path.startsWith(`${key}.`) ||
path.startsWith(`${key}[`)) &&
(!match || key.length > match.length)
)
match = key;
@@ -80,33 +86,49 @@ export const mapAuthoringDiagnostic = (ir, diagnostic) => {
const mapValuePaths = (paths, path, source, value) => {
paths[path] = source;
if (Array.isArray(value))
value.forEach((child, index) => mapValuePaths(paths, `${path}[${index}]`, source, child));
value.forEach((child, index) =>
mapValuePaths(paths, `${path}[${index}]`, source, child),
);
else if (object(value))
for (const key of Object.keys(value))
mapValuePaths(paths, `${path}.${key}`, source, value[key]);
};
function parameterValue(raw, schema, nodeId, key) {
const expected = schema.type === "json" ? "json" : schema.type;
if (
!exactKeys(raw, ["kind", "value"]) ||
raw.kind !== expected ||
!finiteJson(raw.value)
)
fail("AUTHORING_PARAMETER", { nodeId, parameter: key });
if (
(expected === "number" && typeof raw.value !== "number") ||
(expected === "string" && typeof raw.value !== "string") ||
(expected === "boolean" && typeof raw.value !== "boolean") ||
(expected === "json" && !finiteJson(raw.value))
)
if (!exactKeys(raw, ["kind", "value"]) || raw.kind !== schema.type)
fail("AUTHORING_PARAMETER", { nodeId, parameter: key });
const value = raw.value;
const bounded = (number) =>
Number.isFinite(number) &&
(schema.minimum === undefined || number >= schema.minimum) &&
(schema.maximum === undefined || number <= schema.maximum);
const valid =
schema.type === "number"
? bounded(value) && (!schema.integer || Number.isSafeInteger(value))
: schema.type === "string"
? typeof value === "string" &&
(!schema.enum || schema.enum.includes(value))
: schema.type === "boolean"
? typeof value === "boolean"
: schema.type === "vector" || schema.type === "color"
? Array.isArray(value) &&
value.length === (schema.type === "vector" ? 3 : 4) &&
value.every(bounded)
: schema.type === "json" && finiteJson(value);
if (!valid) fail("AUTHORING_PARAMETER", { nodeId, parameter: key });
return canonical(structuredClone(raw.value));
}
export function adaptFxNodeSnapshot(raw, revision = 1) {
try {
const rootKeys = ["graphId", "catalogVersion", "nodes", "links", "metadata", "version"];
const rootKeys = [
"graphId",
"catalogVersion",
"nodes",
"links",
"metadata",
"version",
];
if (
!exactKeys(raw, rootKeys) ||
!Array.isArray(raw.nodes) ||
@@ -117,9 +139,7 @@ export function adaptFxNodeSnapshot(raw, revision = 1) {
fail("AUTHORING_SHAPE");
if (raw.graphId !== GRAPH_ID || raw.catalogVersion !== CATALOG_VERSION)
fail("AUTHORING_CATALOG");
if (
!Number.isSafeInteger(raw.version) || raw.version < 0
)
if (!Number.isSafeInteger(raw.version) || raw.version < 0)
fail("AUTHORING_SHAPE");
if (!Number.isInteger(revision) || revision < 1 || revision > 0xffffffff)
fail("AUTHORING_REVISION");
@@ -134,7 +154,20 @@ export function adaptFxNodeSnapshot(raw, revision = 1) {
definition = nodeDefinitions[n.typeId];
if (!descriptor)
fail("AUTHORING_NODE_TYPE", { nodeId: n.id, typeId: n.typeId });
const nodeKeys = ["id", "typeId", "typeVersion", "position", "size", "label", "parameters", "sockets", "muted", "collapsed", "extensions", "known"];
const nodeKeys = [
"id",
"typeId",
"typeVersion",
"position",
"size",
"label",
"parameters",
"sockets",
"muted",
"collapsed",
"extensions",
"known",
];
if (Object.hasOwn(n, "parentId")) nodeKeys.push("parentId");
if (
!exactKeys(n, nodeKeys) ||
@@ -143,10 +176,17 @@ export function adaptFxNodeSnapshot(raw, revision = 1) {
typeof n.muted !== "boolean" ||
typeof n.collapsed !== "boolean" ||
typeof n.label !== "string" ||
!exactKeys(n.position, ["x", "y"]) || !Number.isFinite(n.position.x) || !Number.isFinite(n.position.y) ||
!exactKeys(n.size, ["x", "y"]) || !Number.isFinite(n.size.x) || !Number.isFinite(n.size.y) || n.size.x <= 0 || n.size.y <= 0 ||
!exactKeys(n.position, ["x", "y"]) ||
!Number.isFinite(n.position.x) ||
!Number.isFinite(n.position.y) ||
!exactKeys(n.size, ["x", "y"]) ||
!Number.isFinite(n.size.x) ||
!Number.isFinite(n.size.y) ||
n.size.x <= 0 ||
n.size.y <= 0 ||
(Object.hasOwn(n, "parentId") && !identifier(n.parentId)) ||
!object(n.extensions) || !finiteJson(n.extensions) ||
!object(n.extensions) ||
!finiteJson(n.extensions) ||
!Array.isArray(n.sockets) ||
!object(n.parameters)
)
@@ -168,6 +208,48 @@ export function adaptFxNodeSnapshot(raw, revision = 1) {
),
]),
);
if (n.typeId === "bloom_blur")
parameters.direction =
parameters.direction === "horizontal" ? [1, 0] : [0, 1];
if (n.typeId === "frustum_cull") {
parameters.camera = parameters.cameraSelection;
delete parameters.cameraSelection;
}
if (n.typeId === "texture") {
const extent =
parameters.extentMode === "absolute"
? {
kind: "absolute",
width: parameters.absoluteWidth,
height: parameters.absoluteHeight,
depthOrArrayLayers: parameters.depthOrArrayLayers,
}
: {
kind: "surface_relative",
width: {
numerator: parameters.relativeWidthNumerator,
denominator: parameters.relativeWidthDenominator,
},
height: {
numerator: parameters.relativeHeightNumerator,
denominator: parameters.relativeHeightDenominator,
},
depthOrArrayLayers: parameters.depthOrArrayLayers,
};
const flat = structuredClone(parameters);
Object.keys(parameters).forEach((key) => delete parameters[key]);
Object.assign(parameters, {
residency: flat.residency,
texture: {
dimension: flat.dimension,
format: flat.format,
extent,
mipLevelCount: flat.mipLevelCount,
sampleCount: Number(flat.sampleCount),
viewFormats: flat.viewFormat === "none" ? [] : [flat.viewFormat],
},
});
}
const expected = [
...Object.keys(descriptor.inputs),
...Object.keys(descriptor.outputs),
@@ -181,17 +263,49 @@ export function adaptFxNodeSnapshot(raw, revision = 1) {
socketDefinition = definition.sockets[s.key],
direction = input ? "input" : "output",
dataType = socketDefinition.type,
socketKeys = ["id", "key", "label", "direction", "dataType", "accepts", "maxIncomingLinks", ...(socketDefinition.value ? ["defaultValue"] : []), "visible"];
socketKeys = [
"id",
"key",
"label",
"direction",
"dataType",
"accepts",
"maxIncomingLinks",
...(socketDefinition.value ? ["defaultValue"] : []),
"visible",
];
if (
!exactKeys(s, socketKeys) ||
s.id !== `${n.id}:${s.key}` ||
s.label !== socketDefinition.title ||
s.direction !== direction ||
s.dataType !== dataType ||
!Array.isArray(s.accepts) || s.accepts.length !== (direction === "input" ? socketTypes[dataType].acceptsFrom.length : 0) ||
!s.accepts.every((v, i) => v === (direction === "input" ? socketTypes[dataType].acceptsFrom[i] : undefined)) ||
!Array.isArray(s.accepts) ||
s.accepts.length !==
(direction === "input"
? socketTypes[dataType].acceptsFrom.length
: 0) ||
!s.accepts.every(
(v, i) =>
v ===
(direction === "input"
? socketTypes[dataType].acceptsFrom[i]
: undefined),
) ||
(socketDefinition.value
? !exactKeys(s.defaultValue, ["kind", "value"]) || !finiteJson(s.defaultValue.value)
? (() => {
try {
parameterValue(
s.defaultValue,
socketDefinition.value,
n.id,
s.key,
);
return false;
} catch {
return true;
}
})()
: s.defaultValue !== undefined) ||
s.visible !== socketDefinition.visible ||
s.maxIncomingLinks !== socketDefinition.maxIncomingLinks
@@ -206,10 +320,21 @@ export function adaptFxNodeSnapshot(raw, revision = 1) {
: descriptor.outputs[s.key].type,
authoringType: s.dataType,
maxIncomingLinks: s.maxIncomingLinks,
defaultValue: socketDefinition.value
? structuredClone(s.defaultValue)
: undefined,
});
}
if (new Set(n.sockets.map((s) => s.key)).size !== expected.length)
fail("AUTHORING_SOCKET_SET", { nodeId: n.id });
if (n.typeId === "mesh_query") {
parameters.visibleDefault = sockets.get(
`${n.id}:isVisible`,
).defaultValue.value;
parameters.frustumCulledDefault = sockets.get(
`${n.id}:isFrustumCulled`,
).defaultValue.value;
}
nodes.set(n.id, {
ordinal,
value: {
@@ -230,8 +355,18 @@ export function adaptFxNodeSnapshot(raw, revision = 1) {
!object(link) ||
!identifier(link.id) ||
linkIds.has(link.id) ||
!exactKeys(link, ["id", "fromNodeId", "fromSocketId", "toNodeId", "toSocketId", "muted", "extensions"]) ||
typeof link.muted !== "boolean" || !object(link.extensions) || !finiteJson(link.extensions)
!exactKeys(link, [
"id",
"fromNodeId",
"fromSocketId",
"toNodeId",
"toSocketId",
"muted",
"extensions",
]) ||
typeof link.muted !== "boolean" ||
!object(link.extensions) ||
!finiteJson(link.extensions)
)
fail("AUTHORING_LINK", { linkId: link?.id });
linkIds.add(link.id);
@@ -244,21 +379,33 @@ export function adaptFxNodeSnapshot(raw, revision = 1) {
link.toNodeId !== to.node ||
from.direction !== "output" ||
to.direction !== "input" ||
(!link.muted && (incoming.get(link.toSocketId) ?? 0) >= to.maxIncomingLinks)
(!link.muted &&
(incoming.get(link.toSocketId) ?? 0) >= to.maxIncomingLinks)
)
fail(
!link.muted && (incoming.get(link.toSocketId) ?? 0) >= (to?.maxIncomingLinks ?? Infinity)
!link.muted &&
(incoming.get(link.toSocketId) ?? 0) >=
(to?.maxIncomingLinks ?? Infinity)
? "AUTHORING_LINK_INCOMING"
: "AUTHORING_LINK",
!link.muted && (incoming.get(link.toSocketId) ?? 0) >= (to?.maxIncomingLinks ?? Infinity)
!link.muted &&
(incoming.get(link.toSocketId) ?? 0) >=
(to?.maxIncomingLinks ?? Infinity)
? { socketId: link.toSocketId }
: { linkId: link.id },
);
const accepted =
descriptors[nodes.get(to.node).value.executor.key].inputs[to.key]
.accepted.types;
const authoringAccepted = socketTypes[nodeDefinitions[nodes.get(to.node).value.executor.key].sockets[to.key].type].acceptsFrom;
if (!accepted.includes(from.semanticType) || !authoringAccepted.includes(from.authoringType))
const authoringAccepted =
socketTypes[
nodeDefinitions[nodes.get(to.node).value.executor.key].sockets[to.key]
.type
].acceptsFrom;
if (
!accepted.includes(from.semanticType) ||
!authoringAccepted.includes(from.authoringType)
)
fail("AUTHORING_LINK_TYPE", { linkId: link.id });
const linkSource = {
kind: "link",
@@ -299,13 +446,92 @@ export function adaptFxNodeSnapshot(raw, revision = 1) {
const base = `nodes[${wireOrdinal}]`;
const nodeSource = { kind: "node", nodeId: item.value.id };
paths[base] = nodeSource;
for (const field of ["id", "state", "executor", "executor.key", "executor.version"])
for (const field of [
"id",
"state",
"executor",
"executor.key",
"executor.version",
])
paths[`${base}.${field}`] = nodeSource;
paths[`${base}.parameters`] = nodeSource;
for (const key of Object.keys(item.value.parameters))
mapValuePaths(paths, `${base}.parameters.${key}`, { kind: "parameter", nodeId: item.value.id, parameter: key }, item.value.parameters[key]);
for (const key of Object.keys(descriptors[item.value.executor.key].inputs)) {
const link = raw.links.find((x) => !x.muted && x.toNodeId === item.value.id && sockets.get(x.toSocketId)?.key === key);
const parameterSource = (parameter) => ({
kind: "parameter",
nodeId: item.value.id,
parameter,
});
if (item.value.executor.key === "texture") {
const root = `${base}.parameters`;
const texture = item.value.parameters.texture;
paths[`${root}.residency`] = parameterSource("residency");
paths[`${root}.texture`] = nodeSource;
paths[`${root}.texture.dimension`] = parameterSource("dimension");
paths[`${root}.texture.format`] = parameterSource("format");
paths[`${root}.texture.extent`] = parameterSource("extentMode");
paths[`${root}.texture.extent.kind`] = parameterSource("extentMode");
paths[`${root}.texture.extent.depthOrArrayLayers`] =
parameterSource("depthOrArrayLayers");
if (texture.extent.kind === "absolute") {
paths[`${root}.texture.extent.width`] =
parameterSource("absoluteWidth");
paths[`${root}.texture.extent.height`] =
parameterSource("absoluteHeight");
} else {
paths[`${root}.texture.extent.width`] = parameterSource("extentMode");
paths[`${root}.texture.extent.width.numerator`] = parameterSource(
"relativeWidthNumerator",
);
paths[`${root}.texture.extent.width.denominator`] = parameterSource(
"relativeWidthDenominator",
);
paths[`${root}.texture.extent.height`] =
parameterSource("extentMode");
paths[`${root}.texture.extent.height.numerator`] = parameterSource(
"relativeHeightNumerator",
);
paths[`${root}.texture.extent.height.denominator`] = parameterSource(
"relativeHeightDenominator",
);
}
paths[`${root}.texture.mipLevelCount`] =
parameterSource("mipLevelCount");
paths[`${root}.texture.sampleCount`] = parameterSource("sampleCount");
mapValuePaths(
paths,
`${root}.texture.viewFormats`,
parameterSource("viewFormat"),
texture.viewFormats,
);
} else
for (const key of Object.keys(item.value.parameters))
mapValuePaths(
paths,
`${base}.parameters.${key}`,
item.value.executor.key === "mesh_query" && key.endsWith("Default")
? {
kind: "input",
nodeId: item.value.id,
input:
key === "visibleDefault" ? "isVisible" : "isFrustumCulled",
socketId: `${item.value.id}:${key === "visibleDefault" ? "isVisible" : "isFrustumCulled"}`,
unconnected: true,
}
: parameterSource(
item.value.executor.key === "frustum_cull" && key === "camera"
? "cameraSelection"
: key,
),
item.value.parameters[key],
);
for (const key of Object.keys(
descriptors[item.value.executor.key].inputs,
)) {
const link = raw.links.find(
(x) =>
!x.muted &&
x.toNodeId === item.value.id &&
sockets.get(x.toSocketId)?.key === key,
);
const source = linkSources.get(link?.id) ?? {
kind: "input",
nodeId: item.value.id,
+2 -1
View File
@@ -3,8 +3,9 @@ import { semanticCatalog } from "./catalog.js";
const GROUPS = Object.freeze([
["source", "Source"],
["compute", "Compute"],
["cpu_preparation", "CPU preparation"],
["render", "Render / post"],
["present", "Present"],
["frame", "Frame"],
]);
const title = (typeId) => typeId.replaceAll("_", " ");
+186 -97
View File
@@ -1,7 +1,6 @@
export const GRAPH_ID = "authored_gpu_culling";
export const CATALOG_VERSION = 2;
export const CATALOG_VERSION = 4;
const exact = (type) => ({ kind: "exact", types: [type] });
const oneOf = (...types) => ({ kind: "one_of", types });
const i = (type, required = true, authoringType) => ({
accepted: typeof type === "string" ? exact(type) : type,
required,
@@ -25,104 +24,91 @@ const texture = {
},
};
export const semanticCatalog = Object.freeze({
surface_target: {
mesh: {
execution: "source",
inputs: {},
outputs: { surface: o("surface_target") },
parameters: {},
},
texture_spec: {
execution: "source",
inputs: {},
outputs: { spec: o("texture_spec") },
parameters: structuredClone(texture),
},
scene_table: {
execution: "source",
inputs: {},
outputs: { scene: o("scene_table") },
parameters: {},
},
local_aabb_buffer: {
execution: "source",
inputs: { scene: i("scene_table") },
outputs: { localAabbs: o("local_aabb_buffer") },
parameters: {},
},
camera_frustum: {
execution: "source",
inputs: {},
outputs: { frustum: o("camera_frustum") },
parameters: {},
},
visibility_flags: {
execution: "source",
inputs: { scene: i("scene_table") },
outputs: {
flags: {
mesh: o("mesh_data"),
localAabbs: o("local_aabb_buffer"),
isVisible: {
...o("boolean_flag_buffer"),
authoringType: "visibility_flag_buffer",
},
pipelineIndices: o("pipeline_index_stream"),
},
parameters: {},
},
texture: {
execution: "source",
inputs: {},
outputs: { texture: o("texture") },
parameters: {
residency: "transient",
format: "rgba16_float",
dimension: "d2",
extentMode: "surface_relative",
absoluteWidth: 1,
absoluteHeight: 1,
relativeWidthNumerator: 1,
relativeWidthDenominator: 1,
relativeHeightNumerator: 1,
relativeHeightDenominator: 1,
depthOrArrayLayers: 1,
mipLevelCount: 1,
sampleCount: "1",
viewFormat: "none",
},
},
frustum_cull: {
execution: "compute",
inputs: {
scene: i("scene_table"),
mesh: i("mesh_data"),
localAabbs: i("local_aabb_buffer"),
frustum: i("camera_frustum"),
},
outputs: {
flags: {
isFrustumCulled: {
...o("boolean_flag_buffer"),
authoringType: "frustum_flag_buffer",
},
},
parameters: {},
parameters: { cameraSelection: "active" },
},
mesh_query: {
execution: "compute",
inputs: {
scene: i("scene_table"),
mesh: i("mesh_data"),
isVisible: i("boolean_flag_buffer", false, "visibility_flag_buffer"),
isFrustumCulled: i("boolean_flag_buffer", false, "frustum_flag_buffer"),
},
outputs: { draws: o("draw_stream") },
parameters: {
filters: [
{ flag: "isVisible", predicate: "required_true" },
{ flag: "isFrustumCulled", predicate: "required_false" },
],
visiblePredicate: "required_true",
frustumCulledPredicate: "required_false",
},
},
depth_stencil_config: {
execution: "source",
inputs: {},
outputs: { config: o("depth_stencil_config") },
parameters: {
depthCompare: "less_equal",
depthWriteEnabled: true,
clearDepth: 1,
},
pipeline_registry: {
execution: "cpu_preparation",
inputs: { pipelineIndices: i("pipeline_index_stream") },
outputs: { activation: o("pipeline_activation") },
parameters: {},
},
legacy_forward: {
pipeline: {
execution: "render",
inputs: {
scene: i("scene_table"),
mesh: i("mesh_data"),
draws: i("draw_stream"),
colorTarget: i(oneOf("surface_target", "texture_spec", "texture")),
depthTarget: i(oneOf("texture_spec", "texture")),
depthStencil: i("depth_stencil_config"),
activation: i("pipeline_activation"),
colorTarget: i("texture"),
depthTarget: i("texture"),
},
outputs: { color: o("texture"), depth: o("texture") },
parameters: { clearColor: [0.015, 0.02, 0.03, 1] },
parameters: { pipeline: "gltf_standard", depthCompare: "less_equal", depthWriteEnabled: true, clearDepth: 1, clearColor: [0.015, 0.02, 0.03, 1] },
},
fullscreen_copy: {
execution: "render",
inputs: {
source: i("texture"),
colorTarget: i(oneOf("surface_target", "texture_spec", "texture")),
colorTarget: i("texture"),
},
outputs: { color: o("texture") },
parameters: {},
@@ -131,7 +117,7 @@ export const semanticCatalog = Object.freeze({
execution: "render",
inputs: {
source: i("texture"),
colorTarget: i(oneOf("surface_target", "texture_spec", "texture")),
colorTarget: i("texture"),
},
outputs: { color: o("texture") },
parameters: { exposure: 1 },
@@ -140,7 +126,7 @@ export const semanticCatalog = Object.freeze({
execution: "render",
inputs: {
source: i("texture"),
colorTarget: i(oneOf("surface_target", "texture_spec", "texture")),
colorTarget: i("texture"),
},
outputs: { color: o("texture") },
parameters: { threshold: 1, knee: 0.5 },
@@ -149,7 +135,7 @@ export const semanticCatalog = Object.freeze({
execution: "render",
inputs: {
source: i("texture"),
colorTarget: i(oneOf("surface_target", "texture_spec", "texture")),
colorTarget: i("texture"),
},
outputs: { color: o("texture") },
parameters: { direction: [1, 0], radius: 1 },
@@ -159,7 +145,7 @@ export const semanticCatalog = Object.freeze({
inputs: {
source: i("texture"),
bloom: i("texture"),
colorTarget: i(oneOf("surface_target", "texture_spec", "texture")),
colorTarget: i("texture"),
},
outputs: { color: o("texture") },
parameters: { intensity: 1 },
@@ -168,14 +154,14 @@ export const semanticCatalog = Object.freeze({
execution: "render",
inputs: {
source: i("texture"),
colorTarget: i(oneOf("surface_target", "texture_spec", "texture")),
colorTarget: i("texture"),
},
outputs: { color: o("texture") },
parameters: { strength: 2 },
},
present: {
execution: "present",
inputs: { surface: i("texture") },
frame_out: {
execution: "frame",
inputs: { color: i("texture") },
outputs: {},
parameters: {},
},
@@ -193,15 +179,13 @@ const socketColors = [
];
export const socketTypes = Object.fromEntries(
[
"surface_target",
"texture_spec",
"texture",
"scene_table",
"mesh_data",
"local_aabb_buffer",
"camera_frustum",
"boolean_flag_buffer",
"pipeline_index_stream",
"draw_stream",
"depth_stencil_config",
"pipeline_activation",
"visibility_flag_buffer",
"frustum_flag_buffer",
].map((type, index) => [
@@ -213,12 +197,6 @@ export const socketTypes = Object.fromEntries(
},
]),
);
socketTypes.surface_target.acceptsFrom = [
"surface_target",
"texture_spec",
"texture",
];
socketTypes.texture_spec.acceptsFrom = ["texture_spec", "texture"];
socketTypes.boolean_flag_buffer.acceptsFrom = [
"boolean_flag_buffer",
"visibility_flag_buffer",
@@ -259,33 +237,138 @@ export const theme = {
export const styles = {
source: { header: "#3977a8" },
compute: { header: "#725a9b" },
cpu_preparation: { header: "#8a6d3b" },
render: { header: "#426b43" },
present: { header: "#a75d37" },
frame: { header: "#a75d37" },
};
const socket = (title, direction, type) => ({
const socket = (title, direction, type, value = null) => ({
title,
direction,
type,
maxIncomingLinks: direction === "input" ? 1 : 0,
visible: true,
value: null,
showValue: false,
value,
showValue: value !== null,
});
const parameterSchema = (value) =>
typeof value === "number"
? { type: "number", default: { kind: "number", value } }
: typeof value === "string"
? { type: "string", default: { kind: "string", value } }
: typeof value === "boolean"
? { type: "boolean", default: { kind: "boolean", value } }
: { type: "json", default: { kind: "json", value } };
const tagged = (kind, value) => ({ kind, value: structuredClone(value) });
const number = (value, minimum, maximum) => ({
type: "number",
default: tagged("number", value),
minimum,
maximum,
});
const enumeration = (value, values) => ({
type: "string",
default: tagged("string", value),
enum: values,
});
const string = (value) => ({ type: "string", default: tagged("string", value) });
const boolean = (value) => ({
type: "boolean",
default: tagged("boolean", value),
});
const color = (value) => ({
type: "color",
default: tagged("color", value),
minimum: 0,
maximum: 1,
});
const json = (value) => ({ type: "json", default: tagged("json", value) });
const parameterSchemas = {
texture: {
residency: enumeration("transient", ["transient", "persistent"]),
format: enumeration("rgba16_float", [
"rgba8_unorm",
"rgba8_unorm_srgb",
"bgra8_unorm",
"bgra8_unorm_srgb",
"rgba16_float",
"r32_float",
"depth32_float",
]),
dimension: enumeration("d2", ["d1", "d2", "d3"]),
extentMode: enumeration("surface_relative", [
"surface_relative",
"absolute",
]),
absoluteWidth: { ...number(1, 1, 0xffffffff), integer: true },
absoluteHeight: { ...number(1, 1, 0xffffffff), integer: true },
relativeWidthNumerator: { ...number(1, 1, 0xffffffff), integer: true },
relativeWidthDenominator: { ...number(1, 1, 0xffffffff), integer: true },
relativeHeightNumerator: { ...number(1, 1, 0xffffffff), integer: true },
relativeHeightDenominator: { ...number(1, 1, 0xffffffff), integer: true },
depthOrArrayLayers: { ...number(1, 1, 0xffffffff), integer: true },
mipLevelCount: { ...number(1, 1, 0xffffffff), integer: true },
sampleCount: enumeration("1", ["1", "4"]),
viewFormat: enumeration("none", [
"none",
"rgba8_unorm",
"rgba8_unorm_srgb",
"bgra8_unorm",
"bgra8_unorm_srgb",
"rgba16_float",
"r32_float",
"depth32_float",
]),
},
mesh: {},
frustum_cull: { cameraSelection: enumeration("active", ["active"]) },
mesh_query: {
visiblePredicate: enumeration("required_true", [
"any",
"required_true",
"required_false",
]),
frustumCulledPredicate: enumeration("required_false", [
"any",
"required_true",
"required_false",
]),
},
pipeline_registry: {},
pipeline: {
pipeline: string("gltf_standard"),
depthCompare: enumeration("less_equal", [
"never",
"less",
"equal",
"less_equal",
"greater",
"not_equal",
"greater_equal",
"always",
]),
depthWriteEnabled: boolean(true),
clearDepth: number(1, 0, 1),
clearColor: color([0.015, 0.02, 0.03, 1]),
},
fullscreen_copy: {},
tone_map: { exposure: number(1, 0, 32) },
bloom_extract: { threshold: number(1, 0, 64), knee: number(0.5, 0, 1) },
bloom_blur: {
direction: enumeration("horizontal", ["horizontal", "vertical"]),
radius: number(1, 1, 16),
},
bloom_composite: { intensity: number(1, 0, 16) },
luminance_edge: { strength: number(2, 0, 16) },
frame_out: {},
};
export const nodeDefinitions = Object.fromEntries(
Object.entries(semanticCatalog).map(([key, c]) => {
const sockets = {
...Object.fromEntries(
Object.entries(c.inputs).map(([n, v]) => [
n,
socket(n, "input", v.authoringType ?? v.accepted.types[0]),
socket(
n,
"input",
v.authoringType ?? v.accepted.types[0],
key === "mesh_query" && n === "isVisible"
? boolean(true)
: key === "mesh_query" && n === "isFrustumCulled"
? boolean(false)
: null,
),
]),
),
...Object.fromEntries(
@@ -295,12 +378,15 @@ export const nodeDefinitions = Object.fromEntries(
]),
),
},
parameters = Object.fromEntries(
Object.entries(c.parameters).map(([name, value]) => [
name,
parameterSchema(value),
]),
);
parameters = parameterSchemas[key];
if (
!parameters ||
Object.keys(parameters).length !== Object.keys(c.parameters).length ||
!Object.keys(c.parameters).every((name) =>
Object.hasOwn(parameters, name),
)
)
throw new Error(`parameter schema mismatch for ${key}`);
return [
key,
{
@@ -314,6 +400,9 @@ export const nodeDefinitions = Object.fromEntries(
...Object.keys(parameters).map((parameter) => ({
kind: "parameter",
parameter,
...(key === "frustum_cull" && parameter === "cameraSelection"
? { title: "Camera" }
: {}),
})),
...Object.keys(sockets).map((socket) => ({ kind: "socket", socket })),
],
+60 -51
View File
@@ -5,19 +5,16 @@ import { createAddNodeMenu } from "./add-node-menu.js";
import { createNodeIdAllocator, spawnRequestedNode } from "./node-spawn.js";
const spec = [
["surface", "surface_target", { x: 40, y: 40 }],
["hdr", "texture_spec", { x: 40, y: 170 }],
["depth", "texture_spec", { x: 40, y: 300 }],
["scene", "scene_table", { x: 40, y: 470 }],
["aabbs", "local_aabb_buffer", { x: 290, y: 430 }],
["frustum", "camera_frustum", { x: 290, y: 590 }],
["visible", "visibility_flags", { x: 290, y: 300 }],
["hdr", "texture", { x: 40, y: 170 }],
["depth", "texture", { x: 40, y: 300 }],
["mesh", "mesh", { x: 40, y: 470 }],
["cull", "frustum_cull", { x: 540, y: 480 }],
["query", "mesh_query", { x: 790, y: 330 }],
["depth_config", "depth_stencil_config", { x: 790, y: 620 }],
["forward", "legacy_forward", { x: 1040, y: 290 }],
["copy", "fullscreen_copy", { x: 1300, y: 250 }],
["present", "present", { x: 1540, y: 250 }],
["registry", "pipeline_registry", { x: 790, y: 620 }],
["ground", "pipeline", { x: 1040, y: 290 }],
["pbr", "pipeline", { x: 1300, y: 290 }],
["pbr_double", "pipeline", { x: 1560, y: 290 }],
["frame_out", "frame_out", { x: 1820, y: 250 }],
];
async function seed(root) {
await root.setState({
@@ -30,22 +27,24 @@ async function seed(root) {
for (const [nodeId, nodeType, position] of spec)
await root.dispatch({ type: "node.add", nodeId, nodeType, position });
const links = [
["scene", "scene", "aabbs", "scene"],
["scene", "scene", "visible", "scene"],
["scene", "scene", "cull", "scene"],
["aabbs", "localAabbs", "cull", "localAabbs"],
["frustum", "frustum", "cull", "frustum"],
["scene", "scene", "query", "scene"],
["visible", "flags", "query", "isVisible"],
["cull", "flags", "query", "isFrustumCulled"],
["scene", "scene", "forward", "scene"],
["query", "draws", "forward", "draws"],
["hdr", "spec", "forward", "colorTarget"],
["depth", "spec", "forward", "depthTarget"],
["depth_config", "config", "forward", "depthStencil"],
["forward", "color", "copy", "source"],
["surface", "surface", "copy", "colorTarget"],
["copy", "color", "present", "surface"],
["mesh", "mesh", "cull", "mesh"],
["mesh", "localAabbs", "cull", "localAabbs"],
["mesh", "mesh", "query", "mesh"],
["mesh", "isVisible", "query", "isVisible"],
["cull", "isFrustumCulled", "query", "isFrustumCulled"],
["mesh", "pipelineIndices", "registry", "pipelineIndices"],
...["ground", "pbr", "pbr_double"].flatMap((pipeline) => [
["mesh", "mesh", pipeline, "mesh"],
["query", "draws", pipeline, "draws"],
["registry", "activation", pipeline, "activation"],
]),
["hdr", "texture", "ground", "colorTarget"],
["depth", "texture", "ground", "depthTarget"],
["ground", "color", "pbr", "colorTarget"],
["ground", "depth", "pbr", "depthTarget"],
["pbr", "color", "pbr_double", "colorTarget"],
["pbr", "depth", "pbr_double", "depthTarget"],
["pbr_double", "color", "frame_out", "color"],
];
for (const [a, as, b, bs] of links) {
const id = `${a}_${as}_${b}_${bs}`;
@@ -64,34 +63,44 @@ async function seed(root) {
}
const authored = await root.getState(),
depth = authored.nodes.find((node) => node.id === "depth");
depth.parameters.texture = {
kind: "json",
value: {
dimension: "d2",
format: "depth32_float",
extent: {
kind: "surface_relative",
width: { numerator: 1, denominator: 1 },
height: { numerator: 1, denominator: 1 },
depthOrArrayLayers: 1,
},
mipLevelCount: 1,
sampleCount: 1,
viewFormats: [],
},
};
depth.parameters.format = { kind: "string", value: "depth32_float" };
for (const [id, name] of [["ground", "ground_plane"], ["pbr", "gltf_standard"], ["pbr_double", "gltf_standard_double_sided"]])
authored.nodes.find((node) => node.id === id).parameters.pipeline = { kind: "string", value: name };
await root.setState(authored);
}
export async function createRenderGraphEditor(canvas) {
const allocateId = createNodeIdAllocator();
let root, view, menu, destroying, dead = false;
const requestAddNode = Object.assign(async (request, point, isCurrent = () => true) => {
let typeId;
try { typeId = await menu?.open(point); } catch (error) { if (!dead && isCurrent()) console.error(error); return; }
if (dead || !isCurrent() || !root || !view) return;
const alive = () => !dead && isCurrent();
try { await spawnRequestedNode(root, view, request, typeId, allocateId, alive); } catch (error) { if (!dead) console.error(error); }
}, { close: () => menu?.close() });
let root,
view,
menu,
destroying,
dead = false;
const requestAddNode = Object.assign(
async (request, point, isCurrent = () => true) => {
let typeId;
try {
typeId = await menu?.open(point);
} catch (error) {
if (!dead && isCurrent()) console.error(error);
return;
}
if (dead || !isCurrent() || !root || !view) return;
const alive = () => !dead && isCurrent();
try {
await spawnRequestedNode(
root,
view,
request,
typeId,
allocateId,
alive,
);
} catch (error) {
if (!dead) console.error(error);
}
},
{ close: () => menu?.close() },
);
const host = prepareBrowserHost(canvas, { requestAddNode });
const destroy = () =>
(destroying ??= (async () => {
+56 -258
View File
@@ -1,278 +1,76 @@
const input = (node, socket) => ({ node, socket });
const node = (id, key, parameters = {}, inputs = {}) => ({
id,
state: "enabled",
executor: { key, version: 1 },
parameters,
inputs,
id, state: "enabled", executor: { key, version: 1 }, parameters, inputs,
});
const texture = (format) => ({
const texture = (format, scale = 1) => ({
texture: {
dimension: "d2",
format,
extent: {
kind: "surface_relative",
width: { numerator: 1, denominator: 1 },
height: { numerator: 1, denominator: 1 },
depthOrArrayLayers: 1,
},
mipLevelCount: 1,
sampleCount: 1,
viewFormats: [],
dimension: "d2", format,
extent: { kind: "surface_relative", width: { numerator: 1, denominator: scale }, height: { numerator: 1, denominator: scale }, depthOrArrayLayers: 1 },
mipLevelCount: 1, sampleCount: 1, viewFormats: [],
},
residency: "transient",
});
const direct = (graphId, clearColor) => Object.freeze({
schemaVersion: 2,
graphId,
revision: 1,
nodes: [
node("surface", "surface_target"),
node("depth", "texture_spec", texture("depth32_float")),
node("scene", "scene_table"),
node("visible", "visibility_flags", {}, { scene: input("scene", "scene") }),
node("query", "mesh_query", {
filters: [
{ flag: "isVisible", predicate: "required_true" },
{ flag: "isFrustumCulled", predicate: "any" },
],
}, { scene: input("scene", "scene"), isVisible: input("visible", "flags") }),
node("depth_config", "depth_stencil_config", {
depthCompare: "less_equal",
depthWriteEnabled: true,
clearDepth: 1,
}),
node("forward", "legacy_forward", { clearColor }, {
scene: input("scene", "scene"),
draws: input("query", "draws"),
colorTarget: input("surface", "surface"),
depthTarget: input("depth", "spec"),
depthStencil: input("depth_config", "config"),
}),
node("present", "present", {}, { surface: input("forward", "color") }),
],
});
const scene = (colorTarget, clearColor = [0.015, 0.02, 0.03, 1]) => [
node("hdr", "texture", texture("rgba16_float")),
node("depth", "texture", texture("depth32_float")),
node("mesh", "mesh"),
node("query", "mesh_query", { visiblePredicate: "required_true", visibleDefault: true, frustumCulledPredicate: "any", frustumCulledDefault: false }, { mesh: input("mesh", "mesh"), isVisible: input("mesh", "isVisible") }),
node("registry", "pipeline_registry", {}, { pipelineIndices: input("mesh", "pipelineIndices") }),
node("ground", "pipeline", { pipeline: "ground_plane", depthCompare: "less_equal", depthWriteEnabled: true, clearDepth: 1, clearColor }, { mesh: input("mesh", "mesh"), draws: input("query", "draws"), activation: input("registry", "activation"), colorTarget: input(colorTarget, "texture"), depthTarget: input("depth", "texture") }),
node("pbr", "pipeline", { pipeline: "gltf_standard", depthCompare: "less_equal", depthWriteEnabled: true, clearDepth: 1, clearColor }, { mesh: input("mesh", "mesh"), draws: input("query", "draws"), activation: input("registry", "activation"), colorTarget: input("ground", "color"), depthTarget: input("ground", "depth") }),
node("pbr_double", "pipeline", { pipeline: "gltf_standard_double_sided", depthCompare: "less_equal", depthWriteEnabled: true, clearDepth: 1, clearColor }, { mesh: input("mesh", "mesh"), draws: input("query", "draws"), activation: input("registry", "activation"), colorTarget: input("pbr", "color"), depthTarget: input("pbr", "depth") }),
];
const graph = (graphId, nodes) => Object.freeze({ schemaVersion: 2, graphId, revision: 1, nodes });
const direct = (graphId, clearColor) => graph(graphId, [
node("ldr", "texture", texture("rgba8_unorm")),
...scene("ldr", clearColor).filter((item) => item.id !== "hdr"),
node("frame_out", "frame_out", {}, { color: input("pbr_double", "color") }),
]);
export const midnight = direct("preset_midnight", [0.015, 0.06, 0.18, 1]);
export const ember = direct("preset_ember", [0.18, 0.035, 0.012, 1]);
export const hdr = Object.freeze({
schemaVersion: 2,
graphId: "preset_hdr_fullscreen",
revision: 1,
nodes: [
node("surface", "surface_target"),
node("hdr", "texture_spec", texture("rgba16_float")),
node("depth", "texture_spec", texture("depth32_float")),
node("scene", "scene_table"),
node("visible", "visibility_flags", {}, { scene: input("scene", "scene") }),
node(
"query",
"mesh_query",
{
filters: [
{ flag: "isVisible", predicate: "required_true" },
{ flag: "isFrustumCulled", predicate: "any" },
],
},
{ scene: input("scene", "scene"), isVisible: input("visible", "flags") },
),
node("depth_config", "depth_stencil_config", {
depthCompare: "less_equal",
depthWriteEnabled: true,
clearDepth: 1,
}),
node(
"forward",
"legacy_forward",
{ clearColor: [0.015, 0.02, 0.03, 1] },
{
scene: input("scene", "scene"),
draws: input("query", "draws"),
colorTarget: input("hdr", "spec"),
depthTarget: input("depth", "spec"),
depthStencil: input("depth_config", "config"),
},
),
node(
"copy",
"fullscreen_copy",
{},
{
source: input("forward", "color"),
colorTarget: input("surface", "surface"),
},
),
node("present", "present", {}, { surface: input("copy", "color") }),
],
});
export const culling = Object.freeze((() => {
const graph = structuredClone(hdr);
graph.graphId = "preset_gpu_culling";
graph.nodes.splice(
5,
0,
node("aabbs", "local_aabb_buffer", {}, { scene: input("scene", "scene") }),
node("frustum", "camera_frustum"),
node("cull", "frustum_cull", {}, {
scene: input("scene", "scene"),
localAabbs: input("aabbs", "localAabbs"),
frustum: input("frustum", "frustum"),
}),
);
const query = graph.nodes.find((x) => x.id === "query");
query.parameters.filters[1].predicate = "required_false";
query.inputs.isFrustumCulled = input("cull", "flags");
return graph;
export const hdr = graph("preset_hdr_fullscreen", [
...scene("hdr"),
node("frame_out", "frame_out", {}, { color: input("pbr_double", "color") }),
]);
export const culling = graph("preset_gpu_culling", (() => {
const nodes = structuredClone(hdr.nodes);
nodes.splice(3, 0, node("cull", "frustum_cull", { camera: "active" }, { mesh: input("mesh", "mesh"), localAabbs: input("mesh", "localAabbs") }));
const query = nodes.find((item) => item.id === "query");
query.parameters.frustumCulledPredicate = "required_false";
query.inputs.isFrustumCulled = input("cull", "isFrustumCulled");
return nodes;
})());
const postPreset = (graphId, kind) => {
const nodes = hdr.nodes.slice(0, 8).map((x) => structuredClone(x));
if (kind === "tone")
nodes.push(
node(
"tone",
"tone_map",
{ exposure: 1 },
{
source: input("forward", "color"),
colorTarget: input("surface", "surface"),
},
),
);
const nodes = [node("ldr", "texture", texture("rgba8_unorm")), ...scene("hdr")];
let source = "pbr_double";
if (kind === "edges") {
nodes.splice(1, 0, node("edge_hdr", "texture_spec", texture("rgba16_float")));
nodes.push(
node(
"edges",
"luminance_edge",
{ strength: 2 },
{
source: input("forward", "color"),
colorTarget: input("edge_hdr", "spec"),
},
),
);
nodes.push(
node(
"tone",
"tone_map",
{ exposure: 1 },
{
source: input("edges", "color"),
colorTarget: input("surface", "surface"),
},
),
);
nodes.splice(1, 0, node("edge_hdr", "texture", texture("rgba16_float")));
nodes.push(node("edges", "luminance_edge", { strength: 2 }, { source: input(source, "color"), colorTarget: input("edge_hdr", "texture") }));
source = "edges";
}
if (kind === "bloom" || kind === "combined") {
const half = {
texture: {
...texture("rgba16_float").texture,
extent: {
kind: "surface_relative",
width: { numerator: 1, denominator: 2 },
height: { numerator: 1, denominator: 2 },
depthOrArrayLayers: 1,
},
},
residency: "transient",
};
nodes.splice(
1,
0,
node("half_a", "texture_spec", structuredClone(half)),
node("half_b", "texture_spec", structuredClone(half)),
node("half_c", "texture_spec", structuredClone(half)),
node("composite_hdr", "texture_spec", texture("rgba16_float")),
);
nodes.splice(1, 0,
node("half_a", "texture", texture("rgba16_float", 2)), node("half_b", "texture", texture("rgba16_float", 2)),
node("half_c", "texture", texture("rgba16_float", 2)), node("composite_hdr", "texture", texture("rgba16_float")));
nodes.push(
node(
"extract",
"bloom_extract",
{ threshold: 1, knee: 0.5 },
{
source: input("forward", "color"),
colorTarget: input("half_a", "spec"),
},
),
node("extract", "bloom_extract", { threshold: 1, knee: 0.5 }, { source: input("pbr_double", "color"), colorTarget: input("half_a", "texture") }),
node("blur_h", "bloom_blur", { direction: [1, 0], radius: 1 }, { source: input("extract", "color"), colorTarget: input("half_b", "texture") }),
node("blur_v", "bloom_blur", { direction: [0, 1], radius: 1 }, { source: input("blur_h", "color"), colorTarget: input("half_c", "texture") }),
node("composite", "bloom_composite", { intensity: 0.8 }, { source: input("pbr_double", "color"), bloom: input("blur_v", "color"), colorTarget: input("composite_hdr", "texture") }),
);
nodes.push(
node(
"blur_h",
"bloom_blur",
{ direction: [1, 0], radius: 1 },
{
source: input("extract", "color"),
colorTarget: input("half_b", "spec"),
},
),
);
nodes.push(
node(
"blur_v",
"bloom_blur",
{ direction: [0, 1], radius: 1 },
{
source: input("blur_h", "color"),
colorTarget: input("half_c", "spec"),
},
),
);
nodes.push(
node(
"composite",
"bloom_composite",
{ intensity: 0.8 },
{
source: input("forward", "color"),
bloom: input("blur_v", "color"),
colorTarget: input("composite_hdr", "spec"),
},
),
);
let toneSource = "composite";
source = "composite";
if (kind === "combined") {
nodes.splice(1, 0, node("edge_hdr", "texture_spec", texture("rgba16_float")));
nodes.push(
node(
"edges",
"luminance_edge",
{ strength: 2 },
{
source: input("composite", "color"),
colorTarget: input("edge_hdr", "spec"),
},
),
);
toneSource = "edges";
nodes.splice(1, 0, node("edge_hdr", "texture", texture("rgba16_float")));
nodes.push(node("edges", "luminance_edge", { strength: 2 }, { source: input(source, "color"), colorTarget: input("edge_hdr", "texture") }));
source = "edges";
}
nodes.push(
node(
"tone",
"tone_map",
{ exposure: 1 },
{
source: input(toneSource, "color"),
colorTarget: input("surface", "surface"),
},
),
);
}
const last = nodes.at(-1);
nodes.push(
node("present", "present", {}, { surface: input(last.id, "color") }),
);
return Object.freeze({ schemaVersion: 2, graphId, revision: 1, nodes });
nodes.push(node("tone", "tone_map", { exposure: 1 }, { source: input(source, "color"), colorTarget: input("ldr", "texture") }));
nodes.push(node("frame_out", "frame_out", {}, { color: input("tone", "color") }));
return graph(graphId, nodes);
};
export const tone = postPreset("preset_tone", "tone"),
edges = postPreset("preset_edges", "edges"),
bloom = postPreset("preset_bloom", "bloom"),
combined = postPreset("preset_combined", "combined");
export const renderGraphPresets = Object.freeze({
midnight,
ember,
hdr,
culling,
tone,
edges,
bloom,
combined,
});
export const tone = postPreset("preset_tone", "tone");
export const edges = postPreset("preset_edges", "edges");
export const bloom = postPreset("preset_bloom", "bloom");
export const combined = postPreset("preset_combined", "combined");
export const renderGraphPresets = Object.freeze({ midnight, ember, hdr, culling, tone, edges, bloom, combined });