Strip core to render data and render graphs
Move glTF, picking, camera controls, and conventional handles into addons. Keep camera and material mutations in SIMD-aligned shared SOA rows and synchronize material updates directly into GPU buffers. 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:
@@ -12,13 +12,15 @@ import {
|
||||
createVelocityColumn,
|
||||
customRenderPipelineExample,
|
||||
defaultPipelineExample,
|
||||
dropActiveGraph,
|
||||
fluentGraphExample,
|
||||
fxNodeExportExample,
|
||||
importGltf,
|
||||
installCameraRenderDataControls,
|
||||
jsoGraphExample,
|
||||
loadCompleteScene,
|
||||
pickNearest,
|
||||
conventionalSceneHandles,
|
||||
restyleScene,
|
||||
setVelocity,
|
||||
simpleSceneShader,
|
||||
updateInstance,
|
||||
@@ -63,7 +65,7 @@ test("cookbook graph recipes produce canonical addon-owned ASTs", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("cookbook graph lifecycle recipe serializes, switches, and drops", async () => {
|
||||
test("cookbook graph lifecycle recipe serializes and switches", async () => {
|
||||
const calls = [];
|
||||
const core = {
|
||||
compileGraph(source) {
|
||||
@@ -74,10 +76,6 @@ test("cookbook graph lifecycle recipe serializes, switches, and drops", async ()
|
||||
calls.push(["switch", id]);
|
||||
return Promise.resolve();
|
||||
},
|
||||
switchToImmediate() {
|
||||
calls.push(["immediate"]);
|
||||
return Promise.resolve();
|
||||
},
|
||||
dropCompiledGraph(id) {
|
||||
calls.push(["drop", id]);
|
||||
return Promise.resolve();
|
||||
@@ -85,13 +83,8 @@ test("cookbook graph lifecycle recipe serializes, switches, and drops", async ()
|
||||
};
|
||||
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]],
|
||||
]);
|
||||
assert.deepEqual(calls.slice(1), [["switch", [3, 4]]]);
|
||||
});
|
||||
|
||||
test("cookbook mutation recipes use handles and SOA writes directly", async () => {
|
||||
@@ -163,7 +156,7 @@ test("cookbook picking and worker-to-worker recipes use public facades", async (
|
||||
[0, 0, 0],
|
||||
[0, 0, -1],
|
||||
);
|
||||
assert.deepEqual(picked.hits[0].instance.handle, [9, 3]);
|
||||
assert.deepEqual(picked.hits[0].instance, [9, 3]);
|
||||
|
||||
class Port extends EventTarget {
|
||||
postMessage() {}
|
||||
@@ -183,5 +176,8 @@ test("cookbook picking and worker-to-worker recipes use public facades", async (
|
||||
core.dispose();
|
||||
|
||||
assert.equal(typeof importGltf, "function");
|
||||
assert.equal(typeof installCameraRenderDataControls, "function");
|
||||
assert.equal(typeof conventionalSceneHandles, "function");
|
||||
assert.equal(typeof restyleScene, "function");
|
||||
assert.equal(typeof loadCompleteScene, "function");
|
||||
});
|
||||
|
||||
@@ -6,6 +6,6 @@ function parseGlb(buffer){const view=new DataView(buffer);assert.equal(view.getU
|
||||
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,"PBR material gallery");});
|
||||
test("PBR gallery is deterministic and covers the default material 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-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/)});
|
||||
|
||||
@@ -2,7 +2,9 @@ import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { GltfImporter } from "@yawn/gltf-import";
|
||||
import { gltfToRenderDataPacket } from "../addons/gltf-import/src/gltf.js";
|
||||
import { writeSharedUpload } from "../addons/gltf-import/src/shared-upload.js";
|
||||
import { createCubeGeometry, encodeGeometryGlb } from "../examples/render-graph-studio/demo-loadouts.js";
|
||||
|
||||
class WorkerMock extends EventTarget {
|
||||
messages = [];
|
||||
@@ -18,7 +20,7 @@ test("glTF addon stages fetched bytes in shared SOA and commits only metadata",
|
||||
const memory = new SharedArrayBuffer(4096);
|
||||
const descriptor = {
|
||||
id: 9,
|
||||
name: "upload.gltf",
|
||||
name: "upload.renderData",
|
||||
domain: "fixed",
|
||||
scalar: "u32",
|
||||
lanes: 4,
|
||||
@@ -36,8 +38,8 @@ test("glTF addon stages fetched bytes in shared SOA and commits only metadata",
|
||||
const calls = [];
|
||||
const core = {
|
||||
async allocateArray(layout) { calls.push(["allocate", layout]); return array; },
|
||||
async commitGlbUpload(value, byteLength, options) {
|
||||
calls.push(["commit", value, byteLength, options]);
|
||||
async commitRenderDataUpload(value, byteLength) {
|
||||
calls.push(["commit", value, byteLength]);
|
||||
return { meshes: [] };
|
||||
},
|
||||
};
|
||||
@@ -54,7 +56,7 @@ test("glTF addon stages fetched bytes in shared SOA and commits only metadata",
|
||||
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,
|
||||
name: "upload.renderData", domain: "fixed", scalar: "u32", lanes: 4, stride: 16, length: 2,
|
||||
}]);
|
||||
assert.equal(worker.messages[1].buffer, memory);
|
||||
assert.equal(worker.messages[1].descriptor, descriptor);
|
||||
@@ -65,7 +67,25 @@ test("glTF addon stages fetched bytes in shared SOA and commits only metadata",
|
||||
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" }]);
|
||||
assert.deepEqual(calls[1], ["commit", array, 20]);
|
||||
importer.dispose();
|
||||
assert.equal(worker.terminated, true);
|
||||
});
|
||||
|
||||
test("glTF parsing produces a format-neutral typed render-data packet in the addon", async () => {
|
||||
const glb = encodeGeometryGlb(createCubeGeometry());
|
||||
const packet = await gltfToRenderDataPacket(
|
||||
new Uint8Array(glb),
|
||||
"https://example.test/scene.glb",
|
||||
);
|
||||
const header = new DataView(packet.buffer, packet.byteOffset, packet.byteLength);
|
||||
assert.equal(header.getUint32(0, true), 0x50445259);
|
||||
assert.equal(header.getUint32(4, true), 1);
|
||||
const metadataLength = header.getUint32(8, true);
|
||||
const payloadLength = header.getUint32(12, true);
|
||||
const metadata = JSON.parse(new TextDecoder().decode(packet.subarray(16, 16 + metadataLength)));
|
||||
assert.equal(metadata.geometries.length, 1);
|
||||
assert.equal(metadata.occurrences.length, 9);
|
||||
assert.deepEqual(metadata.geometries[0].instanceType.slice(0, 2), [5, 0]);
|
||||
assert.equal(((16 + metadataLength + 3) & ~3) + payloadLength, packet.byteLength);
|
||||
});
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { SnapshotReader, SnapshotProtocolError } from "../packages/yawn-core/src/snapshot.js";
|
||||
import { SnapshotReader, SnapshotProtocolError } from "../addons/mesh-handles/src/snapshot.js";
|
||||
import { DerivedBvh } from "../addons/mesh-handles/src/bvh-core.js";
|
||||
import { MeshHandles } from "../addons/mesh-handles/src/index.js";
|
||||
import { YawnCore } from "../packages/yawn-core/src/index.js";
|
||||
|
||||
const align16 = value => (value + 15) & ~15;
|
||||
@@ -108,25 +109,27 @@ class WorkerMock extends EventTarget {
|
||||
reply(data) { this.dispatchEvent(new MessageEvent("message", { data })); }
|
||||
}
|
||||
|
||||
test("core picking returns protocol handles and exact epoch", async () => {
|
||||
test("mesh addon owns picking and returns wrapped handles at the 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, pickingWorkerFactory: () => bvhWorker, free() {} };
|
||||
const bridge = { memory: scene.memory, ringPtr: ring, worker: rendererWorker, free() {} };
|
||||
const client = new YawnCore(bridge);
|
||||
const handles = new MeshHandles(client, { pickingWorkerFactory: () => bvhWorker });
|
||||
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]);
|
||||
const picking = handles.pickRay([0, 0, 0], [1, 0, 0]);
|
||||
const request = bvhWorker.messages.find(message => message.type === "pick");
|
||||
bvhWorker.reply({ type: "pick", request: request.request, epoch: 1, stale: false, hits: [{ slot: 10, generation: 3, distance: 2 }] });
|
||||
const result = await picking;
|
||||
assert.equal(result.epoch, 1);
|
||||
assert.equal(result.hits[0].distance, 2);
|
||||
assert.deepEqual(result.hits[0].instance, [10, 3]);
|
||||
assert.deepEqual(result.hits[0].instance.handle, [10, 3]);
|
||||
handles.dispose();
|
||||
client.dispose();
|
||||
assert.equal(bvhWorker.terminated, true);
|
||||
});
|
||||
|
||||
+80
-20
@@ -1,10 +1,12 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { YawnCore, RendererError } from "@yawn/core";
|
||||
import { MeshHandles } from "@yawn/mesh-handles";
|
||||
import * as coreApi from "@yawn/core";
|
||||
import { CameraHandle, MaterialHandles, MeshHandles } from "@yawn/mesh-handles";
|
||||
import { createGraphAst, serializeGraphAst } from "@yawn/render-graph-ast";
|
||||
|
||||
const { YawnCore, RendererError } = coreApi;
|
||||
|
||||
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];
|
||||
|
||||
@@ -70,19 +72,37 @@ function setup() {
|
||||
byteLength: 256, layoutEpoch: 1, writable: false,
|
||||
});
|
||||
const upload = installArray(memory, {
|
||||
id: 5, name: "upload.gltf", domain: "fixed", scalar: "u32", lanes: 4,
|
||||
id: 5, name: "upload.renderData", domain: "fixed", scalar: "u32", lanes: 4,
|
||||
stride: 16, length: 16, capacity: 16, controlPtr: 200064, dataOffset: 64,
|
||||
byteLength: 256, layoutEpoch: 1, writable: true,
|
||||
});
|
||||
const camera = installArray(memory, {
|
||||
id: 6, name: "camera.state", domain: "fixed", scalar: "f32", lanes: 16,
|
||||
stride: 64, length: 1, capacity: 1, controlPtr: 200384, dataOffset: 64,
|
||||
byteLength: 64, layoutEpoch: 1, writable: true,
|
||||
});
|
||||
new Float32Array(memory.buffer, camera.controlPtr + camera.dataOffset, 16).set([
|
||||
4, 3, 6, 1,
|
||||
0, 0, 0, 1,
|
||||
0, 1, 0, 0,
|
||||
Math.PI / 4, 16 / 9, 0.1, 1000,
|
||||
]);
|
||||
const material = installArray(memory, {
|
||||
id: 7, name: "material.state", domain: "fixed", scalar: "u32", lanes: 28,
|
||||
stride: 112, length: 4, capacity: 4, controlPtr: 200512, dataOffset: 64,
|
||||
byteLength: 448, layoutEpoch: 1, writable: true,
|
||||
});
|
||||
const materialFloats = new Float32Array(memory.buffer, material.controlPtr + material.dataOffset, material.byteLength / 4);
|
||||
materialFloats.set([1, 1, 1, 1, 0, 0, 0, 0, 1, 0.5, 1, 1, 0, 0.5, 1.5, 0.04], 28);
|
||||
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 };
|
||||
worker.reply({ type: "soa-init", arrays: [transform, type, generation, meshGeneration, upload, camera, material] });
|
||||
return { memory, ring, worker, bridge, core, handles: new MeshHandles(core), transform, type, generation, upload, camera, material };
|
||||
}
|
||||
|
||||
async function imported(fixture) {
|
||||
const loading = fixture.core.commitGlbUpload(fixture.core.array("upload.gltf"), 8);
|
||||
const loading = fixture.core.commitRenderDataUpload(fixture.core.array("upload.renderData"), 8);
|
||||
fixture.worker.reply({
|
||||
type: "reply",
|
||||
request: 1,
|
||||
@@ -93,6 +113,7 @@ async function imported(fixture) {
|
||||
defaultInstance: [8, 5],
|
||||
defaultType: TYPE,
|
||||
}],
|
||||
materials: [{ key: 1 }],
|
||||
},
|
||||
});
|
||||
const [mesh] = fixture.handles.fromImportedScene(await loading);
|
||||
@@ -105,10 +126,10 @@ async function imported(fixture) {
|
||||
return mesh;
|
||||
}
|
||||
|
||||
test("core commits shared scene uploads through metadata-only opcode 1", async () => {
|
||||
test("core commits generic shared render-data packets 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]);
|
||||
const pending = fixture.core.commitRenderDataUpload(fixture.core.array("upload.renderData"), 8);
|
||||
assert.deepEqual([...new Int32Array(fixture.memory.buffer, 64, 6)], [2, 1, 1, 5, 8, 0]);
|
||||
fixture.worker.reply({ type: "reply", request: 1, ok: true, result: { meshes: [] } });
|
||||
assert.deepEqual(await pending, { meshes: [] });
|
||||
});
|
||||
@@ -127,17 +148,6 @@ test("mesh handles are a separate conventional facade over core commands", async
|
||||
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);
|
||||
@@ -158,6 +168,55 @@ test("frequent instance mutations write guarded SOA lanes without ring messages"
|
||||
assert.equal(Atomics.load(type, base + 17) >>> 0, 1);
|
||||
});
|
||||
|
||||
test("camera is render data with no dedicated core API", () => {
|
||||
const fixture = setup();
|
||||
const camera = fixture.core.array("camera.state");
|
||||
const ringBefore = Atomics.load(fixture.ring, 5);
|
||||
const control = new Int32Array(fixture.memory.buffer, fixture.camera.controlPtr, 16);
|
||||
const sequenceBefore = Atomics.load(control, 9);
|
||||
const state = camera.read(0);
|
||||
state[0] = 5;
|
||||
state[1] = 4;
|
||||
camera.write(0, state);
|
||||
|
||||
assert.equal("SharedCamera" in coreApi, false);
|
||||
assert.deepEqual(camera.read(0).slice(0, 3), [5, 4, 6]);
|
||||
assert.equal(Atomics.load(fixture.ring, 5), ringBefore);
|
||||
assert.equal(Atomics.load(control, 9), sequenceBefore + 2);
|
||||
});
|
||||
|
||||
test("camera handle exposes conventional properties using only the shared row", () => {
|
||||
const fixture = setup();
|
||||
const camera = new CameraHandle(fixture.core);
|
||||
const ringBefore = Atomics.load(fixture.ring, 5);
|
||||
|
||||
camera.update({ position: [5, 4, 7], target: [1, 0, 0], fovY: Math.PI / 3 });
|
||||
|
||||
assert.deepEqual(camera.position, [5, 4, 7]);
|
||||
assert.deepEqual(camera.target, [1, 0, 0]);
|
||||
assert.ok(Math.abs(camera.fovY - Math.PI / 3) < 1e-6);
|
||||
assert.equal(Atomics.load(fixture.ring, 5), ringBefore);
|
||||
assert.throws(() => camera.update({ near: 2, far: 1 }), RangeError);
|
||||
});
|
||||
|
||||
test("material handles mutate packed GPU material rows without messages", () => {
|
||||
const fixture = setup();
|
||||
const [material] = new MaterialHandles(fixture.core).fromImportedScene({ materials: [{ key: 1 }] });
|
||||
const ringBefore = Atomics.load(fixture.ring, 5);
|
||||
const control = new Int32Array(fixture.memory.buffer, fixture.material.controlPtr, 16);
|
||||
const sequenceBefore = Atomics.load(control, 9);
|
||||
|
||||
material.update({ baseColor: [0.2, 0.4, 0.8, 1], metallic: 0.25, roughness: 0.75, ior: 2 });
|
||||
|
||||
assert.equal(material.key, 1);
|
||||
assert.deepEqual(material.baseColor.map(value => Math.round(value * 10) / 10), [0.2, 0.4, 0.8, 1]);
|
||||
assert.equal(material.metallic, 0.25);
|
||||
assert.equal(material.roughness, 0.75);
|
||||
assert.equal(material.ior, 2);
|
||||
assert.equal(Atomics.load(fixture.ring, 5), ringBefore);
|
||||
assert.equal(Atomics.load(control, 9), sequenceBefore + 2);
|
||||
});
|
||||
|
||||
test("generation columns reject stale handles and are read-only", async () => {
|
||||
const fixture = setup();
|
||||
const mesh = await imported(fixture);
|
||||
@@ -221,6 +280,7 @@ test("graph lifecycle remains FIFO and uses opcodes 8 and 9", async () => {
|
||||
const first = fixture.core.switchCompiledGraph([9, 4]);
|
||||
const second = fixture.core.dropCompiledGraph([2, 1]);
|
||||
assert.equal(Atomics.load(fixture.ring, 5), 1);
|
||||
assert.deepEqual([...new Int32Array(fixture.memory.buffer, 64, 5)], [2, 9, 1, 9, 4]);
|
||||
fixture.worker.reply({ type: "reply", request: 1, ok: true });
|
||||
await first;
|
||||
await new Promise(queueMicrotask);
|
||||
|
||||
Reference in New Issue
Block a user