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:
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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)}); }
|
||||
});
|
||||
@@ -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; }
|
||||
}
|
||||
Reference in New Issue
Block a user