Amp-Thread-ID: https://ampcode.com/threads/T-01a02d1c-27fc-7761-9f51-ed97b2dd4e3e Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
221 lines
5.6 KiB
TypeScript
221 lines
5.6 KiB
TypeScript
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<number>) {
|
|
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<string, unknown> = {};
|
|
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 graphs: any[] = [];
|
|
readonly serializedGraphs: string[] = [];
|
|
readonly loadouts: string[] = [];
|
|
readonly uploadedTextures: { name: string; mipLevel: number }[] = [];
|
|
fps?: number;
|
|
disposed = false;
|
|
#arrays = new Map<string, SharedRows>();
|
|
#nextObjects = new Map<string, number>();
|
|
|
|
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) {
|
|
const id = this.#nextObjects.get(name) ?? 0;
|
|
this.#nextObjects.set(name, id + 1);
|
|
const rows = this.array(name);
|
|
rows.resize(id + 1);
|
|
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;
|
|
}
|
|
}
|