egui-wgpu creates its device on GL with WebGL2's limits (no storage buffers), so the desktop
app on FreeBSD built the compositor's pipelines on it: validation errors (only logged in
release builds) and wrong frames. GpuContext::new now checks the limits its pipelines bind
(storage buffers and textures per stage, bind groups, workgroup sizes) and returns Err, which
keeps the app on the CPU. ui_gpu skips its GPU-vs-CPU comparison when that happens on GL.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FreeBSD's CI VM (Mesa llvmpipe over GL) failed 22 effectcraft-gpu tests:
- Advanced 3D's camera-depth target was R32Float, which GLES can't render to without
EXT_color_buffer_float: create_render_pipeline failed validation (release builds only log
that and draw nothing). The target is now R32Uint holding the f32 bits (renderable on every
backend, lossless), and GpuContext checks the adapter's format features (render attachment,
blendable colour) and independent blending up front; when they're missing the render
pipelines are never built and Advanced 3D renders on the CPU.
- Signed `%` in the shaders: naga emits GLSL `%`, undefined for negative operands, which broke
the VR effects' horizontal wrap and Cell Pattern's tiling. A shared `imod` never takes `%` of
a negative number.
- asin/acos: Mesa's are coarse approximations (~1e-4 rad), visible in every lens/sphere/cylinder
mapping. `asin_p`/`acos_p` (Cephes' asinf polynomial) are f32-accurate on every backend.
- "Could not lock adapter context": the GL backend has one context lock per adapter with a
timeout, so tests driving devices concurrently panic. Every gpu test helper that hands out a
device holds one process-wide lock for the calling test; ui_web_job_gpu's two device tests
share a lock too.
- On GL, up to 1 % of pixels may flip across a decision boundary (division through a
reciprocal, unfused fma); the GPU-vs-CPU comparisons allow that there only.
Also adds the FreeBSD workflow (from the FreeBSD CI PR) so this branch is checked there.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- audio::AudioScrub: an open output that plays a faded snippet (one frame, 30–100 ms) of the
comp's mix at each new frame, replacing what is still queued; holding still repeats nothing.
The mixing is shared with the preview feeder (mix_block); AudioFeed::clear.
- The Timeline ruler: Ctrl/Cmd-drag scrubs audio (EffectcraftApp::scrub_audio opens the output
through the audio device hook on first use). The Audio panel meters follow the snippets; an
idle second closes the output.
- playback.scrubAudio {time?} for agents.
- Tests: ui_audio_scrub (snippet length, fades, same-frame no-op, replace not pile up, silent
comps, Cmd-drag in the ruler, idle close) with a fake device. Docs: parity.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- commands::markers::nested_markers: a precomp layer's comp markers at their times in the
containing comp (start and stretch in closed form; with time remapping or a stretch over
protected regions, the first frame whose source time reaches them), within In–Out.
- markers.nested {layer}: the same list for agents.
- Timeline: drawn on the precomp bar as outlined read-only markers (positions cached per
revision), hover names them, double-click opens the nested comp at the marker; automation id
timeline.layer.<id>.nestedMarker.<i>.
- Tests: engine (start, stretch, trim, time remap, non-precomp error), UI (draw, label, open).
Docs: parity, control protocol.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- PanelKind::Viewer(n): more Composition viewers (panels::viewers). One is active (the full
interactive viewer, showing the active comp); the others show their own comp's frame at that
comp's current time (CPU or GPU frames, their own textures) and become active on a click or
their tab, their comp becoming the active comp.
- view.newViewer (View menu, the Composition panel menu, Alt+Shift+N): a new viewer of the
active comp next to the one in use, which is locked. Opening a comp while the active viewer
is locked shows it in an unlocked viewer, or a new one when every viewer is locked. Closing
the active viewer hands over to another with its comp. Tabs are named after their comp, with
their own lock and swatch; Preview full screen maximizes the active viewer.
- UI state: viewers (comp per viewer) and active_viewer (serde).
- Tests: ui_viewers (lock on New Viewer, routing, click to activate, all locked → new viewer,
close hand-over, tab titles; an ignored snapshot). The bounded-drag frames test asserts what
holds when freed addresses are reused. Docs: parity, control protocol.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- FrameKey's identity is the comp's content (frames::comp_content), not the project revision:
an edit keeps the cached frames of comps it doesn't touch and undo finds the frames of the
state it returns to. The identity hashes the addresses of the comps a comp draws (~0.1 ms on
the 1500-layer test project; the full content hash took ~117 ms), by value the footage, solids,
proxies and project settings; with expressions, every item. Frames::content_of keeps a
project snapshot per identity with frames (or in flight), so Arc::make_mut always copies an
edited comp and its address can't be reused meanwhile; past 64 identities the least recently
used go with their frames (long drags).
- drop_stale is gone (frames of other contents are evicted least-recently-shown first).
- disk_cache::comp_content_key covers items' proxies and Use Proxy (it served stale frames).
- Tests: frames (content keys, edits elsewhere and undo, bounded drags, in-place edit safety),
ui_ram_preview (end to end), the disk cache proxy key. Docs: roadmap (M4 done), parity.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The Timeline shows one tab per open comp (dock DocTab). Clicking a tab shows that comp; ×
or middle-click closes that comp's Timeline (comp.close), not the panel. Opening a comp brings
its viewer and Timeline tab to the front.
- Project panel: a double-click on an item's name opens it (a comp gets its own tab) instead of
renaming; Enter and the context menu rename.
- Tab drags: over a tab strip an insertion mark shows where the tab lands, and it lands there
(Layout::insert_tab), which also reorders a group's own tabs; side zones are unchanged.
- Tests: ui_tabs (double-click, tab switch / close, drag reorder and cross-group insert; an
ignored mid-drag snapshot); the Timeline close test follows the new behaviour.
- Docs: parity, control protocol.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- effect.pickColor {effect, param|prop, x, y (layer px), average?}: the colour of the effect's
input at a point, set on the parameter (one undo step). Keying effects' Effect Controls
eyedroppers use it, since the shown frame is already keyed; Ctrl/Cmd+click averages 5x5.
- effects::aliases / name_matches: Effects & Presets and effect.list (MCP list_effects) find
Key Light by After Effects' name ("Keylight (1.2)").
- End-to-end test: a green-screen plate with an uneven screen, spill and soft edges keyed via
effect.apply "Keylight (1.2)", pick, Clip Black / White and a render; UI eyedropper test.
- Docs: parity, agents (green-screen recipe).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- J / K (Go to Previous / Next Keyframe) and Ctrl+Alt+A (Select All Keyframes) act on what the
Timeline shows, as in After Effects: the keys of its revealed properties, layer and comp
markers and the work area (they used every property of the selected or all layers, revealed
or not). The commands take `visible: [{layer, prop}]`, which the Timeline fills in; agents
calling them without it keep the previous behaviour.
- Graph Editor ▸ Auto-Select Graph Type (an "Auto" button, on by default as in After Effects):
the speed graph when every property shown is spatial (Position, Anchor Point…), else the
value graph; choosing Value or Speed turns it off.
- Docs: parity, agents.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Double-clicking a keyframe of a numeric property (or its context menu ▸ Edit Value…) opens
the property's value dialog for that key, as in After Effects (it only moved the current
time); other keys still move the current time to them.
- The keyframe context menu has Select Equal Keyframes, Select Previous Keyframes and Select
Following Keyframes (`keys.selectEqual` / `selectPrevious` / `selectFollowing`).
- Roving keys draw as small dots in the Timeline, like After Effects.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- `keys.transform {fromStart}`: each step of a merged drag gives the whole transform since the
drag started and applies to the keys as they were then. Keys squeezed onto one frame on the
way (Alt-drag scaling, the transform box) are no longer merged for good, and a slow
transform-box move, whose per-frame offsets rounded to nothing, now moves the keys.
- Dragging a key in the Graph Editor moves every selected key in time and value (it moved only
the grabbed one), by how far the pointer went from where the button went down (the key no
longer jumps under the pointer); Shift keeps the drag to the time or the value axis.
- Shift+click toggles a key in or out of the selection; Ctrl+click switches Linear ↔ Auto
Bezier and Ctrl+Alt+click toggles Hold, as in the Timeline.
- The transform box and the Timeline's Alt-drag scale send whole-gesture values.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A drag (keys.move steps sharing a merge key) applies its total offset to the keys as they
were before it (`Session::key_move`, `prop::shift_keys`): dragging over another key of the
property no longer deletes it; only the drop position replaces a key, as in After Effects.
- Keys of time-stretched layers move with the pointer: offsets are applied in comp time and
snapped to comp frames (drags and Alt+Arrow nudges; they moved by the layer-time distance).
- Shift+click toggles a key in or out of the selection (`keys.select {toggle}`); Ctrl+click
switches Linear ↔ Auto Bezier and Ctrl+Alt+click toggles Hold.
- Keys of locked layers can't be selected (clicks, marquee, Select All Keyframes), so nothing
edits them.
- Toggle Hold off returns an eased key to Bezier (it always restored Linear).
- Shift-dragging a key snaps to everything the current time snaps to — other keys, layer in /
out points, layer and comp markers, the work area — and a tooltip shows the key's time and how
many frames it moved.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The windowing layer delivers Ctrl+C, Ctrl+X and Ctrl+V as clipboard events instead of key
presses, and the shortcut dispatcher only read key presses: in the app, copying and pasting
keyframes, layers and effects with the keyboard did nothing (tests passed because they inject
key presses). Clipboard events now map back to their keys with the modifiers held (so Ctrl+Alt+C
and the other variants work too), except while text is edited in the viewer, which handles them
itself. Ctrl+V sends no event while the system clipboard is empty, so copying now also puts a
line naming what was copied on it ("EffectCraft: 3 keyframes").
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dragging a keyframe moved it a frame or two and then let go: the key's widget id includes its
time, so after the first move it was a new widget and egui ended the drag. The Timeline now
follows the whole gesture itself (`KeyDrag`: from where the button went down, by whole frames,
every selected key, one undo step), like After Effects. Holding Shift snaps the grabbed key to
the current time, the work area and composition markers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Following AGENTS.md's Never crash rules for the M3 / M4 additions:
- Cache Frames When Idle panicked (`clamp` with min > max) when the work area was inverted
(start set past the end); an empty work area now has nothing to cache.
- `file.newCompFromSelection`: the sequence overlap times the layer count overflowed for a huge
`overlapDuration` (wrapping to a wrong comp length without overflow checks); negative or
non-finite values fall back to 1 s and the product saturates. The lead item is read with
`get()` instead of indexing.
- Layer Settings' enablement reads the first selected layer with `first()`.
- Regression tests for both (each failed before the fix).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Composition ▸ Preview ▸ Cache Frames When Idle was only a saved checkbox. After a second
without input or edits it now renders the work area's frames (from the current time on, then
from the start) into the RAM preview, a few at a time behind the viewer's own frame, until the
work area is cached or the RAM preview budget is full.
Docs: preferences.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Viewer Resolution Auto rendered at half its scale while a preview played, so frames cached
while scrubbing were not played and frames cached by the preview were re-rendered once it
stopped. Auto now follows the zoom alike playing and paused (Fast Previews still lowers it
while dragging).
- Cache Before Playback waited for every frame of the range: a range larger than the RAM
preview budget evicted its first frames and never started. The range is now cut to the
frames that fit (90 % of Settings ▸ Memory & CPU's budget) with a message, then cached and
played.
- The Info panel shows the frame rate a playing preview achieves and whether it is real time
(`Playback::achieved_fps` / `real_time`, automation id `info.previewFps`).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- `FrameKey` carries a hash of the render options (`frames::opts_hash`): switching Fast
Previews (Draft / Fast Draft) or Realtime Shadows no longer shows frames rendered the other
way.
- Eviction drops the least recently shown frame (`Frames::get` touches it), not the oldest
inserted: a looping preview a little over budget no longer evicts the frame it shows next.
- Frames of earlier project revisions are dropped on every change (`Frames::drop_stale`); they
could never be shown again but held memory (and GPU textures) until evicted.
- The green cache bars (Timeline, Preview panel) count the frames of exactly what the viewer
shows — scale, 3D view / region of interest and options (`EffectcraftApp::shown_series`) —
instead of any frame at that scale.
- Playback loops compute the key's fixed part once (`EffectcraftApp::frame_series`).
- Docs: architecture.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- GPU in browser job workers: each job worker opens its own WebGPU device
(deferred readbacks); the Render Queue export and the analyses' frame loops
are futures (render::passes, offload::run_request_async) whose frames
render in passes, awaiting the device between them. GPU particles and
Advanced 3D read back under keys too. workers.jobs reports passes and
readbacks; the smoke test measures them.
- Layer buffers in the browser's OPFS disk cache: frame workers back their
layer caches with a PrefetchStore; the page plans what to fetch before a
request from recent misses (LayerPrefetch over the shared DiskIndex).
- Templates embed footage (default on, size-capped with a warning) and
creating from a template extracts it next to the new project / into
browser storage.
- ICC LUT profiles (lut8/lut16/lutAtoB/lutBtoA, from the public ICC spec) in
Simulate Output > My Custom RGB, baked into a 3D LUT for the viewer.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 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 <noreply@anthropic.com>
- footage.check: background footage check after File > Open (Progress panel), not an undo step
- timeline: track-matte / parent menus built only while open (was O(n^2) per frame);
expression-row lookup per layer; panels hold Arc<Comp> snapshots instead of deep clones
- Project panel: virtualised vertical scrolling, selection reveal, thumbnails rendered off
the UI thread
- effectcraft-cli bench --ops (engine::perf + ui-egui::bench)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- command.describe / list_commands {schemas} give each command's params JSON Schema;
MCP describe_command, add_effect, list_effects and get_state (app.capabilities)
- effectcraft-cli exec --list [--schemas]
- headless UI audit: every panel kind's interactive egui widgets are covered by
automation ids (fixed: timeline navigator, label swatch, bar trim edges, group and
property name cells)
- command lookup is indexed
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Effect Controls: Lumetri RGB and hue/saturation curve graphs, Colorama's
palette wheel, Glow's colour map strip and Arbitrary Map curves, Reshape
correspondence buttons and viewer handles (all prop.set, one undo step per
gesture, automation ids). Preview panel scrolls; Composition/Timeline tabs
get close, label swatch and viewer lock; Effect Controls tab the layer
swatch; dropdowns elide long text. Fix the browser start (no default disk
cache folder on wasm32). Browser smoke test covers frame workers, waiting
renders and the Media Browser.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Viewer frames render in long-lived Web Workers holding a project replica
(engine::remote: structural JSON diff/patch, Mirror, FrameServer); the
frame cache hands jobs to a RemoteFrames renderer (UI-thread fallback).
Roto Brush propagation runs in a job worker and streams segmentations
back (FrameSeg binary form), relayed to frame workers. Control-channel
wait:true commands whose job is offloaded reply when it ends. The Media
Browser browses a pluggable Browser: the file system on the desktop;
browser storage and File System Access folders on the web.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Preview panel: five shortcuts (Spacebar, Shift+Spacebar, Numpad 0, Shift+Numpad 0,
Alt+Numpad 0) each with saved options (include video/audio/overlays/layer controls, loop,
cache before playback, range incl. play around current time, play from, frame rate, skip,
resolution, full screen, play cached frames, move time to preview time), persisted in
prefs; playback.settings.get/set; the playback loop follows the preview plan.
Align panel: layer.align {edge, to} and layer.distribute {mode} engine commands (bounds
through transform and parents, one undo step); Align Layers to dropdown and Distribute row.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Render Settings: field render with 3:2 pulldown phases, effects/solo/guide
layer/color depth/frame blending/motion blur overrides, custom time sampling
(duration), storage overflow through a quota hook. Output Module: crop
(edges or region of interest), resize (presets, lock aspect, quality),
straight/premultiplied/alpha-only channels, post-render actions, include
project link, audio channels and PCM formats, WebM bitrate and key frame
interval. Render Settings / Output Module templates with built-ins and
defaults per slot (Edit > Templates dialogs), render logs next to the
output, Notify on completion. All through renderQueue.* commands and
persisted in the project.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- ui-egui: scriptui_view draws script windows at engine layout bounds; PanelKind::ScriptPanel; File ▸ Scripts and Window menu list scripts/panels (egui + native menus)
- examples/plugins/posterize-bands (WebAssembly, API v1); host plug-in tests; desktop loads <config>/Plug-ins
- MCP: script_ui and history tools; docs/plugins.md, agents.md, parity, architecture, ATTRIBUTION
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reconciled with main's keyframe labels (same Keyframe::label field; keys.setLabel and
keys.selectLabelGroup from main are kept, M14.5's duplicates removed; Edit ▸ Label on
selected keys delegates to the label helper), proxies (RenderOpts::proxy next to
nested_switches/draft_shadows), offloaded renders (Work carries nested_switches), wasm
audio pump (preview mix rate), variable font axes and mask feather points.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>