import assert from "node:assert/strict"; import test from "node:test"; import { renderGraphComposition, renderGraphNodeGroups, renderGraphPresets, } from "../src/catalog"; import { buildFxNodeRenderGraph, compileFxNodeRenderGraph, encodeRenderGraph, } from "../src/compiler"; import { isPhysicalRenderGraph, materializeRenderGraph, reconstructRenderGraph, } from "../src/physical"; import { bindFxNodeHeadless } from "../../../vendor/fxnode/src/headless-runtime"; import { compileFxNodeComposition } from "../../../vendor/fxnode/src/composition/compile"; const baseGraph = { id: "scene", resources: { buffers: [ { id: "node-positions", array: "nodePositions", usage: ["storage"] }, { id: "node-rotors", array: "nodeRotors", usage: ["storage"] }, { id: "node-scales", array: "nodeScales", usage: ["storage"] }, { id: "mesh-info", array: "meshInfo", usage: ["storage"] }, { id: "camera-matrices", array: "cameraMatrices", usage: ["storage"] }, { id: "geometry-1-positions", array: "positions", usage: ["vertex"] }, { id: "geometry-1-normals", array: "normals", usage: ["vertex"] }, ], textures: [ { id: "hdr", format: "rgba16float", size: ["canvas", "canvas", 1], usage: ["render", "sampled"], }, { id: "depth", format: "depth24plus", size: ["canvas", "canvas", 1], usage: ["render"], }, ], samplers: [{ id: "linear", descriptor: { magFilter: "linear" } }], }, pipelines: { compute: [], render: [ { id: "forward-pbr-0", code: "original pbr shader", vertex: { entry: "vertex", buffers: [ { arrayStride: 16, attributes: [ { format: "float32x3", offset: 0, shaderLocation: 0 }, ], }, { arrayStride: 16, attributes: [ { format: "float32x3", offset: 0, shaderLocation: 1 }, ], }, { arrayStride: 16, attributes: [ { format: "float32x2", offset: 0, shaderLocation: 2 }, ], }, ], }, fragment: { entry: "fragment", targets: [{ format: "rgba16float" }], }, depthStencil: { format: "depth24plus", depth_write_enabled: true, depth_compare: "less", }, }, { id: "present", code: "present shader", vertex: { entry: "vertex", buffers: [] }, fragment: { entry: "fragment", targets: [{ format: "canvas" }] }, }, ], }, passes: [ { id: "forward-1--1", type: "render", pipeline: "forward-pbr-0", bindings: [{ group: 0, binding: 0, resource: "unrelated" }], color: [{ resource: "hdr", clear: [0, 0, 0, 1] }], depth: { resource: "depth", clear: 1 }, vertexBuffers: [ { slot: 0, resource: "geometry-1-positions" }, { slot: 1, resource: "geometry-1-normals" }, { slot: 2, resource: "geometry-1-uvs" }, ], draw: { vertices: 3, instances: 1 }, }, { id: "present", type: "render", pipeline: "present", after: ["forward-1--1"], bindings: [ { group: 0, binding: 0, resource: "hdr" }, { group: 0, binding: 1, resource: "linear" }, ], color: [{ resource: "canvas", clear: [0, 0, 0, 1] }], draw: { vertices: 3 }, }, ], }; test("catalog exposes grouped, materially rich render graph nodes", () => { assert.equal(renderGraphNodeGroups.length, 9); assert.ok(renderGraphNodeGroups.flatMap((group) => group.items).length >= 32); for (const item of renderGraphNodeGroups.flatMap((group) => group.items)) assert.ok(renderGraphComposition.nodes[item.typeId], item.typeId); assert.doesNotThrow(() => compileFxNodeComposition(renderGraphComposition)); }); test("prepared intent expands into the complete physical render graph", () => { const standard = renderGraphPresets.find((preset) => preset.id === "beauty")!; const concrete = buildFxNodeRenderGraph(standard.layout, baseGraph); const layout = materializeRenderGraph(concrete, "beauty"); assert.equal(isPhysicalRenderGraph(layout), true); assert.ok(layout.nodes.length >= 14); for (const typeId of [ "yawn.rg.physical.frame-in", "yawn.rg.physical.frame-out", "yawn.rg.physical.buffer", "yawn.rg.physical.texture", "yawn.rg.physical.sampler", "yawn.rg.physical.render-pipeline", "yawn.rg.physical.render-pass", ]) { assert.ok( layout.nodes.some((node) => node.typeId === typeId), typeId, ); } assert.ok( layout.links.some((link) => link.extensions.relation === "pipeline"), ); assert.ok( layout.links.some((link) => link.extensions.relation === "binding"), ); assert.ok(layout.links.some((link) => link.extensions.relation === "color")); assert.ok(layout.links.some((link) => link.extensions.relation === "depth")); assert.ok(layout.links.some((link) => link.extensions.relation === "vertex")); assert.deepEqual(reconstructRenderGraph(layout), concrete); }); test("physical pipeline shader edits compile into the S-expression", () => { const concrete = buildFxNodeRenderGraph( renderGraphPresets[0]!.layout, baseGraph, ); const layout = structuredClone(materializeRenderGraph(concrete, "beauty")); const pipeline = layout.nodes.find( (node) => node.typeId === "yawn.rg.physical.render-pipeline" && node.label.includes("forward-pbr-0"), )!; pipeline.parameters.code = { kind: "string", value: "@fragment fn edited_shader() -> @location(0) vec4 { return vec4(0.25); }", }; const serialized = compileFxNodeRenderGraph(layout, baseGraph); assert.match(serialized, /edited_shader/u); assert.doesNotMatch(serialized, /original pbr shader/u); }); test("Color Grade exposes Blender-style grading modes and migrates version 1 nodes", () => { const definition = renderGraphComposition.nodes["yawn.rg.post.color-grade"]; assert.equal(definition.version, 2); assert.deepEqual( definition.ui .filter((row) => row.kind === "widget" && row.widget === "grading-wheels") .map((row) => row.bindings.map((binding) => binding.title)), [ ["Lift", "Gamma", "Gain"], ["Offset", "Power", "Slope"], ], ); assert.deepEqual(definition.parameters.mode.enum, [ "Lift/Gamma/Gain", "Offset/Power/Slope", "White Point", ]); const legacy = structuredClone( renderGraphPresets.find((preset) => preset.id === "cinematic")!.layout, ) as any; legacy.catalogVersion = 1; const grade = legacy.nodes.find((node: any) => node.id === "grade"); grade.typeVersion = 1; grade.parameters = Object.fromEntries( ["exposure", "saturation", "tint", "tintAmount"].map((key) => [ key, grade.parameters[key], ]), ); const runtime = bindFxNodeHeadless( compileFxNodeComposition(renderGraphComposition), ); const decoded = runtime.decodeGraphDocument(legacy); assert.equal(decoded.ok, true); if (!decoded.ok) return; const migrated = decoded.value.nodes.grade!; assert.equal(migrated.known, true); assert.equal(migrated.typeVersion, 2); assert.equal(migrated.parameters.mode.value, "Lift/Gamma/Gain"); assert.equal(migrated.parameters.contrast.value, 1); assert.equal(migrated.parameters.outputTemperature.value, 6500); }); test("prepared loadouts are connected to one Present output", () => { assert.deepEqual( renderGraphPresets.map((preset) => preset.id), ["beauty", "normals", "depth", "clay", "cinematic", "blueprint"], ); assert.equal(renderGraphPresets[0]!.name, "Standard"); for (const preset of renderGraphPresets) { assert.equal( preset.layout.nodes.filter( (node) => node.typeId === "yawn.rg.output.present", ).length, 1, ); assert.equal(preset.layout.links.length, preset.layout.nodes.length - 1); const left = Math.min( ...preset.layout.nodes.map((node) => node.position.x), ); const right = Math.max( ...preset.layout.nodes.map((node) => node.position.x + node.size.x), ); assert.ok(right - left <= 650, `${preset.name} is too wide for the editor`); } }); test("wire encoder is stable and rejects unsupported numbers", () => { assert.equal( encodeRenderGraph({ z: [true, "x"], a: 1 }), '(yawn-graph 1 (object (field "a" 1) (field "z" (array true "x"))))', ); assert.throws(() => encodeRenderGraph({ value: Number.NaN }), /finite/u); }); test("normals loadout replaces scene forward shading without mutating the base", () => { const normals = renderGraphPresets.find((preset) => preset.id === "normals")!; const graph = buildFxNodeRenderGraph(normals.layout, baseGraph) as any; const serialized = encodeRenderGraph(graph); assert.match(serialized, /scene-fxnode-normals/u); assert.match(serialized, /rg-normals-detailed/u); assert.match(serialized, /normalize\(input\.normal\) \* 0\.5 \+ 0\.5/u); assert.match(serialized, /camera-matrices/u); assert.deepEqual( graph.passes.map((pass: any) => pass.id), ["forward-1--1"], ); assert.deepEqual(graph.passes[0].color, [ { resource: "canvas", clear: [0, 0, 0, 1] }, ]); assert.deepEqual( graph.pipelines.render.map((pipeline: any) => pipeline.id), ["rg-normals-detailed"], ); assert.deepEqual( graph.resources.textures.map((texture: any) => texture.id), ["depth"], ); assert.deepEqual(graph.resources.samplers, []); assert.equal(baseGraph.passes[0].pipeline, "forward-pbr-0"); assert.equal(baseGraph.pipelines.render.length, 2); }); test("debug loadouts remove detached clustered lighting work", () => { const graph = structuredClone(baseGraph) as any; graph.resources.buffers.push( { id: "point-lights", array: "pointLights", usage: ["storage"] }, { id: "clusters", array: "clusters", usage: ["storage"] }, ); graph.pipelines.compute.push({ id: "cluster-lights", code: "@compute @workgroup_size(1) fn main() {}", entry: "main", }); graph.passes.unshift({ id: "cluster-lights", type: "compute", pipeline: "cluster-lights", bindings: [ { group: 0, binding: 0, resource: "point-lights" }, { group: 0, binding: 1, resource: "clusters" }, ], dispatch: [1, 1, 1], }); graph.passes[1].after = ["cluster-lights"]; const normals = renderGraphPresets.find((preset) => preset.id === "normals")!; const result = buildFxNodeRenderGraph(normals.layout, graph) as any; assert.equal( result.passes.some((pass: any) => pass.type === "compute"), false, ); assert.deepEqual(result.pipelines.compute, []); assert.equal( result.resources.buffers.some((buffer: any) => ["point-lights", "clusters"].includes(buffer.id), ), false, ); }); test("depth loadout uses position-only geometry and renders to canvas", () => { const depth = renderGraphPresets.find((preset) => preset.id === "depth")!; const graph = buildFxNodeRenderGraph(depth.layout, baseGraph) as any; assert.deepEqual( graph.pipelines.render.map((pipeline: any) => pipeline.id), ["rg-depth-basic"], ); assert.match(graph.pipelines.render[0].code, /0\.2040/u); assert.deepEqual(graph.pipelines.render[0].vertex.buffers.length, 1); assert.deepEqual(graph.passes[0].vertexBuffers, [ { slot: 0, resource: "geometry-1-positions" }, ]); assert.deepEqual(graph.passes[0].color, [ { resource: "canvas", clear: [0, 0, 0, 1] }, ]); assert.equal( graph.resources.buffers.some( (buffer: any) => buffer.id === "geometry-1-normals", ), false, ); }); test("cinematic loadout inserts ordered sampled post passes", () => { const cinematic = renderGraphPresets.find( (preset) => preset.id === "cinematic", )!; const graph = buildFxNodeRenderGraph(cinematic.layout, baseGraph) as any; const serialized = encodeRenderGraph(graph); assert.match(serialized, /rg-effect-pass-0-bloom/u); assert.match(serialized, /rg-effect-pass-1-grade/u); assert.match(serialized, /rg-effect-pass-2-vignette/u); assert.doesNotMatch(serialized, /\(field "id" "rg-effect-2-vignette"\)/u); assert.doesNotMatch(serialized, /\(field "id" "present"\)/u); assert.deepEqual( graph.passes.find((pass: any) => pass.id === "rg-effect-pass-2-vignette") .color, [{ resource: "canvas", clear: [0, 0, 0, 1] }], ); assert.match(serialized, /let liftBalance = vec3/u); assert.match(serialized, /let gammaBalance = vec3/u); assert.match(serialized, /let gainBalance = vec3/u); assert.match(serialized, /color = \(color - vec3\(0\.18000\)\)/u); }); test("Color Grade compiles offset and white-point modes into pixel math", () => { const loadout = structuredClone( renderGraphPresets.find((preset) => preset.id === "cinematic")!.layout, ) as any; const grade = loadout.nodes.find((node: any) => node.id === "grade"); grade.parameters.mode = { kind: "string", value: "Offset/Power/Slope" }; grade.parameters.offset = { kind: "number", value: 0.08 }; grade.parameters.power = { kind: "number", value: 1.12 }; grade.parameters.slope = { kind: "number", value: 0.94 }; let serialized = compileFxNodeRenderGraph(loadout, baseGraph); assert.match(serialized, /let offsetBalance = vec3/u); assert.match(serialized, /let powerBalance = vec3/u); assert.match(serialized, /color \* slopeBalance \+ offsetBalance/u); grade.parameters.mode = { kind: "string", value: "White Point" }; grade.parameters.inputTemperature = { kind: "number", value: 7200 }; grade.parameters.outputTemperature = { kind: "number", value: 4800 }; grade.parameters.outputTint = { kind: "number", value: -0.12 }; serialized = compileFxNodeRenderGraph(loadout, baseGraph); assert.match(serialized, /let whitePointAdaptation = vec3/u); assert.match(serialized, /color \*= whitePointAdaptation/u); }); test("blueprint loadout converts normals into blue paper and linework", () => { const blueprint = renderGraphPresets.find( (preset) => preset.id === "blueprint", )!; const serialized = compileFxNodeRenderGraph(blueprint.layout, baseGraph); assert.match(serialized, /scene-fxnode-normals-edges/u); assert.match(serialized, /vec3\(0\.008, 0\.045, 0\.11\)/u); assert.match(serialized, /vec3\(0\.68, 0\.94, 1\.0\)/u); assert.match(serialized, /fract\(input\.uv \* vec2\(24\.0, 16\.0\)\)/u); assert.match(serialized, /, line\), 1\.0\)/u); }); test("compiler rejects a disconnected output", () => { const disconnected = structuredClone(renderGraphPresets[0]!.layout); disconnected.links = []; assert.throws( () => compileFxNodeRenderGraph(disconnected, baseGraph), /not connected to Scene/u, ); });