core: writer-owned relocation

The worker no longer copies rows when an array cannot grow in place. It
reserves a zeroed region, returns the staged descriptor with a commit
flag, and queues every other request until the main thread (the only
writer) has copied the old bytes and sent commit-rows. Writes that raced
the old worker-side copy were lost; lights in the glTF viewer regularly
ended up at the origin because of it.

Amp-Thread-ID: https://ampcode.com/threads/T-01a0b2dd-db32-712d-99ce-12ce4a86d979
Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
This commit is contained in:
Amp
2026-09-18 06:40:44 +00:00
co-authored by heaust
parent 31d3ae21ff
commit 92638e6c9f
8 changed files with 278 additions and 96 deletions
+34 -14
View File
@@ -75,23 +75,17 @@ export class YawnCore {
async createRows({ name, rows, stride, format }) {
await this.ready;
const descriptor = await this.#request("create-rows", { name, rows, stride, format });
const array = this.#arrays.get(name)?.update(descriptor)
?? new SharedRows(this.#buffer, descriptor);
this.#arrays.set(name, array);
return array;
const result = await this.#request("create-rows", { name, rows, stride, format });
return this.#adopt(result.rows, result.commit ? [name] : []);
}
async createRowsBatch(rows) {
await this.ready;
if (!Array.isArray(rows) || !rows.length) throw new TypeError("ROWS");
const descriptors = await this.#request("create-rows-batch", { rows });
return descriptors.map(descriptor => {
const array = this.#arrays.get(descriptor.name)?.update(descriptor)
?? new SharedRows(this.#buffer, descriptor);
this.#arrays.set(descriptor.name, array);
return array;
});
const result = await this.#request("create-rows-batch", { rows });
const arrays = result.rows.map(descriptor => this.#adopt(descriptor));
this.#commit(result.commit);
return arrays;
}
async deleteRows(name) {
@@ -103,11 +97,37 @@ export class YawnCore {
/** Reserves `count` contiguous rows and resolves with the first row ID. */
async allocateObject(name, count = 1) {
await this.ready;
const { id, rows } = await this.#request("allocate-object", { name, count });
this.#arrays.get(name).update(rows);
const { id, rows, commit } = await this.#request("allocate-object", { name, count });
this.#adopt(rows, commit ? [name] : []);
return id;
}
/**
* Publishes a descriptor the worker returned. When the rows moved, this thread owns the copy:
* the old bytes are copied synchronously into the new region and the worker is told to commit
* before anything else can be requested, so no write to the array is ever lost.
*/
#adopt(descriptor, commit = []) {
const current = this.#arrays.get(descriptor.name);
if (!current) {
const array = new SharedRows(this.#buffer, descriptor);
this.#arrays.set(descriptor.name, array);
return array;
}
if (descriptor.offset !== current.descriptor.offset) {
const { offset, bytes } = current.descriptor;
new Uint8Array(this.#buffer, descriptor.offset, bytes).set(new Uint8Array(this.#buffer, offset, bytes));
}
current.update(descriptor);
this.#commit(commit);
return current;
}
#commit(names) {
if (!names.length) return;
this.#request("commit-rows", { names }).catch(error => this.#fail(error.code));
}
async deleteObject(name, id) {
await this.ready;
return this.#request("delete-object", { name, id });