type RowFormat = "f32" | "u32" | "i32"; type RowDescriptor = { name: string; rows: number; stride: number; format: RowFormat; }; const views = { f32: Float32Array, u32: Uint32Array, i32: Int32Array, }; export class SharedRows { readonly name: string; rows: number; readonly stride: number; readonly format: RowFormat; buffer: SharedArrayBuffer; constructor({ name, rows, stride, format }: RowDescriptor) { this.name = name; this.rows = rows; this.stride = stride; this.format = format; this.buffer = new SharedArrayBuffer(rows * stride); } resize(rows: number) { if (rows <= this.rows) return this; const previous = this.view; this.rows = rows; this.buffer = new SharedArrayBuffer(rows * this.stride); this.view.set(previous); return this; } get view() { const View = views[this.format]; return new View(this.buffer); } row(index: number) { if (!Number.isInteger(index) || index < 0 || index >= this.rows) throw new RangeError(`ROW_RANGE: ${this.name}[${index}]`); const width = this.stride / views[this.format].BYTES_PER_ELEMENT; return this.view.subarray(index * width, (index + 1) * width); } write(index: number, values: ArrayLike) { const row = this.row(index); if (values.length !== row.length) throw new RangeError("ROW_WIDTH"); row.set(values); return this; } } function deserialize(serialized: string) { let offset = 0; const skip = () => { while (/\s/u.test(serialized[offset] ?? "")) offset++; }; const take = (character: string) => { skip(); if (serialized[offset] !== character) throw new SyntaxError(`Expected ${character} at ${offset}`); offset++; }; const word = () => { skip(); const start = offset; while (offset < serialized.length && !/[\s()]/u.test(serialized[offset])) offset++; return serialized.slice(start, offset); }; const value = (): any => { skip(); if (serialized[offset] === '"') { const start = offset++; while (offset < serialized.length) { if (serialized[offset] === "\\") offset += 2; else if (serialized[offset++] === '"') break; } return JSON.parse(serialized.slice(start, offset)); } if (serialized[offset] !== "(") { const token = word(); if (token === "true") return true; if (token === "false") return false; if (token === "null") return null; const number = Number(token); if (!Number.isNaN(number)) return number; throw new SyntaxError(`Unknown value ${token}`); } take("("); const tag = word(); if (tag === "array") { const result = []; skip(); while (serialized[offset] !== ")") result.push(value()); take(")"); return result; } if (tag === "object") { const result: Record = {}; skip(); while (serialized[offset] !== ")") { take("("); if (word() !== "field") throw new SyntaxError("Expected field"); const key = value(); result[key] = value(); take(")"); skip(); } take(")"); return result; } if (tag === "yawn-graph") { if (word() !== "1") throw new SyntaxError("Graph version"); const result = value(); take(")"); return result; } throw new SyntaxError(`Unknown list ${tag}`); }; return value(); } export class YawnCore { readonly ready = Promise.resolve(); readonly features = new Set(["timestamp-query", "indirect-first-instance"]); readonly graphs: any[] = []; readonly serializedGraphs: string[] = []; readonly loadouts: string[] = []; readonly uploadedTextures: { name: string; mipLevel: number }[] = []; fps?: number; disposed = false; #arrays = new Map(); #nextObjects = new Map(); constructor() { this.#arrays.set( "signals", new SharedRows({ name: "signals", rows: 1, stride: 32, format: "f32" }), ); } createRows(descriptor: RowDescriptor) { const current = this.#arrays.get(descriptor.name); if (current) { if ( current.stride !== descriptor.stride || current.format !== descriptor.format ) throw new Error(`ROW_LAYOUT: ${descriptor.name}`); return Promise.resolve(current.resize(descriptor.rows)); } const rows = new SharedRows(descriptor); this.#arrays.set(descriptor.name, rows); return Promise.resolve(rows); } createRowsBatch(descriptors: RowDescriptor[]) { return Promise.all( descriptors.map((descriptor) => this.createRows(descriptor)), ); } deleteRows(name: string) { this.#arrays.delete(name); return Promise.resolve(); } allocateObject(name: string, count = 1) { const id = this.#nextObjects.get(name) ?? 0; this.#nextObjects.set(name, id + count); const rows = this.array(name); rows.resize(id + count); return Promise.resolve(id); } deleteObject() { return Promise.resolve(); } array(name: string) { const rows = this.#arrays.get(name); if (!rows) throw new Error(`UNKNOWN_ARRAY: ${name}`); return rows; } compileGraph(serialized: string) { this.serializedGraphs.push(serialized); this.graphs.push(deserialize(serialized)); return Promise.resolve(`loadout-${this.graphs.length}`); } switchLoadout(id: string) { this.loadouts.push(id); return Promise.resolve(); } setFps(fps: number) { this.fps = fps; return Promise.resolve(); } uploadTexture(name: string, _image: unknown, mipLevel = 0) { this.uploadedTextures.push({ name, mipLevel }); return Promise.resolve(); } deleteTexture() { return Promise.resolve(); } dispose() { this.disposed = true; } }