feat: replace mesh queries with typed predicates
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:
@@ -10,13 +10,15 @@ import {
|
||||
spawnRequestedNode,
|
||||
} from "../static/render-graph/node-spawn.js";
|
||||
|
||||
test("add-node model contains all 16 catalog types in application groups", () => {
|
||||
assert.equal(addNodeItems.length, 16);
|
||||
test("add-node model contains all final catalog types in application groups", () => {
|
||||
assert.equal(addNodeItems.length, 42);
|
||||
assert.deepEqual(
|
||||
[...new Set(addNodeItems.map((item) => item.group))],
|
||||
["Source", "Compute", "CPU preparation", "Render / post", "Frame"],
|
||||
["Source", "Expression", "Render / post", "Frame"],
|
||||
);
|
||||
assert.equal(new Set(addNodeItems.map((item) => item.typeId)).size, 16);
|
||||
assert.equal(new Set(addNodeItems.map((item) => item.typeId)).size, 42);
|
||||
assert.ok(addNodeItems.some((item) => item.typeId === "separate_u32_bits" && item.group === "Expression"));
|
||||
assert.ok(!addNodeItems.some((item) => ["mesh_query", "pipeline_registry"].includes(item.typeId)));
|
||||
assert.deepEqual(searchAddNodeItems("no such node"), []);
|
||||
});
|
||||
|
||||
@@ -36,7 +38,7 @@ test("allocator avoids existing and session-reserved IDs and is bounded", () =>
|
||||
);
|
||||
});
|
||||
|
||||
test("all 16 types spawn with exact position, current version and generated ID", async () => {
|
||||
test("all final types spawn with exact position, current version and generated ID", async () => {
|
||||
let revision = 5,
|
||||
expectedType;
|
||||
const request = { compositionRevision: 5, viewPosition: { x: 12.25, y: -4 } };
|
||||
|
||||
@@ -36,13 +36,30 @@ test("production render graph composition passes fxnode's public validator", asy
|
||||
result.ok ? undefined : JSON.stringify(result.issues, null, 2),
|
||||
);
|
||||
assert.equal(fxNodeComposition.schemaVersion, 2);
|
||||
assert.equal(fxNodeComposition.version, 7);
|
||||
assert.equal(Object.keys(fxNodeComposition.nodes).length, 16);
|
||||
assert.equal(fxNodeComposition.version, 8);
|
||||
assert.equal(Object.keys(fxNodeComposition.nodes).length, 42);
|
||||
assert.ok(
|
||||
Object.values(fxNodeComposition.nodes).every(
|
||||
(definition) => definition.migrations.length === 0,
|
||||
),
|
||||
);
|
||||
for (const [type, descriptor] of Object.entries(fxNodeComposition.nodes)) {
|
||||
const socketKeys = Object.keys(descriptor.sockets);
|
||||
assert.equal(
|
||||
socketKeys.length,
|
||||
new Set(socketKeys).size,
|
||||
`${type} has colliding input and output socket names`,
|
||||
);
|
||||
}
|
||||
assert.deepEqual(fxNodeComposition.nodes.not.sockets.operand, {
|
||||
title: "operand",
|
||||
direction: "input",
|
||||
type: "bool",
|
||||
maxIncomingLinks: 1,
|
||||
visible: true,
|
||||
value: { type: "boolean", default: { kind: "boolean", value: false } },
|
||||
showValue: true,
|
||||
});
|
||||
} finally {
|
||||
await rm(directory, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
@@ -1,660 +1,40 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
adaptFxNodeSnapshot,
|
||||
AuthoringGraphError,
|
||||
getSourceMap,
|
||||
mapAuthoringDiagnostic,
|
||||
} from "../static/render-graph/adapter.js";
|
||||
import { RendererError } from "../static/renderer-client.js";
|
||||
import {
|
||||
semanticCatalog,
|
||||
nodeDefinitions,
|
||||
GRAPH_ID,
|
||||
CATALOG_VERSION,
|
||||
descriptors,
|
||||
socketTypes,
|
||||
} from "../static/render-graph/catalog.js";
|
||||
import { culling } from "../static/render-graph/presets.js";
|
||||
import { AuthoringController } from "../static/render-graph/authoring-controller.js";
|
||||
function fixture() {
|
||||
const nodes = culling.nodes.map((n) => {
|
||||
const d = semanticCatalog[n.executor.key];
|
||||
const definition = nodeDefinitions[n.executor.key];
|
||||
return {
|
||||
id: n.id,
|
||||
typeId: n.executor.key,
|
||||
typeVersion: d.version,
|
||||
known: true,
|
||||
muted: n.state !== "enabled",
|
||||
position: { x: 10, y: 20 },
|
||||
size: { x: 200, y: 120 },
|
||||
label: n.id,
|
||||
collapsed: false,
|
||||
extensions: {},
|
||||
parameters: Object.fromEntries(
|
||||
Object.entries(definition.parameters).map(([key, schema]) => [
|
||||
key,
|
||||
{
|
||||
kind: schema.type,
|
||||
value: structuredClone(n.parameters[key] ?? schema.default.value),
|
||||
},
|
||||
]),
|
||||
),
|
||||
sockets: [
|
||||
...Object.entries(d.inputs).map(([key, x]) => {
|
||||
const socket = definition.sockets[key];
|
||||
return {
|
||||
key,
|
||||
id: `${n.id}:${key}`,
|
||||
direction: "input",
|
||||
dataType: x.authoringType ?? x.accepted.types[0],
|
||||
label: socket.title,
|
||||
accepts:
|
||||
socketTypes[x.authoringType ?? x.accepted.types[0]].acceptsFrom,
|
||||
...(socket.value
|
||||
? { defaultValue: structuredClone(socket.value.default) }
|
||||
: {}),
|
||||
visible: socket.visible,
|
||||
maxIncomingLinks: socket.maxIncomingLinks,
|
||||
};
|
||||
}),
|
||||
...Object.entries(d.outputs).map(([key]) => ({
|
||||
key,
|
||||
id: `${n.id}:${key}`,
|
||||
direction: "output",
|
||||
dataType: definition.sockets[key].type,
|
||||
label: key,
|
||||
accepts: [],
|
||||
visible: true,
|
||||
maxIncomingLinks: 0,
|
||||
})),
|
||||
],
|
||||
};
|
||||
});
|
||||
const links = [];
|
||||
for (const n of culling.nodes)
|
||||
for (const [socket, from] of Object.entries(n.inputs))
|
||||
links.push({
|
||||
id: `l_${from.node}_${from.socket}_${n.id}_${socket}`,
|
||||
fromNodeId: from.node,
|
||||
fromSocketId: `${from.node}:${from.socket}`,
|
||||
toNodeId: n.id,
|
||||
toSocketId: `${n.id}:${socket}`,
|
||||
muted: false,
|
||||
extensions: {},
|
||||
});
|
||||
return {
|
||||
graphId: GRAPH_ID,
|
||||
catalogVersion: CATALOG_VERSION,
|
||||
nodes,
|
||||
links,
|
||||
metadata: { layout: "ignored" },
|
||||
version: 1,
|
||||
};
|
||||
}
|
||||
test("catalog exhaustively mirrors all current contracts", () => {
|
||||
for (const [key, semantic] of Object.entries(semanticCatalog)) {
|
||||
assert.ok(Object.hasOwn(semantic, "version"));
|
||||
assert.equal(semantic.version, key === "frame_out" ? 3 : 1);
|
||||
assert.equal(nodeDefinitions[key].version, semantic.version);
|
||||
assert.equal(descriptors[key].version, semantic.version);
|
||||
}
|
||||
assert.deepEqual(
|
||||
Object.keys(semanticCatalog),
|
||||
[
|
||||
"mesh",
|
||||
"texture",
|
||||
"frustum_cull",
|
||||
"mesh_query",
|
||||
"pipeline_registry",
|
||||
"pipeline",
|
||||
"fullscreen_copy",
|
||||
"color_balance",
|
||||
"exposure_contrast",
|
||||
"saturation",
|
||||
"channel_mixer",
|
||||
"bloom_extract",
|
||||
"bloom_blur",
|
||||
"bloom_composite",
|
||||
"luminance_edge",
|
||||
"frame_out",
|
||||
],
|
||||
);
|
||||
for (const c of Object.values(semanticCatalog)) {
|
||||
assert.ok(c.execution);
|
||||
assert.ok(c.inputs);
|
||||
assert.ok(c.outputs);
|
||||
assert.ok(c.parameters);
|
||||
}
|
||||
for (const [key, contract] of Object.entries(semanticCatalog))
|
||||
assert.deepEqual(
|
||||
Object.keys(nodeDefinitions[key].parameters).sort(),
|
||||
Object.keys(contract.parameters).sort(),
|
||||
key,
|
||||
);
|
||||
assert.equal(CATALOG_VERSION, 7);
|
||||
assert.deepEqual(nodeDefinitions.pipeline.parameters, {
|
||||
pipeline: {
|
||||
type: "string",
|
||||
default: { kind: "string", value: "gltf_standard" },
|
||||
},
|
||||
depthCompare: {
|
||||
type: "string",
|
||||
default: { kind: "string", value: "less_equal" },
|
||||
enum: ["never", "less", "equal", "less_equal", "greater", "not_equal", "greater_equal", "always"],
|
||||
},
|
||||
depthWriteEnabled: {
|
||||
type: "boolean",
|
||||
default: { kind: "boolean", value: true },
|
||||
},
|
||||
clearDepth: {
|
||||
type: "number",
|
||||
default: { kind: "number", value: 1 },
|
||||
minimum: 0,
|
||||
maximum: 1,
|
||||
},
|
||||
clearColor: {
|
||||
type: "color",
|
||||
default: { kind: "color", value: [0.015, 0.02, 0.03, 1] },
|
||||
minimum: 0,
|
||||
maximum: 1,
|
||||
},
|
||||
});
|
||||
assert.deepEqual(nodeDefinitions.bloom_blur.parameters.direction.enum, [
|
||||
"horizontal",
|
||||
"vertical",
|
||||
]);
|
||||
assert.deepEqual(nodeDefinitions.texture.parameters.residency.enum, [
|
||||
"transient",
|
||||
"persistent",
|
||||
]);
|
||||
assert.deepEqual(
|
||||
nodeDefinitions.frustum_cull.parameters.cameraSelection.enum,
|
||||
["active"],
|
||||
);
|
||||
assert.deepEqual(nodeDefinitions.mesh_query.sockets.isVisible.value.default, {
|
||||
kind: "boolean",
|
||||
value: true,
|
||||
});
|
||||
assert.equal(nodeDefinitions.mesh_query.sockets.isVisible.showValue, true);
|
||||
import {
|
||||
CATALOG_VERSION, semanticCatalog, nodeDefinitions, descriptors,
|
||||
} from "../static/render-graph/catalog.js";
|
||||
|
||||
assert.deepEqual(nodeDefinitions.color_balance.ui.slice(0, 4), [
|
||||
{ kind: "parameter", parameter: "mode" },
|
||||
{ kind: "widget", widget: "grading-wheels", bindings: [
|
||||
{ title: "Lift", scalar: "lift", color: "liftColor" },
|
||||
{ title: "Gamma", scalar: "gamma", color: "gammaColor" },
|
||||
{ title: "Gain", scalar: "gain", color: "gainColor" },
|
||||
], visibleWhen: { parameter: "mode", equals: "lift_gamma_gain" } },
|
||||
{ kind: "widget", widget: "grading-wheels", bindings: [
|
||||
{ title: "Offset", scalar: "offset", color: "offsetColor" },
|
||||
{ title: "Power", scalar: "power", color: "powerColor" },
|
||||
{ title: "Slope", scalar: "slope", color: "slopeColor" },
|
||||
], visibleWhen: { parameter: "mode", equals: "offset_power_slope" } },
|
||||
{ kind: "parameter", parameter: "factor" },
|
||||
]);
|
||||
for (const name of ["liftColor", "gammaColor", "gainColor", "offsetColor", "powerColor", "slopeColor"])
|
||||
assert.deepEqual(nodeDefinitions.color_balance.parameters[name].default, { kind: "color", value: [1, 1, 1, 1] });
|
||||
assert.deepEqual(nodeDefinitions.channel_mixer.parameters.redOutput, {
|
||||
type: "vector", default: { kind: "vector", value: [1, 0, 0] }, minimum: -2, maximum: 2,
|
||||
test("catalog v8 exposes the final mesh, pipeline, and typed-expression contracts", () => {
|
||||
assert.equal(CATALOG_VERSION, 8);
|
||||
assert.deepEqual(semanticCatalog.mesh.outputs, {
|
||||
mesh: { type: "mesh_data" }, type: { type: "u32x16" }, localAabb: { type: "local_aabb" },
|
||||
});
|
||||
});
|
||||
|
||||
test("adapter validates and exactly lowers canonical pipeline controls and blur direction", () => {
|
||||
const x = fixture();
|
||||
const pipeline = x.nodes.find((node) => node.id === "ground");
|
||||
assert.equal(pipeline.parameters.clearColor.kind, "color");
|
||||
assert.deepEqual(
|
||||
adaptFxNodeSnapshot(x).nodes.find((node) => node.id === "ground").parameters,
|
||||
{
|
||||
pipeline: "ground_plane",
|
||||
depthCompare: "less_equal",
|
||||
depthWriteEnabled: true,
|
||||
clearDepth: 1,
|
||||
clearColor: [0.015, 0.02, 0.03, 1],
|
||||
},
|
||||
);
|
||||
const schema = nodeDefinitions.bloom_blur.parameters;
|
||||
const blur = structuredClone(x.nodes.find((node) => node.id === "frame_out"));
|
||||
blur.id = "blur";
|
||||
blur.typeId = "bloom_blur";
|
||||
blur.parameters = {
|
||||
direction: { kind: "string", value: "vertical" },
|
||||
radius: structuredClone(schema.radius.default),
|
||||
};
|
||||
blur.sockets = Object.entries(nodeDefinitions.bloom_blur.sockets).map(
|
||||
([key, socket]) => ({
|
||||
key,
|
||||
id: `blur:${key}`,
|
||||
label: socket.title,
|
||||
direction: socket.direction,
|
||||
dataType: socket.type,
|
||||
accepts:
|
||||
socket.direction === "input"
|
||||
? socketTypes[socket.type].acceptsFrom
|
||||
: [],
|
||||
maxIncomingLinks: socket.maxIncomingLinks,
|
||||
visible: socket.visible,
|
||||
}),
|
||||
);
|
||||
blur.typeVersion = descriptors.bloom_blur.version;
|
||||
x.nodes.push(blur);
|
||||
assert.deepEqual(
|
||||
adaptFxNodeSnapshot(x).nodes.find((node) => node.id === "blur").parameters
|
||||
.direction,
|
||||
[0, 1],
|
||||
);
|
||||
pipeline.parameters.clearColor.value[0] = 2;
|
||||
assert.throws(
|
||||
() => adaptFxNodeSnapshot(x),
|
||||
(error) => error.code === "AUTHORING_PARAMETER",
|
||||
);
|
||||
pipeline.parameters.clearColor.value = [0.015, 0.02, 0.03, 1];
|
||||
pipeline.parameters.clearColor.value = [0, 0, 0];
|
||||
assert.throws(
|
||||
() => adaptFxNodeSnapshot(x),
|
||||
(error) => error.code === "AUTHORING_PARAMETER",
|
||||
);
|
||||
pipeline.parameters.clearColor.value = [0.015, 0.02, 0.03, 1];
|
||||
pipeline.parameters.clearColor.value = [0, 0, Number.NaN, 1];
|
||||
assert.throws(
|
||||
() => adaptFxNodeSnapshot(x),
|
||||
(error) => error.code === "AUTHORING_PARAMETER",
|
||||
);
|
||||
pipeline.parameters.clearColor.value = [0.015, 0.02, 0.03, 1];
|
||||
const texture = x.nodes.find((node) => node.id === "hdr");
|
||||
texture.parameters.residency.value = "unknown";
|
||||
assert.throws(
|
||||
() => adaptFxNodeSnapshot(x),
|
||||
(error) => error.code === "AUTHORING_PARAMETER",
|
||||
);
|
||||
texture.parameters.residency.value = "transient";
|
||||
pipeline.parameters.clearDepth.value = -1;
|
||||
assert.throws(
|
||||
() => adaptFxNodeSnapshot(x),
|
||||
(error) => error.code === "AUTHORING_PARAMETER",
|
||||
);
|
||||
});
|
||||
|
||||
test("adapter validates and lowers disconnected query socket defaults", () => {
|
||||
const x = fixture();
|
||||
const query = x.nodes.find((node) => node.id === "query");
|
||||
const visible = query.sockets.find((socket) => socket.key === "isVisible");
|
||||
visible.defaultValue.value = false;
|
||||
const ir = adaptFxNodeSnapshot(x);
|
||||
assert.equal(visible.defaultValue.value, false);
|
||||
const parameters = ir.nodes.find((node) => node.id === "query").parameters;
|
||||
assert.equal(parameters.isVisible, undefined);
|
||||
assert.equal(parameters.visibleDefault, false);
|
||||
assert.equal(parameters.frustumCulledDefault, false);
|
||||
visible.defaultValue = { kind: "number", value: 0 };
|
||||
assert.throws(
|
||||
() => adaptFxNodeSnapshot(x),
|
||||
(error) => error.code === "AUTHORING_SOCKET",
|
||||
);
|
||||
delete visible.defaultValue;
|
||||
assert.throws(
|
||||
() => adaptFxNodeSnapshot(x),
|
||||
(error) => error.code === "AUTHORING_SOCKET",
|
||||
);
|
||||
});
|
||||
test("adapter lowers the authoring-safe camera selector to the Rust wire field", () => {
|
||||
const ir = adaptFxNodeSnapshot(fixture());
|
||||
const parameters = ir.nodes.find((node) => node.id === "cull").parameters;
|
||||
assert.deepEqual(parameters, { camera: "active" });
|
||||
assert.equal(parameters.cameraSelection, undefined);
|
||||
});
|
||||
test("adapter deterministically emits the canonical schema, permits repeated types, omits muted links and maps sources", () => {
|
||||
const x = fixture(),
|
||||
a = adaptFxNodeSnapshot(x, 7);
|
||||
x.nodes.reverse();
|
||||
x.links.reverse();
|
||||
assert.deepEqual(adaptFxNodeSnapshot(x, 7), a);
|
||||
assert.equal(a.schemaVersion, 2);
|
||||
assert.equal(a.graphId, GRAPH_ID);
|
||||
assert.equal(a.nodes.filter((n) => n.executor.key === "texture").length, 2);
|
||||
assert.ok(
|
||||
Object.values(getSourceMap(a)).some((source) => source.input === "color"),
|
||||
);
|
||||
x.links.find((l) => l.id === "l_pbr_double_color_frame_out_color").muted = true;
|
||||
assert.equal(
|
||||
adaptFxNodeSnapshot(x, 8).nodes.find((n) => n.id === "frame_out").inputs
|
||||
.color,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
test("adapter rejects hostile shape, IDs, duplicates, catalog, sockets and type mismatches", () => {
|
||||
const reject = (fn, code) => {
|
||||
const x = fixture();
|
||||
fn(x);
|
||||
assert.throws(
|
||||
() => adaptFxNodeSnapshot(x),
|
||||
(e) => e instanceof AuthoringGraphError && e.code === code,
|
||||
);
|
||||
};
|
||||
reject((x) => (x.graphId = "bad"), "AUTHORING_CATALOG");
|
||||
reject((x) => (x.catalogVersion = 2), "AUTHORING_CATALOG");
|
||||
reject((x) => (x.nodes[0].id = "bad id"), "AUTHORING_ID");
|
||||
reject((x) => (x.nodes[1].id = x.nodes[0].id), "AUTHORING_ID_DUPLICATE");
|
||||
reject((x) => (x.nodes[0].typeId = "wat"), "AUTHORING_NODE_TYPE");
|
||||
reject((x) => (x.nodes[0].typeVersion = 2), "AUTHORING_NODE_INVALID");
|
||||
reject((x) => (x.nodes[0].sockets = []), "AUTHORING_SOCKET_SET");
|
||||
reject((x) => (x.links[0].toSocketId = "missing:x"), "AUTHORING_LINK");
|
||||
reject((x) => {
|
||||
const link = x.links.find((l) => l.toSocketId === "frame_out:color");
|
||||
link.fromNodeId = "mesh";
|
||||
link.fromSocketId = "mesh:mesh";
|
||||
}, "AUTHORING_LINK_TYPE");
|
||||
});
|
||||
test("Frame Out has the exact v3 schema, defaults, UI, and strict authoring validation", () => {
|
||||
const fields = ["surfaceFormat", "hdrEnabled", "toneMapper", "exposureStops", "outputTransfer", "scaleMode", "filter", "backgroundColor"];
|
||||
assert.equal(CATALOG_VERSION, 7);
|
||||
assert.deepEqual(semanticCatalog.frame_out, {
|
||||
version: 3, execution: "frame", inputs: { color: semanticCatalog.frame_out.inputs.color }, outputs: {},
|
||||
parameters: { surfaceFormat: "preferred", hdrEnabled: true, toneMapper: "aces", exposureStops: 0, outputTransfer: "srgb", scaleMode: "stretch", filter: "linear", backgroundColor: [0, 0, 0, 1] },
|
||||
});
|
||||
assert.deepEqual(nodeDefinitions.frame_out.parameters, {
|
||||
surfaceFormat: { type: "string", default: { kind: "string", value: "preferred" }, enum: ["preferred", "rgba8_unorm", "bgra8_unorm", "rgba16_float"] },
|
||||
hdrEnabled: { type: "boolean", default: { kind: "boolean", value: true } },
|
||||
toneMapper: { type: "string", default: { kind: "string", value: "aces" }, enum: ["aces", "reinhard", "none"] },
|
||||
exposureStops: { type: "number", default: { kind: "number", value: 0 }, minimum: -10, maximum: 10 },
|
||||
outputTransfer: { type: "string", default: { kind: "string", value: "srgb" }, enum: ["srgb", "linear"] },
|
||||
scaleMode: { type: "string", default: { kind: "string", value: "stretch" }, enum: ["stretch", "contain", "cover"] },
|
||||
filter: { type: "string", default: { kind: "string", value: "linear" }, enum: ["linear", "nearest"] },
|
||||
backgroundColor: { type: "color", default: { kind: "color", value: [0, 0, 0, 1] }, minimum: 0, maximum: 1 },
|
||||
});
|
||||
assert.deepEqual(nodeDefinitions.frame_out.ui, [
|
||||
{ kind: "text", variant: "section", title: "Canvas Presentation" },
|
||||
{ kind: "parameter", parameter: "surfaceFormat", title: "Surface Format" },
|
||||
{ kind: "text", variant: "section", title: "Display Transform" },
|
||||
{ kind: "parameter", parameter: "hdrEnabled", title: "HDR" },
|
||||
{ kind: "parameter", parameter: "toneMapper", title: "Tone Mapper", visibleWhen: { parameter: "hdrEnabled", equals: true } },
|
||||
{ kind: "parameter", parameter: "exposureStops", title: "Exposure", visibleWhen: { parameter: "hdrEnabled", equals: true } },
|
||||
{ kind: "parameter", parameter: "outputTransfer", title: "Transfer" },
|
||||
{ kind: "parameter", parameter: "scaleMode", title: "Scale" },
|
||||
{ kind: "parameter", parameter: "filter" },
|
||||
{ kind: "parameter", parameter: "backgroundColor", title: "Background", visibleWhen: { parameter: "scaleMode", equals: "contain" } },
|
||||
{ kind: "socket", socket: "color" },
|
||||
]);
|
||||
const reject = (mutate, code, parameter) => {
|
||||
const x = fixture(), n = x.nodes.find((node) => node.typeId === "frame_out");
|
||||
mutate(x, n);
|
||||
assert.throws(() => adaptFxNodeSnapshot(x), (e) => e instanceof AuthoringGraphError && e.code === code && (!parameter || e.details.nodeId === n.id && e.details.parameter === parameter));
|
||||
};
|
||||
reject((x) => x.catalogVersion = 5, "AUTHORING_CATALOG");
|
||||
reject((x, n) => n.typeVersion = 2, "AUTHORING_NODE_INVALID");
|
||||
for (const field of fields) reject((x, n) => delete n.parameters[field], "AUTHORING_PARAMETER_SET");
|
||||
reject((x, n) => n.parameters.extra = { kind: "number", value: 0 }, "AUTHORING_PARAMETER_SET");
|
||||
for (const [field, value] of [
|
||||
["surfaceFormat", "bad"], ["hdrEnabled", 1], ["toneMapper", "bad"], ["outputTransfer", "bad"], ["scaleMode", "bad"], ["filter", "bad"],
|
||||
["exposureStops", NaN], ["exposureStops", -10.01], ["exposureStops", 10.01],
|
||||
["backgroundColor", [0, 0, 0]], ["backgroundColor", [0, 0, Infinity, 1]], ["backgroundColor", [-0.01, 0, 0, 1]], ["backgroundColor", [0, 0, 0, 1.01]],
|
||||
]) reject((x, n) => n.parameters[field].value = value, "AUTHORING_PARAMETER", field);
|
||||
for (const [hidden, value] of [["toneMapper", "bad"], ["exposureStops", Infinity]])
|
||||
reject((x, n) => { n.parameters.hdrEnabled.value = false; n.parameters[hidden].value = value; }, "AUTHORING_PARAMETER", hidden);
|
||||
reject((x, n) => { n.parameters.scaleMode.value = "stretch"; n.parameters.backgroundColor.value = [2, 0, 0, 1]; }, "AUTHORING_PARAMETER", "backgroundColor");
|
||||
});
|
||||
test("adapter counts only active incoming links and reports socket overflow", () => {
|
||||
const x = fixture();
|
||||
const active = x.links.find((link) => link.toSocketId === "frame_out:color");
|
||||
x.links.push({
|
||||
...structuredClone(active),
|
||||
id: "muted_duplicate",
|
||||
muted: true,
|
||||
});
|
||||
assert.doesNotThrow(() => adaptFxNodeSnapshot(x));
|
||||
x.links.push({ ...structuredClone(active), id: "active_overflow" });
|
||||
assert.throws(
|
||||
() => adaptFxNodeSnapshot(x),
|
||||
(error) =>
|
||||
error.code === "AUTHORING_LINK_INCOMING" &&
|
||||
error.details.socketId === "frame_out:color",
|
||||
);
|
||||
});
|
||||
test("source map covers Rust fields, nested values, every input and is deeply frozen", () => {
|
||||
const snapshot = fixture();
|
||||
snapshot.links.find((link) => link.toSocketId === "frame_out:color").muted =
|
||||
true;
|
||||
const ir = adaptFxNodeSnapshot(snapshot, 9),
|
||||
map = getSourceMap(ir);
|
||||
for (const path of [
|
||||
"schemaVersion",
|
||||
"graphId",
|
||||
"revision",
|
||||
"nodes",
|
||||
"nodes[0].id",
|
||||
"nodes[0].state",
|
||||
"nodes[0].executor.key",
|
||||
"nodes[0].executor.version",
|
||||
"nodes[0].parameters",
|
||||
"nodes[0].inputs",
|
||||
])
|
||||
assert.ok(map[path], path);
|
||||
for (const [index, node] of ir.nodes.entries())
|
||||
for (const input of Object.keys(semanticCatalog[node.executor.key].inputs))
|
||||
assert.ok(map[`nodes[${index}].inputs.${input}`]);
|
||||
assert.ok(
|
||||
Object.keys(map).some((path) =>
|
||||
/parameters\..+\[|parameters\..+\..+/.test(path),
|
||||
),
|
||||
);
|
||||
const socket = Object.values(map).find((source) => source.kind === "socket");
|
||||
const unconnected = Object.values(map).find(
|
||||
(source) => source.unconnected === true,
|
||||
);
|
||||
const link = Object.values(map).find((source) => source.kind === "link");
|
||||
assert.ok(socket?.socketId && unconnected?.socketId);
|
||||
assert.equal(
|
||||
ir.nodes.find((node) => node.id === "frame_out").inputs.source,
|
||||
undefined,
|
||||
);
|
||||
for (const field of [
|
||||
"linkId",
|
||||
"fromNodeId",
|
||||
"fromSocketId",
|
||||
"toNodeId",
|
||||
"toSocketId",
|
||||
"muted",
|
||||
])
|
||||
assert.ok(Object.hasOwn(link, field), field);
|
||||
assert.ok(Object.isFrozen(map) && Object.isFrozen(link));
|
||||
});
|
||||
test("texture source maps identify every flat authored control", () => {
|
||||
const snapshot = fixture();
|
||||
const hdr = snapshot.nodes.find((node) => node.id === "hdr");
|
||||
hdr.parameters.viewFormat.value = "rgba16_float";
|
||||
const ir = adaptFxNodeSnapshot(snapshot);
|
||||
const index = ir.nodes.findIndex((node) => node.id === "hdr");
|
||||
const root = `nodes[${index}].parameters`;
|
||||
const map = getSourceMap(ir);
|
||||
const source = (parameter) => ({
|
||||
kind: "parameter",
|
||||
nodeId: "hdr",
|
||||
parameter,
|
||||
});
|
||||
assert.deepEqual(map[`${root}.residency`], source("residency"));
|
||||
assert.deepEqual(map[`${root}.texture`], { kind: "node", nodeId: "hdr" });
|
||||
for (const [path, parameter] of [
|
||||
["dimension", "dimension"],
|
||||
["format", "format"],
|
||||
["extent", "extentMode"],
|
||||
["extent.kind", "extentMode"],
|
||||
["extent.depthOrArrayLayers", "depthOrArrayLayers"],
|
||||
["extent.width", "extentMode"],
|
||||
["extent.width.numerator", "relativeWidthNumerator"],
|
||||
["extent.width.denominator", "relativeWidthDenominator"],
|
||||
["extent.height", "extentMode"],
|
||||
["extent.height.numerator", "relativeHeightNumerator"],
|
||||
["extent.height.denominator", "relativeHeightDenominator"],
|
||||
["mipLevelCount", "mipLevelCount"],
|
||||
["sampleCount", "sampleCount"],
|
||||
["viewFormats", "viewFormat"],
|
||||
["viewFormats[0]", "viewFormat"],
|
||||
])
|
||||
assert.deepEqual(map[`${root}.texture.${path}`], source(parameter), path);
|
||||
assert.ok(!Object.values(map).some((value) => value.parameter === "texture"));
|
||||
|
||||
const absolute = fixture();
|
||||
absolute.nodes.find((node) => node.id === "hdr").parameters.extentMode.value =
|
||||
"absolute";
|
||||
const absoluteIr = adaptFxNodeSnapshot(absolute);
|
||||
const absoluteIndex = absoluteIr.nodes.findIndex((node) => node.id === "hdr");
|
||||
const absoluteMap = getSourceMap(absoluteIr);
|
||||
assert.deepEqual(
|
||||
absoluteMap[`nodes[${absoluteIndex}].parameters.texture.extent.width`],
|
||||
source("absoluteWidth"),
|
||||
);
|
||||
assert.deepEqual(
|
||||
absoluteMap[`nodes[${absoluteIndex}].parameters.texture.extent.height`],
|
||||
source("absoluteHeight"),
|
||||
);
|
||||
});
|
||||
test("unsupported texture diagnostics map to their exact authored controls", () => {
|
||||
const ir = adaptFxNodeSnapshot(fixture());
|
||||
const index = ir.nodes.findIndex((node) => node.id === "hdr");
|
||||
for (const [suffix, parameter] of [
|
||||
["dimension", "dimension"],
|
||||
["mipLevelCount", "mipLevelCount"],
|
||||
["sampleCount", "sampleCount"],
|
||||
["extent.depthOrArrayLayers", "depthOrArrayLayers"],
|
||||
]) {
|
||||
const path = `nodes[${index}].parameters.texture.${suffix}`;
|
||||
const mapped = mapAuthoringDiagnostic(
|
||||
ir,
|
||||
new RendererError("GRAPH_UNSUPPORTED_FEATURE", {
|
||||
message: "unsupported",
|
||||
path,
|
||||
}),
|
||||
);
|
||||
assert.equal(mapped.path, path);
|
||||
assert.deepEqual(mapped.source, {
|
||||
kind: "parameter",
|
||||
nodeId: "hdr",
|
||||
parameter,
|
||||
});
|
||||
assert.equal(semanticCatalog.mesh.version, 2);
|
||||
assert.equal(nodeDefinitions.mesh.sockets.localAabb.title, "Local AABB");
|
||||
assert.equal(semanticCatalog.pipeline.version, 2);
|
||||
assert.equal(semanticCatalog.pipeline.inputs.predicate.required, false);
|
||||
for (const key of ["and", "xnor", "equals_f32", "greater_than_u32", "combine_vec4",
|
||||
"separate_mat4", "combine_u32_bits", "separate_u32x16", "separate_local_aabb"])
|
||||
assert.equal(semanticCatalog[key].execution, "expression", key);
|
||||
for (const [key, contract] of Object.entries(semanticCatalog)) {
|
||||
assert.equal(nodeDefinitions[key].version, contract.version, key);
|
||||
assert.equal(descriptors[key].version, contract.version, key);
|
||||
}
|
||||
});
|
||||
test("diagnostic mapper creates a frozen RendererError DTO with fallbacks and prefix matching", () => {
|
||||
const ir = adaptFxNodeSnapshot(fixture());
|
||||
const original = new RendererError("GRAPH_INPUT", {
|
||||
message: "bad",
|
||||
field: "nodes[0].executor.key.more",
|
||||
nested: { x: 1 },
|
||||
});
|
||||
const mapped = mapAuthoringDiagnostic(ir, original);
|
||||
assert.notStrictEqual(mapped, original);
|
||||
assert.equal(mapped.code, original.code);
|
||||
assert.equal(mapped.source.kind, "node");
|
||||
assert.equal(mapped.diagnostic, undefined);
|
||||
assert.ok(Object.isFrozen(mapped) && Object.isFrozen(mapped.details.nested));
|
||||
assert.equal(Object.isFrozen(original), false);
|
||||
original.details.nested.x = 2;
|
||||
assert.equal(mapped.details.nested.x, 1);
|
||||
const unmatchedOriginal = new RendererError("GRAPH_INPUT", {
|
||||
message: "unmapped",
|
||||
path: "resources[0]",
|
||||
});
|
||||
const unmatched = mapAuthoringDiagnostic(ir, unmatchedOriginal);
|
||||
assert.notStrictEqual(unmatched, unmatchedOriginal);
|
||||
assert.equal(unmatched.source, undefined);
|
||||
assert.ok(Object.isFrozen(unmatched) && Object.isFrozen(unmatched.details));
|
||||
assert.equal(Object.isFrozen(unmatchedOriginal), false);
|
||||
|
||||
test("current culling fixture uses type-bit predicates and final socket versions", () => {
|
||||
const byId = Object.fromEntries(culling.nodes.map((node) => [node.id, node]));
|
||||
assert.deepEqual(byId.cull.inputs.localAabb, { node: "mesh", socket: "localAabb" });
|
||||
assert.deepEqual(byId.type_words.inputs.value, { node: "mesh", socket: "type" });
|
||||
assert.equal(byId.ground.executor.version, 2);
|
||||
assert.equal(byId.ground.inputs.predicate.node, "ground_final");
|
||||
});
|
||||
test("controller keeps last-good through failures and only drops after successful switch", async () => {
|
||||
let fail = false;
|
||||
const calls = [];
|
||||
const renderer = {
|
||||
compileGraph: async (ir) => ({
|
||||
compiledId: [ir.revision, 1],
|
||||
revision: ir.revision,
|
||||
}),
|
||||
switchCompiledGraph: async (id) => {
|
||||
calls.push(["switch", id]);
|
||||
if (fail) throw Error("switch");
|
||||
},
|
||||
dropCompiledGraph: async (id) => calls.push(["drop", id]),
|
||||
};
|
||||
const c = new AuthoringController({
|
||||
renderer,
|
||||
adapt: (_, revision) => ({ revision }),
|
||||
});
|
||||
c.markDirty({});
|
||||
await c.apply();
|
||||
fail = true;
|
||||
c.markDirty({});
|
||||
await assert.rejects(c.apply());
|
||||
assert.deepEqual(calls, [
|
||||
["switch", [1, 1]],
|
||||
["switch", [2, 1]],
|
||||
]);
|
||||
fail = false;
|
||||
await c.apply();
|
||||
assert.deepEqual(calls.at(-1), ["drop", [1, 1]]);
|
||||
await c.destroy();
|
||||
});
|
||||
test("controller shares in-flight apply", async () => {
|
||||
let release;
|
||||
const gate = new Promise((r) => (release = r));
|
||||
const c = new AuthoringController({
|
||||
adapt: (_, revision) => ({ revision }),
|
||||
renderer: {
|
||||
compileGraph: async (ir) => {
|
||||
await gate;
|
||||
return { compiledId: [ir.revision, 1], revision: ir.revision };
|
||||
},
|
||||
switchCompiledGraph: async () => {},
|
||||
dropCompiledGraph: async () => {},
|
||||
},
|
||||
});
|
||||
c.markDirty({});
|
||||
const a = c.apply();
|
||||
assert.strictEqual(c.apply(), a);
|
||||
release();
|
||||
await a;
|
||||
});
|
||||
test("controller retains mapped diagnostic while apply rejects the original and subscriptions agree", async () => {
|
||||
const original = new RendererError("GRAPH_BAD", {
|
||||
path: "nodes[0].id",
|
||||
message: "bad",
|
||||
});
|
||||
const states = [];
|
||||
const c = new AuthoringController({
|
||||
adapt: (snapshot, revision) => adaptFxNodeSnapshot(snapshot, revision),
|
||||
renderer: {
|
||||
compileGraph: async () => {
|
||||
throw original;
|
||||
},
|
||||
switchCompiledGraph: async () => {},
|
||||
dropCompiledGraph: async () => {},
|
||||
},
|
||||
});
|
||||
c.subscribe((state) => states.push(state));
|
||||
c.markDirty(fixture());
|
||||
await assert.rejects(c.apply(), (error) => error === original);
|
||||
assert.notStrictEqual(states.at(-1).error, original);
|
||||
let subscribed;
|
||||
c.subscribe((state) => {
|
||||
subscribed = state;
|
||||
})();
|
||||
assert.strictEqual(subscribed.error, states.at(-1).error);
|
||||
await c.destroy();
|
||||
});
|
||||
test("apply after destroy does not compile and destroy returns one strict promise", async () => {
|
||||
let compiles = 0;
|
||||
const c = new AuthoringController({
|
||||
adapt: () => ({}),
|
||||
renderer: {
|
||||
compileGraph: async () => {
|
||||
compiles++;
|
||||
},
|
||||
dropCompiledGraph: async () => {},
|
||||
switchCompiledGraph: async () => {},
|
||||
},
|
||||
});
|
||||
c.markDirty({});
|
||||
const first = c.destroy();
|
||||
assert.strictEqual(c.destroy(), first);
|
||||
assert.strictEqual(await c.apply(), null);
|
||||
await first;
|
||||
assert.equal(compiles, 0);
|
||||
|
||||
test("removed architecture is absent from the authoring catalog", () => {
|
||||
for (const removed of ["mesh_query", "pipeline_registry"])
|
||||
assert.equal(semanticCatalog[removed], undefined);
|
||||
const serialized = JSON.stringify(semanticCatalog);
|
||||
for (const removedSocket of ["isVisible", "localAabbs", "activation"])
|
||||
assert.equal(serialized.includes(`\"${removedSocket}\"`), false);
|
||||
});
|
||||
|
||||
@@ -3,304 +3,39 @@ import assert from "node:assert/strict";
|
||||
import * as presets from "../static/render-graph/presets.js";
|
||||
import { descriptors } from "../static/render-graph/catalog.js";
|
||||
|
||||
const order = [
|
||||
"midnight",
|
||||
"ember",
|
||||
"hdr",
|
||||
"culling",
|
||||
"tone",
|
||||
"contain",
|
||||
"reinhard",
|
||||
"linear",
|
||||
"grading",
|
||||
"edges",
|
||||
"bloom",
|
||||
"combined",
|
||||
];
|
||||
const sequences = {
|
||||
midnight: [
|
||||
["ldr", "texture"],
|
||||
["depth", "texture"],
|
||||
["mesh", "mesh"],
|
||||
["query", "mesh_query"],
|
||||
["registry", "pipeline_registry"],
|
||||
["ground", "pipeline"],
|
||||
["pbr", "pipeline"],
|
||||
["pbr_double", "pipeline"],
|
||||
["frame_out", "frame_out"],
|
||||
],
|
||||
ember: [
|
||||
["ldr", "texture"],
|
||||
["depth", "texture"],
|
||||
["mesh", "mesh"],
|
||||
["query", "mesh_query"],
|
||||
["registry", "pipeline_registry"],
|
||||
["ground", "pipeline"],
|
||||
["pbr", "pipeline"],
|
||||
["pbr_double", "pipeline"],
|
||||
["frame_out", "frame_out"],
|
||||
],
|
||||
hdr: [
|
||||
["hdr", "texture"],
|
||||
["depth", "texture"],
|
||||
["mesh", "mesh"],
|
||||
["query", "mesh_query"],
|
||||
["registry", "pipeline_registry"],
|
||||
["ground", "pipeline"],
|
||||
["pbr", "pipeline"],
|
||||
["pbr_double", "pipeline"],
|
||||
["frame_out", "frame_out"],
|
||||
],
|
||||
culling: [
|
||||
["hdr", "texture"],
|
||||
["depth", "texture"],
|
||||
["mesh", "mesh"],
|
||||
["cull", "frustum_cull"],
|
||||
["query", "mesh_query"],
|
||||
["registry", "pipeline_registry"],
|
||||
["ground", "pipeline"],
|
||||
["pbr", "pipeline"],
|
||||
["pbr_double", "pipeline"],
|
||||
["frame_out", "frame_out"],
|
||||
],
|
||||
tone: [
|
||||
["hdr", "texture"],
|
||||
["depth", "texture"],
|
||||
["mesh", "mesh"],
|
||||
["query", "mesh_query"],
|
||||
["registry", "pipeline_registry"],
|
||||
["ground", "pipeline"],
|
||||
["pbr", "pipeline"],
|
||||
["pbr_double", "pipeline"],
|
||||
["frame_out", "frame_out"],
|
||||
],
|
||||
grading: [
|
||||
["balance_hdr", "texture"], ["exposure_hdr", "texture"], ["saturation_hdr", "texture"], ["mixer_hdr", "texture"],
|
||||
["hdr", "texture"], ["depth", "texture"], ["mesh", "mesh"], ["query", "mesh_query"], ["registry", "pipeline_registry"],
|
||||
["ground", "pipeline"], ["pbr", "pipeline"], ["pbr_double", "pipeline"], ["balance", "color_balance"], ["exposure", "exposure_contrast"],
|
||||
["saturation", "saturation"], ["mixer", "channel_mixer"], ["frame_out", "frame_out"],
|
||||
],
|
||||
edges: [
|
||||
["edge_hdr", "texture"],
|
||||
["hdr", "texture"],
|
||||
["depth", "texture"],
|
||||
["mesh", "mesh"],
|
||||
["query", "mesh_query"],
|
||||
["registry", "pipeline_registry"],
|
||||
["ground", "pipeline"],
|
||||
["pbr", "pipeline"],
|
||||
["pbr_double", "pipeline"],
|
||||
["edges", "luminance_edge"],
|
||||
["frame_out", "frame_out"],
|
||||
],
|
||||
bloom: [
|
||||
["half_a", "texture"],
|
||||
["half_b", "texture"],
|
||||
["half_c", "texture"],
|
||||
["composite_hdr", "texture"],
|
||||
["hdr", "texture"],
|
||||
["depth", "texture"],
|
||||
["mesh", "mesh"],
|
||||
["query", "mesh_query"],
|
||||
["registry", "pipeline_registry"],
|
||||
["ground", "pipeline"],
|
||||
["pbr", "pipeline"],
|
||||
["pbr_double", "pipeline"],
|
||||
["extract", "bloom_extract"],
|
||||
["blur_h", "bloom_blur"],
|
||||
["blur_v", "bloom_blur"],
|
||||
["composite", "bloom_composite"],
|
||||
["frame_out", "frame_out"],
|
||||
],
|
||||
combined: [
|
||||
["edge_hdr", "texture"],
|
||||
["half_a", "texture"],
|
||||
["half_b", "texture"],
|
||||
["half_c", "texture"],
|
||||
["composite_hdr", "texture"],
|
||||
["hdr", "texture"],
|
||||
["depth", "texture"],
|
||||
["mesh", "mesh"],
|
||||
["query", "mesh_query"],
|
||||
["registry", "pipeline_registry"],
|
||||
["ground", "pipeline"],
|
||||
["pbr", "pipeline"],
|
||||
["pbr_double", "pipeline"],
|
||||
["extract", "bloom_extract"],
|
||||
["blur_h", "bloom_blur"],
|
||||
["blur_v", "bloom_blur"],
|
||||
["composite", "bloom_composite"],
|
||||
["edges", "luminance_edge"],
|
||||
["frame_out", "frame_out"],
|
||||
],
|
||||
};
|
||||
for (const name of ["contain", "reinhard", "linear"])
|
||||
sequences[name] = sequences.tone;
|
||||
|
||||
test("presets have the exact canonical pipeline identities, schemas, and node sequences", () => {
|
||||
assert.deepEqual(Object.keys(presets.renderGraphPresets), order);
|
||||
assert.deepEqual(
|
||||
order.map((name) => presets[name].graphId),
|
||||
[
|
||||
"preset_midnight",
|
||||
"preset_ember",
|
||||
"preset_hdr_fullscreen",
|
||||
"preset_gpu_culling",
|
||||
"preset_tone",
|
||||
"preset_contain",
|
||||
"preset_reinhard",
|
||||
"preset_linear",
|
||||
"preset_grading",
|
||||
"preset_edges",
|
||||
"preset_bloom",
|
||||
"preset_combined",
|
||||
],
|
||||
);
|
||||
for (const name of order) {
|
||||
const graph = presets[name];
|
||||
assert.deepEqual([graph.schemaVersion, graph.revision], [2, 1]);
|
||||
assert.equal(
|
||||
new Set(graph.nodes.map((node) => node.id)).size,
|
||||
graph.nodes.length,
|
||||
);
|
||||
assert.deepEqual(
|
||||
graph.nodes.map((node) => [node.id, node.executor.key]),
|
||||
sequences[name],
|
||||
);
|
||||
assert.equal(
|
||||
graph.nodes.filter((node) => node.executor.key === "frame_out").length,
|
||||
1,
|
||||
);
|
||||
assert.ok(
|
||||
graph.nodes.every(
|
||||
(node) => !["surface_target", "present"].includes(node.executor.key),
|
||||
),
|
||||
);
|
||||
assert.ok(!graph.nodes.some((node) => node.id === "copy"));
|
||||
assert.ok(
|
||||
graph.nodes.every(
|
||||
(node) => node.executor.version === descriptors[node.executor.key].version,
|
||||
),
|
||||
);
|
||||
test("all presets use current schemas, versions, and one frame output", () => {
|
||||
assert.equal(Object.keys(presets.renderGraphPresets).length, 12);
|
||||
for (const [name, graph] of Object.entries(presets.renderGraphPresets)) {
|
||||
assert.deepEqual([graph.schemaVersion, graph.revision], [2, 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}`);
|
||||
}
|
||||
const grading = presets.grading;
|
||||
assert.deepEqual(grading.nodes.find((n) => n.id === "balance").parameters, {
|
||||
mode: "lift_gamma_gain", factor: 1, lift: 0, liftColor: [1,1,1,1],
|
||||
gamma: 1, gammaColor: [1,1,1,1], gain: 1, gainColor: [1,1,1,1],
|
||||
offset: 0, offsetColor: [1,1,1,1], power: 1, powerColor: [1,1,1,1],
|
||||
slope: 1, slopeColor: [1,1,1,1],
|
||||
});
|
||||
assert.deepEqual(grading.nodes.find((n) => n.id === "exposure").parameters, { exposureStops: 0, contrast: 1, pivot: .18, factor: 1 });
|
||||
assert.deepEqual(grading.nodes.find((n) => n.id === "saturation").parameters, { saturation: 1, factor: 1 });
|
||||
assert.deepEqual(grading.nodes.find((n) => n.id === "mixer").parameters, { redOutput: [1,0,0], greenOutput: [0,1,0], blueOutput: [0,0,1], factor: 1 });
|
||||
});
|
||||
|
||||
test("presets preserve common mesh, texture, query, pipeline, culling and post wiring", () => {
|
||||
const removed = [
|
||||
"texture_spec",
|
||||
"scene_table",
|
||||
"local_aabb_buffer",
|
||||
"camera_frustum",
|
||||
"visibility_flags",
|
||||
];
|
||||
test("presets classify visibility and material through type.words[0] predicates", () => {
|
||||
for (const [name, graph] of Object.entries(presets.renderGraphPresets)) {
|
||||
const byId = Object.fromEntries(graph.nodes.map((node) => [node.id, node]));
|
||||
assert.deepEqual(byId.query.parameters, {
|
||||
visiblePredicate: "required_true",
|
||||
visibleDefault: true,
|
||||
frustumCulledPredicate: name === "culling" ? "required_false" : "any",
|
||||
frustumCulledDefault: false,
|
||||
});
|
||||
assert.deepEqual(byId.query.inputs.mesh, { node: "mesh", socket: "mesh" });
|
||||
assert.deepEqual(byId.query.inputs.isVisible, {
|
||||
node: "mesh",
|
||||
socket: "isVisible",
|
||||
});
|
||||
assert.deepEqual(byId.ground.inputs.mesh, {
|
||||
node: "mesh",
|
||||
socket: "mesh",
|
||||
});
|
||||
assert.deepEqual(byId.ground.inputs.draws, {
|
||||
node: "query",
|
||||
socket: "draws",
|
||||
});
|
||||
assert.deepEqual(byId.ground.inputs.depthTarget, {
|
||||
node: "depth",
|
||||
socket: "texture",
|
||||
});
|
||||
assert.equal(
|
||||
graph.nodes.filter((node) => node.executor.key === "pipeline_registry")
|
||||
.length,
|
||||
1,
|
||||
);
|
||||
const pipelines = graph.nodes.filter(
|
||||
(node) => node.executor.key === "pipeline",
|
||||
);
|
||||
assert.deepEqual(
|
||||
pipelines.map((node) => node.parameters.pipeline),
|
||||
["ground_plane", "gltf_standard", "gltf_standard_double_sided"],
|
||||
);
|
||||
for (const pipeline of pipelines)
|
||||
assert.deepEqual(pipeline.inputs.activation, {
|
||||
node: "registry",
|
||||
socket: "activation",
|
||||
});
|
||||
assert.deepEqual(byId.pbr.inputs.colorTarget, {
|
||||
node: "ground",
|
||||
socket: "color",
|
||||
});
|
||||
assert.deepEqual(byId.pbr.inputs.depthTarget, {
|
||||
node: "ground",
|
||||
socket: "depth",
|
||||
});
|
||||
assert.deepEqual(byId.pbr_double.inputs.colorTarget, {
|
||||
node: "pbr",
|
||||
socket: "color",
|
||||
});
|
||||
assert.deepEqual(byId.pbr_double.inputs.depthTarget, {
|
||||
node: "pbr",
|
||||
socket: "depth",
|
||||
});
|
||||
assert.ok(
|
||||
graph.nodes
|
||||
.filter((node) => node.executor.key === "texture")
|
||||
.every((node) => node.parameters.texture.dimension === "d2"),
|
||||
);
|
||||
assert.ok(
|
||||
graph.nodes.every((node) => !removed.includes(node.executor.key)),
|
||||
);
|
||||
assert.deepEqual(byId.type_words.inputs.value, { node: "mesh", socket: "type" }, name);
|
||||
assert.deepEqual(byId.type_bits.inputs.value, { node: "type_words", socket: "word0" }, name);
|
||||
const suffix = name === "culling" ? "_final" : "_class";
|
||||
assert.deepEqual(byId.ground.inputs.predicate, { node: `ground${suffix}`, socket: "value" }, name);
|
||||
assert.deepEqual(byId.pbr.inputs.predicate, { node: name === "culling" ? "pbr_final" : "standard_class", socket: "value" }, name);
|
||||
assert.deepEqual(byId.pbr_double.inputs.predicate, { node: name === "culling" ? "pbr_double_final" : "double_class", socket: "value" }, name);
|
||||
for (const pipeline of [byId.ground, byId.pbr, byId.pbr_double]) {
|
||||
assert.deepEqual(pipeline.inputs.mesh, { node: "mesh", socket: "mesh" });
|
||||
assert.equal(pipeline.executor.version, 2);
|
||||
}
|
||||
}
|
||||
const cull = Object.fromEntries(
|
||||
presets.culling.nodes.map((node) => [node.id, node]),
|
||||
);
|
||||
assert.deepEqual(cull.cull.parameters, { camera: "active" });
|
||||
assert.deepEqual(cull.cull.inputs, {
|
||||
mesh: { node: "mesh", socket: "mesh" },
|
||||
localAabbs: { node: "mesh", socket: "localAabbs" },
|
||||
});
|
||||
|
||||
test("culling 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" },
|
||||
});
|
||||
assert.deepEqual(cull.query.inputs.isFrustumCulled, {
|
||||
node: "cull",
|
||||
socket: "isFrustumCulled",
|
||||
});
|
||||
for (const name of ["tone", "contain", "reinhard", "linear", "grading", "edges", "bloom", "combined"])
|
||||
assert.ok(!presets[name].nodes.some((node) => node.id === "copy"));
|
||||
const finalSource = {
|
||||
hdr: "pbr_double",
|
||||
culling: "pbr_double",
|
||||
tone: "pbr_double",
|
||||
contain: "pbr_double",
|
||||
reinhard: "pbr_double",
|
||||
linear: "pbr_double",
|
||||
grading: "mixer",
|
||||
edges: "edges",
|
||||
bloom: "composite",
|
||||
combined: "edges",
|
||||
midnight: "pbr_double",
|
||||
ember: "pbr_double",
|
||||
};
|
||||
for (const name of order)
|
||||
assert.deepEqual(presets[name].nodes.at(-1).inputs.color, {
|
||||
node: finalSource[name],
|
||||
socket: "color",
|
||||
});
|
||||
assert.deepEqual(byId.not_culled.inputs.operand, { node: "cull", socket: "isFrustumCulled" });
|
||||
for (const id of ["ground", "pbr", "pbr_double"])
|
||||
assert.equal(byId[id].inputs.predicate.node.endsWith("_final"), true);
|
||||
});
|
||||
|
||||
@@ -10,43 +10,41 @@ class WorkerMock extends EventTarget {
|
||||
reply(data) { this.dispatchEvent(new MessageEvent("message", { data })); }
|
||||
}
|
||||
function fixture() {
|
||||
const memory = new WebAssembly.Memory({initial:2, maximum:4, shared:true});
|
||||
const memory = new WebAssembly.Memory({initial:4, maximum:8, shared:true});
|
||||
const header = new Int32Array(memory.buffer, 0, 16);
|
||||
header.set([0x4e574159,1,1024,24,0,0]);
|
||||
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.importGlb(new ArrayBuffer(8));
|
||||
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:[[7,3]]}});
|
||||
f.worker.reply({type:"reply",request:1,ok:true,result:{meshes:[{handle:[7,3],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 and importGlb remains an alias", async()=>{
|
||||
for(const method of ["replaceSceneGlb","importGlb"]){const f=fixture(),pending=f.client[method](new ArrayBuffer(8));f.worker.reply({type:"payload-ready",id:1});await Promise.resolve();assert.equal(new Int32Array(f.memory.buffer,64,24)[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,24).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("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);
|
||||
const pending=mesh.setVisible(true);
|
||||
const {memory,header,worker}=f;
|
||||
assert.equal(Atomics.load(header,5),2);
|
||||
const slot=new Int32Array(memory.buffer,64+96,24);
|
||||
assert.deepEqual([...slot.slice(0,6)],[1,2,2,7,3,1]);
|
||||
const slot=new Int32Array(memory.buffer,64+160,40);
|
||||
assert.deepEqual([...slot.slice(0,6)],[2,2,2,7,3,1]);
|
||||
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]),false);
|
||||
const creating=mesh.createInstance(new Float32Array([1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1]),{visible:false});
|
||||
worker.reply({type:"reply",request:2,ok:true,result:[4,2]}); const instance=await creating;
|
||||
const destroying=instance.destroy(); worker.reply({type:"reply",request:3,ok:true}); await destroying;
|
||||
assert.throws(()=>instance.setVisible(true), error=>error instanceof RendererError&&error.code==="STALE_HANDLE");
|
||||
});
|
||||
test("rejects protocol mismatch", () => {
|
||||
const {memory,worker}=fixture(); new Int32Array(memory.buffer)[1]=2;
|
||||
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 () => {
|
||||
@@ -70,7 +68,7 @@ test("worker failures and dispose reject every pending operation", async () => {
|
||||
});
|
||||
test("import always releases staged payload when ring is full", async () => {
|
||||
const {header,worker,client}=fixture(); Atomics.store(header,5,1024);
|
||||
const loading=client.importGlb(new ArrayBuffer(8));
|
||||
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");
|
||||
@@ -84,7 +82,7 @@ test("does not export handle constructors or internal mutation methods", () => {
|
||||
});
|
||||
test("corrupt backlog closes and terminates the transport", async () => {
|
||||
const f=fixture(); Atomics.store(f.header,5,1025);
|
||||
const loading=f.client.importGlb(new ArrayBuffer(8));
|
||||
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/);
|
||||
@@ -94,7 +92,7 @@ test("corrupt backlog closes and terminates the transport", async () => {
|
||||
test("import rejects immediately after disposal", async () => {
|
||||
const f=fixture();
|
||||
f.client.dispose();
|
||||
await assert.rejects(f.client.importGlb(new ArrayBuffer(8)),/DISPOSED/);
|
||||
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 () => {
|
||||
@@ -103,7 +101,7 @@ test("import rejects when disposed during asynchronous source loading", async ()
|
||||
let finishFetch;
|
||||
globalThis.fetch=()=>new Promise(resolve=>{finishFetch=resolve;});
|
||||
try {
|
||||
const loading=f.client.importGlb("model.glb");
|
||||
const loading=f.client.replaceSceneGlb("model.glb");
|
||||
f.client.dispose();
|
||||
finishFetch({arrayBuffer:async()=>new ArrayBuffer(8)});
|
||||
await assert.rejects(loading,/DISPOSED/);
|
||||
@@ -117,7 +115,7 @@ 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,24)[1],7);
|
||||
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]});
|
||||
});
|
||||
@@ -130,12 +128,12 @@ test("compile releases payload after success", async()=>{const f=fixture(),p=f.c
|
||||
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,24);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("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,24).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,160,24).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("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/);});
|
||||
|
||||
+17
-17
@@ -5,37 +5,37 @@ import { DerivedBvh } from "../static/bvh-core.js";
|
||||
import { RendererClient } from "../static/renderer-client.js";
|
||||
|
||||
const align16 = value => (value + 15) & ~15;
|
||||
const componentCounts = [1, 1, 1, 3, 3, 1, 1, 1, 1, 1, 16, 3, 3, 1];
|
||||
const scalarTypes = [1, 1, 1, 2, 2, 1, 1, 1, 1, 1, 2, 2, 2, 1];
|
||||
const componentCounts = [1, 1, 3, 3, 1, 1, 1, 1, 16, 3, 3, 16];
|
||||
const scalarTypes = [1, 1, 2, 2, 1, 1, 1, 1, 2, 2, 2, 1];
|
||||
|
||||
function snapshotFixture({ instances = 1 } = {}) {
|
||||
const memory = new WebAssembly.Memory({ initial: 4, maximum: 8, shared: true });
|
||||
const words = new Uint32Array(memory.buffer);
|
||||
const control = new Int32Array(memory.buffer, 0, 64);
|
||||
const ptr = 256;
|
||||
const counts = [1, 1, 1, 1, 1, ...Array(9).fill(instances)];
|
||||
const counts = [1, 1, 1, 1, ...Array(8).fill(instances)];
|
||||
const offsets = [];
|
||||
let cursor = 512;
|
||||
for (let i = 0; i < 14; i++) {
|
||||
let cursor = 448;
|
||||
for (let i = 0; i < 12; i++) {
|
||||
offsets.push(cursor);
|
||||
cursor = align16(cursor + counts[i] * componentCounts[i] * 4);
|
||||
}
|
||||
control.set([0x504e5359, 1, 256, 3, 64, 1, 1, 2, 1, 0, 7, 0, 4, 1, 0, 0]);
|
||||
control.set([2, 1, 1, ptr, cursor, 7, 0, 1, instances, 1, 64, 0, 0, 0, 0, 0], 16);
|
||||
control.set([0x504e5359, 1, 256, 3, 64, 2, 1, 2, 1, 0, 7, 0, 4, 1, 0, 0]);
|
||||
control.set([2, 1, 1, ptr, cursor, 7, 0, 1, instances, 2, 64, 0, 0, 0, 0, 0], 16);
|
||||
const blob = new Uint32Array(memory.buffer, ptr, cursor / 4);
|
||||
blob.set([0x31534452, 1, 64, cursor, 1, 7, 0, 14, 64, 32, 1, instances, 0x01020304, 3, 0, 0]);
|
||||
for (let i = 0; i < 14; i++) {
|
||||
blob.set([0x32534452, 2, 64, cursor, 1, 7, 0, 12, 64, 32, 1, instances, 0x01020304, 3, 0, 0]);
|
||||
for (let i = 0; i < 12; i++) {
|
||||
blob.set([i + 1, scalarTypes[i], offsets[i], counts[i], componentCounts[i], componentCounts[i] * 4, 4, 0], 16 + i * 8);
|
||||
}
|
||||
const stream = i => scalarTypes[i] === 2
|
||||
? new Float32Array(memory.buffer, ptr + offsets[i], counts[i] * componentCounts[i])
|
||||
: new Uint32Array(memory.buffer, ptr + offsets[i], counts[i] * componentCounts[i]);
|
||||
stream(0)[0] = 4; stream(1)[0] = 2; stream(2)[0] = 1;
|
||||
stream(3).set([-1, -1, -1]); stream(4).set([1, 1, 1]);
|
||||
stream(0)[0] = 4; stream(1)[0] = 2;
|
||||
stream(2).set([-1, -1, -1]); stream(3).set([1, 1, 1]);
|
||||
for (let i = 0; i < instances; i++) {
|
||||
stream(5)[i] = 10 + i; stream(6)[i] = 3; stream(7)[i] = 4; stream(8)[i] = 2;
|
||||
stream(9)[i] = 1; stream(13)[i] = 1;
|
||||
stream(11).set([i * 4, -1, -1], i * 3); stream(12).set([i * 4 + 2, 1, 1], i * 3);
|
||||
stream(4)[i] = 10 + i; stream(5)[i] = 3; stream(6)[i] = 4; stream(7)[i] = 2;
|
||||
stream(11)[i * 16] = 1;
|
||||
stream(9).set([i * 4, -1, -1], i * 3); stream(10).set([i * 4 + 2, 1, 1], i * 3);
|
||||
}
|
||||
return { memory, control, ptr, cursor };
|
||||
}
|
||||
@@ -84,7 +84,7 @@ function bvhSnapshot({ pickable = [1, 1], shifted = false } = {}) {
|
||||
return { instanceCount: count, streams: {
|
||||
instanceSlot: Uint32Array.from([5, 6]), instanceGeneration: Uint32Array.from([1, 1]),
|
||||
instanceMeshSlot: Uint32Array.from([2, 2]), instanceMeshGeneration: Uint32Array.from([4, 4]),
|
||||
instancePickable: Uint32Array.from(pickable),
|
||||
instanceType: Uint32Array.from(pickable.flatMap(x => [x, ...Array(15).fill(0)])),
|
||||
instanceWorldMin: Float32Array.from(shifted ? [10, -1, -1, 4, -1, -1] : [2, -1, -1, 4, -1, -1]),
|
||||
instanceWorldMax: Float32Array.from(shifted ? [12, 1, 1, 6, 1, 1] : [3, 1, 1, 6, 1, 1]),
|
||||
}};
|
||||
@@ -112,11 +112,11 @@ test("renderer pick returns gated instances and exact epoch", async () => {
|
||||
const scene = snapshotFixture();
|
||||
const ring = 8192;
|
||||
const ringHeader = new Int32Array(scene.memory.buffer, ring, 16);
|
||||
ringHeader.set([0x4e574159, 1, 1024, 24]);
|
||||
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);
|
||||
rendererWorker.reply({ type: "snapshot-init", controlPtr: 0, controlVersion: 1, schemaVersion: 1 });
|
||||
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]);
|
||||
const request = bvhWorker.messages.find(message => message.type === "pick");
|
||||
|
||||
Reference in New Issue
Block a user