Build Rust render graph core
Keep hot render state in a generic shared SOA arena and use worker messages only for allocation, graph compilation, loadout changes, and render pacing. Compile external S-expression graphs into upfront WebGPU resources, transient aliases, and reusable render bundles. 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:
+154
@@ -0,0 +1,154 @@
|
||||
const views = Object.freeze({ f32: Float32Array, u32: Uint32Array, i32: Int32Array });
|
||||
|
||||
export class SharedRows {
|
||||
constructor(buffer, descriptor) {
|
||||
if (!(buffer instanceof SharedArrayBuffer)) throw new TypeError("ROWS_DESCRIPTOR");
|
||||
this.buffer = buffer;
|
||||
this.update(descriptor);
|
||||
}
|
||||
|
||||
update(descriptor) {
|
||||
if (!views[descriptor.format] || descriptor.name !== (this.descriptor?.name ?? descriptor.name))
|
||||
throw new TypeError("ROWS_DESCRIPTOR");
|
||||
this.descriptor = Object.freeze(descriptor);
|
||||
return this;
|
||||
}
|
||||
|
||||
get name() { return this.descriptor.name; }
|
||||
get rows() { return this.descriptor.rows; }
|
||||
get stride() { return this.descriptor.stride; }
|
||||
get format() { return this.descriptor.format; }
|
||||
get view() {
|
||||
const View = views[this.format];
|
||||
return new View(this.buffer, this.descriptor.offset, this.descriptor.bytes / View.BYTES_PER_ELEMENT);
|
||||
}
|
||||
|
||||
row(index) {
|
||||
if (!Number.isInteger(index) || index < 0 || index >= this.rows) throw new RangeError("ROW_RANGE");
|
||||
const width = this.stride / views[this.format].BYTES_PER_ELEMENT;
|
||||
return this.view.subarray(index * width, (index + 1) * width);
|
||||
}
|
||||
|
||||
read(index) { return Array.from(this.row(index)); }
|
||||
|
||||
write(index, values) {
|
||||
const row = this.row(index);
|
||||
if (!values || values.length !== row.length) throw new RangeError("ROW_WIDTH");
|
||||
row.set(values);
|
||||
return this;
|
||||
}
|
||||
|
||||
share() { return { buffer: this.buffer, descriptor: this.descriptor }; }
|
||||
}
|
||||
|
||||
export class YawnCore {
|
||||
#worker;
|
||||
#buffer;
|
||||
#arrays = new Map();
|
||||
#pending = new Map();
|
||||
#next = 1;
|
||||
|
||||
constructor(canvas, { arenaBytes = 64 * 1024 * 1024, workerFactory } = {}) {
|
||||
if (!canvas) throw new TypeError("canvas is required");
|
||||
this.#worker = workerFactory?.() ?? new Worker(new URL("./worker.js", import.meta.url), {
|
||||
type: "module",
|
||||
name: "yawn-core",
|
||||
});
|
||||
this.#worker.addEventListener("message", ({ data }) => this.#message(data));
|
||||
this.#worker.addEventListener("error", () => this.#fail("WORKER_ERROR"));
|
||||
this.#worker.addEventListener("messageerror", () => this.#fail("WORKER_ERROR"));
|
||||
this.#worker.start?.();
|
||||
const offscreen = canvas.transferControlToOffscreen?.() ?? canvas;
|
||||
this.ready = this.#request("init", { canvas: offscreen, arenaBytes }, [offscreen]).then(result => {
|
||||
this.#buffer = result.buffer;
|
||||
for (const descriptor of result.rows) this.#arrays.set(
|
||||
descriptor.name,
|
||||
new SharedRows(this.#buffer, descriptor),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async createRows({ name, rows, stride, format }) {
|
||||
await this.ready;
|
||||
const descriptor = await this.#request("create-rows", { name, rows, stride, format });
|
||||
const array = this.#arrays.get(name)?.update(descriptor)
|
||||
?? new SharedRows(this.#buffer, descriptor);
|
||||
this.#arrays.set(name, array);
|
||||
return array;
|
||||
}
|
||||
|
||||
async deleteRows(name) {
|
||||
await this.ready;
|
||||
await this.#request("delete-rows", { name });
|
||||
this.#arrays.delete(name);
|
||||
}
|
||||
|
||||
async allocateObject(name) {
|
||||
await this.ready;
|
||||
const { id, rows } = await this.#request("allocate-object", { name });
|
||||
this.#arrays.get(name).update(rows);
|
||||
return id;
|
||||
}
|
||||
|
||||
async deleteObject(name, id) {
|
||||
await this.ready;
|
||||
return this.#request("delete-object", { name, id });
|
||||
}
|
||||
|
||||
async compileGraph(serialized) {
|
||||
await this.ready;
|
||||
return this.#request("compile-graph", { serialized });
|
||||
}
|
||||
|
||||
async switchLoadout(id) {
|
||||
await this.ready;
|
||||
return this.#request("switch-loadout", { id });
|
||||
}
|
||||
|
||||
async play() {
|
||||
await this.ready;
|
||||
return this.#request("play");
|
||||
}
|
||||
|
||||
async pause() {
|
||||
await this.ready;
|
||||
return this.#request("pause");
|
||||
}
|
||||
|
||||
async setFps(fps) {
|
||||
await this.ready;
|
||||
return this.#request("set-fps", { fps });
|
||||
}
|
||||
|
||||
array(name) {
|
||||
const array = this.#arrays.get(name);
|
||||
if (!array) throw new Error(`UNKNOWN_ARRAY: ${name}`);
|
||||
return array;
|
||||
}
|
||||
|
||||
#request(type, payload = {}, transfer = []) {
|
||||
const request = this.#next++;
|
||||
return new Promise((resolve, reject) => {
|
||||
this.#pending.set(request, { resolve, reject });
|
||||
this.#worker.postMessage({ type, request, ...payload }, transfer);
|
||||
});
|
||||
}
|
||||
|
||||
#message(message) {
|
||||
const pending = this.#pending.get(message?.request);
|
||||
if (!pending) return;
|
||||
this.#pending.delete(message.request);
|
||||
if (message.error) pending.reject(Object.assign(new Error(message.error), { code: message.error }));
|
||||
else pending.resolve(message.result);
|
||||
}
|
||||
|
||||
#fail(code) {
|
||||
for (const { reject } of this.#pending.values()) reject(Object.assign(new Error(code), { code }));
|
||||
this.#pending.clear();
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.#fail("DISPOSED");
|
||||
this.#worker.terminate();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user