Files
yawn/addons/handles/test/core-mock.ts
T
Ampandheaust 31d3ae21ff core: manual buffer sync with sync-rows, reuse unchanged buffers and pipelines across activations
Also lets the screenshot suite point at another reference directory and settle longer.

Amp-Thread-ID: https://ampcode.com/threads/T-01a0b2dd-db32-712d-99ce-12ce4a86d979
Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
2026-09-18 05:53:44 +00:00

226 lines
5.7 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 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<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, 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();
}
syncRows() {
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;
}
}