import assert from "node:assert/strict"; import test from "node:test"; import { Mesh } from "../src/Mesh"; import { FXAA } from "../src/PostProcesses/FXAA"; import { PBRMaterial } from "../src/materials/PBRMaterial"; import { Texture } from "../src/materials/Texture"; import { ShaderMaterial } from "../src/materials/ShaderMaterial"; import { PointLight } from "../src/lights/PointLight"; import { SpotLight } from "../src/lights/SpotLight"; import { DirectionalLight } from "../src/lights/DirectionalLight"; import { ComputePass } from "../src/ComputePass"; import { bucketDraws, createScene, detailedMesh, forwardPasses, latestGraph, pbrPipelines, renderPipeline, testCore, triangle, } from "./scene-helpers"; if (typeof globalThis.ImageBitmap === "undefined") Object.defineProperty(globalThis, "ImageBitmap", { value: class ImageBitmap {}, }); test("empty scenes use neutral authored defaults without placeholder geometry", async () => { const scene = await createScene(); const emptyPass = latestGraph(scene).passes.find( (pass: any) => pass.id === "forward-empty", ); assert.equal(emptyPass.draw.vertices, 0); assert.deepEqual(emptyPass.color[0].clear, [0, 0, 0, 1]); assert.deepEqual(Array.from(scene.array("sceneAccent").row(0)), [1, 1, 1, 1]); }); test("opaque PBR graph has no implicit environment and compacts sparse lights", async () => { const scene = await createScene(); await Promise.all([ new PointLight(scene).ready, new SpotLight(scene).ready, new DirectionalLight(scene).ready, ]); const material = new PBRMaterial(scene); await material.ready; const mesh = detailedMesh(scene, material); await mesh.ready; const graph = latestGraph(scene); const pipeline = pbrPipelines(graph)[0]; const forward = forwardPasses(graph)[0]; const present = renderPipeline(graph, "present"); const clusters = graph.pipelines.compute.find( (candidate: any) => candidate.id === "cluster-lights", ); assert.equal(pipeline.primitive.frontFace, "ccw"); assert.equal( graph.passes.some((pass: any) => pass.id.includes("background")), false, ); assert.doesNotMatch(JSON.stringify(graph), /studio/iu); assert.deepEqual(forward.color[0].clear, [0, 0, 0, 1]); assert.match(pipeline.code, /@interpolate\(flat\) material: u32/u); assert.match(pipeline.code, /materials\[input\.material \* 3u\]/u); assert.doesNotMatch(pipeline.code, /defaultLight|studioEnvironment/u); assert.doesNotMatch(pipeline.code, /0\.4, 0\.7, 0\.6/u); assert.match( pipeline.code, /let ambient = vec3\(bitcast\(clusters\[1012\]\)/u, ); assert.match(pipeline.code, /fn directContribution/u); assert.match(pipeline.code, /fn directionalContribution/u); assert.equal( forward.bindings.find((binding: any) => binding.binding === 10)?.resource, "directional-lights", ); assert.match(clusters.code, /clusters\[11u \+ pointCount\] = i/u); assert.match(clusters.code, /clusters\[512u \+ spotCount\] = i/u); assert.match(pipeline.code, /let pointCount = clusters\[10\]/u); assert.match(pipeline.code, /pointCount <= 500u/u); assert.match(pipeline.code, /arrayLength\(&pointLights\) \/ 2u/u); assert.match( pipeline.code, /let sourceRoughness = clamp\(roughness \+ max\(state\.z/u, ); assert.match(pipeline.code, /let spotCount = clusters\[511\]/u); assert.match(pipeline.code, /spotCount <= 500u/u); }); test("materials with identical shader features share one pipeline", async () => { const scene = await createScene(); const firstMaterial = new PBRMaterial(scene, { roughness: 0.2 }); const secondMaterial = new PBRMaterial(scene, { roughness: 0.8 }); await Promise.all([firstMaterial.ready, secondMaterial.ready]); const firstMesh = detailedMesh(scene, firstMaterial); const secondMesh = detailedMesh(scene, secondMaterial); await Promise.all([firstMesh.ready, secondMesh.ready]); const graph = latestGraph(scene); const draws = bucketDraws(scene, graph); assert.equal(pbrPipelines(graph).length, 1); // One indirect bucket pass holds both meshes: same pipeline and bind set. assert.equal(forwardPasses(graph).length, 1); assert.deepEqual( draws.map((draw) => draw.firstInstance & 0xffff), [firstMesh.id, secondMesh.id], ); assert.deepEqual( draws.map((draw) => (draw.firstInstance >>> 16) - 1), [firstMaterial.id, secondMaterial.id], ); }); test("transmission renders directly into a composited layer with five taps", async () => { const scene = await createScene(); const material = new PBRMaterial(scene, { transmission: 0.9, thickness: 0.25, attenuationDistance: 2, }); await material.ready; const mesh = detailedMesh(scene, material); await mesh.ready; const graph = latestGraph(scene); const pipeline = pbrPipelines(graph)[0]; const present = renderPipeline(graph, "present"); const presentPass = graph.passes.find((pass: any) => pass.id === "present"); assert.equal( graph.passes.some((pass: any) => pass.id === "scene-color-clear"), true, ); const sceneColorClear = graph.passes.find( (pass: any) => pass.id === "scene-color-clear", ); assert.equal(sceneColorClear.draw.vertices, 0); assert.deepEqual(sceneColorClear.color[0].clear, [0, 0, 0, 1]); assert.equal( graph.passes.some((pass: any) => pass.id === "copy-scene-color"), false, ); assert.equal( presentPass.bindings.some( (binding: any) => binding.resource === "scene-color", ), true, ); assert.match(present.code, /textureSample\(scene, sourceSampler/u); assert.equal( pipeline.code.match(/textureSampleLevel\(sceneColor/g)?.length, 5, ); assert.match(pipeline.code, /transmitted \*= pow/u); }); test("clear color is explicit scene data used by empty and forward graphs", async () => { const clearColor = [0.03, 0.06, 0.09, 1] as const; const scene = await createScene({ clearColor }); const empty = latestGraph(scene).passes.find( (pass: any) => pass.id === "forward-empty", ); assert.deepEqual(empty.color[0].clear, clearColor); const material = new PBRMaterial(scene); await material.ready; const mesh = detailedMesh(scene, material); await mesh.ready; const graph = latestGraph(scene); assert.deepEqual(forwardPasses(graph)[0].color[0].clear, clearColor); assert.deepEqual( graph.passes.find((pass: any) => pass.id === "present").color[0].clear, clearColor, ); }); test("post-processing keeps the conservative transmission copy", async () => { const scene = await createScene(); const material = new PBRMaterial(scene, { transmission: 1 }); await material.ready; const mesh = detailedMesh(scene, material); await mesh.ready; const effect = new FXAA(scene); await effect.ready; const graph = latestGraph(scene); const presentPass = graph.passes.find((pass: any) => pass.id === "present"); assert.equal( graph.passes.some((pass: any) => pass.id === "copy-scene-color"), true, ); assert.equal( presentPass.bindings.some( (binding: any) => binding.resource === "scene-color", ), false, ); assert.equal( graph.passes.some((pass: any) => pass.pipeline.startsWith("post-fxaa-")), true, ); }); test("PBR generation eliminates inactive features and specializes active ones", async () => { const scene = await createScene(); const textures = Array.from( { length: 13 }, () => new Texture(scene, { size: [1, 1, 1] }), ); await Promise.all(textures.map((texture) => texture.ready)); const textureOptions = { baseColorTexture: textures[0], metallicRoughnessTexture: textures[1], normalTexture: textures[2], emissiveTexture: textures[3], occlusionTexture: textures[4], transmissionTexture: textures[5], thicknessTexture: textures[6], clearcoatTexture: textures[7], iridescenceTexture: textures[8], iridescenceThicknessTexture: textures[9], anisotropyTexture: textures[10], specularTexture: textures[11], specularColorTexture: textures[12], }; const active = new PBRMaterial(scene, { ...textureOptions, transmission: 1, thickness: 0.5, attenuationDistance: 2, clearcoat: 0.7, sheenColor: [0.2, 0.3, 0.4], iridescence: 0.8, anisotropy: 0.6, textureInfos: Array.from({ length: 13 }, (_, slot) => ({ texCoord: slot === 1 ? 1 : 0, })), }); const unlit = new PBRMaterial(scene, { ...textureOptions, flags: 8 }); await Promise.all([active.ready, unlit.ready]); const activeMesh = detailedMesh(scene, active); const unlitMesh = detailedMesh(scene, unlit); await Promise.all([activeMesh.ready, unlitMesh.ready]); const pipelines = pbrPipelines(latestGraph(scene)); const activeCode = pipelines.find((pipeline: any) => pipeline.code.includes("transmissionAmount"), ).code; const unlitCode = pipelines.find((pipeline: any) => pipeline.code.includes("color = base.rgb + emissive"), ).code; for (const binding of [ "baseTexture", "materialTexture", "normalTexture", "emissiveTexture", "occlusionTexture", "transmissionTexture", "thicknessTexture", "clearcoatTexture", "iridescenceTexture", "iridescenceThicknessTexture", "anisotropyTexture", "specularTexture", "specularColorTexture", ]) assert.match(activeCode, new RegExp(`var ${binding}: texture_2d`)); assert.match( activeCode, /textureSample\(baseTexture, materialSampler, input\.uv\)/u, ); assert.match( activeCode, /textureSample\(materialTexture, materialSampler, input\.uv2\)/u, ); assert.match(activeCode, /let anisotropyAmount/u); assert.match(activeCode, /let iridescenceAmount/u); assert.match(activeCode, /let clearcoatFresnel/u); assert.match(activeCode, /let sheen =/u); assert.match(unlitCode, /var baseTexture: texture_2d/u); assert.match(unlitCode, /var emissiveTexture: texture_2d/u); assert.doesNotMatch(unlitCode, /var normalTexture: texture_2d/u); assert.doesNotMatch(unlitCode, /var clearcoatTexture: texture_2d/u); assert.doesNotMatch(unlitCode, /let clearcoatFresnel/u); }); test("alpha blending uses source-over and disables depth writes", async () => { const scene = await createScene(); const material = new PBRMaterial(scene, { flags: 2 }); await material.ready; const mesh = detailedMesh(scene, material); await mesh.ready; const pipeline = pbrPipelines(latestGraph(scene))[0]; assert.deepEqual(pipeline.fragment.targets[0].blend, { color: { srcFactor: "src-alpha", dstFactor: "one-minus-src-alpha", operation: "add", }, alpha: { srcFactor: "one", dstFactor: "one-minus-src-alpha", operation: "add", }, }); assert.equal(pipeline.depthStencil.depth_write_enabled, false); }); test("batchGraphUpdates compiles once for multiple mesh registrations", async () => { const scene = await createScene(); const material = new PBRMaterial(scene); await material.ready; const before = testCore(scene).graphs.length; await scene.batchGraphUpdates(async () => { const first = new Mesh(scene, { material, vertexData: triangle }); const second = new Mesh(scene, { material, vertexData: triangle }); await Promise.all([first.ready, second.ready]); }); assert.equal(testCore(scene).graphs.length - before, 1); assert.equal(forwardPasses(latestGraph(scene)).length, 1); assert.equal(bucketDraws(scene, latestGraph(scene)).length, 2); }); test("draw buckets persist per bind set, zero their inactive tail, and stay blocked while compiling", async () => { const scene = await createScene(); const core = testCore(scene); const plain = new PBRMaterial(scene); const texture = new Texture(scene, { size: [1, 1, 1] }); await texture.ready; const textured = new PBRMaterial(scene, { baseColorTexture: texture }); await Promise.all([plain.ready, textured.ready]); const meshes: Mesh[] = []; for (let index = 0; index < 3; index++) meshes.push(await detailedMesh(scene, plain).ready); // Three plain meshes fill one bucket; the array grew geometrically from 1 → 2 → 4 rows. let graph = latestGraph(scene); const bucket = graph.resources.buffers.find((buffer: any) => buffer.id.startsWith("draws-"), ); assert.deepEqual( { usage: bucket.usage, sync: bucket.sync }, { usage: ["indirect"], sync: "manual" }, ); assert.equal(core.array(bucket.array).rows, 4); assert.equal(bucketDraws(scene, graph).length, 3); assert.equal(forwardPasses(graph).length, 1); // Moving one mesh to a textured material creates a second bucket; the first keeps two rows // and its freed tail is zero (instanceCount 0) so the GPU skips it. meshes[1].material = textured; await meshes[1].ready; await scene.updateRenderGraph(); graph = latestGraph(scene); assert.equal(forwardPasses(graph).length, 2); const draws = bucketDraws(scene, graph); assert.deepEqual( draws.map((draw) => [draw.pass, draw.firstInstance & 0xffff]), [ ["forward-bucket-0", meshes[0].id], ["forward-bucket-0", meshes[2].id], ["forward-bucket-1", meshes[1].id], ], ); assert.deepEqual( [...core.array(bucket.array).view.subarray(16, 32)], Array(16).fill(0), ); // Back to plain: the second bucket vanishes from the graph, the first regains its row. meshes[1].material = plain; await meshes[1].ready; await scene.updateRenderGraph(); graph = latestGraph(scene); assert.equal(forwardPasses(graph).length, 1); assert.equal(bucketDraws(scene, graph).length, 3); // A compile that starts after an earlier switch cleared signal 6 re-asserts it before // touching bucket rows, so no frame renders the old graph with new bucket contents. const signals = core.array("signals").row(0); const switchLoadout = core.switchLoadout.bind(core); let observed = -1; core.switchLoadout = async (id) => { await switchLoadout(id); signals[6] = 0; core.switchLoadout = async (next) => { observed = signals[6]; await switchLoadout(next); }; }; await Promise.all([scene.updateRenderGraph(), scene.updateRenderGraph()]); assert.equal(observed, 1); core.switchLoadout = switchLoadout; }); test("opaque buckets render into the composited layer ahead of transmission", async () => { const scene = await createScene(); const opaque = new PBRMaterial(scene); const glass = new PBRMaterial(scene, { transmission: 0.9 }); await Promise.all([opaque.ready, glass.ready]); const solid = detailedMesh(scene, opaque); const window = detailedMesh(scene, glass); await Promise.all([solid.ready, window.ready]); const passes = forwardPasses(latestGraph(scene)); assert.deepEqual( passes.map((pass: any) => [ pass.id, pass.color[0].resource, Boolean(pass.indirect), ]), [ ["forward-bucket-0", "scene-color", true], [ `forward-${window.id}-${scene.geometry(window.geometryId).indexRow * 4}`, "hdr", false, ], ], ); }); test("render graph snapshots expose isolated scene-owned declarations", async () => { const scene = await createScene(); const source: any = await scene.getRenderGraph(); assert.equal(source.id, "scene"); assert.ok( source.resources.buffers.some((buffer: any) => buffer.id === "clusters"), ); source.id = "mutated"; source.resources.buffers.length = 0; const next: any = await scene.getRenderGraph(); assert.equal(next.id, "scene"); assert.ok( next.resources.buffers.some((buffer: any) => buffer.id === "clusters"), ); }); test("serialized render graphs compile into explicitly selectable loadouts", async () => { const scene = await createScene(); const core = testCore(scene); const serialized = core.serializedGraphs.at(-1)!; const before = core.graphs.length; const loadoutId = await scene.compileRenderGraph(serialized); await scene.setLoadout(loadoutId); assert.equal(core.graphs.length, before + 1); assert.equal(loadoutId, `loadout-${before + 1}`); assert.equal(core.loadouts.at(-1), loadoutId); }); test("packed ORM reads are shared only for the same texture and UV transform", async () => { const scene = await createScene(); const orm = new Texture(scene, { size: [1, 1, 1] }); const other = new Texture(scene, { size: [1, 1, 1] }); await Promise.all([orm.ready, other.ready]); for (const [occlusion, info, shared] of [ [orm, {}, true], [other, {}, false], [orm, { texCoord: 1 }, false], [orm, { offset: [0.25, -0.1] }, false], [orm, { scale: [2, 1] }, false], [orm, { rotation: 0.4 }, false], ] as const) { const material = new PBRMaterial(scene, { metallicRoughnessTexture: orm, occlusionTexture: occlusion, textureInfos: [{}, {}, {}, {}, info], }); await material.ready; const mesh = await detailedMesh(scene, material).ready; const graph = latestGraph(scene); const pass = forwardPasses(graph).at(-1); const code = renderPipeline(graph, pass.pipeline).code; assert.equal(code.includes("mix(1.0, packedMaterial.r"), shared); assert.equal(code.includes("textureSample(occlusionTexture"), !shared); assert.equal( pass.bindings.some((b: any) => b.group === 1 && b.binding === 5), !shared, ); await mesh.dispose(); } }); test("light variants follow first/last light lifetimes and keep hot edits rebuild-free", async () => { const scene = await createScene(); const material = await new PBRMaterial(scene).ready; await detailedMesh(scene, material).ready; const code = () => pbrPipelines(latestGraph(scene))[0].code; assert.doesNotMatch(code(), /fn (point|spot|directional)Contribution/); const first = await new PointLight(scene, { intensity: 0 }).ready; assert.match(code(), /fn pointContribution/); assert.doesNotMatch(code(), /fn (spot|directional)Contribution/); assert.equal(code().match(/let nDotV =/g)?.length, 1); assert.match(code(), /let diffuseColor = base.rgb \* \(1.0 - metallic\)/); const before = testCore(scene).graphs.length; first.intensity = 3; first.radius = 0.4; const second = await new PointLight(scene).ready; await first.dispose(); assert.equal(testCore(scene).graphs.length, before); assert.match(code(), /fn pointContribution/); const spot = await new SpotLight(scene).ready; const directional = await new DirectionalLight(scene).ready; assert.match(code(), /fn spotContribution/); assert.match(code(), /fn directionalContribution/); await second.dispose(); assert.doesNotMatch(code(), /fn pointContribution/); await spot.dispose(); await directional.dispose(); assert.doesNotMatch(code(), /fn (point|spot|directional)Contribution/); }); test("direct canvas loadout preserves clear/depth and falls back for linear-color consumers", async () => { const scene = await createScene({ directToCanvas: true, clearColor: [0.02, 0.09, 0.3, 0.5], }); const material = await new PBRMaterial(scene).ready; const mesh = await detailedMesh(scene, material).ready; const assertDirect = () => { const graph = latestGraph(scene); assert.equal( graph.resources.textures.some((t: any) => t.id === "hdr"), false, ); assert.equal( graph.passes.some((p: any) => p.id === "present"), false, ); const background = graph.passes.find( (p: any) => p.id === "canvas-background", ); assert.deepEqual(background.depth, { resource: "depth", clear: 1 }); assert.match( renderPipeline(graph, "canvas-background").code, /canvasOutput\(vec4\(0.02, 0.09, 0.3, 0.5\)\)/, ); const forward = forwardPasses(graph)[0]; assert.deepEqual(forward.color, [{ resource: "canvas", load: "load" }]); assert.deepEqual(forward.depth, { resource: "depth", load: "load" }); assert.equal(pbrPipelines(graph)[0].fragment.targets[0].format, "canvas"); assert.match(pbrPipelines(graph)[0].code, /unpack2x16float\(pack2x16float/); }; const assertHdr = () => assert.ok(renderPipeline(latestGraph(scene), "present")); assertDirect(); const effect = new FXAA(scene); await effect.ready; assertHdr(); await effect.dispose(); assertDirect(); for (const options of [{ flags: 2 }, { transmission: 0.5 }]) { const special = await new PBRMaterial(scene, options).ready; const extra = await detailedMesh(scene, special).ready; assertHdr(); await extra.dispose(); assertDirect(); } const compute = new ComputePass({ code: "@compute @workgroup_size(1) fn main() {}", }); await scene.addComputePass(compute); assertHdr(); assert.match( pbrPipelines(latestGraph(scene))[0].code, /fn directionalContribution/, ); await scene.removeComputePass(compute.id); assertDirect(); const custom = new ShaderMaterial(scene, { code: "custom WGSL" }); await custom.ready; assertHdr(); await custom.dispose(); assertDirect(); const basic = await new Mesh(scene, { vertexData: { positions: triangle.positions, indices: triangle.indices }, }).ready; assertHdr(); await basic.dispose(); assertDirect(); const mask = await new PBRMaterial(scene, { flags: 1 }).ready; const masked = await detailedMesh(scene, mask).ready; assertDirect(); assert.match( pbrPipelines(latestGraph(scene)).find((p: any) => p.code.includes("discard"), ).code, /canvasOutput/, ); await masked.dispose(); await mesh.dispose(); assertHdr(); const ldr = await createScene({ directToCanvas: true, hdr: false }); const ldrMaterial = await new PBRMaterial(ldr).ready; await detailedMesh(ldr, ldrMaterial).ready; assert.ok(renderPipeline(latestGraph(ldr), "present")); });