Add pressure-aware worker sculpting, bounded shared input, project files and history, and compact icon navigation with an XYZ orbit gizmo. Amp-Thread-ID: https://ampcode.com/threads/T-01a0b4b4-202c-73f1-984d-9a55c88f9c83 Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
149 lines
5.0 KiB
JavaScript
149 lines
5.0 KiB
JavaScript
// Explicit browser benchmark; never imported by the application.
|
|
// await (await import('/assets/sculpt/latency-check.js')).check()
|
|
import { Input } from "./input.js";
|
|
import { Renderer } from "./renderer.js";
|
|
import { StrokeClient } from "./stroke-client.js";
|
|
import { Sculpt } from "./engine.js";
|
|
|
|
export async function check() {
|
|
const viewport = document.getElementById("viewport");
|
|
const move = Input.prototype.move,
|
|
publish = Renderer.prototype.publish,
|
|
message = StrokeClient.prototype.message,
|
|
dab = Sculpt.prototype.dab;
|
|
const capture = viewport.setPointerCapture;
|
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
const frame = () => new Promise(requestAnimationFrame);
|
|
const wait = async (predicate) => {
|
|
const start = performance.now();
|
|
while (!predicate()) {
|
|
if (performance.now() - start > 15000)
|
|
throw new Error("Latency check timed out");
|
|
await sleep(10);
|
|
}
|
|
};
|
|
let input,
|
|
durations = [],
|
|
uploads = [],
|
|
packets = [],
|
|
mainDabs = 0,
|
|
canceled = false;
|
|
Input.prototype.move = function (event) {
|
|
input = this;
|
|
const start = performance.now();
|
|
const result = move.call(this, event);
|
|
durations.push(performance.now() - start);
|
|
return result;
|
|
};
|
|
Sculpt.prototype.dab = function (...args) {
|
|
mainDabs++;
|
|
return dab.apply(this, args);
|
|
};
|
|
StrokeClient.prototype.message = function (data) {
|
|
const start = performance.now();
|
|
const result = message.call(this, data);
|
|
if (data.type === "geometry") packets.push(performance.now() - start);
|
|
return result;
|
|
};
|
|
Renderer.prototype.publish = async function (...args) {
|
|
const start = performance.now();
|
|
try {
|
|
return await publish.apply(this, args);
|
|
} finally {
|
|
uploads.push(performance.now() - start);
|
|
}
|
|
};
|
|
viewport.setPointerCapture = () => {};
|
|
const pointer = (type, x, y, pressure = 0.8) => {
|
|
const r = viewport.getBoundingClientRect();
|
|
return new PointerEvent(type, {
|
|
bubbles: true,
|
|
pointerId: 7,
|
|
pointerType: "pen",
|
|
clientX: r.left + r.width / 2 + x,
|
|
clientY: r.top + r.height / 2 + y,
|
|
pressure,
|
|
button: 0,
|
|
buttons: type === "pointerup" ? 0 : 1,
|
|
});
|
|
};
|
|
const send = (event) => viewport.querySelector("canvas").dispatchEvent(event);
|
|
const percentile = (values, p) =>
|
|
[...values].sort((a, b) => a - b)[
|
|
Math.min(values.length - 1, Math.floor(values.length * p))
|
|
] ?? 0;
|
|
const results = [];
|
|
try {
|
|
for (const brush of ["draw", "grab"])
|
|
for (const perFrame of [1, 4, 16]) {
|
|
await wait(
|
|
() =>
|
|
document.getElementById("loading").hidden &&
|
|
!document.getElementById("new").disabled,
|
|
);
|
|
const old = viewport.querySelector("canvas");
|
|
document.getElementById("new").click();
|
|
document.getElementById("density").value = "5";
|
|
document.getElementById("new-dialog").close("create");
|
|
await wait(
|
|
() =>
|
|
old !== viewport.querySelector("canvas") &&
|
|
document.getElementById("loading").hidden,
|
|
);
|
|
document.querySelector(`[data-brush="${brush}"]`).click();
|
|
await sleep(300);
|
|
durations = [];
|
|
uploads = [];
|
|
packets = [];
|
|
mainDabs = 0;
|
|
canceled = false;
|
|
send(pointer("pointerdown", -80, -30));
|
|
for (let f = 0; f < 60; f++) {
|
|
await frame();
|
|
const samples = Array.from({ length: perFrame }, (_, i) => {
|
|
const t = (f + (i + 1) / perFrame) / 60;
|
|
return pointer(
|
|
"pointermove",
|
|
-80 + 160 * t,
|
|
-30 + 30 * Math.sin(t * Math.PI),
|
|
0.3 + 0.6 * t,
|
|
);
|
|
});
|
|
const event = samples.at(-1);
|
|
Object.defineProperty(event, "getCoalescedEvents", {
|
|
value: () => samples,
|
|
});
|
|
send(event);
|
|
if (input && !input.busy) canceled = true;
|
|
}
|
|
const release = performance.now();
|
|
send(pointer("pointerup", 80, -30, 0));
|
|
await wait(() => !document.getElementById("save").disabled);
|
|
const releaseMs = performance.now() - release;
|
|
const undoEntries = input.host.engine().undoStack.length;
|
|
canceled ||= undoEntries !== 1;
|
|
if (canceled || mainDabs || input.queue !== undefined)
|
|
throw new Error("Worker input regression");
|
|
results.push({
|
|
brush,
|
|
samplesPerFrame: perFrame,
|
|
sharedInputBytes: input.host.processor().mailbox.words.byteLength,
|
|
mainDabs,
|
|
canceled,
|
|
releaseMs,
|
|
mainInputP95: percentile(durations, 0.95),
|
|
mainPacketP95: percentile(packets, 0.95),
|
|
uploadP95: percentile(uploads, 0.95),
|
|
undoEntries,
|
|
});
|
|
}
|
|
return results;
|
|
} finally {
|
|
Input.prototype.move = move;
|
|
StrokeClient.prototype.message = message;
|
|
Sculpt.prototype.dab = dab;
|
|
Renderer.prototype.publish = publish;
|
|
viewport.setPointerCapture = capture;
|
|
}
|
|
}
|