Rebuild core around render graph AST and shared memory

Amp-Thread-ID: https://ampcode.com/threads/T-01a01380-b478-77d0-84a0-102880a5c5ae
Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
This commit is contained in:
Amp
2026-08-18 11:58:31 +00:00
co-authored by heaust
parent a23ef37b4d
commit 0e44917f9e
101 changed files with 4409 additions and 2610 deletions
+10
View File
@@ -0,0 +1,10 @@
{
"name": "@yawn/mesh-handles",
"version": "0.1.0",
"description": "Conventional mesh handles over the Yawn worker and shared-SOA protocol",
"type": "module",
"exports": "./src/index.js",
"dependencies": {
"@yawn/core": "0.1.0"
}
}
+26
View File
@@ -0,0 +1,26 @@
/** Worker-local acceleration structure used by the optional mesh-handle addon. */
export class DerivedBvh {
constructor() { this.count = 0; this.identity = new Uint32Array(); this.meshIdentity = new Uint32Array(); this.bounds = new Float32Array(); this.nodeBounds = new Float32Array(); this.left = this.right = new Int32Array(); this.leafStart = this.leafCount = new Uint32Array(); this.leaves = new Uint32Array(); this.root = -1; this.rebuilds = 0; this.refits = 0; }
update(snapshot) {
const s = snapshot.streams, n = snapshot.instanceCount;
let changed = n !== this.count;
if (!changed) for (let i = 0; i < n; i++) if (this.identity[i * 2] !== s.instanceSlot[i] || this.identity[i * 2 + 1] !== s.instanceGeneration[i] || this.meshIdentity[i * 2] !== s.instanceMeshSlot[i] || this.meshIdentity[i * 2 + 1] !== s.instanceMeshGeneration[i]) { changed = true; break; }
this.count = n; this.identity = new Uint32Array(n * 2); this.meshIdentity = new Uint32Array(n * 2); this.bounds = new Float32Array(n * 6);
for (let i = 0; i < n; i++) { this.identity.set([s.instanceSlot[i], s.instanceGeneration[i]], i * 2); this.meshIdentity.set([s.instanceMeshSlot[i], s.instanceMeshGeneration[i]], i * 2); this.bounds.set(s.instanceWorldMin.subarray(i * 3, i * 3 + 3), i * 6); this.bounds.set(s.instanceWorldMax.subarray(i * 3, i * 3 + 3), i * 6 + 3); }
changed ? this.rebuild() : this.refit();
}
rebuild() {
this.rebuilds++; const nodes = [], leaves = [];
const build = indices => { const at = nodes.length, node = {left: -1, right: -1, start: 0, count: 0, bounds: [Infinity, Infinity, Infinity, -Infinity, -Infinity, -Infinity]}; nodes.push(node); for (const i of indices) for (let a = 0; a < 3; a++) { node.bounds[a] = Math.min(node.bounds[a], this.bounds[i * 6 + a]); node.bounds[a + 3] = Math.max(node.bounds[a + 3], this.bounds[i * 6 + a + 3]); } if (indices.length <= 2) { node.start = leaves.length; node.count = indices.length; leaves.push(...indices); return at; } let axis = 0, extent = node.bounds[3] - node.bounds[0]; for (let a = 1; a < 3; a++) if (node.bounds[a + 3] - node.bounds[a] > extent) { axis = a; extent = node.bounds[a + 3] - node.bounds[a]; } indices.sort((a, b) => (this.bounds[a * 6 + axis] + this.bounds[a * 6 + axis + 3]) - (this.bounds[b * 6 + axis] + this.bounds[b * 6 + axis + 3]) || a - b); const mid = indices.length >> 1; node.left = build(indices.slice(0, mid)); node.right = build(indices.slice(mid)); return at; };
this.root = this.count ? build(Array.from({length: this.count}, (_, i) => i)) : -1; const n = nodes.length;
this.nodeBounds = new Float32Array(n * 6); this.left = new Int32Array(n); this.right = new Int32Array(n); this.leafStart = new Uint32Array(n); this.leafCount = new Uint32Array(n); this.leaves = Uint32Array.from(leaves);
nodes.forEach((x, i) => { this.nodeBounds.set(x.bounds, i * 6); this.left[i] = x.left; this.right[i] = x.right; this.leafStart[i] = x.start; this.leafCount[i] = x.count; });
}
refit() { this.refits++; for (let n = this.left.length - 1; n >= 0; n--) { const at = n * 6; for (let a = 0; a < 3; a++) { let lo = Infinity, hi = -Infinity; if (this.leafCount[n]) for (let j = 0; j < this.leafCount[n]; j++) { const i = this.leaves[this.leafStart[n] + j]; lo = Math.min(lo, this.bounds[i * 6 + a]); hi = Math.max(hi, this.bounds[i * 6 + a + 3]); } else { lo = Math.min(this.nodeBounds[this.left[n] * 6 + a], this.nodeBounds[this.right[n] * 6 + a]); hi = Math.max(this.nodeBounds[this.left[n] * 6 + a + 3], this.nodeBounds[this.right[n] * 6 + a + 3]); } this.nodeBounds[at + a] = lo; this.nodeBounds[at + a + 3] = hi; } } }
pick(origin, direction, maxDistance = Infinity, maxHits = 1) {
if (this.root < 0) return []; let magnitude = Math.hypot(...direction); const dir = direction.map(v => v / magnitude);
const intersect = (array, at) => { let lo = 0, hi = maxDistance; for (let a = 0; a < 3; a++) { const min = array[at + a], max = array[at + a + 3]; if (dir[a] === 0) { if (origin[a] < min || origin[a] > max) return Infinity; } else { let x = (min - origin[a]) / dir[a], y = (max - origin[a]) / dir[a]; if (x > y) [x, y] = [y, x]; lo = Math.max(lo, x); hi = Math.min(hi, y); if (lo > hi) return Infinity; } } return lo; };
const hits = [], stack = [this.root]; while (stack.length) { const n = stack.pop(); if (intersect(this.nodeBounds, n * 6) === Infinity) continue; if (this.leafCount[n]) for (let j = 0; j < this.leafCount[n]; j++) { const i = this.leaves[this.leafStart[n] + j]; const distance = intersect(this.bounds, i * 6); if (distance !== Infinity) hits.push({slot: this.identity[i * 2], generation: this.identity[i * 2 + 1], distance}); } else stack.push(this.right[n], this.left[n]); }
hits.sort((a, b) => a.distance - b.distance || a.slot - b.slot || a.generation - b.generation); return hits.slice(0, maxHits);
}
}
+27
View File
@@ -0,0 +1,27 @@
import { SnapshotReader } from "@yawn/core/snapshot";
import { DerivedBvh } from "./bvh-core.js";
let reader, bvh = new DerivedBvh(), epoch = 0, updating = false, requestedEpoch = 0;
function ensureEpoch(expected) {
if (!reader) return false;
const latest = reader.latest();
if (latest.epoch !== expected) return false;
if (epoch === expected) return true;
const result = reader.transaction(snapshot => { bvh.update(snapshot); epoch = snapshot.epoch; }, expected);
return result !== null && epoch === expected;
}
function coalescedUpdate(hint = 0) {
requestedEpoch = Math.max(requestedEpoch, hint >>> 0);
if (updating) return;
updating = true;
queueMicrotask(() => { try { const latest = reader?.latest(); if (latest?.epoch && latest.epoch !== epoch) ensureEpoch(latest.epoch); if (epoch) postMessage({type: "updated", epoch}); } catch (error) { postMessage({type: "fatal", code: "PICK_PROTOCOL_MISMATCH", message: String(error)}); } finally { updating = false; if (requestedEpoch > epoch) coalescedUpdate(); } });
}
addEventListener("message", event => {
const m = event.data;
try {
if (m.type === "init") { reader = new SnapshotReader(m.memory, m.controlPtr); if (m.controlVersion !== 1 || m.schemaVersion !== 2) throw Object.assign(new Error("version"), {code: "PICK_PROTOCOL_MISMATCH"}); postMessage({type: "ready"}); }
else if (m.type === "update") coalescedUpdate(m.epoch);
else if (m.type === "pick") { if (!ensureEpoch(m.epoch)) { postMessage({type: "pick", request: m.request, stale: true, epoch}); return; } const hits = bvh.pick(m.origin, m.direction, m.maxDistance, m.maxHits); const latest = reader.latest().epoch; postMessage({type: "pick", request: m.request, stale: latest !== m.epoch || epoch !== m.epoch, epoch, hits}); }
else if (m.type === "dispose") close();
} catch (error) { postMessage({type: "fatal", code: error.name === "SnapshotProtocolError" || error.code === "PICK_PROTOCOL_MISMATCH" ? "PICK_PROTOCOL_MISMATCH" : "PICK_WORKER_ERROR", message: String(error)}); }
});
+67
View File
@@ -0,0 +1,67 @@
const TOKEN = Symbol("yawn mesh handle addon");
/** Creates the optional snapshot/BVH worker used by core's picking protocol. */
export const createPickingWorker = () => new Worker(
new URL("./bvh-worker.js", import.meta.url),
{ type: "module", name: "yawn-spatial-query" },
);
/** Conventional mesh/instance objects layered entirely over the Yawn core protocol. */
export class MeshHandles {
#core;
constructor(core) {
this.#core = core;
}
fromImportedScene(result) {
if (!result || !Array.isArray(result.meshes)) throw new TypeError("invalid imported scene");
return result.meshes.map((mesh) => new Mesh(TOKEN, this.#core, mesh));
}
async pickRay(origin, direction, options) {
const result = await this.#core.pickRay(origin, direction, options);
return {
...result,
hits: result.hits.map((hit) => ({
...hit,
instance: new Instance(TOKEN, this.#core, hit.instance),
})),
};
}
}
export class Mesh {
#core; #handle; #defaultInstance; #defaultType;
constructor(token, core, descriptor) {
if (token !== TOKEN) throw new TypeError("Mesh cannot be constructed directly");
this.#core = core;
this.#handle = Object.freeze([...descriptor.handle]);
this.#defaultInstance = new Instance(TOKEN, core, descriptor.defaultInstance);
this.#defaultType = Object.freeze([...descriptor.defaultType]);
}
get handle() { return this.#handle; }
get defaultInstance() { return this.#defaultInstance; }
async createInstance(transform, { type = this.#defaultType } = {}) {
return new Instance(TOKEN, this.#core, await this.#core.createInstance(this.#handle, transform, { type }));
}
}
export class Instance {
#core; #handle; #dead = false;
constructor(token, core, handle) {
if (token !== TOKEN) throw new TypeError("Instance cannot be constructed directly");
this.#core = core;
this.#handle = Object.freeze([...handle]);
}
get handle() { return this.#handle; }
#live() { if (this.#dead) throw new Error("STALE_HANDLE"); }
setType(words) { this.#live(); this.#core.setInstanceType(this.#handle, words); }
setTransform(transform) { this.#live(); this.#core.setInstanceTransform(this.#handle, transform); }
async destroy() { this.#live(); await this.#core.destroyInstance(this.#handle); this.#dead = true; }
}