Merge branch 'main' into worktree-agent-a139f16b3a4130998

This commit is contained in:
Brandon Thomas
2026-10-03 23:57:57 -04:00
40 changed files with 2811 additions and 197 deletions
+11 -1
View File
@@ -126,7 +126,17 @@ preview frame are written (LZ4, atomic rename, checksummed) under 128-bit conten
comp's content hash (the comp, what it uses, footage file size and time) plus frame, scale and
view for frames; the layer key salted with the footage fingerprint for layers — so entries
survive restarts and never go stale. LRU eviction keeps it under the size limit; Edit ▸ Purge
empties it, `cache.diskStats` reports it, and the timeline draws disk-only frames in blue.
empties it, `cache.diskStats` reports it, and the timeline draws disk-only frames in blue. In the
browser the frames live in the Origin Private File System: frame workers write them, the page
keeps the index (`DiskIndex`) and reads hits ([web.md](web.md#disk-cache)).
**Deferred readbacks** (`effectcraft-gpu` `deferred`): where a readback can't be waited for (WebGPU
in a browser worker) a frame renders in passes. `Accelerator::frame_begin` / `pass_begin` /
`pass_missed` / `miss_gate`: a readback not there yet starts asynchronously and the step returns a
placeholder; the pass is missed, the layer cache's gate drops inserts until the pass ends, and the
caller (`remote::FrameServer::resume`) renders again once the device delivered. Readbacks are keyed
by content (a chain's input pixels and parameters; the frame's comp, time and options), so later
passes are served from them; a pass without a miss is the frame.
**Time effects** (Echo, Posterize Time, Timewarp…) read the layer at other times through
`EffectHost::self_at`: the renderer renders the layer's source and masks (plus, optionally, the
+4 -3
View File
@@ -20,7 +20,7 @@ Partial features:
| id | Tier | Done | What is missing |
|---|---|---|---|
| EFF-5 GPU effects | P1 | 0.85 | GPU kernels for the CC light / transition families, Numbers and Timecode, 3D Channel, the VR family, OCIO / LUT, Time effects and the simulations; Advanced 3D runs with blend modes, track mattes or environment backgrounds composite on the CPU |
| WEB-1 Web app | P0 | 0.9 | GPU effects in the browser (needs WebGPU in the render workers or async readback), a disk cache in browser storage (OPFS), a storage manager, threads inside one engine instance |
| WEB-1 Web app | P0 | 0.97 | GPU effects in Render Queue / analysis job workers (they render on their CPU); layer buffers in the browser's disk cache (frames only); threads inside one engine instance (the decided design is one engine instance per worker; a threaded build needs nightly `build-std`). M13.24 added GPU effects and the GPU compositor in the frame workers (their own WebGPU devices, deferred readbacks: frames render in passes; Backend Auto per comp), the disk cache in the Origin Private File System (written by the workers with sync access handles, LRU under the settings' limit, served after a reload), the storage manager (Settings ▸ Disk ▸ Browser Storage, `storage.*`) and fixed a WGSL constant Chrome rejected (it disabled every GPU kernel in Chrome) |
| UI-7 Home screen | P1 | 0.95 | A "start from a template" gallery |
| MSK-4 Roto Brush | P2 | 0.85 | Segmentation is classical (graph cut + optical flow), not a learned model |
| TRK-3 Face tracking | P2 | 0.9 | Classical fitter: weak on profile and occluded faces; Rolling Shutter Ripple is approximated |
@@ -35,7 +35,8 @@ effects, a texture pool and per-comp CPU/GPU choice; M13.10 render workers in th
visual editors for curves and palettes; M13.11 Advanced 3D on the GPU and Essential Graphics
mirrors; M13.14–M13.20 puppet pin editing and rigging; M13.15 end-to-end tests through MCP and
the CLI; M13.21 lazy project open, virtualised panels, background auto-save and an audit that
every command is reachable over MCP, the control channel and the CLI.
every command is reachable over MCP, the control channel and the CLI; M13.24 GPU effects in the
browser's frame workers, the browser disk cache (OPFS) and a storage manager.
Out of clean-room scope (no public specification or no permissively licensed data): `.jsxbin`,
Vanishing Point `.vpe`, the predefined CJK CMaps in PDF import, Kodak film emulations. Adobe
@@ -153,7 +154,7 @@ the disk cache in the formats wave; the 3D Camera Tracker in M12.6.)
| Compositions | ≈ 81% | 3.3 | Mocha-style planar tracks for templates (CMP-7: Essential Graphics mirrored and linked properties landed in M13.11; Font and uniform Scale controls, Composition ▸ Open in Essential Graphics, Save Frame As ▸ Photoshop Layers / ProEXR and the VR Comp Editor landed in M13.7; the marker dialog, Composition Flowchart, Essential Graphics with master properties, `.ectemplate` templates and Responsive Design — Time landed: CMP-6, CMP-7) |
| Animation | 70% | 9.0 | puppet depth beyond pins, recording, rigging and follow-through (puppet pin recording with Record Options landed in M13.1; pin selection, rotate/scale handles, nulls for pins and Follow-Through in M13.14–M13.16), Wiggler/Smoother/Motion Sketch (motion-path handles and the graph editor transform box landed in M5.8; keyframe colour labels and Select Keyframe Label Group, Graph Editor snapping to markers / layer ends in M13.5) |
| Text | ≈ 96% | 0.3 | no extruded strokes (M13.12: the Variable Font Axes animator re-spaces the text — advances follow the animated axes; M13.6: variable font axes in the character style — `layer.setText variations`, the Character panel's Variable Font Axes fields — shape with HVAR / gvar advances and draw at that design-space position; OpenType features — stylistic sets, discretionary ligatures, contextual / stylistic alternates, swash, titling, ordinals, fractions, figure styles, true small caps / all small caps and superior / inferior glyphs with faux fallback — per character with the Character panel's OpenType popup and `text.fontFeatures` landed in M13.2; vertical Roman / Tate-Chu-Yoko, forced LTR paragraphs, caret on animated and path text, Variable Font Axes and Lottie style runs landed in M13.5; extruded, bevelled text in M7.6; per-character styles, paragraph settings, on-canvas editing and the `sourceText` style API in M9.9–M9.10) |
| Web | 93% | 0.5 | GPU effects in the browser (WebGPU compositing only on the page), no disk cache, no shared-memory threads inside one engine instance (browser storage, Web Audio, Web Worker renders/analyses, WebGPU viewer and offline install landed in M15.2; viewer frames in frame workers fed by project diffs, Roto Brush propagation in a worker, non-blocking `wait: true` jobs and a browser Media Browser (File System Access folders, browser storage) in M13.10) |
| Web | 97% | 0.2 | GPU effects in Render Queue / analysis workers (job workers render on their CPU), layer buffers in the browser's disk cache (frames only), no shared-memory threads inside one engine instance (the decided design is one engine instance per worker; a threaded build needs nightly `build-std`) (browser storage, Web Audio, Web Worker renders/analyses, WebGPU viewer and offline install landed in M15.2; viewer frames in frame workers fed by project diffs, Roto Brush propagation in a worker, non-blocking `wait: true` jobs and a browser Media Browser (File System Access folders, browser storage) in M13.10; WEB-1 in M13.24: GPU effects and the GPU compositor in the frame workers on their own WebGPU devices with deferred readbacks (frames render in passes; Backend Auto per comp), the disk cache in the Origin Private File System (written by the workers with sync access handles, LRU under the settings' limit, served after a reload), the storage manager (Settings ▸ Disk ▸ Browser Storage, `storage.info` / `storage.persist` / `storage.clear`), and a WGSL constant Chrome rejected (it disabled every GPU kernel in Chrome) fixed) |
| 3D | 88% | 5.5 | multi-view layouts, the Extended Viewer for Advanced 3D comps (Classic 3D Extended Viewer landed in M13.5 UI completion); collapsed precomps of another size seen through the parent's camera render (fixed in M13.2); stereo rigs, orbit nulls, lights controlled by the camera, cameras/lights from glTF models, environment backgrounds, Advanced 3D motion blur, blend modes and track mattes landed in M7.7; Classic 3D iris-shaped bokeh with highlights, progressive depth of field on tilted layers and the focus-link commands landed in M13.5; Advanced 3D depth of field with the iris and highlight options, collapsed precomps as real Advanced 3D geometry and extruded text/shape strokes landed in M13.8; Advanced 3D (glTF/OBJ models, primitives, extruded text and shapes, PBR, image-based light, shadow maps, GPU rasteriser) in M7.4–M7.6; Advanced 3D end to end on the GPU (motion blur, iris depth of field, compositing) in M13.11 |
| Effects | ≈ 85% | 3.0 | GPU versions of the remaining effects (166 run on the GPU since M13.13, EFF-5: Warp, Bezier Warp, Reshape, Smear, CC Bend It, CC Page Turn, Cartoon, Color Emboss, Circle, Ellipse, Iris Wipe, Bevel Alpha / Edges and Gaussian Blur (Legacy) joined the 152 of M13.9; simulations, 3D channel, VR, audio-driven, OCIO, text and the CC light / transition families still render on the CPU) and the missing controls listed as partial in [effects.md](effects.md) (every After Effects effect exists since M9.11, M12.5 and M12.6; parameter names, order, twirl-downs, popups, units and defaults were aligned in M9.12) |
| Interface | 75% | 5.0 | more Learn tutorials and pixel-level fidelity of dialogs (the Home ▸ Templates gallery (eight original built-in templates, user templates from File ▸ Save as Template…) and View ▸ Simulate Output ▸ My Custom RGB… landed in M13.25; Timeline layer reordering by drag, a non-snapping viewer pan, a working rename field and twirl arrows, and the full set of property reveal shortcuts — double presses, Alt+Shift keyframes, Ctrl+` — landed in M13.17–M13.20; the Home ▸ Learn tab with interactive tutorials and a UI fidelity pass landed in M13.5 UI completion; visual editors for Lumetri RGB / hue-saturation curves, Colorama's output cycle wheel, Glow's colour map and Reshape's correspondence points (viewer handles), a scrolling Preview panel and AE-style Composition / Timeline tabs (close, label swatch, viewer lock) landed in M13.10; Timeline outline and Project panel columns scroll horizontally, the Layer Style dialog, ROI resize handles, Pan Behind snapping and 3D Reference Axes landed in M13.5; native macOS menu bar, Timeline columns/search/reveal-add, Home screen with recent projects and all AE workspaces landed; viewer rulers/snapping/channels/snapshots landed in M0.13) |
+4 -1
View File
@@ -112,7 +112,10 @@ Commands (CLI, MCP, control channel):
- `audio.previewSampleRate`: the rate previews are mixed at (and the device is opened at, when
it supports it)
- `disk.diskCacheEnabled`, `disk.diskCacheMaxGb`, `disk.diskCacheFolder`: the persistent disk
cache of processed layers
cache of processed layers and frames (in the browser: frames in the Origin Private File System,
at most half the origin's quota; the folder setting does not apply there). The Disk page also
shows **Browser Storage** in the web app: usage and quota, persistent storage, Clear buttons
(`storage.info` / `storage.persist` / `storage.clear`, see [web.md](web.md))
- `disk.mediaCacheFolder`: audio waveform summaries kept between sessions (`Peaks/`)
- `disk.conformedMediaFolder`: decoded (conformed) footage audio, written once per file and
sample rate and read back instead of decoding again
+135 -27
View File
@@ -43,13 +43,14 @@ URL flags: `?empty` (a blank project; the session is not restored or recorded),
project instead of the last session), `?home` (show the start screen), `?storage=indexeddb` /
`?storage=memory` (force a storage backend), `?noworkers` (render, analyse and draw viewer
frames on the page's thread), `?frameworkers=N` (frame workers, default 2; 0 = viewer frames on
the page's thread), `?nosw` (don't register the service worker).
the page's thread), `?nogpuworkers` (frame workers render on their CPU, without a WebGPU device of
their own), `?nosw` (don't register the service worker).
## How the desktop pieces map to the browser
| Desktop | Web |
|---|---|
| frame render thread pool (`ui-egui/src/frames.rs`) | frame workers (below): `Frames::pump` hands queued frames to them, most important first (the viewer's frame, then prefetch); frames the GPU compositor can keep on the GPU render on the page. Without workers, queued frames render on the UI thread after each egui frame within 40 ms (24 ms while playing) |
| frame render thread pool (`ui-egui/src/frames.rs`) | frame workers (below), each with its own WebGPU device: `Frames::pump` hands queued frames to them, most important first (the viewer's frame, then prefetch); frames without effects that the page's GPU compositor can keep on the GPU render on the page. Without workers, queued frames render on the UI thread after each egui frame within 40 ms (24 ms while playing) |
| Render Queue on a background thread | a Web Worker running a second engine instance (below); progress streams back, the page never waits |
| tracker / mask tracker / Warp Stabilizer / 3D Camera Tracker / Roto Brush Freeze threads | the same workers; the analysed property group comes back as one undo step |
| `rayon` parallel loops (raster, effects, export batches) | rayon's global pool falls back to the calling thread when threads are unavailable: same code, serial |
@@ -64,7 +65,9 @@ the page's thread), `?nosw` (don't register the service worker).
| system fonts | not scanned; the bundled fonts (Inter, Noto Serif, JetBrains Mono) are always there |
| TCP control channel / MCP | `window.effectcraft` (below) |
| cpal audio output | Web Audio (below) |
| GPU compositor on the desktop's wgpu device | the same compositor on eframe's WebGPU device (below) |
| GPU compositor on the desktop's wgpu device | the same compositor on eframe's WebGPU device, and GPU effects on each frame worker's own WebGPU device (below) |
| disk cache folder (Settings ▸ Disk ▸ Disk Cache) | viewer frames in the Origin Private File System, written by the frame workers (below) |
| (the file system's own tools) | the storage manager: Settings ▸ Disk ▸ Browser Storage and `storage.*` (below) |
### Persistence
@@ -144,17 +147,26 @@ of the project:
before prefetch); a request for a revision the worker does not hold is refused, a worker that
lost its replica asks for the whole project again (`FrameReply::Resync`).
Frames the WebGPU compositor can finish alone still render on the page (they stay on the GPU, no
readback); everything else (CPU-only effects, 3D runs, the Software Only renderer, WebGL2) goes to
the workers. If no worker is usable, frames render on the page's thread as before.
`effectcraft.info().frameWorkers` reports `{workers, alive, busy, rendered, failed, lastMs,
syncs, syncBytes}`.
Frames without effects that the WebGPU compositor can finish alone still render on the page
(they stay on the GPU, no readback); everything else (frames with effects, 3D runs, the Software
Only renderer, WebGL2) goes to the workers. If no worker is usable, frames render on the page's
thread as before. `effectcraft.info().frameWorkers` reports `{workers, alive, busy, rendered,
failed, lastMs, syncs, syncBytes, gpu, gpuFrames, lastPasses, diskHits}` (`gpu`: each worker's
adapter, `{cpu: why}` without one, `null` until it reported).
**Why workers and not wasm threads:** shared-memory threads (SharedArrayBuffer + atomics, a
`wasm-bindgen-rayon`-style pool) need the standard library rebuilt with atomics (`build-std`,
nightly Rust); the repository builds on stable, so each worker runs its own engine instance with
its own memory. The page is still cross-origin isolated (`--serve` and the service worker send
COOP / COEP), so a threaded build can be added later without changing the deployment.
**Threading design: one engine instance per worker (decided, M13.24).** Shared-memory threads
inside one instance (SharedArrayBuffer + atomics, a `wasm-bindgen-rayon`-style pool) need the
standard library rebuilt with atomics (`-Z build-std`, nightly Rust) and every crate's statics
made thread-safe for the browser's rules (no blocking on the page's thread). The repository
builds on stable Rust, so concurrency comes from workers that each run their own engine instance
with their own memory: job workers (renders, analyses), frame workers (viewer frames, each with
its own GPU device) and the page. What crosses between them is explicit: project diffs, footage
bytes, frames, Roto Brush segmentations, cache keys. The cost is memory per worker (a project
replica, the footage it reads, a layer cache) and serial loops inside one frame (rayon's pool
falls back to the calling thread). The page is still cross-origin isolated (`--serve` and the
service worker send COOP / COEP), so a threaded build (`build-std` with `+atomics,+bulk-memory`,
`wasm-bindgen-rayon`) could be added later without changing the deployment; it would parallelise
the loops inside a frame, not replace the workers.
### Media Browser
@@ -189,13 +201,94 @@ key press.
eframe starts on WebGPU where the browser has it (WebGL2 otherwise), and the GPU compositor
(`effectcraft-gpu`) runs on the same device: viewer frames are composited by compute shaders and
drawn straight from their texture, with no readback. Steps that need the CPU mid-render (3D runs,
adjustment layers, CPU-only effects) fall back to the CPU renderer for that frame, because the
page's thread can't wait for a GPU readback. The Info panel's pixel readout and the eyedroppers
read GPU frames back asynchronously (`Gpu::read_display_async`: the pixels arrive a frame
later). On WebGL2, or without a usable adapter, everything renders on the CPU.
drawn straight from their texture, with no readback. The Info panel's pixel readout and the
eyedroppers read GPU frames back asynchronously (`Gpu::read_display_async`: the pixels arrive a
frame later). On WebGL2, or without a usable adapter, everything renders on the CPU.
`effectcraft.info().gpu` reports `{compositor, viewerOnGpu}`.
**GPU effects run in the frame workers.** A browser never lets JavaScript (or wasm) wait for a GPU
readback, not even in a worker: a buffer maps only once the thread returns to its event loop.
The CPU renderer, which runs GPU effect chains and reads their results back mid-frame, therefore
can't use the page's device. Instead each frame worker opens its own WebGPU device
(`navigator.gpu` in dedicated workers: Chromium, Safari Technology Preview; `Gpu::request_deferred`)
and renders with **deferred readbacks** (`crates/gpu/src/deferred.rs`):
1. Every readback the renderer needs is keyed by what determines its pixels: a GPU effect chain by
its input buffer's pixels and the chain's parameters, the top-level frame by comp, time and
render options.
2. A key seen for the first time records the GPU work, starts an asynchronous copy (`map_async`)
and *misses*: the step returns a cheap placeholder (its input), and the pass is marked missed.
From the first miss to the end of the pass, the layer cache drops inserts
(`LayerCache::set_gate`), so no placeholder-derived buffer is kept.
3. The worker awaits the device (`Gpu::settled`) and renders the frame again
(`FrameServer::resume`); keys whose bytes arrived are served from them. A pass without a miss
is the frame. Chains that depend on other chains (a precomp with effects inside, effects that
read other layers) take one more pass per level; independent chains resolve together. A chain
whose kernels read data that missed while they ran (through the effect host) is not started,
so a result is only ever computed from genuine inputs. After 24 passes a frame renders on the
worker's CPU.
The whole frame renders on the GPU (the compositor walk with GPU effects, then one readback) when
it can; steps that would need a mid-walk readback (an Advanced 3D run the kernels decline, CPU-only
effects on an adjustment layer) make the frame composite on the CPU instead, with its GPU effect
chains still on the GPU. Backend **Auto** works in the workers: it picks CPU or GPU per comp from
the frames' total times across their passes (`AutoPick`), as on the desktop. Without WebGPU in
workers (Firefox, older Safari) or with `?nogpuworkers`, the workers render on their CPU, as
before. A frame with effects goes to a worker when the workers have a GPU, so the page never runs
an effect on its thread. Particles and Advanced 3D scenes (which read back without a key) simulate
and rasterise on the worker's CPU.
`effectcraft.workerFrameCheck({time?, scale?, backend?})` renders the active comp's frame in a
worker (its GPU by default) and on the page's CPU and reports the differences (`{maxDiff,
meanDiff, over4, workerMs}` in 8-bit levels): the smoke test uses it to check the GPU workers'
pixels.
### Disk cache
Settings ▸ Disk ▸ Disk Cache works in the browser, backed by the **Origin Private File System**
(`src/diskcache.rs`): viewer frames are kept under `effectcraft-cache/v1/frames/<32 hex>.ecc`
between visits, in the desktop cache's format (`disk_cache::frame_entry`: 8-bit premultiplied
RGBA, LZ4, a checksum), keyed by the same content hash (the comp and everything it uses, frame,
scale, view, render options).
- A render request to a frame worker carries the frame's disk key; the worker posts the frame,
then writes the entry with a **synchronous access handle** into a temporary file moved into
place (`cacheWrite` in `js/host.js`; where `move()` is missing, the entry is written in place
and its checksum catches a partial file), and reports `FrameReply::Stored`.
- The page keeps the index (`DiskIndex`, shared with the desktop's cache): rebuilt at startup from
the folder listing (oldest files first, stale temporaries removed), updated by the workers'
writes and by reads. The size limit is Maximum Disk Cache Size, at most half the origin's quota;
the least recently used entries are deleted beyond it.
- A frame whose key is indexed is read and decoded on the page (asynchronously) instead of being
rendered; a damaged or missing file is a miss and is forgotten. The timeline's blue cache bar
shows the frames on disk.
- `cache.diskStats` reports the browser's cache (`{enabled, available, loaded, entries, bytes,
maxBytes, hits, misses, writes, evictions}`); Empty Disk Cache / `edit.purge {what: "disk"}`
clears it. `effectcraft.info().diskCache` has the same.
Only frames are cached on disk in the browser: the desktop also keeps slow layer buffers, but a
layer lookup happens in the middle of a synchronous render, where the browser's file system can't
be read.
### Storage manager
Settings ▸ Disk ▸ **Browser Storage** (shown in the browser only) reports where the data lives
(Origin Private File System, IndexedDB or memory), the origin's usage and quota
(`navigator.storage.estimate()`), what the store holds (projects, imported media, auto-saves:
count and size) and the disk cache, whether the storage is persistent (with **Request Persistent
Storage**, `navigator.storage.persist()`), and **Clear** buttons for the disk cache, imported
media, auto-saves and saved projects. Agents use the same through engine commands
(`crates/engine/src/storage.rs`, the session's `StorageHost`):
| Command | |
|---|---|
| `storage.info` | `{available, backend, usage, quota, persisted, files: {projects, media, autoSaves: {count, bytes}}, diskCache: {...}}` (`{available: false}` on the desktop) |
| `storage.persist` | ask for persistent storage; `persisted` in a later `storage.info` says whether it was granted |
| `storage.clear {what: diskCache\|media\|projects\|autoSaves\|all}` | delete them (settings stay); `{cleared, entries, bytes}` |
The widgets have automation ids `settings.storage.backend`, `.usage`, `.diskCache`, `.persist`
and `.clear.<what>`.
### Offline and install (PWA)
`manifest.webmanifest` (name, icons, standalone display) makes the app installable; `sw.js`
@@ -222,7 +315,8 @@ with the method's `result`, errors reject with the message.
| `effectcraft.listStored()` | `{backend, usage, quota, persisted, pending, files: [{path, size, modified}], config: [name]}` |
| `effectcraft.removeStored(path)` | delete a stored file |
| `effectcraft.flush()` | resolves once every change is written to browser storage |
| `effectcraft.info()` | graphics backend, `gpu`, `storage`, `audio` (`{state, sampleRate, backend, posted, played, underruns}`), `workers`, `restored`, `webgpu`, `serviceWorker`, version, `crossOriginIsolated`, load timings |
| `effectcraft.info()` | graphics backend, `gpu`, `storage`, `audio` (`{state, sampleRate, backend, posted, played, underruns}`), `workers`, `frameWorkers`, `diskCache`, `restored`, `webgpu`, `serviceWorker`, version, `crossOriginIsolated`, load timings |
| `effectcraft.workerFrameCheck(params)` | a frame rendered in a frame worker (its GPU by default) against the page's CPU render: `{width, height, maxDiff, meanDiff, over4, workerMs}` |
`window.effectcraftLoad` holds `{wasmMs, readyMs}` (module fetch + compile, and until the app runs).
@@ -251,7 +345,12 @@ node apps/effectcraft-web/tests/smoke.mjs --url http://127.0.0.1:8765/ --out tar
It also checks the M13.10 paths: viewer frames rendered in frame workers while scrubbing with the
CPU renderer (project synced as diffs, event-loop gaps under 400 ms), a `wait: true` render that
replies when done while the page keeps running, and the Media Browser over browser storage.
replies when done while the page keeps running, and the Media Browser over browser storage; and
the M13.24 ones: GPU effects (Gaussian Blur, Glow) rendered on the frame workers' own WebGPU
devices in passes while the page's event loop stays free, the worker's GPU frame matching the
page's CPU render (`workerFrameCheck`), the storage manager (`storage.info`, `storage.persist`,
the Settings ▸ Disk widgets), and the disk cache: frames written to the Origin Private File
System, served from it after a reload without rendering, and cleared with `storage.clear`.
Native unit tests cover the storage model (`apps/effectcraft-web/src/store.rs`: write
coalescing, file table, `ConfigStore` / `FileOps`, auto-save and crash recovery through the
@@ -261,15 +360,24 @@ propagation through an offload streaming segmentations), the frame-worker protoc
(`crates/engine/src/remote.rs`: diff / patch, a replica following edits renders the same pixels;
`crates/ui-egui/tests/ui_web_offload.rs`: the frame cache through a remote renderer, `wait: true`
deferred until the offloaded job ends) and the Media Browser's virtual tree (`media_browser.rs`,
`tests_panels.rs`).
`tests_panels.rs`). M13.24: deferred readbacks on a real device (`crates/gpu/src/tests_deferred.rs`:
whole-frame GPU and CPU-composited frames in passes match the CPU, placeholders never reach the
layer cache; `deferred.rs`: keys, late readbacks of an older frame dropped), the frame server's
passes and Auto timing (`remote.rs`, a deferred stand-in accelerator), the browser's routing and
disk cache (`crates/ui-egui/tests/ui_web_gpu_storage.rs`: a GPU frame worker renders effect frames,
the page's GPU keeps frames without effects, frames stored under their disk keys come back after a
"reload" without rendering, the blue cache bar sees them; the storage manager's widgets and
commands), the disk index (`disk_cache.rs`), the store's usage by kind and clearing (`store.rs`),
`storage.*` (`storage.rs`), and every WGSL float literal fitting f32 (browsers reject the whole
module otherwise; `tests.rs`).
## Gaps
- No shared-memory threads: each worker is a separate engine instance (memory per worker: the
project replica and the footage it reads).
- GPU effects run only in the desktop GPU path: in the browser, effects run on the CPU (inside
workers for renders) because the page's thread can't wait for mid-render readbacks.
- No disk cache (Settings ▸ Media & Disk Cache) in the browser: the layer cache stays in memory.
project replica and the footage it reads); see the threading design above.
- GPU effects run in the frame workers (viewer frames, RAM preview). Render Queue renders and
analyses in job workers still run on their CPU (their loops are synchronous); particles and
Advanced 3D scenes in frame workers too. Browsers without WebGPU in workers render frames on
the workers' CPU.
- The browser's disk cache holds frames, not layer buffers.
- Cancelling an analysis drops its partial result (the worker is terminated).
- Stored media count against the origin's storage quota; `effectcraft.listStored()` shows usage,
`removeStored` frees it (there is no storage manager in the UI yet).