M13.24: web app depth (WEB-1) — GPU effects in frame workers, OPFS disk cache, storage manager

- GPU in the browser: each frame worker opens its own WebGPU device (Gpu::request_deferred) and
  renders the GPU compositor and GPU effects with deferred readbacks (effectcraft-gpu deferred):
  readbacks keyed by content start with map_async and miss, the pass returns placeholders, the
  layer cache's gate drops inserts, and FrameServer::resume renders again once the device
  delivered. Backend Auto times whole frames per comp in the workers. Frames with effects go to
  GPU workers instead of the page. CPU fallback without WebGPU in workers (?nogpuworkers).
- Fix: a WGSL constant (3.40282347e38) that Chrome rejects invalidated every GPU kernel in Chrome
  (the page's compositor too); a test checks every float literal fits f32.
- Disk cache in the Origin Private File System: workers write sealed frame entries with sync
  access handles (temp file + move), the page keeps the DiskIndex (LRU under the settings' limit,
  at most half the quota) and reads hits instead of rendering; survives reloads; blue cache bar.
- Storage manager: Settings ▸ Disk ▸ Browser Storage (usage/quota, persistent storage, Clear
  buttons, automation ids) and storage.info / storage.persist / storage.clear (StorageHost).
- Docs: web.md (GPU workers, deferred passes, disk cache, storage manager, threading design:
  one engine instance per worker), architecture, preferences, parity (WEB-1), roadmap.
- Tests: deferred passes vs CPU on a real device, placeholder gating, frame server passes and
  Auto, GPU worker routing + disk cache across a reload, storage commands and widgets, store
  usage by kind; smoke test: GPU effect frames in workers with pixel parity, disk hits after a
  reload, storage manager.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Brandon ThomasandClaude Opus 5.5 committed 2026-10-03 18:35:03 -04:00
1 parent 081ce79de3
commit 1b726349ab
40 files changed
+2812 -197

No files matched your search

+2
View File
@@ -62,6 +62,8 @@
listStored: () => ec.listStored(),
removeStored: (path) => ec.removeStored(path),
flush: () => ec.flush(),
// A frame rendered in a frame worker (its GPU by default) vs the page's CPU: {maxDiff, meanDiff, over4, …}.
workerFrameCheck: (params) => ec.workerFrameCheck(params ?? {}).then(JSON.parse),
};
// Offline start and install (PWA): the service worker caches the app's files.
if ("serviceWorker" in navigator && !new URLSearchParams(location.search).has("nosw")) {
+26 -12
View File
@@ -2,21 +2,35 @@
// crates/engine/src/offload.rs and remote.rs). The page sends the compiled module, then:
// - job workers: footage files and jobs (Render Queue, analyses, Roto Brush); the worker replies
// with JSON messages and rendered files;
// - frame workers: footage files and frame messages (project syncs as diffs, render requests,
// Roto Brush segmentations); the worker replies with frames (pixels transferred).
// - frame workers (`frames: true`): footage files and frame messages (project syncs as diffs,
// render requests, Roto Brush segmentations); the worker replies with frames (pixels
// transferred). With `gpu: true` it opens its own WebGPU device and renders GPU effects on
// it; a frame then takes several passes, awaiting the device's readbacks in between.
// Messages are handled one at a time, in order: a frame awaiting its readbacks finishes before
// the next message (a project sync, the next request) is looked at.
import init, * as ec from "./effectcraft_web.js";
let ready = null;
self.onmessage = async (e) => {
const m = e.data;
if (m.type === "init") {
ready = init({ module_or_path: m.module ?? new URL("effectcraft_web_bg.wasm", m.base) }).then(() => ec.workerInit());
await ready;
self.postMessage({ type: "ready" });
return;
}
await ready;
let queue = Promise.resolve();
async function handle(m) {
if (m.type === "file") ec.workerFile(m.path, m.bytes);
else if (m.type === "job") ec.workerJob(m.json);
else if (m.type === "frame") ec.workerFrame(m.json);
else if (m.type === "frame") await ec.workerFrame(m.json);
}
self.onmessage = (e) => {
const m = e.data;
if (m.type === "init") {
ready = (async () => {
await init({ module_or_path: m.module ?? new URL("effectcraft_web_bg.wasm", m.base) });
ec.workerInit();
if (m.frames) await ec.workerFrameInit(!!m.gpu);
self.postMessage({ type: "ready" });
})();
return;
}
// A failure (a panic) is re-raised outside the queue so the page's `onerror` sees it, as
// before the queue: the page then drops the worker.
queue = queue.then(() => ready).then(() => handle(m)).catch((err) => setTimeout(() => { throw err; }));
};