Rewrite core around shared rows and render graphs
Co-authored-by: Heaust Azure <heaust.azure@gmail.com> Amp-Thread-ID: https://ampcode.com/threads/T-01a01380-b478-77d0-84a0-102880a5c5ae
This commit is contained in:
@@ -1,75 +1,29 @@
|
||||
export class GltfImportError extends Error {
|
||||
constructor(code) {
|
||||
super(code);
|
||||
this.name = "GltfImportError";
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
|
||||
function frameCamera(core, bounds, framing) {
|
||||
if (!bounds || framing === false) return;
|
||||
if (framing !== undefined && framing !== "exterior" && framing !== "interior")
|
||||
throw new TypeError("framing must be exterior, interior, or false");
|
||||
const min = bounds.min, max = bounds.max;
|
||||
if (!Array.isArray(min) || !Array.isArray(max) || min.length !== 3 || max.length !== 3) return;
|
||||
const center = min.map((value, axis) => (value + max[axis]) * 0.5);
|
||||
const extent = max.map((value, axis) => value - min[axis]);
|
||||
const radius = Math.max(1, Math.hypot(...extent) * 0.5);
|
||||
const camera = core.array("camera.state");
|
||||
const state = camera.read(0);
|
||||
const interior = framing === "interior";
|
||||
const eye = interior
|
||||
? [center[0], center[1] + radius * 0.05, center[2]]
|
||||
: [center[0] + radius * 1.8, center[1] + radius * 1.4, center[2] + radius * 1.8];
|
||||
const target = interior ? [center[0] + radius, center[1], center[2]] : center;
|
||||
state.splice(0, 3, ...eye);
|
||||
state.splice(4, 3, ...target);
|
||||
state.splice(8, 3, 0, 1, 0);
|
||||
state[14] = Math.max(radius * 0.001, 0.1);
|
||||
state[15] = Math.max(radius * 6, 1.1);
|
||||
camera.write(0, state);
|
||||
}
|
||||
|
||||
/** Parses glTF in a dedicated worker and publishes a generic render-data packet through shared memory. */
|
||||
/** Fetches and parses glTF in a worker, then lets that worker write the packet into Yawn's SAB arena. */
|
||||
export class GltfImporter {
|
||||
#core;
|
||||
#worker;
|
||||
#next = 1;
|
||||
#pending = new Map();
|
||||
#tail = Promise.resolve();
|
||||
#disposed = false;
|
||||
|
||||
constructor(core, { workerFactory } = {}) {
|
||||
if (!core?.allocateArray || !core?.commitRenderDataUpload)
|
||||
throw new TypeError("core must implement the Yawn shared render-data protocol");
|
||||
if (!core?.allocateRows) throw new TypeError("core must be a YawnCore instance");
|
||||
this.#core = core;
|
||||
this.#worker = workerFactory
|
||||
? workerFactory()
|
||||
: new Worker(new URL("./worker.js", import.meta.url), {
|
||||
type: "module",
|
||||
name: "yawn-gltf-import",
|
||||
});
|
||||
this.#worker.addEventListener("message", event => this.#message(event.data));
|
||||
this.#worker = workerFactory?.() ?? new Worker(new URL("./worker.js", import.meta.url), {
|
||||
type: "module",
|
||||
name: "yawn-gltf-import",
|
||||
});
|
||||
this.#worker.addEventListener("message", ({ data }) => this.#message(data));
|
||||
this.#worker.addEventListener("error", () => this.#fail("GLTF_WORKER_ERROR"));
|
||||
this.#worker.addEventListener("messageerror", () => this.#fail("GLTF_WORKER_ERROR"));
|
||||
this.#worker.start?.();
|
||||
}
|
||||
|
||||
load(url, options = {}) {
|
||||
if (this.#disposed) return Promise.reject(new GltfImportError("DISPOSED"));
|
||||
load(url) {
|
||||
const source = url instanceof URL ? url.href : url;
|
||||
if (typeof source !== "string" || !source) return Promise.reject(new TypeError("url must be a URL or nonempty string"));
|
||||
const operation = this.#tail.then(() => this.#start(source, options));
|
||||
this.#tail = operation.catch(() => {});
|
||||
return operation;
|
||||
}
|
||||
|
||||
#start(url, options) {
|
||||
let request = this.#next++ >>> 0;
|
||||
if (!request) request = this.#next++ >>> 0;
|
||||
if (typeof source !== "string" || !source) throw new TypeError("url is required");
|
||||
const request = this.#next++;
|
||||
return new Promise((resolve, reject) => {
|
||||
this.#pending.set(request, { resolve, reject, options, array: null });
|
||||
this.#worker.postMessage({ type: "load", request, url });
|
||||
this.#pending.set(request, { resolve, reject });
|
||||
this.#worker.postMessage({ type: "load", request, url: source });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -78,31 +32,17 @@ export class GltfImporter {
|
||||
if (!pending) return;
|
||||
try {
|
||||
if (message.type === "allocate") {
|
||||
const length = Math.ceil(message.byteLength / 16);
|
||||
pending.array = await this.#core.allocateArray({
|
||||
name: "upload.renderData",
|
||||
domain: "fixed",
|
||||
scalar: "u32",
|
||||
lanes: 4,
|
||||
pending.array = await this.#core.allocateRows({
|
||||
name: `gltf.${message.request}`,
|
||||
rows: Math.ceil(message.byteLength / 16),
|
||||
stride: 16,
|
||||
length,
|
||||
format: "u32",
|
||||
});
|
||||
this.#worker.postMessage({
|
||||
type: "storage",
|
||||
request: message.request,
|
||||
...pending.array.share(),
|
||||
});
|
||||
} else if (message.type === "ready") {
|
||||
const result = await this.#core.commitRenderDataUpload(
|
||||
pending.array,
|
||||
message.byteLength,
|
||||
);
|
||||
frameCamera(this.#core, result.bounds, pending.options.framing);
|
||||
this.#worker.postMessage({ type: "storage", request: message.request, ...pending.array.share() });
|
||||
} else {
|
||||
this.#pending.delete(message.request);
|
||||
pending.resolve(result);
|
||||
} else if (message.type === "error") {
|
||||
this.#pending.delete(message.request);
|
||||
pending.reject(new GltfImportError(message.code || "GLTF_IMPORT_FAILED"));
|
||||
if (message.type === "ready") pending.resolve({ array: pending.array, byteLength: message.byteLength });
|
||||
else pending.reject(new Error(message.error ?? "GLTF_IMPORT_FAILED"));
|
||||
}
|
||||
} catch (error) {
|
||||
this.#pending.delete(message.request);
|
||||
@@ -111,16 +51,12 @@ export class GltfImporter {
|
||||
}
|
||||
|
||||
#fail(code) {
|
||||
if (this.#disposed) return;
|
||||
this.#disposed = true;
|
||||
const error = new GltfImportError(code);
|
||||
for (const pending of this.#pending.values()) pending.reject(error);
|
||||
for (const { reject } of this.#pending.values()) reject(new Error(code));
|
||||
this.#pending.clear();
|
||||
this.#worker.terminate?.();
|
||||
}
|
||||
|
||||
dispose() {
|
||||
if (this.#disposed) return;
|
||||
this.#fail("DISPOSED");
|
||||
this.#worker.terminate();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
const MAGIC = 0x414f5359;
|
||||
|
||||
/** Publishes one byte payload into a packed fixed SOA allocation. */
|
||||
export function writeSharedUpload(buffer, descriptor, bytes) {
|
||||
if (!(buffer instanceof SharedArrayBuffer) || !(bytes instanceof Uint8Array))
|
||||
throw new TypeError("shared upload requires SharedArrayBuffer storage and Uint8Array bytes");
|
||||
if (
|
||||
!descriptor ||
|
||||
descriptor.domain !== "fixed" ||
|
||||
descriptor.scalar !== "u32" ||
|
||||
descriptor.stride !== descriptor.lanes * 4 ||
|
||||
descriptor.controlPtr % 64 !== 0 ||
|
||||
descriptor.dataOffset !== 64 ||
|
||||
bytes.byteLength < 1 ||
|
||||
bytes.byteLength > descriptor.length * descriptor.lanes * 4
|
||||
) throw new TypeError("invalid packed fixed SOA upload");
|
||||
|
||||
const control = new Int32Array(buffer, descriptor.controlPtr, 16);
|
||||
if (
|
||||
(Atomics.load(control, 0) >>> 0) !== MAGIC ||
|
||||
(Atomics.load(control, 2) >>> 0) !== descriptor.id
|
||||
) throw new Error("SOA_PROTOCOL_MISMATCH");
|
||||
|
||||
let sequence;
|
||||
for (let attempt = 0; attempt < 1024; attempt++) {
|
||||
const candidate = Atomics.load(control, 9) >>> 0;
|
||||
if (!(candidate & 1) && (Atomics.compareExchange(control, 9, candidate | 0, (candidate + 1) | 0) >>> 0) === candidate) {
|
||||
sequence = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (sequence === undefined) throw new Error("SOA_BUSY");
|
||||
try {
|
||||
new Uint8Array(
|
||||
buffer,
|
||||
descriptor.controlPtr + descriptor.dataOffset,
|
||||
bytes.byteLength,
|
||||
).set(bytes);
|
||||
} finally {
|
||||
Atomics.store(control, 9, (sequence + 2) | 0);
|
||||
Atomics.notify(control, 9);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import { writeSharedUpload } from "./shared-upload.js";
|
||||
import { gltfToRenderDataPacket } from "./gltf.js";
|
||||
|
||||
const downloads = new Map();
|
||||
@@ -19,12 +18,16 @@ addEventListener("message", async ({ data: message }) => {
|
||||
if (message?.type === "storage") {
|
||||
const packet = downloads.get(request);
|
||||
if (!packet) throw new Error("GLTF_REQUEST_UNKNOWN");
|
||||
writeSharedUpload(message.buffer, message.descriptor, packet);
|
||||
const { buffer, descriptor } = message;
|
||||
if (!(buffer instanceof SharedArrayBuffer) || descriptor?.format !== "u32" ||
|
||||
descriptor.stride !== 16 || descriptor.offset % 64 || packet.byteLength > descriptor.rows * descriptor.stride)
|
||||
throw new Error("GLTF_STORAGE_INVALID");
|
||||
new Uint8Array(buffer, descriptor.offset, packet.byteLength).set(packet);
|
||||
downloads.delete(request);
|
||||
postMessage({ type: "ready", request, byteLength: packet.byteLength });
|
||||
}
|
||||
} catch (error) {
|
||||
downloads.delete(request);
|
||||
postMessage({ type: "error", request, code: error?.message || "GLTF_IMPORT_FAILED" });
|
||||
postMessage({ type: "error", request, error: error?.message || "GLTF_IMPORT_FAILED" });
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user