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
+2 -2
View File
@@ -4,11 +4,11 @@ import {
addNodeItems,
moveAddNodeSelection,
searchAddNodeItems,
} from "../static/render-graph/add-node-menu.js";
} from "../examples/render-graph-studio/render-graph/add-node-menu.js";
import {
createNodeIdAllocator,
spawnRequestedNode,
} from "../static/render-graph/node-spawn.js";
} from "../examples/render-graph-studio/render-graph/node-spawn.js";
test("add-node model contains all final catalog types in application groups", () => {
assert.equal(addNodeItems.length, 44);
+187
View File
@@ -0,0 +1,187 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
animateInstance,
canonicalAstExample,
classifyInstance,
compileAndSwitch,
computePipelineExample,
connectFromWorker,
createInstance,
createVelocityColumn,
customRenderPipelineExample,
defaultPipelineExample,
dropActiveGraph,
fluentGraphExample,
fxNodeExportExample,
importGltf,
jsoGraphExample,
loadCompleteScene,
pickNearest,
setVelocity,
simpleSceneShader,
updateInstance,
} from "../examples/cookbook/index.js";
test("cookbook graph recipes produce canonical addon-owned ASTs", () => {
const canonical = canonicalAstExample();
assert.equal(canonical.ast.id, "shared_dag");
assert.equal(
(canonical.source.match(/\(ref "source" "value"\)/g) ?? []).length,
2,
);
assert.deepEqual(
[jsoGraphExample().id, fluentGraphExample().id],
["jso_graph", "fluent_graph"],
);
const fxnode = fxNodeExportExample();
assert.equal(fxnode.kind, "yawn-render-graph");
assert.equal(fxnode.pipelines.render.length, 4);
const defaults = defaultPipelineExample();
assert.deepEqual(
defaults.pipelines.render.map(({ name }) => name),
[
"ground_plane",
"gltf_standard",
"gltf_standard_double_sided",
"frame_out",
],
);
assert.equal(defaults.pipelines.compute.length, 1);
const custom = customRenderPipelineExample();
assert.equal(custom.pipelines.render[0].shader, simpleSceneShader);
assert.match(simpleSceneShader, /@vertex fn vertex_main/);
const compute = computePipelineExample().pipelines.compute[0];
assert.deepEqual(
[compute.entry, compute.dispatch],
["initialize", [4, 1, 1]],
);
});
test("cookbook graph lifecycle recipe serializes, switches, and drops", async () => {
const calls = [];
const core = {
compileGraph(source) {
calls.push(["compile", source]);
return Promise.resolve({ compiledId: [3, 4] });
},
switchCompiledGraph(id) {
calls.push(["switch", id]);
return Promise.resolve();
},
switchToImmediate() {
calls.push(["immediate"]);
return Promise.resolve();
},
dropCompiledGraph(id) {
calls.push(["drop", id]);
return Promise.resolve();
},
};
const graph = { id: "lifecycle", revision: 1, nodes: [] };
const compiled = await compileAndSwitch(core, graph);
await dropActiveGraph(core, compiled);
assert.match(calls[0][1], /^\(yawn-graph 1/);
assert.deepEqual(calls.slice(1), [
["switch", [3, 4]],
["immediate"],
["drop", [3, 4]],
]);
});
test("cookbook mutation recipes use handles and SOA writes directly", async () => {
const calls = [];
const column = {
write(slot, values) {
calls.push(["velocity", slot, values]);
},
};
const core = {
allocateArray(layout) {
calls.push(["allocate", layout]);
return Promise.resolve(column);
},
setInstanceTransform(handle, transform) {
calls.push(["transform", handle, transform]);
},
setInstanceType(handle, words) {
calls.push(["type", handle, words]);
},
};
const instance = {
handle: [7, 2],
setTransform(transform) {
calls.push(["wrapped-transform", transform]);
},
setType(words) {
calls.push(["wrapped-type", words]);
},
};
const mesh = {
createInstance(transform) {
calls.push(["create", transform]);
return Promise.resolve(instance);
},
};
const transform = Array.from({ length: 16 }, (_, index) => index);
const words = Array(16).fill(1);
assert.equal(await createInstance(mesh, transform), instance);
updateInstance(instance, transform, words);
const velocity = await createVelocityColumn(core);
setVelocity(velocity, instance, [1, 2, 3]);
animateInstance(core, instance, transform);
classifyInstance(core, instance, words);
assert.deepEqual(calls.find(([name]) => name === "allocate")[1], {
name: "instance.velocity",
domain: "instance",
scalar: "f32",
lanes: 4,
});
assert.deepEqual(
calls.find(([name]) => name === "velocity"),
["velocity", 7, [1, 2, 3, 0]],
);
assert.ok(calls.some(([name]) => name === "transform"));
assert.ok(calls.some(([name]) => name === "type"));
});
test("cookbook picking and worker-to-worker recipes use public facades", async () => {
const picked = await pickNearest(
{
pickRay: async () => ({
epoch: 5,
hits: [{ instance: [9, 3], distance: 2 }],
}),
},
[0, 0, 0],
[0, 0, -1],
);
assert.deepEqual(picked.hits[0].instance.handle, [9, 3]);
class Port extends EventTarget {
postMessage() {}
start() {
queueMicrotask(() =>
this.dispatchEvent(
new MessageEvent("message", {
data: { type: "soa-init", arrays: [] },
}),
),
);
}
terminate() {}
}
const core = await connectFromWorker(new Port());
assert.equal(core.constructor.name, "YawnCore");
core.dispose();
assert.equal(typeof importGltf, "function");
assert.equal(typeof loadCompleteScene, "function");
});
+3 -4
View File
@@ -1,12 +1,11 @@
import test from "node:test";
import assert from "node:assert/strict";
import { createCubeGeometry, createMaterialGalleryGlb, createUvSphereGeometry, encodeGeometryGlb, isGitLfsPointer, loadDemoLoadout, loadouts, LoadoutError } from "../static/demo-loadouts.js";
import { createCubeGeometry, createMaterialGalleryGlb, createUvSphereGeometry, encodeGeometryGlb, loadDemoLoadout, loadouts } from "../examples/render-graph-studio/demo-loadouts.js";
function parseGlb(buffer){const view=new DataView(buffer);assert.equal(view.getUint32(0,true),0x46546c67);assert.equal(view.getUint32(4,true),2);assert.equal(view.getUint32(8,true),buffer.byteLength);const length=view.getUint32(12,true);assert.equal(view.getUint32(16,true),0x4e4f534a);const json=JSON.parse(new TextDecoder().decode(new Uint8Array(buffer,20,length)).trim());const bin=20+length;assert.equal(view.getUint32(bin+4,true),0x004e4942);assert.equal(view.getUint32(bin,true),json.buffers[0].byteLength);return json;}
test("procedural cube and sphere have complete indexed vertex streams",()=>{const cube=createCubeGeometry(),sphere=createUvSphereGeometry();assert.deepEqual([cube.positions.length/3,cube.indices.length],[24,36]);assert.ok(sphere.positions.length/3>300);for(const geometry of [cube,sphere]){assert.equal(geometry.normals.length,geometry.positions.length);assert.equal(geometry.texcoords.length,geometry.positions.length/3*2);assert.ok(geometry.indices.every(i=>i>=0&&i<geometry.positions.length/3));}});
test("every cube triangle has counter-clockwise outward winding",()=>{const g=createCubeGeometry();for(let i=0;i<g.indices.length;i+=3){const ids=g.indices.slice(i,i+3),p=ids.map(id=>g.positions.slice(id*3,id*3+3)),a=p[1].map((v,j)=>v-p[0][j]),b=p[2].map((v,j)=>v-p[0][j]),cross=[a[1]*b[2]-a[2]*b[1],a[2]*b[0]-a[0]*b[2],a[0]*b[1]-a[1]*b[0]],normal=g.normals.slice(ids[0]*3,ids[0]*3+3);assert.ok(cross.reduce((sum,v,j)=>sum+v*normal[j],0)>0)}});
test("loadouts return fresh deterministic GLBs with one mesh and nine nodes",async()=>{for(const name of ["cubes","spheres"]){const a=await loadDemoLoadout(name),b=await loadDemoLoadout(name);assert.notStrictEqual(a,b);assert.deepEqual(new Uint8Array(a),new Uint8Array(b));const json=parseGlb(a);assert.equal(json.meshes.length,1);assert.equal(json.nodes.length,9);assert.ok(json.nodes.every(n=>n.mesh===0));assert.deepEqual(json.meshes[0].primitives[0].attributes,{POSITION:0,NORMAL:1,TEXCOORD_0:2});assert.equal(json.accessors[3].componentType,5125);}});
test("Phase 6 gallery is deterministic and covers core PBR shader semantics",async()=>{const a=createMaterialGalleryGlb(),b=await loadDemoLoadout("materials");assert.deepEqual(new Uint8Array(a),new Uint8Array(b));const json=parseGlb(a);assert.equal(json.asset.generator,"yawn-phase6-pbr-gallery");assert.deepEqual(json.extensionsUsed,["KHR_materials_ior"]);assert.equal(json.materials.length,16);assert.equal(json.nodes.length,16);assert.equal(json.images.length,3);assert.ok(json.images.every(image=>image.mimeType==="image/png"&&image.bufferView!==undefined));assert.ok(json.meshes.every(mesh=>JSON.stringify(mesh.primitives[0].attributes)===JSON.stringify({POSITION:0,NORMAL:1,TEXCOORD_0:2})));assert.deepEqual(json.materials.slice(0,4).map(x=>x.pbrMetallicRoughness.roughnessFactor),[.08,.3,.6,1]);assert.ok(json.materials.slice(0,4).every(x=>x.pbrMetallicRoughness.metallicFactor===0));assert.ok(json.materials.slice(4,8).every(x=>x.pbrMetallicRoughness.metallicFactor===1));assert.deepEqual(json.materials.slice(8,11).map(x=>x.extensions.KHR_materials_ior.ior),[1,1.5,2]);assert.equal(json.materials[11].normalTexture.index,2);assert.equal(json.materials[12].occlusionTexture.index,1);assert.equal(json.materials[13].emissiveTexture.index,0);assert.equal(json.materials[14].alphaMode,"MASK");assert.equal(json.materials[15].doubleSided,true);assert.ok(json.nodes[15].scale[0]<0);assert.equal(loadouts.materials.label,"Phase 6 deterministic PBR gallery");});
test("loadout dropdown preserves every demo scene and exposes the material gallery",async()=>{const html=await (await import("node:fs/promises")).readFile(new URL("../static/index.html",import.meta.url),"utf8");for(const id of ["cubes","spheres","materials","manor","sponza"])assert.match(html,new RegExp(`<option value="${id}">`));});
test("LFS pointers and HTTP failures are explicit and stable",async()=>{const pointer=new TextEncoder().encode("version https://git-lfs.github.com/spec/v1\noid sha256:abc\n").buffer;assert.equal(isGitLfsPointer(pointer),true);await assert.rejects(loadDemoLoadout("manor",{fetchImpl:async()=>({ok:true,arrayBuffer:async()=>pointer})}),e=>e instanceof LoadoutError&&e.code==="LOADOUT_LFS_POINTER");await assert.rejects(loadDemoLoadout("sponza",{fetchImpl:async()=>({ok:false,status:404})}),e=>e.code==="LOADOUT_HTTP"&&/404/.test(e.message));});
test("Phase 6 gallery is deterministic and covers core PBR shader semantics",async()=>{const a=createMaterialGalleryGlb(),b=await loadDemoLoadout("materials");assert.deepEqual(new Uint8Array(a),new Uint8Array(b));const json=parseGlb(a);assert.equal(json.asset.generator,"yawn-phase6-pbr-gallery");assert.deepEqual(json.extensionsUsed,["KHR_materials_ior"]);assert.equal(json.materials.length,16);assert.equal(json.nodes.length,16);assert.equal(json.images.length,3);assert.ok(json.images.every(image=>image.mimeType==="image/png"&&image.bufferView!==undefined));assert.ok(json.meshes.every(mesh=>JSON.stringify(mesh.primitives[0].attributes)===JSON.stringify({POSITION:0,NORMAL:1,TEXCOORD_0:2})));assert.deepEqual(json.materials.slice(0,4).map(x=>x.pbrMetallicRoughness.roughnessFactor),[.08,.3,.6,1]);assert.ok(json.materials.slice(0,4).every(x=>x.pbrMetallicRoughness.metallicFactor===0));assert.ok(json.materials.slice(4,8).every(x=>x.pbrMetallicRoughness.metallicFactor===1));assert.deepEqual(json.materials.slice(8,11).map(x=>x.extensions.KHR_materials_ior.ior),[1,1.5,2]);assert.equal(json.materials[11].normalTexture.index,2);assert.equal(json.materials[12].occlusionTexture.index,1);assert.equal(json.materials[13].emissiveTexture.index,0);assert.equal(json.materials[14].alphaMode,"MASK");assert.equal(json.materials[15].doubleSided,true);assert.ok(json.nodes[15].scale[0]<0);assert.equal(loadouts.materials.label,"PBR material gallery");});
test("the example offers only self-contained procedural scenes",async()=>{const html=await (await import("node:fs/promises")).readFile(new URL("../examples/render-graph-studio/index.html",import.meta.url),"utf8");assert.deepEqual(Object.keys(loadouts),["cubes","spheres","materials"]);for(const id of Object.keys(loadouts))assert.match(html,new RegExp(`<option value="${id}">`));await assert.rejects(loadDemoLoadout("unknown"),RangeError);});
test("GLB encoder rejects non-finite and out-of-range geometry",()=>{const valid=createCubeGeometry();assert.throws(()=>encodeGeometryGlb({...valid,positions:[...valid.positions.slice(0,-1),NaN]}),/Invalid/);assert.throws(()=>encodeGeometryGlb({...valid,indices:[...valid.indices,0,1,999]}),/Invalid/)});
+1 -1
View File
@@ -5,7 +5,7 @@ import { tmpdir } from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { build } from "vite";
import { fxNodeComposition } from "../static/render-graph/catalog.js";
import { fxNodeComposition } from "@yawn/render-graph-fxnode/catalog";
test("production render graph composition passes fxnode's public validator", async () => {
const directory = await mkdtemp(
+71
View File
@@ -0,0 +1,71 @@
import test from "node:test";
import assert from "node:assert/strict";
import { GltfImporter } from "@yawn/gltf-import";
import { writeSharedUpload } from "../addons/gltf-import/src/shared-upload.js";
class WorkerMock extends EventTarget {
messages = [];
terminated = false;
postMessage(message) { this.messages.push(message); }
reply(data) { this.dispatchEvent(new MessageEvent("message", { data })); }
terminate() { this.terminated = true; }
}
const tick = () => new Promise(resolve => setImmediate(resolve));
test("glTF addon stages fetched bytes in shared SOA and commits only metadata", async () => {
const memory = new SharedArrayBuffer(4096);
const descriptor = {
id: 9,
name: "upload.gltf",
domain: "fixed",
scalar: "u32",
lanes: 4,
stride: 16,
length: 2,
capacity: 2,
controlPtr: 0,
dataOffset: 64,
byteLength: 32,
layoutEpoch: 1,
writable: true,
};
new Int32Array(memory, 0, 16).set([0x414f5359, 1, 9, 1, 4, 4, 2, 2, 3]);
const array = { share: () => ({ buffer: memory, descriptor }) };
const calls = [];
const core = {
async allocateArray(layout) { calls.push(["allocate", layout]); return array; },
async commitGlbUpload(value, byteLength, options) {
calls.push(["commit", value, byteLength, options]);
return { meshes: [] };
},
};
const worker = new WorkerMock();
const importer = new GltfImporter(core, { workerFactory: () => worker });
const loading = importer.load("https://example.test/scene.glb", { framing: "interior" });
await tick();
assert.deepEqual(worker.messages[0], {
type: "load",
request: 1,
url: "https://example.test/scene.glb",
});
worker.reply({ type: "allocate", request: 1, byteLength: 20 });
await tick();
assert.deepEqual(calls[0], ["allocate", {
name: "upload.gltf", domain: "fixed", scalar: "u32", lanes: 4, stride: 16, length: 2,
}]);
assert.equal(worker.messages[1].buffer, memory);
assert.equal(worker.messages[1].descriptor, descriptor);
assert.equal(Object.hasOwn(worker.messages[1], "bytes"), false);
const bytes = Uint8Array.from({ length: 20 }, (_, index) => index + 1);
writeSharedUpload(memory, descriptor, bytes);
worker.reply({ type: "ready", request: 1, byteLength: bytes.byteLength });
assert.deepEqual(await loading, { meshes: [] });
assert.deepEqual(new Uint8Array(memory, 64, 20), bytes);
assert.deepEqual(calls[1], ["commit", array, 20, { framing: "interior" }]);
importer.dispose();
assert.equal(worker.terminated, true);
});
+4 -4
View File
@@ -1,10 +1,10 @@
import test from "node:test";
import assert from "node:assert/strict";
import { culling } from "../static/render-graph/presets.js";
import { culling } from "../examples/render-graph-studio/render-graph/presets.js";
import {
CATALOG_VERSION, GRAPH_ID, semanticCatalog, nodeDefinitions, descriptors, socketTypes,
} from "../static/render-graph/catalog.js";
import { adaptFxNodeSnapshot, mapAuthoringDiagnostic } from "../static/render-graph/adapter.js";
} from "@yawn/render-graph-fxnode/catalog";
import { adaptFxNodeSnapshot, mapAuthoringDiagnostic } from "@yawn/render-graph-fxnode";
const authoredNode = (id, typeId) => {
const definition = nodeDefinitions[typeId];
@@ -123,7 +123,7 @@ test("adapter preserves ordered multisocket links and indexed diagnostics", () =
};
const graph = adaptFxNodeSnapshot(raw, 2);
const targetIndex = graph.nodes.findIndex((node) => node.id === "target");
assert.equal(graph.schemaVersion, 3);
assert.deepEqual([graph.kind, graph.version, graph.id], ["yawn-render-graph", 1, GRAPH_ID]);
assert.deepEqual(graph.nodes[targetIndex].inputs.inputs, [
{ node: "source_a", socket: "value" },
{ node: "source_b", socket: "value" },
+90
View File
@@ -0,0 +1,90 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
GraphAstError,
createGraphAst,
reference,
serializeGraphAst,
} from "@yawn/render-graph-ast";
import { RenderGraph, graphFromObject, loadGraph } from "@yawn/render-graph-js";
import { defaultPipelines } from "@yawn/default-pipelines";
const node = (id, inputs = {}) => ({
id,
state: "enabled",
executor: { key: "and", version: 2 },
parameters: {},
inputs,
});
test("JSO and builder frontends export the same canonical AST", () => {
const description = {
id: "shared_dag",
revision: 3,
pipelines: {
compute: [{
name: "prepare",
shader: "@compute @workgroup_size(1) fn main() {}",
entry: "main",
dispatch: [1, 1, 1],
}],
},
nodes: [
node("source"),
node("left", { inputs: [reference("source", "value")] }),
node("right", { inputs: [reference("source", "value")] }),
],
};
const objectAst = graphFromObject(description);
const builderAst = new RenderGraph("shared_dag", 3)
.computePipeline(description.pipelines.compute[0])
.node("source", "and", { version: 2 })
.node("left", "and", { version: 2, inputs: description.nodes[1].inputs })
.node("right", "and", { version: 2, inputs: description.nodes[2].inputs })
.ast();
assert.deepEqual(builderAst, objectAst);
const source = serializeGraphAst(objectAst);
assert.equal((source.match(/\(ref "source" "value"\)/g) ?? []).length, 2);
assert.match(source, /^\(yawn-graph 1/);
assert.equal(source.trimEnd().endsWith("))"), true);
});
test("canonical AST is immutable and rejects duplicate declarations", () => {
const ast = createGraphAst({ id: "immutable", revision: 1, nodes: [] });
assert.equal(Object.isFrozen(ast), true);
assert.equal(Object.isFrozen(ast.nodes), true);
assert.throws(
() => createGraphAst({
id: "duplicates",
revision: 1,
pipelines: {
render: [{ name: "same", shader: "", vertexEntry: "vs_main", fragmentEntry: "fs_main" }],
compute: [{ name: "same", shader: "", entry: "main", dispatch: [1, 1, 1] }],
},
nodes: [],
}),
error => error instanceof GraphAstError && error.code === "AST_PIPELINE_DUPLICATE",
);
});
test("JSO addon owns AST serialization and graph loading", async () => {
const calls = [];
const core = { compileGraph(source) { calls.push(source); return Promise.resolve({ compiledId: [1, 2] }); } };
const description = { id: "loaded", revision: 1, nodes: [] };
assert.deepEqual(await loadGraph(core, description), { compiledId: [1, 2] });
assert.match(calls[0], /^\(yawn-graph 1/);
await new RenderGraph("builder", 1).load(core);
assert.match(calls[1], /\(id "builder"\)/);
});
test("optional pipelines carry every shader and compute declaration outside core", () => {
assert.deepEqual(defaultPipelines.render.map(({ name }) => name), [
"ground_plane", "gltf_standard", "gltf_standard_double_sided", "frame_out",
]);
assert.match(defaultPipelines.render[1].shader, /@vertex/);
assert.match(defaultPipelines.render[3].shader, /@fragment/);
assert.match(defaultPipelines.compute[0].shader, /@compute/);
assert.deepEqual(defaultPipelines.compute[0].dispatch, [1, 1, 1]);
});
+11 -13
View File
@@ -1,23 +1,21 @@
import test from "node:test";
import assert from "node:assert/strict";
import * as presets from "../static/render-graph/presets.js";
import { descriptors } from "../static/render-graph/catalog.js";
import * as presets from "../examples/render-graph-studio/render-graph/presets.js";
import { descriptors } from "@yawn/render-graph-fxnode/catalog";
test("all presets use current schemas, versions, and one frame output", () => {
assert.equal(Object.keys(presets.renderGraphPresets).length, 13);
test("the JSO example is a complete canonical AST with external pipelines", () => {
assert.deepEqual(Object.keys(presets.renderGraphPresets), ["jso"]);
for (const [name, graph] of Object.entries(presets.renderGraphPresets)) {
assert.deepEqual([graph.schemaVersion, graph.revision], [3, 4], name);
assert.deepEqual([graph.kind, graph.version, graph.revision], ["yawn-render-graph", 1, 1], name);
assert.equal(new Set(graph.nodes.map((node) => node.id)).size, graph.nodes.length, name);
assert.equal(graph.nodes.filter((node) => node.executor.key === "frame_out").length, 1, name);
for (const node of graph.nodes)
assert.equal(node.executor.version, descriptors[node.executor.key].version, `${name}:${node.id}`);
assert.deepEqual(graph.pipelines.render.map(({ name }) => name), [
"ground_plane", "gltf_standard", "gltf_standard_double_sided", "frame_out",
]);
assert.deepEqual(graph.pipelines.compute.map(({ name }) => name), ["initialize_scene"]);
}
const authoredX4 = Object.entries(presets.renderGraphPresets).flatMap(([name, graph]) =>
graph.nodes.filter((node) => node.parameters?.texture?.sampleCount === 4)
.map((node) => `${name}:${node.id}`));
assert.deepEqual(authoredX4, ["msaa:msaa_hdr", "msaa:scene_depth"]);
assert.equal(typeof presets.msaa.nodes.find((node) => node.id === "msaa_hdr")
.parameters.texture.sampleCount, "number");
});
test("presets classify demo-owned enable and material bits through type.words[0] predicates", () => {
@@ -35,7 +33,7 @@ test("presets classify demo-owned enable and material bits through type.words[0]
}
});
test("culling adds a local-AABB expression to each material predicate", () => {
test("the example adds a local-AABB expression to each material predicate", () => {
const byId = Object.fromEntries(presets.culling.nodes.map((node) => [node.id, node]));
assert.deepEqual(byId.cull.inputs, {
mesh: [{ node: "mesh", socket: "mesh" }], localAabb: [{ node: "mesh", socket: "localAabb" }],
@@ -45,7 +43,7 @@ test("culling adds a local-AABB expression to each material predicate", () => {
assert.equal(byId[id].inputs.inputs.at(-1).node, "not_culled");
});
test("scene pipelines directly share matching explicit color and depth targets", () => {
test("scene pipelines directly share matching transient color and depth targets", () => {
for (const [name, graph] of Object.entries(presets.renderGraphPresets)) {
const byId = Object.fromEntries(graph.nodes.map((node) => [node.id, node]));
const color = byId.ground.inputs.color;
-145
View File
@@ -1,145 +0,0 @@
import test from "node:test";
import assert from "node:assert/strict";
import * as rendererModule from "../static/renderer-client.js";
const { RendererClient, RendererError } = rendererModule;
const TYPE = [0,1,2,4,8,16,32,64,128,256,512,1024,2048,4096,0x80000000,0xffffffff];
class WorkerMock extends EventTarget {
messages=[]; transfers=[]; terminated=false;
postMessage(message, transfer=[]) { this.messages.push(message); this.transfers.push(transfer); }
terminate(){this.terminated=true;}
reply(data) { this.dispatchEvent(new MessageEvent("message", { data })); }
}
function fixture() {
const memory = new WebAssembly.Memory({initial:4, maximum:8, shared:true});
const header = new Int32Array(memory.buffer, 0, 16);
header.set([0x4e574159,2,1024,40,0,0]);
const worker = new WorkerMock();
const bridge = {memory,ringPtr:0,worker,freed:false,free(){this.freed=true;}};
const client = new RendererClient(bridge);
return {memory,header,worker,bridge,client};
}
async function imported(f) {
const loading=f.client.replaceSceneGlb(new ArrayBuffer(8));
f.worker.reply({type:"payload-ready",id:1});
await Promise.resolve();
f.worker.reply({type:"reply",request:1,ok:true,result:{meshes:[{handle:[7,3],defaultInstance:[8,5],defaultType:[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}]}});
return (await loading)[0];
}
test("replaceSceneGlb is opcode 1", async()=>{const f=fixture(),pending=f.client.replaceSceneGlb(new ArrayBuffer(8));f.worker.reply({type:"payload-ready",id:1});await Promise.resolve();assert.equal(new Int32Array(f.memory.buffer,64,40)[1],1);f.worker.reply({type:"reply",request:1,ok:true,result:{meshes:[]}});assert.deepEqual(await pending,[]);});
test("scene replacement carries the framing mode in opcode 1",async()=>{const f=fixture(),pending=f.client.replaceSceneGlb(new ArrayBuffer(8),{framing:"interior"});f.worker.reply({type:"payload-ready",id:1});await Promise.resolve();assert.deepEqual([...new Int32Array(f.memory.buffer,64,40).slice(1,5)],[1,1,1,1]);f.worker.reply({type:"reply",request:1,ok:true,result:{meshes:[]}});await pending;await assert.rejects(f.client.replaceSceneGlb(new ArrayBuffer(8),{framing:"bad"}),TypeError)});
test("writes tagged fixed-slot protocol and resolves reply", async () => {
const f=fixture(); const mesh=await imported(f);
assert.equal(mesh.setVisible,undefined);
assert.equal(mesh.setType,undefined);
assert.equal(typeof mesh.defaultInstance.setType,"function");
const pending=mesh.defaultInstance.setType(TYPE);
const {memory,header,worker}=f;
assert.equal(Atomics.load(header,5),2);
const slot=new Int32Array(memory.buffer,64+160,40);
assert.deepEqual([...slot.slice(0,21)].map(x=>x>>>0),[2,10,2,8,5,...TYPE]);
worker.reply({type:"reply",request:2,ok:true,code:"OK"}); await pending;
});
test("maps stable errors and gates destroyed instances", async () => {
const f=fixture(), mesh=await imported(f); const {worker}=f;
const creating=mesh.createInstance(new Float32Array([1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1]),{type:TYPE});
worker.reply({type:"reply",request:2,ok:true,result:[4,2]}); const instance=await creating;
assert.equal(instance.setVisible,undefined);
const destroying=instance.destroy(); worker.reply({type:"reply",request:3,ok:true}); await destroying;
assert.throws(()=>instance.setType(TYPE), error=>error instanceof RendererError&&error.code==="STALE_HANDLE");
});
test("rejects protocol mismatch", () => {
const {memory,worker}=fixture(); new Int32Array(memory.buffer)[1]=1;
assert.throws(()=>new RendererClient({memory,ringPtr:0,worker}), /PROTOCOL_MISMATCH/);
});
test("pending reply exists before ring publication", async () => {
const f=fixture(), mesh=await imported(f); const {worker}=f;
const pending=mesh.defaultInstance.setType(TYPE);
worker.reply({type:"reply",request:2,ok:true});
await pending;
});
test("profile snapshots have a dedicated getter", () => {
const f=fixture(), snapshot={type:"profile-snapshot",available:true,epoch:3,passes:{forward:1.25}};
const dispatch=globalThis.dispatchEvent; globalThis.dispatchEvent=()=>true;
try { f.worker.reply(snapshot); assert.strictEqual(f.client.profile,snapshot); }
finally { globalThis.dispatchEvent=dispatch; f.client.dispose(); }
});
test("worker failures and dispose reject every pending operation", async () => {
const f=fixture(), mesh=await imported(f); const {worker,client,bridge}=f;
const a=mesh.defaultInstance.setType(TYPE), b=mesh.defaultInstance.setType([...TYPE].reverse());
worker.dispatchEvent(new Event("error"));
await assert.rejects(a,/WORKER_ERROR/); await assert.rejects(b,/WORKER_ERROR/);
client.dispose(); assert.equal(worker.terminated,true); assert.equal(bridge.freed,true);
});
test("import always releases staged payload when ring is full", async () => {
const {header,worker,client}=fixture(); Atomics.store(header,5,1024);
const loading=client.replaceSceneGlb(new ArrayBuffer(8));
worker.reply({type:"payload-ready",id:1});
await assert.rejects(loading,/RING_FULL/);
assert.equal(worker.messages.at(-1).type,"payload-release");
});
test("does not export handle constructors or internal mutation methods", () => {
const {client}=fixture();
assert.equal(rendererModule.VISIBLE,undefined);
assert.equal(rendererModule.Mesh,undefined);
assert.equal(rendererModule.Instance,undefined);
assert.equal(client._meshFlags,undefined);
assert.equal(client._createInstance,undefined);
});
test("corrupt backlog closes and terminates the transport", async () => {
const f=fixture(); Atomics.store(f.header,5,1025);
const loading=f.client.replaceSceneGlb(new ArrayBuffer(8));
// Payload staging must first acknowledge before enqueue sees corruption.
f.worker.reply({type:"payload-ready",id:1});
await assert.rejects(loading,/RING_CORRUPT/);
assert.equal(Atomics.load(f.header,6),1);
assert.equal(f.worker.terminated,true);
});
test("import rejects immediately after disposal", async () => {
const f=fixture();
f.client.dispose();
await assert.rejects(f.client.replaceSceneGlb(new ArrayBuffer(8)),/DISPOSED/);
assert.equal(f.worker.messages.length,0);
});
test("import rejects when disposed during asynchronous source loading", async () => {
const f=fixture();
const originalFetch=globalThis.fetch;
let finishFetch;
globalThis.fetch=()=>new Promise(resolve=>{finishFetch=resolve;});
try {
const loading=f.client.replaceSceneGlb("model.glb");
f.client.dispose();
finishFetch({arrayBuffer:async()=>new ArrayBuffer(8)});
await assert.rejects(loading,/DISPOSED/);
assert.equal(f.worker.messages.length,0);
} finally {
globalThis.fetch=originalFetch;
}
});
test("compile transfers payload and waits for ready before opcode 7", async()=>{
const f=fixture(), pending=f.client.compileGraph({schemaVersion:2});
assert.equal(f.worker.transfers[0].length,1); assert.equal(Atomics.load(f.header,5),0);
f.worker.reply({type:"payload-ready",id:1}); await Promise.resolve();
assert.equal(new Int32Array(f.memory.buffer,64,40)[1],7);
f.worker.reply({type:"reply",request:1,ok:true,result:{compiledId:[2,3]}});
assert.deepEqual(await pending,{compiledId:[2,3]});
});
test("flat error reply preserves structured details", async()=>{
const f=fixture(), pending=f.client.compileGraph({}); f.worker.reply({type:"payload-ready",id:1}); await Promise.resolve();
f.worker.reply({type:"reply",request:1,ok:false,code:"GRAPH_INVALID_ID",details:{message:"bad",path:"graphId"}});
await assert.rejects(pending,e=>e instanceof RendererError&&e.code==="GRAPH_INVALID_ID"&&e.details.path==="graphId"&&e.message==="bad");
});
test("compile releases payload after success", async()=>{const f=fixture(),p=f.client.compileGraph({});f.worker.reply({type:"payload-ready",id:1});await Promise.resolve();f.worker.reply({type:"reply",request:1,ok:true,result:{}});await p;assert.equal(f.worker.messages.at(-1).type,"payload-release");});
test("compile releases payload after backend error", async()=>{const f=fixture(),p=f.client.compileGraph({});f.worker.reply({type:"payload-ready",id:1});await Promise.resolve();f.worker.reply({type:"reply",request:1,ok:false,code:"X",details:{message:"x"}});await assert.rejects(p);assert.equal(f.worker.messages.at(-1).type,"payload-release");});
test("compile rejects circular and BigInt JSON", async()=>{const f=fixture(),x={};x.x=x;await assert.rejects(f.client.compileGraph(x),/circular/i);await assert.rejects(f.client.compileGraph({x:1n}),e=>e.code==="GRAPH_JSON_INVALID");});
test("compile rejects oversized encoding", async()=>{const f=fixture();await assert.rejects(f.client.compileGraph({x:"x".repeat(1024*1024)}),e=>e.code==="GRAPH_PAYLOAD_TOO_LARGE");assert.equal(f.worker.messages.length,0);});
test("drop graph emits opcode 8 and validates id", async()=>{const f=fixture(),p=f.client.dropCompiledGraph([9,4]);const slot=new Int32Array(f.memory.buffer,64,40);assert.deepEqual([...slot.slice(1,5)],[8,1,9,4]);f.worker.reply({type:"reply",request:1,ok:true});await p;assert.throws(()=>f.client.dropCompiledGraph([1]),TypeError);});
test("ring-full graph compile releases staged payload", async()=>{const f=fixture();Atomics.store(f.header,5,1024);const p=f.client.compileGraph({});f.worker.reply({type:"payload-ready",id:1});await assert.rejects(p,e=>e.code==="RING_FULL");assert.equal(f.worker.messages.at(-1).type,"payload-release");});
test("disposal while graph payload is pending releases and rejects", async()=>{const f=fixture(),p=f.client.compileGraph({});f.client.dispose();await assert.rejects(p,e=>e.code==="DISPOSED");assert.equal(f.worker.messages.at(-1).type,"payload-release");});
test("payload transfer uses the exact encoded ArrayBuffer", async()=>{const f=fixture(),graph={schemaVersion:2};const expected=new TextEncoder().encode(JSON.stringify(graph));const p=f.client.compileGraph(graph);const transferred=f.worker.transfers[0][0];assert.strictEqual(f.worker.messages[0].buffer,transferred);assert.deepEqual(new Uint8Array(transferred),expected);f.client.dispose();await assert.rejects(p);});
test("cycle error details are preserved exactly", async()=>{const f=fixture(),p=f.client.compileGraph({});f.worker.reply({type:"payload-ready",id:1});await Promise.resolve();const details={message:"cycle",kind:"cycle",edges:[{from:"a",resource:{id:"r",version:0},to:"b"}]};f.worker.reply({type:"reply",request:1,ok:false,code:"GRAPH_CYCLE",details});await assert.rejects(p,e=>e.details===details&&e.details.edges[0].from==="a");});
test("error without details leaves details undefined", async()=>{const f=fixture(),p=f.client.dropCompiledGraph([1,1]);f.worker.reply({type:"reply",request:1,ok:false,code:"STALE_GRAPH_ID"});await assert.rejects(p,e=>e instanceof RendererError&&e.details===undefined&&e.message==="STALE_GRAPH_ID");});
test("switch methods emit exact opcode 9 words", async()=>{const f=fixture();const a=f.client.switchCompiledGraph([9,4]);assert.deepEqual([...new Int32Array(f.memory.buffer,64,40).slice(1,7)],[9,1,1,9,4,0]);f.worker.reply({type:"reply",request:1,ok:true});await a;await new Promise(queueMicrotask);const b=f.client.switchToImmediate();assert.deepEqual([...new Int32Array(f.memory.buffer,224,40).slice(1,7)],[9,2,0,0,0,0]);f.worker.reply({type:"reply",request:2,ok:true});await b;assert.throws(()=>f.client.switchCompiledGraph([0,0]),TypeError);});
test("graph lifecycle FIFO recovers after failure", async()=>{const f=fixture();const a=f.client.switchCompiledGraph([1,1]),b=f.client.switchToImmediate();assert.equal(Atomics.load(f.header,5),1);f.worker.reply({type:"reply",request:1,ok:false,code:"X"});await assert.rejects(a);await new Promise(queueMicrotask);assert.equal(Atomics.load(f.header,5),2);f.worker.reply({type:"reply",request:2,ok:true});await b;});
test("dispose rejects queued graph lifecycle calls", async()=>{const f=fixture();const a=f.client.switchCompiledGraph([1,1]),b=f.client.switchToImmediate();f.client.dispose();await assert.rejects(a,/DISPOSED/);await assert.rejects(b,/DISPOSED/);});
+9 -8
View File
@@ -1,8 +1,8 @@
import test from "node:test";
import assert from "node:assert/strict";
import { SnapshotReader, SnapshotProtocolError } from "../static/render-data-snapshot.js";
import { DerivedBvh } from "../static/bvh-core.js";
import { RendererClient } from "../static/renderer-client.js";
import { SnapshotReader, SnapshotProtocolError } from "../packages/yawn-core/src/snapshot.js";
import { DerivedBvh } from "../addons/mesh-handles/src/bvh-core.js";
import { YawnCore } from "../packages/yawn-core/src/index.js";
const align16 = value => (value + 15) & ~15;
const componentCounts = [1, 1, 3, 3, 1, 1, 1, 1, 16, 3, 3, 16];
@@ -108,14 +108,16 @@ class WorkerMock extends EventTarget {
reply(data) { this.dispatchEvent(new MessageEvent("message", { data })); }
}
test("renderer pick returns instance metadata handles and exact epoch", async () => {
test("core picking returns protocol handles and exact epoch", async () => {
const scene = snapshotFixture();
const ring = 8192;
const ringHeader = new Int32Array(scene.memory.buffer, ring, 16);
ringHeader.set([0x4e574159, 2, 1024, 40]);
const rendererWorker = new WorkerMock(), bvhWorker = new WorkerMock();
const bridge = { memory: scene.memory, ringPtr: ring, worker: rendererWorker, workerFactory: () => bvhWorker, free() {} };
const client = new RendererClient(bridge);
const bridge = { memory: scene.memory, ringPtr: ring, worker: rendererWorker, pickingWorkerFactory: () => bvhWorker, free() {} };
const client = new YawnCore(bridge);
rendererWorker.reply({ type: "soa-init", arrays: [] });
await client.ready;
rendererWorker.reply({ type: "snapshot-init", controlPtr: 0, controlVersion: 1, schemaVersion: 2 });
rendererWorker.reply({ type: "snapshot-published", epoch: 1 });
const picking = client.pickRay([0, 0, 0], [1, 0, 0]);
@@ -124,8 +126,7 @@ test("renderer pick returns instance metadata handles and exact epoch", async ()
const result = await picking;
assert.equal(result.epoch, 1);
assert.equal(result.hits[0].distance, 2);
assert.equal(typeof result.hits[0].instance.setType, "function");
assert.equal(result.hits[0].instance.setVisible, undefined);
assert.deepEqual(result.hits[0].instance, [10, 3]);
client.dispose();
assert.equal(bvhWorker.terminated, true);
});
+241
View File
@@ -0,0 +1,241 @@
import test from "node:test";
import assert from "node:assert/strict";
import { YawnCore, RendererError } from "@yawn/core";
import { MeshHandles } from "@yawn/mesh-handles";
import { createGraphAst, serializeGraphAst } from "@yawn/render-graph-ast";
const TYPE = [0, 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 0x80000000, 0xffffffff];
const IDENTITY = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
class WorkerMock extends EventTarget {
messages = [];
transfers = [];
terminated = false;
postMessage(message, transfer = []) {
this.messages.push(message);
this.transfers.push(transfer);
}
terminate() {
this.terminated = true;
}
reply(data) {
this.dispatchEvent(new MessageEvent("message", { data }));
}
}
function installArray(memory, descriptor) {
const control = new Int32Array(memory.buffer, descriptor.controlPtr, 16);
control.set([
0x414f5359,
1,
descriptor.id,
{ u32: 1, i32: 2, f32: 3 }[descriptor.scalar],
descriptor.lanes,
descriptor.stride / 4,
descriptor.length,
descriptor.capacity,
{ mesh: 1, instance: 2, fixed: 3 }[descriptor.domain],
0,
descriptor.layoutEpoch,
]);
return descriptor;
}
function setup() {
const memory = new WebAssembly.Memory({ initial: 8, maximum: 16, shared: true });
const ring = new Int32Array(memory.buffer, 0, 16);
ring.set([0x4e574159, 2, 1024, 40, 0, 0]);
const transform = installArray(memory, {
id: 1, name: "instance.transform", domain: "instance", scalar: "f32", lanes: 16,
stride: 80, length: 16, capacity: 16, controlPtr: 196608, dataOffset: 64,
byteLength: 1280, layoutEpoch: 1, writable: true, generationGuard: "instance",
});
const type = installArray(memory, {
id: 2, name: "instance.type", domain: "instance", scalar: "u32", lanes: 16,
stride: 80, length: 16, capacity: 16, controlPtr: 198016, dataOffset: 64,
byteLength: 1280, layoutEpoch: 1, writable: true, generationGuard: "instance",
});
const generation = installArray(memory, {
id: 3, name: "instance.generation", domain: "instance", scalar: "u32", lanes: 1,
stride: 16, length: 16, capacity: 16, controlPtr: 199424, dataOffset: 64,
byteLength: 256, layoutEpoch: 1, writable: false,
});
const meshGeneration = installArray(memory, {
id: 4, name: "mesh.generation", domain: "mesh", scalar: "u32", lanes: 1,
stride: 16, length: 16, capacity: 16, controlPtr: 199744, dataOffset: 64,
byteLength: 256, layoutEpoch: 1, writable: false,
});
const upload = installArray(memory, {
id: 5, name: "upload.gltf", domain: "fixed", scalar: "u32", lanes: 4,
stride: 16, length: 16, capacity: 16, controlPtr: 200064, dataOffset: 64,
byteLength: 256, layoutEpoch: 1, writable: true,
});
const worker = new WorkerMock();
const bridge = { memory, ringPtr: 0, worker, freed: false, free() { this.freed = true; } };
const core = new YawnCore(bridge);
worker.reply({ type: "soa-init", arrays: [transform, type, generation, meshGeneration, upload] });
return { memory, ring, worker, bridge, core, handles: new MeshHandles(core), transform, type, generation, upload };
}
async function imported(fixture) {
const loading = fixture.core.commitGlbUpload(fixture.core.array("upload.gltf"), 8);
fixture.worker.reply({
type: "reply",
request: 1,
ok: true,
result: {
meshes: [{
handle: [7, 3],
defaultInstance: [8, 5],
defaultType: TYPE,
}],
},
});
const [mesh] = fixture.handles.fromImportedScene(await loading);
const generations = new Int32Array(
fixture.memory.buffer,
fixture.generation.controlPtr + fixture.generation.dataOffset,
fixture.generation.byteLength / 4,
);
Atomics.store(generations, 8 * (fixture.generation.stride / 4), 5);
return mesh;
}
test("core commits shared scene uploads through metadata-only opcode 1", async () => {
const fixture = setup();
const pending = fixture.core.commitGlbUpload(fixture.core.array("upload.gltf"), 8, { framing: "interior" });
assert.deepEqual([...new Int32Array(fixture.memory.buffer, 64, 6)], [2, 1, 1, 5, 8, 1]);
fixture.worker.reply({ type: "reply", request: 1, ok: true, result: { meshes: [] } });
assert.deepEqual(await pending, { meshes: [] });
});
test("mesh handles are a separate conventional facade over core commands", async () => {
const fixture = setup();
const mesh = await imported(fixture);
assert.deepEqual(mesh.handle, [7, 3]);
assert.deepEqual(mesh.defaultInstance.handle, [8, 5]);
const creating = mesh.createInstance(IDENTITY, { type: TYPE });
const slot = new Int32Array(fixture.memory.buffer, 64 + 160, 40);
assert.equal(slot[1], 3);
fixture.worker.reply({ type: "reply", request: 2, ok: true, result: [4, 2] });
const instance = await creating;
assert.deepEqual(instance.handle, [4, 2]);
});
test("mesh handle picking wraps core protocol handles", async () => {
const core = {
async pickRay() {
return { epoch: 4, hits: [{ instance: [3, 9], distance: 2 }] };
},
};
const result = await new MeshHandles(core).pickRay([0, 0, 0], [1, 0, 0]);
assert.deepEqual(result.hits[0].instance.handle, [3, 9]);
assert.equal(result.hits[0].distance, 2);
});
test("frequent instance mutations write guarded SOA lanes without ring messages", async () => {
const fixture = setup();
const mesh = await imported(fixture);
const before = Atomics.load(fixture.ring, 5);
mesh.defaultInstance.setType(TYPE);
mesh.defaultInstance.setTransform(IDENTITY);
assert.equal(Atomics.load(fixture.ring, 5), before);
const type = new Int32Array(
fixture.memory.buffer,
fixture.type.controlPtr + fixture.type.dataOffset,
fixture.type.byteLength / 4,
);
const base = 8 * (fixture.type.stride / 4);
assert.deepEqual([...type.slice(base, base + 16)].map(value => value >>> 0), TYPE);
assert.equal(Atomics.load(type, base + 16) >>> 0, 5);
assert.equal(Atomics.load(type, base + 17) >>> 0, 1);
});
test("generation columns reject stale handles and are read-only", async () => {
const fixture = setup();
const mesh = await imported(fixture);
const generations = new Int32Array(
fixture.memory.buffer,
fixture.generation.controlPtr + fixture.generation.dataOffset,
fixture.generation.byteLength / 4,
);
Atomics.store(generations, 8 * (fixture.generation.stride / 4), 6);
assert.throws(() => mesh.defaultInstance.setTransform(IDENTITY), error => error.code === "STALE_HANDLE");
assert.throws(() => fixture.core.array("instance.generation").write(8, [6]), error => error.code === "SOA_READ_ONLY");
});
test("custom SOA columns are allocated through the payload command", async () => {
const fixture = setup();
const pending = fixture.core.allocateArray({
name: "instance.velocity", domain: "instance", scalar: "f32", lanes: 4,
});
await Promise.resolve();
fixture.worker.reply({ type: "payload-ready", id: 1 });
await Promise.resolve();
assert.equal(new Int32Array(fixture.memory.buffer, 64, 40)[1], 11);
const descriptor = installArray(fixture.memory, {
id: 5, name: "instance.velocity", domain: "instance", scalar: "f32", lanes: 4,
stride: 16, length: 16, capacity: 16, controlPtr: 200064, dataOffset: 64,
byteLength: 256, layoutEpoch: 1, writable: true,
});
fixture.worker.reply({ type: "reply", request: 1, ok: true, result: descriptor });
const array = await pending;
array.write(3, [1, 2, 3, 4]);
assert.deepEqual(array.read(3), [1, 2, 3, 4]);
});
test("graph compilation transfers canonical S-expressions, including pipeline declarations", async () => {
const fixture = setup();
const graph = createGraphAst({
id: "compute_graph",
revision: 1,
pipelines: {
compute: [{
name: "prepare",
shader: "@compute @workgroup_size(1) fn main() {}",
entry: "main",
dispatch: [1, 2, 3],
}],
},
nodes: [],
});
const pending = fixture.core.compileGraph(serializeGraphAst(graph));
const payload = fixture.worker.messages[0];
assert.match(new TextDecoder().decode(payload.buffer), /^\(yawn-graph 1/);
assert.match(new TextDecoder().decode(payload.buffer), /\"dispatch\" \(array 1 2 3\)/);
fixture.worker.reply({ type: "payload-ready", id: 1 });
await Promise.resolve();
fixture.worker.reply({ type: "reply", request: 1, ok: true, result: { compiledId: [2, 3] } });
assert.deepEqual(await pending, { compiledId: [2, 3] });
});
test("graph lifecycle remains FIFO and uses opcodes 8 and 9", async () => {
const fixture = setup();
const first = fixture.core.switchCompiledGraph([9, 4]);
const second = fixture.core.dropCompiledGraph([2, 1]);
assert.equal(Atomics.load(fixture.ring, 5), 1);
fixture.worker.reply({ type: "reply", request: 1, ok: true });
await first;
await new Promise(queueMicrotask);
assert.equal(Atomics.load(fixture.ring, 5), 2);
const secondSlot = new Int32Array(fixture.memory.buffer, 64 + 160, 40);
assert.equal(secondSlot[1], 8);
fixture.worker.reply({ type: "reply", request: 2, ok: true });
await second;
});
test("transport failures reject pending work and dispose owned resources", async () => {
const fixture = setup();
const pending = fixture.core.dropCompiledGraph([1, 1]);
fixture.worker.dispatchEvent(new Event("error"));
await assert.rejects(pending, error => error instanceof RendererError && error.code === "WORKER_ERROR");
assert.equal(fixture.worker.terminated, true);
assert.equal(fixture.bridge.freed, true);
});