refactor: make instance type metadata opaque

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:
Amp
2026-07-29 06:13:25 +00:00
co-authored by heaust
parent 092b319d75
commit 0e866596c0
11 changed files with 110 additions and 166 deletions
+1 -1
View File
@@ -20,7 +20,7 @@ test("all presets use current schemas, versions, and one frame output", () => {
.parameters.texture.sampleCount, "number");
});
test("presets classify visibility and material through type.words[0] predicates", () => {
test("presets classify demo-owned enable and material bits 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.type_words.inputs.value, { node: "mesh", socket: "type" }, name);
+13 -7
View File
@@ -2,6 +2,7 @@ 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;
@@ -22,26 +23,30 @@ 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],defaultType:[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}]}});
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);
const pending=mesh.setVisible(true);
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,6)],[2,2,2,7,3,1]);
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]),{visible:false});
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.setVisible(true), error=>error instanceof RendererError&&error.code==="STALE_HANDLE");
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;
@@ -49,7 +54,7 @@ test("rejects protocol mismatch", () => {
});
test("pending reply exists before ring publication", async () => {
const f=fixture(), mesh=await imported(f); const {worker}=f;
const pending=mesh.setVisible(true);
const pending=mesh.defaultInstance.setType(TYPE);
worker.reply({type:"reply",request:2,ok:true});
await pending;
});
@@ -61,7 +66,7 @@ test("profile snapshots have a dedicated getter", () => {
});
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.setVisible(true), b=mesh.setVisible(false);
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);
@@ -75,6 +80,7 @@ test("import always releases staged payload when ring is full", async () => {
});
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);
+8 -7
View File
@@ -79,26 +79,26 @@ test("snapshot reader refreshes memory after pinning", () => {
}
});
function bvhSnapshot({ pickable = [1, 1], shifted = false } = {}) {
function bvhSnapshot({ metadata = [0, 0x80000000], shifted = false } = {}) {
const count = 2;
return { instanceCount: count, streams: {
instanceSlot: Uint32Array.from([5, 6]), instanceGeneration: Uint32Array.from([1, 1]),
instanceMeshSlot: Uint32Array.from([2, 2]), instanceMeshGeneration: Uint32Array.from([4, 4]),
instanceType: Uint32Array.from(pickable.flatMap(x => [x, ...Array(15).fill(0)])),
instanceType: Uint32Array.from(metadata.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]),
}};
}
test("BVH preserves topology for visibility/refit and reports world distances", () => {
test("BVH ignores opaque instance metadata during refit and picking", () => {
const bvh = new DerivedBvh();
bvh.update(bvhSnapshot());
assert.equal(bvh.rebuilds, 1);
assert.equal(bvh.pick([0, 0, 0], [2, 0, 0], Infinity, 2)[0].distance, 2);
bvh.update(bvhSnapshot({ pickable: [0, 1], shifted: true }));
bvh.update(bvhSnapshot({ metadata: [0xffffffff, 0], shifted: true }));
assert.equal(bvh.rebuilds, 1);
assert.equal(bvh.refits, 1);
assert.deepEqual(bvh.pick([0, 0, 0], [1, 0, 0], Infinity, 2).map(hit => hit.slot), [6]);
assert.deepEqual(bvh.pick([0, 0, 0], [1, 0, 0], Infinity, 2).map(hit => hit.slot), [6, 5]);
});
class WorkerMock extends EventTarget {
@@ -108,7 +108,7 @@ class WorkerMock extends EventTarget {
reply(data) { this.dispatchEvent(new MessageEvent("message", { data })); }
}
test("renderer pick returns gated instances and exact epoch", async () => {
test("renderer pick returns instance metadata handles and exact epoch", async () => {
const scene = snapshotFixture();
const ring = 8192;
const ringHeader = new Int32Array(scene.memory.buffer, ring, 16);
@@ -124,7 +124,8 @@ test("renderer pick returns gated instances 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.setVisible, "function");
assert.equal(typeof result.hits[0].instance.setType, "function");
assert.equal(result.hits[0].instance.setVisible, undefined);
client.dispose();
assert.equal(bvhWorker.terminated, true);
});