* Disk cache: record the source files each frame reads Part of #3184 (step 1): renders now report which footage files they read, each with its identity (size and modification time) as read. Nothing uses the list yet: images, cache keys and cache behaviour are unchanged. - render: `reads` module with `FileStamp`, `ReadFile`, `ReadFiles` (sorted, deduplicated, bounded; marked incomplete when it can't vouch for every file) and the thread-safe `ReadLog`. `Renderer::reads` collects a frame's files; `FootageSource::recording` lets a source note what it reads. `file_stamp` uses the same identity. - Layer cache entries keep the files their buffer was made of and report them on every hit; derived entries (styles, blend space, flattened) keep their base's. Buffers from the disk cache or the browser store don't know their files yet and mark the list incomplete. - Media pool: every file read notes its identity, taken from the file it opens. Decoded frames, opened movies, GIFs, models and EXR channels keep it with the cache entry, so a frame served from memory reports its file without touching the disk. - Viewer frame worker collects each frame's list and carries it to where the disk entry is written (step 2 stores it). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Automation: confined footage passes on the files it reads Part of #3184: ConfinedFootage forwards FootageSource::recording to the source it wraps, so renders of an automation session list their files instead of coming back incomplete. The read tests also render through the viewer's CancellableFootage wrapper. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Disk cache: frame entries keep their source files, comp key from memory Part of #3184 (step 2). Fixes #3175: opening a comp no longer stamps every footage file of the project before its first frame can show. - The comp's content key (`disk_cache::comp_content_key`) is taken from the project in memory alone: no footage or proxy file is stamped, no `VirtualFileId` is asked. `comp_content_key_with` is gone; `frame_key` hashes a new entry version, so entries of the earlier format are never looked up again (LRU evicts them). - Frame entries (formats 2 and 3) store the files the frame read (`EntryFiles`: size and modification time as read; size and content hash through `VirtualFileId` for files from memory; missing files), then the pixels. Formats 0 and 1 read as misses and are deleted. - Check on hit: `get_frame`/`get_frame_f16` look only at the listed files (one metadata call each) on the frame worker. A changed, back or unreadable file, or a damaged entry, is a miss: the entry is deleted and the frame rendered and stored again. - A frame whose file list is incomplete is shown but not stored. The RAM preview keeps each frame's files, so `persist_missing` writes only frames with a complete list. - Remote frame worker: `FrameServer` records each frame's reads (`take_files`); the web worker stores them with the entry and the page checks them through the file table before using it. The browser's media epoch existed only for the old key and is removed. Export does not use frame entries. - The cache indicator still only asks whether an entry exists. Tests: entry format round trip and damage, the check per file, and in the viewer: a changed file re-renders exactly its frames (and a reopen reads the rest from disk); a frame checks only its own files in a project with a large unused sequence and the key looks at no file; missing, unreadable files and damaged entries never serve a stale frame. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Disk cache: layer entries keep their source files, no footage salt Part of #3184 (step 3 of 4). Layer buffers on disk (and in the browser's layer store) now carry the files they were made of, like frame entries since step 2, and are checked against exactly those files when used. The project-wide footage salt is gone: nothing stamps every footage file any more. - Layer entry format 4: the buffer's EntryFiles, then the pixels. A hit checks only the listed files (metadata, or the VirtualFileId hook for files from memory; Missing must still be missing); a change, a damaged entry or the old format 0 is a miss and the entry is deleted, so the re-rendered buffer replaces it. A hit reports its file list, so frames made of buffers from disk are complete and get stored again. - Buffers whose files can't all be named are not written. - Layer disk keys: layer_disk_key = hash of the layer entry version, the renderer version and the layer cache key (which names the footage items with their paths and settings, from memory). - PrefetchStore checks on use; an entry made of changed files is dropped and forgotten, so the new buffer is queued over it. FrameServer passes its virtual-file hook to its layer cache. - Removed: footage_salt(_with), file_stamp, footage_stamp, set_disk_salt, set_store_salt, Session::refresh_disk_salt, poll_disk_salt, footage_fingerprint, disk_salt, disk_salt_background, disk_salt_job, DiskSaltJob, the UI's per-frame poll, the salt in FrameServer and in the availability worker (with its cache_enabled plumbing). The disk cache is attached as soon as it is configured. - Docs: architecture.md, web.md. Tests: layer entries check their files (size, time, missing/back, folder, damaged, old format, incomplete); virtual files; prefetch store; media tests with a real pool across restarts (only the changed file is read, projects with the same item ids never share an entry, missing, unreadable and damaged never serve stale buffers); engine tests that the cache is attached at once and edits do no file work (#1926, #3106). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Disk cache: no-salt worker test, sequence paths in the frame-blend key Part of #3184 (step 3 of 4). - A frame-blended footage frame's layer-cache key now hashes the sequence's file list: with no project-wide salt, two projects whose items share an id and a first frame must not share its disk entry. - Test: a frame worker syncing, patching and rendering a project with an unused 5000-frame sequence asks its file hook only about the file its buffer reads (the salt asked about every file at every sync). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Disk cache: the comp key covers what its expressions can read With an expression anywhere in the project, every comp's disk key hashed every item of the project, so any edit (another comp, an unused footage item) threw away every comp's frames on disk. The key now covers what the comp can draw plus what the expressions among it can reach (Project::pixel_reach): - without expressions: the items the comp draws, without silent layers, plus the footage its precomp layers swap in (Media Replacement); - with expressions: every layer of the comps they reach, their markers, the comps and footage they name with string literals (comp("Name"), footage("Name")) together with which items have those names now, and the footage named like a reached source (its data is read by name); - the whole project when an expression computes a name, uses the object model's constructors or internals, dynamic code, computed keys or writes, or text the scanner can't read for sure (regex literals, template strings, escapes): expression_reach errs towards reaching more. Markers count only where an expression can read them (#1083). Memory only, no file is looked at. Tests: scanner and reach unit tests in the project crate; engine tests for the key (own-comp expressions keep the key through edits elsewhere, comp("Other") follows Other and not a third comp, a second comp taking the name counts, a computed name counts every item, markers read from another comp); a ui-egui test that the disk frames outside an expression layer are carried when the comp it names changes. Part of #3184. Part of #1492. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Disk cache: Reload Footage drops the entries of its files, docs File > Reload Footage now deletes the frame and layer entries whose file lists name the reloaded item's files (DiskCache::drop_files), so a file replaced by one with the same size and modification time, which the check on hit can't tell apart, is read again. The index knows keys, not files: the writer thread reads the files section at the start of each frame and layer entry (not the pixels) after the writes queued before it, and deletes the entries that list one of the files. Until it is done, an entry made of one of them is a miss and frames aren't linked to new keys. The entries are gone for good, so they don't come back after a restart. Automatic reloads don't drop anything: they follow a changed stamp, which the check on hit catches. The browser needs nothing (its file identities are content hashes). Removed: disk_cache::seal, which steps 2 and 3 left without callers (the tests use seal_format). Updated the module, seal_format and forget_media docs, and docs/architecture.md / docs/web.md describe the final design: what is cached, keys from memory, file lists checked on hit, nothing scans the project, incomplete lists stay in memory, Reload Footage drops entries, edits keep the frames they can't change. Tests: render unit tests (same size and time passes the check but not the drop; other files' entries stay; an entry queued before the drop is dropped; a layer entry too; written again afterwards it is used; the drop survives a restart; a pending drop is a miss), and an engine test through file.reloadFootage. Closes #3184. Part of #1492. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Disk cache: file stamps keep the whole modification time A file's modification time was nanoseconds since 1970 as u64: a time before 1970 read as "no time" (two such files of one size looked alike) and times after 2554 were truncated. It is now i128 nanoseconds, negative before the epoch; None only means the file system has no time. The content-key hash writes a presence tag and the full value. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Automation: a refused footage read leaves the render's file list incomplete Footage outside the read roots shows nothing and was noted as no file at all, so a picture made without it had a complete, empty list and could be kept on disk for a later session with other roots. A refused read during `recording` now marks the list incomplete. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Render: a buffer kept by another worker comes with its own files LayerCache::insert returns the files of the buffer it keeps, and Renderer::keep notes them in the render's log and returns them, so a render that uses another worker's buffer lists that buffer's files (a file changed between the two reads then has two identities and the list is incomplete). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Render: sampleImage and text fonts note the files they read EvalCtx carries the render's read log. A sampleImage render records its footage reads, keeps them with its per-thread cached buffer and notes them in the evaluating render's log on every use. Text notes the font files it is drawn with, with the identity their bytes were read with; a family that isn't installed, a fallback or missing glyph, or a font from memory leaves the list incomplete. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Disk cache: frame entries store the whole modification time A frame entry's file list stores a file's modification time as i128 nanoseconds since 1970 (negative before), as the file stamps now keep it; the frame entry format version goes to frame-entry-files-2 so entries in the old layout are not read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Disk cache: layer entries store the whole modification time Layer entries share the frame entries' file list layout, which now stores the full i128 modification time: the layer entry format version goes to layer-entry-files-2. The module doc names the font files text is drawn with among an entry's source files, and what keeps a list incomplete. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Layer cache: buffers that read other layers are keyed by the switches passed down A layer whose effect reads another layer (Gradient Wipe, Set Matte, Blend…) gets that layer drawn by its own renderer. Through a precomp layer, Switches Affect Nested Comps, the switches inherited from the precomp layers above and motion blur on change those pixels, but the key held only the layers' own switches: a buffer made with the setting on was reused (from memory or the disk cache) with it off. Such layers now derive their key with what the renderer passes down. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Layer cache: masked buffers are keyed by the mask motion blur gate A mask with Mask Motion Blur On blurs even when the layer's own switch is off, as long as the render's gate is open (Render Settings motion blur, and with Switches Affect Nested Comps the precomp layers above). The key held the layer's own motion blur state only, so a buffer blurred with the gate open was reused with it closed (and the other way round), also from the disk cache. Masked layer, input and footprint keys now tell a closed gate apart. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Render: cached buffers list what their expressions and substituted characters read - Buffers made under `Renderer::recorded` (layer content, a Time effect's input, an adjustment layer's footprint) evaluate their masks, shapes and effects with the context that notes into the buffer's own read log, so a file an expression reads (`sampleImage`, also at shutter times mask motion blur adds and the cache key doesn't see) is in the buffer's files. - A styled layer served from the cache lists the files of the cached body and passes too, so the buffers made of it (flattened, blending space) don't get a complete list when one of them came with an incomplete one. - A character put in by Character Offset or Character Value without a relayout notes the font it is drawn with; a fallback face (or none) leaves the list incomplete. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Render: text lists the font file of a run that draws no glyph; tests clean their temp dirs An empty first paragraph draws no glyph, but in a box its face sets the first baseline that places every later line. The read list took faces only from glyphs, so it could be complete without that file. Note each style run's resolved face as well. The font-reads and confine tests use tempfile::tempdir(): a fresh directory per run, removed on panic too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Disk cache: Reload Footage closes the gaps around its drop (#3184) - A reload while the disk cache is off is kept on the session (pending_disk_drops) and applied when the cache is switched on, before anything is read from it. - Each render takes the cache's drop generation before it reads (frame worker; layer cache at the miss). An entry listing a file dropped after that is refused under the queue's lock, so a render that was under way during the reload can't store pixels from before it. Such a frame is shown with an incomplete file list, so persist_missing skips it too. The per-file record is bounded (past it, everything older is refused). - An entry read while its drop was pending is a miss even when the writer ends the sweep between the read and the check (a stale hit found under load): the pending drops and the generation are taken before the entry is read. - Test: a reopened project serves its frame from disk with no source file opened or decoded. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Disk cache: RAM frames keep the drop generation their render started at Reload Footage empties the RAM preview, but an idle pass of persist_missing may already hold a frame from before the reload. It took the cache's drop generation when it ran, after the drop, so the old pixels could be written back and, with the file's size and time unchanged, served again after a restart. Each RAM frame now keeps the disk cache its render started with and that cache's drop generation then. persist_missing writes a frame only to that cache, with that generation, and skips it once one of its files was dropped since; a frame rendered without that cache is not written from there. Tests use tempfile directories (the layer drop test, the frame_files folders under target/). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Doc comments for the functions this PR touches * Disk cache: cap the free-space reserve at 10 GB, skip encodes the limit refuses The reserve the cache leaves free on its disk was max(2 GB, 5% of the disk) (#2551). On a 4 TB disk with 190 GB free that is 200 GB, so a cache set to 20 GB had a limit of 0: every write was refused, the cache never held anything, and only a log warning said so. The reserve is now clamp(5% of the disk, 2 GB, 10 GB). It is there to keep the cache from filling the disk, not to keep a share of a large disk free: 10 GB leaves the system room to work on any disk, a small disk still keeps 2 GB, and a 100 GB disk keeps 5 GB as before. After Effects has only a maximum cache size and no reserve; a clamp keeps the floor for small disks, where a fixed 10 GB would take most of the space. Writes the limit refuses cost no work: DiskCache::accepts_writes (one atomic read) is checked before a frame is converted (put_disk_frame) and before frames and layers are compressed (put_frame, put_frame_f16, put_layer). The frame workers spent about 8 ms per frame and 14 ms per layer on encodes that were then refused. persist_missing returns at once at limit 0 instead of converting the RAM frames on every idle pass. Settings > Disk shows the limit in effect in MB under Maximum Disk Cache Size, with a note when free disk space lowers it (settings.disk.cacheLimit). * Disk cache: refuse writes below 64 MB of room, show this disk's reserve Review follow-ups to the reserve fix: - accepts_writes also refuses while free disk space leaves less than 64 MB of room (a limit the setting asks for is always used): a few frames would fill it, and the rest would be compressed only to be refused. - While writes are refused the idle passes ask for no repaint, so the UI keeps a frame coming every 5 s: the free space is read again and the RAM frames are written once there is room. - Settings > Disk names the space kept free on this disk (the reserve the last reading applied; the rule from MIN/MAX_DISK_RESERVE when it is unknown), and the "no room" note follows accepts_writes. - The settings.disk.cacheLimit automation label is machine-readable and the same in every language: limitMb, maxMb, reserveMb, spaceLimited, writes. - cargo fmt. * Disk cache: follow 3D model sources in the comp's reach The reach walk followed comp, footage and solid sources but not LayerSource::Model. An expression reading a model layer's source (thisComp.layer("Model").source.name) left the model item out of the comp's content key, so renaming the model kept the key and the old frames on disk were served. Every source item is followed now (LayerSource::item), which also counts the model when no expression reads it (Advanced 3D draws it). The same omission is fixed in the RAM preview's content walk (frames.rs) and in the layer key's walk of referenced layers (cache/dependencies.rs). * Disk cache: a render's drop generation travels with its reads; no disk entries for Expression Selectors Drop generation per render: the layer cache recovered a buffer's drop generation from the latest miss of its key, so a second render of the key that started after File > Reload Footage lent its generation to a render still running from before, and that render's buffer (the file as it was) was written after the sweep. Now Renderer::recorded starts each buffer's read log at the store's drop generation before anything is read (ReadLog::starting_at); ReadFiles carries the earliest generation through merges, so derived buffers (styles, the blend-space copy, the flattened layer) and frames that use a buffer from memory carry it too. LayerCache::insert keeps a buffer made of a file dropped since its reads began neither on disk nor in memory; a list that doesn't say counts as made before every drop. DiskCache::dropped_since takes the list's own generation too. missed_at keeps only the miss time. Expression Selectors: the layer key evaluates a text Expression Selector's amount for the first character only, so a per-character dependency such as textIndex == 1 ? 0 : footage("ref.png").width was not in the key, and after the footage item's width changed a reopened session reused the old layer entry. A text layer with an Expression Selector now marks its reads unkeyed (ReadFiles::is_unkeyed), which merges like the generation; such buffers stay in memory and are never written to disk. Frame entries are unaffected: the comp's frame key hashes what the expressions name. The equality and hash of ReadFiles ignore both new fields. * Disk cache: never serve a RAM layer buffer made before a drop of its files A render that passed the drop check could sit in put_layer's encode while Reload Footage cleared the layer cache and dropped the file, then publish its old pixels to memory after the purge; get, and a fresh render's insert, then served them. The disk-hit promotion went through the same insert_mem. Every way the cache hands out a RAM buffer (get, the kept-first path of insert, an existing entry in insert_mem) now checks the entry's files against the store's drops from their own generation (ReadFiles::since): an entry made before a drop of one of its files is forgotten and the lookup misses, so a fresh render's buffer replaces it. insert_mem also re-checks a buffer just before publishing it. --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
23 KiB
Settings, keyboard shortcuts, auto-save and crash recovery
Settings (Preferences)
EffectCraft ▸ Settings (macOS) or Edit ▸ Preferences (Windows/Linux), Cmd+Alt+;. The dialog has
After Effects 2026's pages (General, Startup & Repair, Project, Composition, Previews, Appearance,
Grids & Guides, Labels, Type, Import, Export, Audio, Disk, Memory & CPU, Video, 3D, Scripting &
Expressions) with OK, Cancel, Previous and Next. Older page names still open the right page
(prefs.open {"page": "Auto-Save"} opens Project, Media & Disk Cache opens Disk, Memory & Performance opens Memory & CPU).
The model is effectcraft_engine::prefs::Prefs: serde, versioned (version), addressed by dotted
keys (general.undoLevels, autoSave.intervalMinutes, labels.3.name). It is stored as
prefs.json through the session's ConfigStore (the desktop app uses the platform config
directory: ~/Library/Application Support/EffectCraft on macOS, %APPDATA%\EffectCraft on
Windows, $XDG_CONFIG_HOME/effectcraft on Linux, or EffectCraftData beside the executable when a
portable.txt sits there, as in the Windows portable zip; the web app keeps it in browser storage: the
Origin Private File System, or IndexedDB where OPFS can't write, see web.md). Loading migrates older layouts, keeps unknown keys (from newer versions) and
falls back to defaults for values that don't parse.
Commands (CLI, MCP, control channel):
| Command | Parameters |
|---|---|
prefs.get |
{key?} (omit for everything) |
prefs.set |
{key, value} or {values: {key: value}} |
prefs.reset |
{page?} (omit for everything) |
prefs.open |
{page?} opens the Settings dialog |
prefs.pages |
the page schema: every row, its key, type, range and whether it is wired |
Settings that change behaviour
general.language: interface language (system/en/ja/zh-hans/zh-hant/uk/es/pt-br/ka/sv), Settings ▸ General ▸ Language; menu labels change immediately.system(Match System, the default) follows the operating system's interface language where EffectCraft has a translation and is English otherwise: a Simplified Chinese locale (zh,zh-CN,zh_CN.UTF-8,zh-Hans-CN,zh-SG) selectszh-hans, a Traditional one (zh-TW,zh-HK,zh-Hant,zh_MO) selectszh-hant, a Ukrainian one (uk,uk-UA,uk_UA.UTF-8) selectsuk, a Spanish one (es,es-ES,es_MX.UTF-8) selectses, a Portuguese one (pt,pt-BR,pt_PT.UTF-8) selectspt-br, a Georgian one (ka,ka-GE,ka_GE.UTF-8) selectska, a Swedish one (sv,sv-SE,sv_SE.UTF-8) selectssv; the browser build follows?lang=in the address, then the browser's languages (navigator.languages). Choose Español or Português (Brasil) for Spanish or Brazilian Portuguese menus (panels stay in English for now); choose Українська for Ukrainian menus and cataloged panel, dialog, button and tooltip text; Ukrainian uses the bundled UI fonts. Choose Svenska for Swedish menus, panels and effect names. Native Japanese, Simplified and Traditional Chinese UI uses installed system fonts; the web host must supply a Japanese font. The same setting picks the CJK fallback for the interface and the Han preference of the text engine, so the composition's own text is drawn with the same language's glyph forms (the text engine's preference is process-global); swapping the language reinstalls the fonts, so it applies without a restart. Two catalogs back the text: the menus are keyed by(command, English label)(crates/ui-egui/src/i18n.rs), and the rest of the interface - panels, dialogs, buttons, tooltips - is keyed by its English source string (crates/ui-egui/src/i18n/ui.rs, with rows for Japanese, Simplified and Traditional Chinese and Ukrainian), where a panel writestr("Rename"), ortr_args("Proxy Use: {}", &[&quality])when the row carries a value. A row that is missing falls back to English, so a new string shows up untranslated rather than empty; themissing_translations_reporttest lists what each language lacks, and the tests inui.rsfail on an unused row.general.undoLevels: Levels of Undogeneral.pathPointSize: Path Point and Handle Sizegeneral.recentItems: Recent Projects Showngeneral.showToolTips: Show Tool Tipsgeneral.viewerWheelScrubbing: Scrub Composition Frames with Mouse Wheel (off by default). On: an unmodified mouse wheel over the Composition viewer steps frames, and the smoothed tail of that notch does not zoom or pan. Ctrl/Cmd+wheel and a trackpad still zoom and pan.general.createLayersAtCompStart: Create Layers at Composition Start Timegeneral.defaultSpatialLinear: Default Spatial Interpolation to Linearstartup.showHomeOnLaunch: Show Home Screen When Launchingstartup.offerCrashRecovery: Offer to Open the Latest Auto-Save After a Crashstartup.windowGraphics: Window Graphics (auto/gl), what the desktop window draws with from the next launch.autolets the platform pick (DirectX 12, Vulkan or Metal);gluses OpenGL, with CPU compositing, for graphics drivers that crash with the others. A launch leaves alaunch-pendingmarker in the settings folder until its window has been drawing for a few seconds (or the app quits normally before that); a launch that stops earlier (a graphics driver crashing as the window opens or as its surface is re-created) leaves the marker behind, and the next launch switches togland says so (not on macOS, which has no OpenGL backend).WGPU_BACKENDoverrides both.project.useTemplate: New Project Loads Templateproject.templatePath: Template Projectproject.compactProjectFiles: Save Compact Project Files (default off). Off: File ▸ Save writes.ecprojfiles as indented JSON, one value per line, easy to read and diff in version control. On: compact JSON on one line, smaller and faster to save and open. Either opens the same; auto-saves, XML copies and template packages are not affected.autoSave.enabled: Automatically Save ProjectsautoSave.intervalMinutes: Save EveryautoSave.maxVersions: Maximum Project VersionsautoSave.location: Auto-Save LocationautoSave.folder: Custom LocationautoSave.saveOnRenderStart: Save When Starting Render Queuecomposition.showRenderingProgress: Show Rendering Progress in Info Panel and Flowchartpreviews.adaptiveResolutionLimit: Adaptive Resolution Limit (the lowest resolution Fast Previews ▸ Adaptive Resolution drops to while you drag; a playing preview keeps the viewer's resolution)previews.cacheFramesWhenIdle: Cache Frames When Idle (Composition ▸ Preview): after a second without input or edits, the viewer renders the work area into the RAM preview in the background, from the current time on, until it is cached or the budget is fullpreviews.fastPreviews: Fast Previews (Draft 3D, Faster Effects)appearance.appearanceMode: Appearance Mode (auto/dark/light).auto(Sync with System) follows the operating system's light or dark appearance while the app runs: on Linux the desktop app reads the XDG desktop portal (org.freedesktop.appearancecolor-scheme) once and then listens for itsSettingChangedsignal on a worker thread, repainting only when the value changes (nothing polls);gsettingsruns at most once at start-up, by absolute path, when the portal gives no answer. Elsewhere it uses what the window system reports. With no answer it shows the dark theme. New installs staydark; Sync with System is opt-in. The Tools bar's appearance button (a monitor, sun or moon) cycles Sync with System, Light and Dark (view.theme.toggle, Next Appearance Mode).appearance.darkTheme: Dark Theme (dark/darker), shown in dark appearanceappearance.lightTheme: Light Theme (light), shown in light appearance. Settings ▸ Appearance shows the mode above a light and a dark theme card, each with a preview of the EffectCraft window in that theme.appearance.favoriteThemes: a list of unique theme ids (dark,darker,light), empty by default. Hover a theme row to star it; saved favorites keep an accent-colored star. Right-click the appearance button for Appearance Settings and favorites grouped light first, then dark. Selecting a favorite fixes its appearance family and preserves the opposite family choice; left-click still cycles modes. Settings OK saves favorites and Cancel restores them. Unknown or duplicate ids are rejected byprefs.setand removed when loading old settings.appearance.theme: the single theme from before appearance modes, kept for older settings files and automation. A saved file with onlythemeloads as a fixed Dark or Light mode showing it; setting it (prefs.set,ui.set {"theme": …},view.theme.dark|darker|light) selects that theme and fixes the mode to its family. While the mode is Dark or Light it mirrors the theme in use.appearance.brightness: Brightnessappearance.uiScale: UI Scale, the size of the whole interface (any whole percentage from 75 to 200, on top of the display's own scaling), dragged or typed; it applies as soon as it is dragged or enteredappearance.useLabelColorForHandles: Use Label Color for Layer Handles and Pathsgrids.gridColor: Colorgrids.gridSpacing: Gridline Everygrids.gridSubdivisions: Subdivisionsgrids.guideColor: Colorgrids.actionSafe: Action-safegrids.titleSafe: Title-safeimport.stillFootage: Still Footageimport.stillSeconds: Still Durationimport.sequenceFps: Sequence Footageaudio.outputDevice: Default Output (device list from the desktop host)audio.outputLeft: Left (Audio Output Mapping)audio.outputRight: Right (Audio Output Mapping)memory.layerCacheMb: Layer Cache (in the browser at most 512 MB, see web.md)memory.mediaCacheMb: Footage Frame Cache (in the browser at most 256 MB)memory.previewCacheMb: Preview (RAM) CachethreeD.defaultRenderer: Default 3D Rendererlabels.N.name/labels.N.color: the 16 label names and colours, used by the Label menu, the timeline, project panel, render queue and viewer handlesgeneral.switchesAffectNestedComps: a precomp layer's Quality (Draft / Wireframe) and Motion Blur switches limit the layers of the nested comp (viewer, Render Queue,render)general.preserveConstantVertexCount: adding or deleting mask / shape path vertices at one keyframe does the same on every other keyframe (added vertices split the matching segment)general.syncTimeRelatedItems: moving the current time moves it in the comps nested in this one and in the comps that nest it (through the precomp layers' timing)general.expressionPickWhipCompact: off, the pick whip writes match names (thisComp.layer("A")("transform")("opacity"))general.createSplitLayersAbove: where Edit ▸ Split Layer puts the new layer (on by default, as in After Effects)startup.showHomeOnOpenProject: the Home screen comes up after File ▸ Open / Open Recentcomposition.motionPath,composition.motionPathSeconds,composition.motionPathKeyframes: how much of a position motion path the viewer draws (all, none, N seconds or N keyframes around the current time)previews.showInternalWireframes: outlines of the layers inside a selected collapsed precomppreviews.zoomQuality: Faster = nearest-neighbour viewer scaling, More Accurate = averaged (bilinear, mipmapped) when the frame is shown below 100 %; magnified frames show their pixels as blocks in both modes, and the frame sits on the physical pixel grid, so 100 % is exactpreviews.displayProfile: the monitor's colour space for View ▸ Use Display Color Managementappearance.useLabelColorForTabs: Composition / Timeline tabs show the comp's label colour, Effect Controls / Properties the layer'sappearance.cycleMaskColors: new masks cycle through the mask colours (off: all the first)appearance.useGradients: soft gradient on panel tab stripsappearance.inWindowMenuBarMac: on macOS, draw the menu bar inside the window instead of the native menu bargrids.gridStyle,grids.guideStyle: lines, dashed lines or dots (grid, proportional grid, guides);grids.proportionalHorizontal/grids.proportionalVertical: View ▸ Show Proportional Grid divisionstype.textEngine: South Asian and Middle Eastern shows the paragraph direction and the World-Ready composers in the Paragraph paneltype.fontPreview: a "Sample" preview in each font of the Character panel's font menutype.recentFonts: how many recently used fonts head the font menutype.fontNamesInEnglish: off, fonts with a native-language family name show itimport.reportMissingFrames: gaps in an image sequence's numbering are reported on import (they show as colour-bar placeholders, as in After Effects; see footage.md)import.autoReloadFootage: Automatically Reload Footage,off,nonSequence(the default, as in After Effects) orall(image sequences too). Every two seconds while its window is in front (every ten seconds while another app has the front), and as soon as the window comes back to the front, the desktop app compares each footage file's size and modification time with what they were, on a worker thread that only wakes the interface when something changed, and reloads what another app saved: a Photoshop file edited in PhotoCraft or Photoshop shows its new pixels without File ▸ Reload Footage. It is not an undo step and doesn't modify the project; a layer item stays on its layer (found again by name), and a file that can't be read yet (still being written) is tried again once it changes again.footage.reloadChangedruns the same check once (CLI, agents)import.unlabeledAlpha: alpha of TGA / TIFF / movie footage (Ask opens Interpret Footage; Guess takes premultiplied for movies, straight for stills)import.dragImportAs: layered files dropped on the window import as footage or a compexport.defaultOutputFolder: where relative Render Queue outputs goexport.segmentSequences,export.segmentSequenceFiles: sequences split into numbered folders of N files;export.segmentMovies,export.segmentMovieMb: movies split into numbered files of about N MB (cut at the frame count the output's data rate gives)export.appendBitsToName:Comp 1.png→Comp 1_16bpc.pngaudio.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 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). On the desktop the cache never fills its disk: it leaves 5% of the disk free, at least 2 GB and at most 10 GB, and is smaller thandisk.diskCacheMaxGbwhen free space requires it. The Disk page shows the limit in effect in MB under Maximum Disk Cache Size, with a note naming the space kept free when free disk space lowers it (with less than 64 MB of room nothing is written, and frames and layers are not compressed for it). 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)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 againmemory.ramReservedGb: the cache budgets together leave this much physical memory freememory.reduceCacheWhenLow: cache budgets halve while the system is low on memory (checked every 30 s by the desktop app, on a background thread)video.enableOutput,video.device,video.outputDuringPlayback,video.mirrorOnMonitor,video.disableWhenBackground: Video Preview, a second window (or full screen on the display it is on) showing the composition frame, Mercury Transmit-stylethreeD.showReferenceAxes: world X/Y/Z axes in the viewer corner of comps with 3D layersthreeD.extendedViewer: 3D layers reaching beyond the comp frame are outlined on the pasteboardthreeD.realtimeShadows: draft (Fast Previews) renders keep 3D shadowsscripting.allowScriptsWriteFiles: the scripting file / network gate (andFile.execute())scripting.warnExecutingFiles:File.execute()asks before opening a filescripting.editorFontSize,scripting.syntaxHighlighting,scripting.lineNumbers,scripting.autoComplete(Tab accepts),scripting.bracketMatching,scripting.wordWrap: the Timeline's expression editorscripting.errorBanner: a banner along the bottom of the Composition panel names the first failing expression (click: reveal it)roto.model(Settings ▸ Roto Brush): the segmentation model Roto Brush 2.0 / 3.0 use,classical(built in) ormobilesam(once installed). The page lists every registered model with its authors, licence, size, source and status, with Download, Install from File… and Remove (roto.models/roto.model.*); weights live in themodelsfolder next to the settings, each with a.NOTICE.txtnaming its authors and licenceface.model(Settings ▸ Face Tracking): the model face tracking uses,classical(built in) ormediapipe-face(once installed); the same page of models (face.models/face.model.*)
Display-only settings
These rows are in the dialog for After Effects parity and are stored, but have no EffectCraft
behaviour to change. prefs.pages reports them with "live": false; a test keeps this list in
step with the schema.
general.useSystemColorPicker: EffectCraft has one colour picker on every platform and the web; there is no pure-Rust way to open the system colour panels.composition.hardwareAcceleratePanels: the Composition, Layer and Footage panels are always drawn by the GPU (egui on wgpu).scripting.enableJsDebugger: the JavaScript engine has no step debugger; script errors report their file and line in the Script Console.
Effect Controls uses compact, fixed 20 pt parameter rows, as After Effects does: each group level is indented beneath its effect, sibling names and values align, and a gap separates effects. The timeline keeps its fixed 19 pt rows. Both follow the whole-interface UI Scale.
Keyboard shortcuts
Edit ▸ Keyboard Shortcuts (Cmd+Alt+') opens the shortcut editor: an on-screen keyboard showing
which keys are assigned for the held modifiers (purple: application-wide, green: panel-specific,
both: split), modifier toggles, a searchable list of every command (all engine commands, menu
entries with bound parameters, the scripts File ▸ Scripts lists and the frontend's tool / panel
commands), and a preset menu.
- Presets. "EffectCraft Default" is After Effects' default layout and is read-only; editing it creates a "Custom" copy. Custom presets store only their differences from the default and can be duplicated, renamed, deleted, exported and imported (JSON). The active preset is what the menus show and what the shortcut dispatcher runs.
- Assigning. Select a command, then press keys in the shortcut field (or type them), or click a key on the keyboard with the modifiers toggled. Conflicts with other commands in an overlapping scope are listed before you assign.
- Scope. A shortcut belongs to the application, to a panel (Project, Timeline, Composition, Render Queue…) or to a context (editing text, a paint tool or a camera tool active, a mask selected). One chord can be bound once per scope, and the narrowest active scope runs it: an active context, then the panel with the keyboard, then the application. The Timeline and the Composition panel share their shortcuts. A context's shortcuts work only while it is active; a panel's shortcut that nothing nearer claims still works from the other panels.
- Conflicts. Two commands of one scope on one chord are a conflict (
shortcuts.conflictslists them; the default preset has none). The same chord in two scopes is an override: both keep working, andshortcuts.setreports it withoverrides: true.
Commands: shortcuts.list {query?, keys?, assigned?}, shortcuts.set {command, params?, keys},
shortcuts.reset {command?}, shortcuts.preset {op: select|new|duplicate|delete|rename, name?, from?, newName?}, shortcuts.export {preset?, path?}, shortcuts.import {path? | preset?},
shortcuts.conflicts.
Auto-save and crash recovery
- Every n minutes (Settings ▸ Project ▸ Auto-Save) a project with unsaved changes is written
to an
EffectCraft Auto-Savefolder next to it (or the custom folder) as<name> auto-save N.ecproj. Slots rotate through 1…maximum versions, overwriting the oldest. Untitled projects go to the custom folder, or toEffectCraft Auto-Savein the config directory. - Every project and auto-save write is atomic: a temporary file is written and flushed, then renamed over the target, so a crash mid-write leaves the previous file intact.
- While the app runs,
session.lockin the config directory records the open project and its latest auto-save. A clean quit removes it. If it is still there at the next launch, the app offers to open the latest auto-save (Settings ▸ Startup & Repair can turn this off). - File ▸ Open Recent lists recent projects (stored in the settings), File ▸ Revert reloads the
saved project, File ▸ Increment and Save saves
Intro.ecprojasIntro 2.ecproj.
Headless MCP clients can opt in with effectcraft-cli mcp --autosave. Unlike the desktop's
interval, this writes changed dirty projects before sending each tool reply, with separate
version slots per server under EffectCraft Auto-Save/MCP/ (or the custom folder's MCP/).
It reads settings without sharing the desktop's writable settings store or session.lock.
See headless recovery for how to reopen a
checkpoint after a client restart.
Commands: file.autoSave (now), file.recoveryInfo, file.openRecent {index? | path?},
file.clearRecent, file.revert, file.incrementAndSave.
Logs
The desktop app writes its log records to standard error and to Logs/effectcraft.log in the
config directory. Each launch moves the previous log to effectcraft.1.log (and that one to
effectcraft.2.log), so the log of a run that crashed survives the next start; a log stops
growing at 16 MiB. A launch from the Dock, the Start menu or a launcher has no terminal, so
attach these files to a bug report. Help ▸ Reveal Logging File shows this run's log (or Help ▸
Enable Logging's EffectCraft Log.txt while that is on, which keeps every EffectCraft record
across launches).
By default the log has info records from EffectCraft and warnings and errors from everything
else. RUST_LOG replaces that with env_logger-style directives: debug,
warn,effectcraft_render=trace, wgpu_core=info. The control-channel token is never logged.