view() allocated a fresh typed array on every access (and row() a second subarray); the view is now built once per descriptor in update(). Amp-Thread-ID: https://ampcode.com/threads/T-01a0b2dd-db32-712d-99ce-12ce4a86d979 Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
238 lines
7.5 KiB
JavaScript
238 lines
7.5 KiB
JavaScript
const views = Object.freeze({ f32: Float32Array, u32: Uint32Array, i32: Int32Array });
|
|
|
|
export class SharedRows {
|
|
#view;
|
|
|
|
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);
|
|
const View = views[descriptor.format];
|
|
this.#view = new View(this.buffer, descriptor.offset, descriptor.bytes / View.BYTES_PER_ELEMENT);
|
|
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; }
|
|
/** One typed view over the whole array; replaced only when the descriptor changes. */
|
|
get view() { return this.#view; }
|
|
|
|
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();
|
|
#profileListeners = new Set();
|
|
#next = 1;
|
|
/** Optional WebGPU features the device has (WebGPU spelling); populated once `ready` resolves. */
|
|
features = new Set();
|
|
|
|
constructor(canvas, { arenaBytes = 64 * 1024 * 1024, debug = false, 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(async result => {
|
|
this.#buffer = result.buffer;
|
|
this.features = new Set(result.features);
|
|
for (const descriptor of result.rows) this.#arrays.set(
|
|
descriptor.name,
|
|
new SharedRows(this.#buffer, descriptor),
|
|
);
|
|
if (debug) await this.#request("set-profiler", { enabled: true });
|
|
});
|
|
}
|
|
|
|
async createRows({ name, rows, stride, format }) {
|
|
await this.ready;
|
|
const result = await this.#request("create-rows", { name, rows, stride, format });
|
|
return this.#adopt(result.rows, result.commit ? [name] : []);
|
|
}
|
|
|
|
async createRowsBatch(rows) {
|
|
await this.ready;
|
|
if (!Array.isArray(rows) || !rows.length) throw new TypeError("ROWS");
|
|
const result = await this.#request("create-rows-batch", { rows });
|
|
const arrays = result.rows.map(descriptor => this.#adopt(descriptor));
|
|
this.#commit(result.commit);
|
|
return arrays;
|
|
}
|
|
|
|
async deleteRows(name) {
|
|
await this.ready;
|
|
await this.#request("delete-rows", { name });
|
|
this.#arrays.delete(name);
|
|
}
|
|
|
|
/** Reserves `count` contiguous rows and resolves with the first row ID. */
|
|
async allocateObject(name, count = 1) {
|
|
await this.ready;
|
|
const { id, rows, commit } = await this.#request("allocate-object", { name, count });
|
|
this.#adopt(rows, commit ? [name] : []);
|
|
return id;
|
|
}
|
|
|
|
/**
|
|
* Publishes a descriptor the worker returned. When the rows moved, this thread owns the copy:
|
|
* the old bytes are copied synchronously into the new region and the worker is told to commit
|
|
* before anything else can be requested, so no write to the array is ever lost.
|
|
*/
|
|
#adopt(descriptor, commit = []) {
|
|
const current = this.#arrays.get(descriptor.name);
|
|
if (!current) {
|
|
const array = new SharedRows(this.#buffer, descriptor);
|
|
this.#arrays.set(descriptor.name, array);
|
|
return array;
|
|
}
|
|
if (descriptor.offset !== current.descriptor.offset) {
|
|
const { offset, bytes } = current.descriptor;
|
|
new Uint8Array(this.#buffer, descriptor.offset, bytes).set(new Uint8Array(this.#buffer, offset, bytes));
|
|
}
|
|
current.update(descriptor);
|
|
this.#commit(commit);
|
|
return current;
|
|
}
|
|
|
|
#commit(names) {
|
|
if (!names.length) return;
|
|
this.#request("commit-rows", { names }).catch(error => this.#fail(error.code));
|
|
}
|
|
|
|
async deleteObject(name, id) {
|
|
await this.ready;
|
|
return this.#request("delete-object", { name, id });
|
|
}
|
|
|
|
/** Uploads rows `first .. first + count` of `name` to every active GPU buffer bound to that array. */
|
|
async syncRows(name, first, count) {
|
|
await this.ready;
|
|
return this.#request("sync-rows", { name, first, count });
|
|
}
|
|
|
|
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 uploadTexture(name, image, mipLevel = 0) {
|
|
await this.ready;
|
|
if (
|
|
typeof name !== "string" ||
|
|
!(image instanceof ImageBitmap) ||
|
|
!Number.isInteger(mipLevel) ||
|
|
mipLevel < 0
|
|
)
|
|
throw new TypeError("TEXTURE_SOURCE");
|
|
return this.#request(
|
|
"upload-texture",
|
|
{ name, image, mipLevel },
|
|
[image],
|
|
);
|
|
}
|
|
|
|
async deleteTexture(name) {
|
|
await this.ready;
|
|
return this.#request("delete-texture", { name });
|
|
}
|
|
|
|
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 });
|
|
}
|
|
|
|
async setProfiler(enabled) {
|
|
await this.ready;
|
|
return this.#request("set-profiler", { enabled: Boolean(enabled) });
|
|
}
|
|
|
|
onProfile(listener) {
|
|
if (typeof listener !== "function") throw new TypeError("PROFILE_LISTENER");
|
|
this.#profileListeners.add(listener);
|
|
return () => this.#profileListeners.delete(listener);
|
|
}
|
|
|
|
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) {
|
|
if (message?.type === "profile") {
|
|
for (const listener of this.#profileListeners) listener(message.stats);
|
|
return;
|
|
}
|
|
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.#profileListeners.clear();
|
|
this.#worker.terminate();
|
|
}
|
|
}
|