Amp-Thread-ID: https://ampcode.com/threads/T-019f9d91-77c1-7206-a60f-ed6554ce92ab Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
96 lines
6.2 KiB
JavaScript
96 lines
6.2 KiB
JavaScript
export const SNAPSHOT = Object.freeze({
|
|
MAGIC: 0x504e5359, BLOB_MAGIC: 0x32534452, VERSION: 1, BYTES: 256,
|
|
SLOTS: 3, SLOT_BYTES: 64, SCHEMA: 2, INIT: 0, OPEN: 1, FAILED: 2,
|
|
CLOSED: 3, FREE: 0, WRITING: 1, READY: 2, READING: 3,
|
|
});
|
|
export const STREAM_NAMES = ["meshSlot", "meshGeneration", "meshLocalMin", "meshLocalMax", "instanceSlot", "instanceGeneration", "instanceMeshSlot", "instanceMeshGeneration", "instanceModel", "instanceWorldMin", "instanceWorldMax", "instanceType"];
|
|
const COMPONENTS = [1, 1, 3, 3, 1, 1, 1, 1, 16, 3, 3, 16];
|
|
const SCALARS = [1, 1, 2, 2, 1, 1, 1, 1, 2, 2, 2, 1];
|
|
const STRIDES = COMPONENTS.map(n => n * 4);
|
|
|
|
export class SnapshotProtocolError extends Error {
|
|
constructor(code) { super(code); this.code = code; this.name = "SnapshotProtocolError"; }
|
|
}
|
|
|
|
const bad = code => { throw new SnapshotProtocolError(code); };
|
|
const add = (a, b) => { const n = a + b; if (!Number.isSafeInteger(n) || n > 0xffffffff) bad("BAD_RANGE"); return n; };
|
|
const mul = (a, b) => { const n = a * b; if (!Number.isSafeInteger(n) || n > 0xffffffff) bad("BAD_RANGE"); return n; };
|
|
|
|
export class SnapshotReader {
|
|
constructor(memory, controlPtr) {
|
|
if (!memory || !(memory.buffer instanceof SharedArrayBuffer)) bad("BAD_MEMORY");
|
|
if (!Number.isInteger(controlPtr) || controlPtr < 0 || controlPtr % 64 || add(controlPtr, 256) > memory.buffer.byteLength) bad("BAD_CONTROL_POINTER");
|
|
this.memory = memory; this.controlPtr = controlPtr; this.buffer = null;
|
|
this.refresh(); this.validateControl();
|
|
}
|
|
refresh() {
|
|
if (this.buffer === this.memory.buffer) return;
|
|
this.buffer = this.memory.buffer;
|
|
if (add(this.controlPtr, 256) > this.buffer.byteLength) bad("BAD_CONTROL_POINTER");
|
|
this.control = new Int32Array(this.buffer, this.controlPtr, 64);
|
|
}
|
|
validateControl() {
|
|
const h = this.control;
|
|
if ((Atomics.load(h, 0) >>> 0) !== SNAPSHOT.MAGIC) bad("BAD_MAGIC");
|
|
if ((Atomics.load(h, 1) >>> 0) !== SNAPSHOT.VERSION) bad("BAD_VERSION");
|
|
if ((Atomics.load(h, 2) >>> 0) !== SNAPSHOT.BYTES || (Atomics.load(h, 3) >>> 0) !== SNAPSHOT.SLOTS || (Atomics.load(h, 4) >>> 0) !== SNAPSHOT.SLOT_BYTES || (Atomics.load(h, 5) >>> 0) !== SNAPSHOT.SCHEMA) bad("BAD_LAYOUT");
|
|
const lifecycle = Atomics.load(h, 6) >>> 0;
|
|
if (lifecycle > SNAPSHOT.CLOSED) bad("BAD_LIFECYCLE");
|
|
if ((Atomics.load(h, 15) >>> 0) !== 0) bad("BAD_RESERVED");
|
|
}
|
|
latest() {
|
|
this.refresh(); this.validateControl();
|
|
for (let tries = 0; tries < 16; tries++) {
|
|
const a = Atomics.load(this.control, 7) >>> 0;
|
|
if (a & 1) continue;
|
|
const lifecycle = Atomics.load(this.control, 6) >>> 0;
|
|
const value = { lifecycle, epoch: Atomics.load(this.control, 8) >>> 0, slot: Atomics.load(this.control, 9) >>> 0, revisionLo: Atomics.load(this.control, 10) >>> 0, revisionHi: Atomics.load(this.control, 11) >>> 0, wasmPages: Atomics.load(this.control, 12) >>> 0, layoutEpoch: Atomics.load(this.control, 13) >>> 0, error: Atomics.load(this.control, 14) >>> 0 };
|
|
const b = Atomics.load(this.control, 7) >>> 0;
|
|
if (a === b && !(b & 1)) {
|
|
if (lifecycle === SNAPSHOT.FAILED) bad("SNAPSHOT_FAILED");
|
|
if (lifecycle === SNAPSHOT.CLOSED) bad("SNAPSHOT_CLOSED");
|
|
if (lifecycle !== SNAPSHOT.INIT && lifecycle !== SNAPSHOT.OPEN) bad("BAD_LIFECYCLE");
|
|
if (value.wasmPages && value.wasmPages > this.buffer.byteLength / 65536) bad("BAD_WASM_PAGES");
|
|
return value;
|
|
}
|
|
}
|
|
bad("UNSTABLE_CONTROL");
|
|
}
|
|
transaction(fn, expectedEpoch = 0) {
|
|
this.refresh();
|
|
const latest = this.latest();
|
|
if (!latest.epoch || latest.slot >= SNAPSHOT.SLOTS || (expectedEpoch && latest.epoch !== expectedEpoch)) return null;
|
|
let control = this.control;
|
|
const base = 16 + latest.slot * 16;
|
|
if (Atomics.compareExchange(control, base, SNAPSHOT.READY, SNAPSHOT.READING) !== SNAPSHOT.READY) return null;
|
|
try {
|
|
// memory.grow replaces memory.buffer even after the slot has been pinned.
|
|
this.refresh(); control = this.control;
|
|
const slot = Array.from({length: 16}, (_, i) => Atomics.load(control, base + i) >>> 0);
|
|
if (slot[0] !== SNAPSHOT.READING || slot[1] !== latest.epoch || slot[2] !== latest.layoutEpoch || slot[5] !== latest.revisionLo || slot[6] !== latest.revisionHi || slot[9] !== SNAPSHOT.SCHEMA || slot[10] !== 64) return null;
|
|
if (slot.slice(11).some(Boolean)) bad("BAD_SLOT_RESERVED");
|
|
const ptr = slot[3], bytes = slot[4];
|
|
if (ptr % 16 || bytes < 448 || bytes % 16 || add(ptr, bytes) > this.buffer.byteLength) bad("BAD_SLOT");
|
|
const u32 = new Uint32Array(this.buffer, ptr, bytes / 4);
|
|
if (u32[0] !== SNAPSHOT.BLOB_MAGIC || u32[1] !== SNAPSHOT.SCHEMA || u32[2] !== 64 || u32[3] !== bytes || u32[4] !== slot[1] || u32[5] !== slot[5] || u32[6] !== slot[6] || u32[7] !== 12 || u32[8] !== 64 || u32[9] !== 32 || u32[10] !== slot[7] || u32[11] !== slot[8] || u32[12] !== 0x01020304 || u32[13] !== 3) bad("BAD_BLOB");
|
|
if (u32[14] || u32[15]) bad("BAD_BLOB_RESERVED");
|
|
const ranges = [], streams = {};
|
|
for (let i = 0; i < 12; i++) {
|
|
const d = 16 + i * 8, semantic = u32[d], scalar = u32[d + 1], offset = u32[d + 2], count = u32[d + 3], components = u32[d + 4], stride = u32[d + 5], width = u32[d + 6], reserved = u32[d + 7];
|
|
const want = i < 4 ? slot[7] : slot[8];
|
|
if (semantic !== i + 1 || scalar !== SCALARS[i] || count !== want || components !== COMPONENTS[i] || stride !== STRIDES[i] || width !== 4 || reserved || offset < 448 || offset % 16) bad("BAD_DESCRIPTOR");
|
|
const end = add(offset, mul(stride, count));
|
|
if (end > bytes) bad("BAD_DESCRIPTOR_RANGE");
|
|
if (count) ranges.push([offset, end]);
|
|
const Type = scalar === 2 ? Float32Array : Uint32Array;
|
|
streams[STREAM_NAMES[i]] = new Type(this.buffer, add(ptr, offset), mul(count, components));
|
|
}
|
|
ranges.sort((a, b) => a[0] - b[0]);
|
|
for (let i = 1; i < ranges.length; i++) if (ranges[i][0] < ranges[i - 1][1]) bad("OVERLAPPING_STREAMS");
|
|
return fn(Object.freeze({epoch: slot[1], revisionLo: slot[5], revisionHi: slot[6], meshCount: slot[7], instanceCount: slot[8], streams: Object.freeze(streams)}));
|
|
} finally {
|
|
Atomics.store(control, base, SNAPSHOT.FREE); Atomics.notify(control, base);
|
|
}
|
|
}
|
|
}
|