* 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>
A shortcut belongs to the application, a panel or a context (editing text, a paint or
camera tool active, a mask selected). The narrowest active scope runs a chord: an active
context, the focused panel, the panel sharing its shortcuts (Timeline and Composition),
the application. A context's shortcuts never run outside it.
- shortcuts.rs: scope table for engine commands and menu entries, scope_rank,
scope_label; UiCommand can state its scope.
- Conflicts: the same chord in two scopes is an override (both keep working), reported
with overrides: true; shortcuts.conflicts lists same-scope pairs only.
- Scoped now, keys unchanged: Remember / Apply Interpretation and Delete Without
Confirmation (Project), Duplicate Render Item (Render Queue).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds Swedish (Svenska) as an interface language in the menu, panel and
effect catalogs, written from the English source. The effect rows are
partial like Japanese and Georgian: an effect without a Swedish row draws
its English name, so a new effect does not block other PRs (#2244).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main routes viewer scrolling through viewer_scroll, so a raw-event scrub fought the wheel zoom. With general.viewerWheelScrubbing on, an unmodified wheel steps frames and the rest of that notch does not zoom or pan. The preference stays off, so the wheel still zooms.
Settings ▸ Import ▸ Reload Footage Changed on Disk used to spawn a stat thread every two seconds
from the UI thread, request a repaint 100 ms later to collect it, and request another repaint every
2 s in between, for every project with items, focused or not: the whole interface redrew on a timer
for footage that hadn't changed.
`footage_reload::Watcher` is one long-lived worker: the interface tells it what to watch (recomputed
when the project revision or the preference changes) and whether the window is in front; it stamps
the files every 2 s while the window is in front, every 10 s while another app has the front, at once
on focus regain and on a new set, and hands the stamps over — calling `ctx.request_repaint()` — only
when one differs from its last look. Nothing watched means the thread sleeps. `apply_stamps` and the
reload semantics are unchanged; `start_scan`/`poll_scan`/`Session::footage_scan` are gone. No new
dependencies: the stat loop is also what footage on a network share relies on, where FSEvents would
not report another machine's writes.
Tests: the watcher wakes the interface for the first stamps and for each change and never for an
unchanged file; behind another app it rests until the window is back; nothing watched is nothing
stamped and a new set is reported whole (and dropping it ends the thread); the host test
`the_watcher_notices_a_footage_file_changed_on_disk` saves a PSD from "another app" and sees it
reloaded once.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Settings ▸ Project ▸ Project Files ▸ Save Compact Project Files (off by
default) saves .ecproj files as compact JSON: smaller, faster to save and
open. Off, they stay indented, one value per line, so they read and diff
cleanly in version control. XML copies are not affected; auto-saves were
already compact. The project.ecproj inside exported template and Motion
Graphics template packages is written compact.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Edit ▸ Keyboard Shortcuts now lists the scripts File ▸ Scripts lists, under
File ▸ Scripts, and a key given to one runs it as its menu entry does. The
key is bound to the script's file name (file.runScript {"name": ...}) and is
saved with the preset. File ▸ Scripts and the macOS menu show it.
The shortcut table lists the Scripts folder again when a script is installed
or uninstalled, when the editor opens and when shortcuts.list runs.
shortcuts.set and shortcuts.reset look a script up in File ▸ Scripts' list
as it is now, and a script it doesn't list is an error instead of a key for
Run Script File....
* Translations: delete coverage and wording tests
A missing translation shows in English and never fails a test. Delete the
coverage and per-language parity tests, the tests that pin one language's
wording and the source scans for strings not routed through the catalog.
Keep the stale-row, duplicate-row and placeholder checks, and make the
engine tests that switch language compare against the catalog instead of a
literal. Drop the Traditional Chinese panel rows that only repeated the
English source to satisfy the parity test.
* Translations: missing-strings report in xtask ci
missing_translations_report prints, per language, the fixed menu entries,
panel strings, effect names and effect categories with no row. It always
passes. cargo xtask ci runs it with --nocapture after the test runs, so the
list shows in the gate log. The docs say a missing string shows in English
and where the list is.
Adds Georgian (ქართული, code ka) to Settings > General > Language, with
Match System for ka / ka-GE / ka_GE.UTF-8 locales:
- i18n.rs: the menu catalog, the same 628 (command, English label) keys as
the other languages, plus the Undo/Redo entries, the workspace and 3D
view shortcut submenus, the Reveal labels on Windows and Linux and the
macOS Services items.
- i18n/ui.rs: GEORGIAN, every row of the Chinese panel catalogs (687), and
GEORGIAN_PANELS, 195 rows for text the panels send through tr() that no
catalog has yet (form dialogs, drop messages, screen-reader names,
context menus, Project and Timeline column headers). The tests hold
them to the same rules (unique, same placeholders, used by a converted
panel) and keep them from shadowing a Chinese row; prefs_live.rs joins
the converted files.
- i18n/effects.rs: Georgian names for all 307 effects and 21 categories;
the CC series and brand names stay in English.
- Properties > Text Animation: the Add Animator and Presets buttons grow
with their translated labels (English keeps 116 / 84 px).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YMTAbGNRxnMEzRvYnQGphF
Match System (the default) stayed English in the browser build, which has
no system-locale probe (#229). The page now passes ?lang= from the address,
then navigator.languages, and Match System takes the first of them with a
catalog (an English tag counts), else English. A language chosen in
Settings still wins; native builds are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YMTAbGNRxnMEzRvYnQGphF
(cherry picked from commit 73c064e3858068ed19fbfafbdd0b3c8cc229a42e)
A launch from the Dock, the Start menu or a launcher has no terminal, and
without RUST_LOG nothing reached standard error anyway, so start-up and
GPU warnings were lost unless Help > Enable Logging was already on.
The logger now writes EffectCraft's info records and everyone's warnings
to standard error and to Logs/effectcraft.log in the settings folder.
Each launch keeps the previous two runs' logs; the file is capped at
16 MiB. RUST_LOG still overrides the filter. Help > Reveal Logging File
shows this run's log unless Enable Logging is on.
Ported from storytold/pdfcraft#239.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: the viewer blurred hard edges even at Full resolution and 100 %
(reported on Discord). The frame rect was centred without snapping to
physical pixels, so bilinear sampling blended neighbouring pixels at 1:1
(worst at 125 %/150 % display scaling); More Accurate also magnified
bilinearly, so 200 %+ and lower resolutions were smoothed; and Zoom In /
Zoom Out stepped in points, so at 125 % scaling they never reached 100 %.
Fix: snap the frame origin (and the ViewerMap overlays use) to the
physical pixel grid; magnify with nearest in both Zoom Quality modes
(CPU texture options and the GPU frame sampler), keeping the averaged
minification below 100 % (#417); the snapshot texture follows the same
options; Zoom In / Out step through screen-pixel magnifications.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Port of storytold/photocraft#2445. Each theme in the appearance picker gets a
star at the right edge of its row. Starred themes are saved with the
preferences and listed in a right-click menu on the header's appearance button,
next to Appearance Settings, so switching between favorites takes one click.
Settings > Appearance > UI Scale offered only 75, 100, 125, 150, 175 and
200 %, so a size between 75 and 100 wasn't possible. It is now the shared
numeric field (Kind::Int(75, 200, "%")): type or drag any whole
percentage. The engine already stored the scale as a clamped integer and
the UI applies it with set_zoom_factor, so the range, the default, live
preview, Cancel and saved preferences are unchanged; the preset list
goes.
Numeric settings now apply a typed number once it is entered (Enter or
leaving the field), not at every keystroke: typing 85 used to apply the
partial 8, clamped to 75, and resize the whole interface before the 5.
Dragging still applies as it moves.
Tests: the UI Scale test types 85 a key at a time (nothing applies and
the field keeps the keyboard until Enter), Cancel, 50 and 300 clamp to
75 and 200, 90 with OK, and a new app with the saved preferences starts
at 90 %. The GPU Information transaction test types 125.
On an ASUS G513QY (Radeon RX 6800M, Adrenalin 26.9.2, Windows 11) every
launch died with STATUS_ACCESS_VIOLATION inside amdvlk64.dll, on a
driver worker thread, a moment after the first frame: eframe shows the
window it created hidden, the resulting resize re-creates the swapchain,
and the driver crashes there (same offset on two driver versions, with
the OBS/Steam/Epic Vulkan layers disabled, and with FIFO, immediate and
latency-1 presentation). DirectX 12 and OpenGL open the window fine.
A crash in a driver can't be caught, and the launch guard (#243) removed
its marker at the first frame, so this looked like a successful launch
every time and Window Graphics never switched. The marker now stays
until the window has kept drawing for SETTLE (3 s) or the app quits
normally before that; frame_drawn returns the time left and the app
asks for a repaint then, so an idle window settles too. The notice,
the Settings note and docs/preferences.md describe the new rule.
Verified on that machine with a scratch settings folder: launch 1
(Vulkan) crashed and left the marker, launch 2 switched to OpenGL with
the notice and closed with exit code 0, launch 3 stayed on OpenGL
without the notice.
Tests: effectcraft launch_guard
a_launch_that_stops_after_its_first_frame_switches_the_next_to_opengl
(a first frame then drop: the next launch gets GL and the notice; a
normal quit in that time leaves no marker), and the existing test now
asserts that the first frames don't settle a launch (marker present at
SETTLE/2, gone at SETTLE).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Ported onto the current language plumbing next to Spanish (catalog codes,
Match System for pt-* locales, native menu Services label, docs, tests);
the language list and settings validation stay the shared LANGUAGES table.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
Ported onto the current language plumbing next to Ukrainian (catalog
codes, Match System, native menu Services label, docs, tests), and the
stale Ease Presets row (a ScriptUI panel since #404) dropped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
Image sequences are watched only with All Footage (the default,
Non-Sequence Footage, leaves them alone). Purge also forgets the viewer's
per-revision disk keys, which hash footage files' size and time, so a
file reloaded without a new revision is keyed again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
A footage file the automatic reload couldn't read (still being written, or
one the importer doesn't read) kept its old stamp, so every two-second look
saw it as changed and probed it again. It now keeps its new stamp and is
tried again once it changes again (when the other app finishes writing it).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reload Footage (Cmd+Alt+L) updated an item's metadata but left the media
pool's decoded frames and the layer cache in place, so the old image kept
rendering until Edit > Purge. It also replaced the footage with a probe of
the merged document, so a Photoshop layer item lost its layer and its
Retain Layer Sizes bounds.
Reload now re-reads only what the file decides (size, a movie's duration,
streams and frame rate, codec, a data file's text), keeps the layer or
page and the interpretation, finds a Photoshop layer again by name, and
makes the media layer forget the file (FootageSource::forget, which also
drops the EXR aux-channel cache) and clears the layer cache.
Settings > Import > Reload Footage Changed on Disk (on by default): the
desktop app stamps footage files (size + modification time) on a worker
thread every two seconds and when the window comes back to the front, and
reloads what changed. Not an undo step and it doesn't modify the project;
a file still being written is retried on the next check.
footage.reloadChanged runs one check (CLI, MCP, agents).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cause: the browser build kept the desktop's cache budgets (1 GB layer
cache, 1 GB footage frame cache) inside the page's memory, which wasm32
caps at 4 GiB. FilmCraft's decoders keep up to 384 MB of decoded frames
per open movie beside them, so a comp of eight stacked 1080p H.264 clips
grew the page by about 55 MB/s while playing and ran it out of memory
after about 50 s ("unreachable": a failed allocation).
Fix: in the browser Prefs::cache_budgets keeps the Layer Cache within
512 MB and the Footage Frame Cache within 256 MB (WEB_LAYER_CACHE,
WEB_MEDIA_CACHE); smaller settings and the desktop are unchanged, and
the RAM preview keeps its budget. Docs (web, preferences) say so.
Tests: tests_m145::web_cache_budgets_leave_room_for_the_decoders: with
4 GB settings the web budgets are the caps, the desktop's are the
settings, and smaller settings stay as they are.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Translate the 396 current panel catalog rows and route Ukrainian Reveal
labels for Finder, Explorer and File Manager. Enforce catalog parity,
placeholder preservation and platform label coverage; update language docs.
Use a fixed-size OCIO row iterator to satisfy the current Clippy gate.
Co-authored-by: Codex <noreply@openai.com>
ui.rs gets a Japanese table with the same 396 rows as the Simplified and
Traditional Chinese ones, so Japanese now translates the interface below
the menu bar too (it covered the menus only). The shape test keeps the
three tables listing the same source strings; a new test checks that
Japanese reads its own rows and fills values in order. The existing
"no rows yet" check uses Ukrainian instead.
ROADMAP.md and docs/preferences.md say which catalogs have panel rows.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Add a Spanish catalog for the menus (618 entries), the computed titles and the macOS Services label, and list Español in the Language preference. Match System now maps es, es-ES, es_MX and es-419 to it. Add llopjulen to contributors/people.toml.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HcDWLRR3UfieXAyFa9cq4p
#402 made Effect Controls' rows compact and aligned, as After Effects
draws them, and also added thin separator lines between parameter rows
in Effect Controls and the Timeline, with a Settings > Appearance > Show
Parameter Row Separators preference. After Effects has neither, so the
lines, the preference and the Tokens field go; the compact rows, the
alignment and the automation ids stay.
Saved settings with the old key load as before (unknown keys are
ignored).
Test: ui_parameter_spacing keeps the row geometry, hit target and
nesting tests; the separator tests go with the feature.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>