Render only when shared state changes
Add shared-data and bundle invalidation signals, and have handles publish them automatically for SAB and graph mutations. Document the raw core worker protocol in a dedicated VitePress site and simplify the glTF import and picking example. Amp-Thread-ID: https://ampcode.com/threads/T-01a01ff8-b91f-724f-8952-f07c6b5042fd Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
This commit is contained in:
@@ -61,7 +61,9 @@ export class Mesh extends Node {
|
||||
|
||||
set material(value: PBRMaterial) {
|
||||
if (value.scene !== this.scene || value.id < 0) throw new Error("Await a material from the same Scene");
|
||||
this.scene.markDirty(true);
|
||||
this.scene.array("meshInfo").row(this.id)[1] = value.id;
|
||||
void this.scene.updateRenderGraph().catch(() => undefined);
|
||||
}
|
||||
|
||||
clone(options: Omit<MeshOptions, "geometryId" | "vertexData"> = {}) {
|
||||
@@ -83,6 +85,7 @@ export class Mesh extends Node {
|
||||
this.scene.releaseGeometry(original);
|
||||
this.scene.referenceGeometry(this.geometryId);
|
||||
this.instanceOf = this.id;
|
||||
this.scene.markDirty(true);
|
||||
this.scene.array("meshInfo").row(this.id).set([this.geometryId, this.materialId, this.isVisible ? 1 : 0, this.id]);
|
||||
}
|
||||
if (kind === "positions") this.vertexCount = data.length / 3;
|
||||
@@ -97,6 +100,7 @@ export class Mesh extends Node {
|
||||
const list = Array.isArray(faces) ? faces : [faces];
|
||||
const maximum = Math.max(...list);
|
||||
const array = await this.scene.ensureRows(`mesh.${this.id}.faceMaterials`, maximum + 1, 16, "u32");
|
||||
this.scene.markDirty(true);
|
||||
for (const face of list) {
|
||||
if (!Number.isInteger(face) || face < 0) throw new RangeError("face");
|
||||
array.row(face)[0] = material.id + 1;
|
||||
|
||||
@@ -326,6 +326,14 @@ function serialize(graph: object) {
|
||||
return `(yawn-graph 1 ${encode(graph)})`;
|
||||
}
|
||||
|
||||
const typedArrayMutators = new Set([
|
||||
"copyWithin",
|
||||
"fill",
|
||||
"reverse",
|
||||
"set",
|
||||
"sort",
|
||||
]);
|
||||
|
||||
/** The conventional single-loadout scene layer; hot values always remain direct SAB writes. */
|
||||
export class Scene {
|
||||
readonly core: YawnCore;
|
||||
@@ -343,6 +351,9 @@ export class Scene {
|
||||
#nextTexture = 0;
|
||||
#graphBatchDepth = 0;
|
||||
#graphBatchDirty = false;
|
||||
#signals?: Float32Array;
|
||||
#arrays = new WeakMap<object, object>();
|
||||
#views = new WeakMap<object, object>();
|
||||
|
||||
constructor(
|
||||
canvas: HTMLCanvasElement,
|
||||
@@ -358,6 +369,7 @@ export class Scene {
|
||||
|
||||
async #initialize(fps?: number) {
|
||||
await this.core.ready;
|
||||
this.#signals = this.core.array("signals").row(0);
|
||||
for (const [name, stride, format] of rows)
|
||||
await this.core.createRows({ name, rows: 1, stride, format });
|
||||
await this.core.createRows({
|
||||
@@ -379,7 +391,62 @@ export class Scene {
|
||||
}
|
||||
|
||||
array(name: string) {
|
||||
return this.core.array(name);
|
||||
const array = this.core.array(name);
|
||||
const current = this.#arrays.get(array);
|
||||
if (current) return current as typeof array;
|
||||
let proxy: typeof array;
|
||||
proxy = new Proxy(array, {
|
||||
get: (target, property) => {
|
||||
if (property === "row")
|
||||
return (index: number) => this.#mutable(target.row(index));
|
||||
if (property === "view") return this.#mutable(target.view);
|
||||
if (property === "write")
|
||||
return (index: number, values: ArrayLike<number>) => {
|
||||
target.write(index, values);
|
||||
this.markDirty();
|
||||
return proxy;
|
||||
};
|
||||
const value = Reflect.get(target, property, target);
|
||||
return typeof value === "function" ? value.bind(target) : value;
|
||||
},
|
||||
});
|
||||
this.#arrays.set(array, proxy);
|
||||
return proxy;
|
||||
}
|
||||
|
||||
#mutable<T extends Float32Array | Uint32Array | Int32Array>(view: T): T {
|
||||
const current = this.#views.get(view);
|
||||
if (current) return current as T;
|
||||
let proxy: T;
|
||||
proxy = new Proxy(view, {
|
||||
get: (target, property) => {
|
||||
if (property === "subarray")
|
||||
return (begin?: number, end?: number) =>
|
||||
this.#mutable(target.subarray(begin, end) as T);
|
||||
const value = Reflect.get(target, property, target);
|
||||
if (typeof value !== "function") return value;
|
||||
if (property === "constructor") return value;
|
||||
if (!typedArrayMutators.has(String(property))) return value.bind(target);
|
||||
return (...arguments_: unknown[]) => {
|
||||
const result = Reflect.apply(value, target, arguments_);
|
||||
this.markDirty();
|
||||
return result === target ? proxy : result;
|
||||
};
|
||||
},
|
||||
set: (target, property, value) => {
|
||||
const written = Reflect.set(target, property, value, target);
|
||||
if (written) this.markDirty();
|
||||
return written;
|
||||
},
|
||||
});
|
||||
this.#views.set(view, proxy);
|
||||
return proxy;
|
||||
}
|
||||
|
||||
markDirty(bundle = false) {
|
||||
if (!this.#signals) return;
|
||||
this.#signals[5] = 1;
|
||||
if (bundle) this.#signals[6] = 1;
|
||||
}
|
||||
|
||||
async ensureRows(
|
||||
@@ -596,6 +663,7 @@ export class Scene {
|
||||
16,
|
||||
integer ? "u32" : "f32",
|
||||
);
|
||||
if (updateGraph) this.markDirty(true);
|
||||
const view = target.view;
|
||||
view.fill(0);
|
||||
if (integer || width === 4) view.set(data);
|
||||
@@ -642,6 +710,7 @@ export class Scene {
|
||||
}
|
||||
|
||||
updateRenderGraph() {
|
||||
this.markDirty(true);
|
||||
if (this.#graphBatchDepth) {
|
||||
this.#graphBatchDirty = true;
|
||||
return Promise.resolve();
|
||||
|
||||
@@ -35,7 +35,7 @@ export class Picking {
|
||||
return this.#request("sync", {
|
||||
shares: Object.fromEntries(
|
||||
[
|
||||
"info",
|
||||
"signals",
|
||||
"nodes",
|
||||
"nodePositions",
|
||||
"nodeQuaternions",
|
||||
|
||||
@@ -102,7 +102,7 @@ function rebuild() {
|
||||
boxes.push({ id, min, max });
|
||||
}
|
||||
root = build(boxes);
|
||||
builtFrame = Number(view("info")?.[1] ?? builtFrame + 1);
|
||||
builtFrame = Number(view("signals")?.[1] ?? builtFrame + 1);
|
||||
}
|
||||
|
||||
function intersection(
|
||||
@@ -149,7 +149,7 @@ addEventListener("message", ({ data }) => {
|
||||
return;
|
||||
}
|
||||
if (data.type === "pick") {
|
||||
const frame = Number(view("info")?.[1] ?? -1);
|
||||
const frame = Number(view("signals")?.[1] ?? -1);
|
||||
if (frame !== builtFrame) rebuild();
|
||||
const inverse = data.direction.map((lane: number) => 1 / lane);
|
||||
const hits: { id: number; distance: number }[] = [];
|
||||
|
||||
Reference in New Issue
Block a user