Files
yawn/server/web/assets/sculpt/worker.test.js
T
Ampandheaust d279ec4123 Add clay pieces, masking, and topology editing to sculpt
Add pastel presets, piece selection and colors, mask brushes, subdivision and conservative decimation with bounded worker operations and mixed undo history.

Amp-Thread-ID: https://ampcode.com/threads/T-01a0b4b4-202c-73f1-984d-9a55c88f9c83
Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
2026-10-01 07:55:48 +00:00

260 lines
9.1 KiB
JavaScript

import test from "node:test";
import assert from "node:assert/strict";
import { Worker } from "node:worker_threads";
import { Mailbox } from "./mailbox.js";
import { StrokeProcessor } from "./stroke-worker.js";
import { Sculpt, defaults, StrokePath } from "./engine.js";
import { sphere } from "./mesh.js";
const row = () => ({
buffer: new SharedArrayBuffer(128),
descriptor: { offset: 64 },
});
test("mailbox overwrites one slot, keeps a newer publication pending, wraps and rejects torn reads", (t) => {
const m = new Mailbox(row()),
p = { x: 12, y: -8, pressure: 0.5 };
const first = m.write(7, p);
assert.deepEqual(m.read(7), { ...p, seed: undefined, version: first });
const second = m.write(7, { x: 20, y: 3, pressure: 0.75 });
m.acknowledge(first);
assert.equal(m.read(7).version, second);
m.acknowledge(second);
assert.equal(m.read(7), null);
Atomics.store(m.words, 0, 0x7ffffffe);
const wrapped = m.write(7, p);
assert.equal(wrapped, -2147483648);
assert.equal(m.read(7).x, 12);
const load = Atomics.load;
let changed = false;
t.mock.method(Atomics, "load", (array, index) => {
const v = load(array, index);
if (array === m.words && index === 5 && !changed) {
changed = true;
m.write(7, { x: 99, y: 100, pressure: 0.2 });
}
return v;
});
assert.equal(m.read(7), null);
Atomics.store(m.words, 0, 11);
assert.equal(m.read(7), null);
});
test("actual concurrent SAB writer and reader never observe mixed coordinates", async (t) => {
const shared = row(),
m = new Mailbox(shared),
control = new Int32Array(shared.buffer, 0, 1);
const code = `import {parentPort,workerData} from 'node:worker_threads';
const {Mailbox}=await import(workerData.module);
const m=new Mailbox(workerData.row),control=new Int32Array(workerData.row.buffer,0,1);
let reads=0,last=0;
parentPort.postMessage('ready');
while(!Atomics.load(control,0)||last!==100000){
const p=m.read(7);if(!p)continue;
if(p.y!==-p.x||p.pressure!==p.x*2||p.seed!==p.x*3)throw new Error('torn tuple');
last=p.x;reads++;m.acknowledge(p.version);
}parentPort.postMessage({reads,last});`;
const worker = new Worker(
new URL("data:text/javascript," + encodeURIComponent(code)),
{
workerData: {
module: new URL("./mailbox.js", import.meta.url).href,
row: shared,
},
},
);
t.after(() => worker.terminate());
await new Promise((resolve, reject) => {
worker.once("message", resolve);
worker.once("error", reject);
});
const result = new Promise((resolve, reject) => {
worker.once("message", resolve);
worker.once("error", reject);
});
for (let i = 1; i <= 100000; i++) {
m.write(7, { x: i, y: -i, pressure: i * 2 }, i * 3);
if (i % 1000 === 0) await new Promise((resolve) => setImmediate(resolve));
}
Atomics.store(control, 0, 1);
const out = await result;
assert.ok(out.reads > 0);
assert.equal(out.last, 100000);
});
function setup(settings = {}, sample = { x: 400, y: 300, pressure: 0.7 }) {
const mailbox = new Mailbox(row()),
messages = [],
mesh = sphere(4);
let now = 0;
const processor = new StrokeProcessor(
mesh,
mailbox,
(m) => messages.push(m),
() => now,
);
const version = mailbox.write(1, sample);
processor.begin({
id: 1,
version,
sample,
positions: mesh.positions.slice(),
settings: { ...defaults, symmetry: false, ...settings },
camera: { yaw: 0, pitch: 0, zoom: 1.5, target: [0, 0, 0] },
rect: { width: 800, height: 600 },
hit: { position: [0, 0, 1], normal: [0, 0, 1] },
});
const ack = () => {
for (const m of messages)
if (m.type === "geometry") processor.acknowledge(m.packet);
};
return {
processor,
mailbox,
messages,
ack,
advance: (ms) => {
now += ms;
},
};
}
test("mask packets are single-flight and canceled geometry is repaired before masking", () => {
const { processor, mailbox, messages } = setup();
// Cancel intentionally leaves provisional worker geometry.
const before = processor.engine.mesh.positions.slice();
processor.frame();
assert.notDeepEqual(processor.engine.mesh.positions, before);
processor.cancel(1);
const sample = { x: 420, y: 290, pressure: 0.8 };
const version = mailbox.write(2, sample);
processor.begin({
id: 2,
version,
sample,
positions: before,
settings: { ...defaults, brush: "mask", symmetry: false },
camera: { yaw: 0, pitch: 0, zoom: 1.5, target: [0, 0, 0] },
rect: { width: 800, height: 600 },
hit: { position: [0, 0, 1], normal: [0, 0, 1] },
});
// Independently rebuild at the fixture's actual level.
const fresh = new Sculpt(sphere(processor.engine.mesh.level));
assert.deepEqual(processor.engine.mesh.positions, fresh.mesh.positions);
assert.deepEqual(processor.engine.mesh.normals, fresh.mesh.normals);
const start = messages.length;
processor.frame();
const packet = messages.at(-1);
assert.equal(packet.type, "mask");
const values = packet.masks.slice();
const end = mailbox.write(2, { x: 450, y: 310, pressure: 0.9 });
processor.end(2, end);
processor.frame();
assert.equal(messages.length, start + 1);
assert.deepEqual(packet.masks, values);
processor.acknowledge(packet.packet);
processor.frame();
const last = messages.at(-1);
assert.equal(last.type, "mask");
processor.acknowledge(last.packet);
processor.frame();
assert.equal(messages.at(-1).type, "complete");
assert.deepEqual(processor.engine.mesh.positions, before);
});
test("latest frame target equals feeding only that target, intermediate samples create no hidden queue", () => {
const a = setup(),
b = setup();
a.processor.frame();
b.processor.frame();
a.ack();
b.ack();
for (let i = 0; i < 10000; i++)
a.mailbox.write(1, {
x: 410 + i / 100,
y: 300 + Math.sin(i) * 30,
pressure: 0.4,
});
const end = { x: 470, y: 310, pressure: 0.8 };
const version = a.mailbox.write(1, end);
b.mailbox.write(1, end);
a.processor.frame();
b.processor.frame();
assert.deepEqual(
a.processor.engine.mesh.positions,
b.processor.engine.mesh.positions,
);
assert.equal(a.mailbox.processed, version);
assert.equal(a.processor.job.target, null);
});
test("Grab seeds pressure-radius from first effective pressure and ignores a zero-pressure tail", () => {
const a = setup(
{ brush: "grab", pressureRadius: true },
{ x: 400, y: 300, pressure: 0 },
);
a.processor.frame();
const version = a.mailbox.write(1, { x: 460, y: 310, pressure: 0.8 }, 0.25);
a.processor.frame();
const expected = new Sculpt(sphere(4));
expected.begin({ brush: "grab", pressureRadius: true, symmetry: false });
expected.dab({ position: [0, 0, 1], normal: [0, 0, 1] }, 0.25, [0, 0, 0]);
expected.dab(
{ position: [0, 0, 1], normal: [0, 0, 1] },
Math.fround(0.8),
[0.3, -0.05, 0],
);
assert.deepEqual(a.processor.engine.mesh.positions, expected.mesh.positions);
assert.equal(a.mailbox.processed, version);
});
test("one immutable output in flight, new dirty work survives acknowledgement, end waits for applied output", () => {
const a = setup();
a.processor.frame();
const first = a.messages.find((m) => m.type === "geometry"),
bytes = first.vertices.slice();
const version = a.mailbox.write(1, { x: 470, y: 310, pressure: 0.9 });
a.processor.end(1, version);
a.processor.frame();
assert.deepEqual(first.vertices, bytes);
assert.equal(a.messages.filter((m) => m.type === "geometry").length, 1);
assert.ok(a.processor.engine.dirty.size);
assert.ok(!a.messages.some((m) => m.type === "complete"));
a.processor.acknowledge(first.packet);
a.processor.frame();
const second = a.messages.filter((m) => m.type === "geometry").at(-1);
assert.notEqual(second.packet, first.packet);
a.processor.acknowledge(first.packet);
assert.equal(a.processor.outstanding, second.packet);
a.processor.acknowledge(second.packet);
a.processor.frame();
assert.equal(a.messages.at(-1).type, "complete");
});
test("worker bounds real work; unfinished segment age is not reset by release or newer samples", () => {
const a = setup();
a.processor.frame();
a.ack();
const dab = a.processor.engine.dab.bind(a.processor.engine);
let calls = 0;
a.processor.engine.dab = (...args) => {
calls++;
a.advance(5);
dab(...args);
};
a.mailbox.write(1, { x: 590, y: 300, pressure: 0.8 });
a.processor.frame();
assert.equal(calls, 1);
assert.ok(a.processor.job.target);
a.advance(250);
const latest = a.mailbox.write(1, { x: 610, y: 305, pressure: 0.9 });
a.processor.end(1, latest);
a.processor.frame();
assert.equal(a.processor.job, null);
assert.equal(a.messages.at(-1).type, "canceled");
});
test("resuming a segment preserves independently computed positions and pressure", () => {
const emitted = [],
path = new StrokePath(5, (p) => emitted.push(p));
path.push({ x: 0, y: 0, pressure: 0.2 });
while (!path.push({ x: 24, y: 18, pressure: 0.8 }, 1)) {}
assert.equal(emitted.length, 7);
emitted.forEach((p, i) => {
assert.ok(Math.abs(p.x - 4 * i) < 1e-10);
assert.ok(Math.abs(p.y - 3 * i) < 1e-10);
assert.ok(Math.abs(p.pressure - (0.2 + 0.1 * i)) < 1e-10);
});
});