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,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/);});
|
||||
|
||||
Reference in New Issue
Block a user