mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-08 11:31:25 +00:00
dbcae60e1911adb286b28b0f77e99d345ac1be39
399
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
84382487bb |
Disk cache: entries keep the files they read, keys from memory (#3184) (#3191)
* 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> |
||
|
|
719b9170dd |
3D rotation order, keyed Orientation, linear-to-eased keyframes and Timewarp behave as in After Effects (#1876)
* 3D: Orientation and X/Y/Z Rotation turn Z first, keyed Orientation slerps, as After Effects After Effects composes a 3D layer's (and a camera's) Orientation and its X/Y/Z Rotation as Rx * Ry * Rz each, Orientation before the axis rotations; EffectCraft used Rz * Ry * Rx. Measured on synthetic AE 2026 cases: Orientation [10,20,30] (488) was 5.2 % of the frame off and now matches to the anti-aliased edge (0.03 %); X/Y/Z Rotation 10/20/30 (800), Orientation + X Rotation (801) and one-node cameras (804, 805) likewise. Keyed Orientation moves along the shortest arc between the keys (quaternion slerp, eased in time, angles written in [0, 360)): keys [0,0,0] -> [10,20,30] read back [6.2643, 9.3531, 15.2190] half-way where EffectCraft gave [5,10,15]; [0,0,0] -> [60,80,40] linear and eased match AE's read-backs within 0.002 deg. lookAt() returns AE's angles (z axis on the target, x axis level; two AE read-backs within 1e-3 deg). The camera tracker, VR editor, Look At and orbit commands use the new decomposition. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Keyframes: a linear side next to an eased side has 16.67 % influence, as After Effects In a segment that is Bezier on one side and linear on the other, the linear side keeps the segment's average speed, but with the 16.67 % influence After Effects shows for linear keys in the Keyframe Velocity dialog (and reports to scripts), not 33.3 %. Measured on an After Effects 2026 time-remap render (a square at x = 50 + 455 * remap): out of a key at 2 s (3.5, speed 0, influence 50 %) into a linear key at 4 s (0.5), AE plays 3.290 at 2.5 s and 2.657 at 3 s; EffectCraft played 3.308 and 2.629 (8 and 13 px off), now the same square positions to the pixel. LINEAR_SIDE_INFLUENCE is used by evaluation (scalar, per-dimension, paths and spatial), side_ease (Graph Editor, velocity dialog, keyIn/OutTemporalEase) and the Lottie export, so exported easing plays as EffectCraft does. Segments linear on both sides stay straight lines. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Timewarp: retime the layer's source without the effects above it, as After Effects After Effects' Timewarp reads the layer's source (with its masks) at the retimed times and ignores the effects applied before it: a red solid with a Gradient Ramp and then Timewarp (Speed 25) renders as the red solid in AE 2026 (EXPORTER e2 read-back). EffectCraft retimed the layer with its effects (mean difference 127.5/255 over the comp), now identical (0, CPU and GPU). Echo, Posterize Time and the other time effects keep reading the effects before them, as measured before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Image sequences: a gap in the numbering holds the previous frame, as After Effects After Effects 2026 shows the last existing file across a gap in a sequence's numbering (case 860: files 1, 2, 4, 5 show frame 2 at frame 3; VERIFY read-back). EffectCraft drew colour bars there. The media pool now holds the previous file (Footage::sequence_file_held); colour bars remain only where no file precedes the frame. Missing-frame reports on import are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Review: keyed Orientation uses LINEAR_SIDE_INFLUENCE for linear sides; real NaN assert evaluate_orientation still gave a linear side (and the unreachable missing-key fallback) 1/3 influence while segment_value, side_ease and the Lottie export use LINEAR_SIDE_INFLUENCE (1/6), so a keyed Orientation next to an eased key moved on a different curve than every other property (CodeRabbit on #1876). New test: orientation_linear_side_next_to_an_eased_side_matches_a_scalar (both directions). The always-true NaN assert in keyed_orientation_slerps_as_after_effects now checks that a non-finite key comes back as given; Property::value_at says why the match_id "orientation" routing is safe. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Format keyframe orientation test (cargo fmt) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * Move the image-sequence gap change out of this branch The gap change (hold the previous file instead of colour bars) reverses #297's documented behaviour and goes to its own PR, as requested in review. pool.rs, sequence.rs, the footage test and docs/footage.md / docs/agents.md are back to main. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Codex <noreply@openai.com> Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com> |
||
|
|
fdcc715829 |
Viewer exposure on the full float range of 32 bpc frames (#2937)
* Viewer exposure on the full float range of 32 bpc frames (#2797) 32 bpc viewer frames on the desktop are rendered without the HDR step (skip_hdr_step, #2916) and kept as half floats in the RAM preview. A display pass on egui-wgpu's device turns each frame into one RGBA8 texture with mips per viewer, in this order: decode, exposure in linear light, Compand / Tone Map, encode, display colour, Show Channel, rounding. Before, exposure was an 8-bit lookup applied after tone mapping, clamping and the display conversion, so -2 EV could not bring values above 1.0 back. - engine: viewer::DisplayPass describes the pass as data; its pixel() is the CPU fallback (no device) and the shader's reference. It replaces exposure_lut / display_transform. FrameEncoding records a frame's curve and the HDR step it still needs. 32 bpc snapshots are taken without the HDR step; snapshot_rgba8 applies it. - ui-egui: FrameImage::CpuF16 (half crate, F16C at run time), byte accounting at 8 bytes a pixel in the cache and the prefetch estimates; display_gpu.rs + display_gpu.wgsl; main viewer, snapshot and passive viewers draw through the pass; the 8-bit image other panels read is made on demand with the HDR step; the Info panel shows float values. - Half-float frames are not written to the disk cache yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Viewer display pass: f16 disk cache entries, window-device gating, review fixes - Disk cache: half-float frame entries (format byte in the entry header, byte planes + LZ4); 8-bit entries keep format 0 and stay valid. 32 bpc viewer frames are written and read again, and the timeline's disk bar sees them. A damaged half-float entry is deleted. - The display pass runs on the window's egui-wgpu device, with or without the GPU compositor (still building, failed, GL fallback). Its pipelines are made inside error scopes; a backend that rejects them gets the CPU pass instead of a blank viewer. - A new LUT of the same size (another ICC profile, Preserve RGB, another display) runs the pass again. - f32 to f16: one pass per L1-sized block, the NaN / infinity fix-up on the bits, no fill of the buffer (4K: 4.7 ms, as the 8-bit conversion). - Mips of odd-sized levels include the last column / row; the snapshot's half floats and texture go with the snapshot; no placeholder texture ids; u64 size checks. - Tests: LUT profiles and negative values against the shader, Bake reruns, the passive viewer's colour, f16 disk round trips; exact comparisons allow 1 LSB on software rasterizers. Docs: the display pass and the f16 disk entries in architecture.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Viewer f16 disk-cache test: temp dir removed on failure too, wait for the queued disk write Review points: the test's directory was only removed by its last statement (now tempfile::TempDir), and the frame is released before its disk write is queued, so flush() could return early (now waits for the entry). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * Viewer: oversized half-float frames use the CPU display pass, so exposure/HDR come before the averaging down Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * Viewer: fmt and a debug assert for the half-float upload path Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * Disk cache: persist_missing also writes half-float frames Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * Passive viewer: oversized half-float frames take the CPU display pass instead of leaving a stale texture Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
ae068ff931 |
EXR export: write the output space's chromaticities attribute (#3038)
* EXR export: write the output space's chromaticities attribute (#3037) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * EXR chromaticities: tests for unmanaged and PQ ProEXR, every space once Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * EXR export: note why the comp background is added before decoding (review point) CodeRabbit asked to convert Comp.background to the output space before compositing. Every output path (Image::to_rgba8_over) adds the background to the output-encoded frame as it is, so the EXR now matches the other outputs; converting would make the EXR colour differ from the PNG of the same comp. Comment only. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * Format the merged code with rustfmt Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com> |
||
|
|
7fc1be0bf2 |
Report offline footage and recover automatically when media returns (#3161)
* 3118: report and recover offline footage consistently * 3118: preserve manual reload and refresh data watch paths * 3118: document availability and reload contracts |
||
|
|
8b63d4308c |
Merge pull request #2916 from vangel76/fix/exr-scene-linear-output
EXR export: no compand / tone mapping, and the output space's own curve (#2913) |
||
|
|
39af86e9f4 |
Merge pull request #2929 from vangel76/perf/aux-footage-hint-474
perf(media): tell the media pool which EXR footage effects read (#474) |
||
|
|
611a282bea |
Plug-ins: external effect helpers (safe, out-of-process native effects) + plug-in API 2 (#889)
* Plug-ins: API 2 (PluginHost, padding, layer_span) Split out of the OpenFX work so it can be reviewed on its own. Additive; the WebAssembly ABI stays at 1. - point3 / layer / text / hidden parameters, integer sliders, parameter groups. - EffectPlugin::padding (transparent pixels around the buffer, at most 4096 per side). - EffectPlugin::render_v2 with a PluginHost: the input at other times, other layers, parameters at other times, frame rate, per-instance key, the layer's source range. - EffectEnv::layer_span from the renderer; MAX_PLUGINS 1024. - plugin_error: a plug-in effect's last render error, for the UI. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KxacwTpUwqyfpcLgKW8yjS * Plug-ins: external effect helpers (crates/helper-host) Native plug-in formats run in separate helper programs, never in EffectCraft's process (docs/adr/0001-external-effect-helpers.md). New safe-Rust crate effectcraft-helper-host (L3, native only, unsafe forbidden): it finds helper programs, starts each as a child process and registers its effects through plug-in API 2, so the UI and command surface need nothing new. - Protocol v1 (docs/external-helpers.md): JSON lines over stdin / stdout (helper replies are prefixed so plug-in chatter is ignored); pixels go through a frame exchange file (/dev/shm on Linux), never through the pipe. Render-time callbacks for the input at other times, other layers and parameters at other times, with nested renders through the same helper. - A helper that exits, hangs past its timeout or breaks the protocol is stopped. The render fails with a message, the frame is left as it was, and the helper restarts for the next render. An effect that stops its helper 3 times is turned off (helper_blocklist.txt). - Opt-in: nothing starts helpers unless the user asks (Effect > Scan for Effect Helpers), or opts in to starting them at launch with EFFECTCRAFT_HELPERS=1, an environment variable, so writing to the settings folder alone can't run code. effect.helpers.scan / load are refused for confined agent sessions (confine.rs), like file.executeFile. - Discovery: helpers/ next to the app (Contents/Helpers on macOS), helpers/ in the settings folder, EFFECTCRAFT_HELPER_PATH. - Commands effect.helpers.list / scan / load; Effect menu entries (translated); the last render error of an effect shows as a warning sign after its name in Effect Controls. - serve(): the helper side for Rust helpers. tests/it/main.rs runs the whole protocol (render, padding, callbacks, nesting, threads, crash, restart, block list, hang) against a fake helper, with nothing to install. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KxacwTpUwqyfpcLgKW8yjS * Effect helpers: address review (start failures, frame file mode, nextest listing) - A helper that can't start (not installed any more, restarting too often, no frame exchange file) is reported but no longer charged to the effect being rendered: it is Failure::Refused, not Failure::Broken, so it never counts toward turning that effect off. New check in tests/it: once the helper is out of restarts, renders of an effect fail without blocking it (fails without the fix: the effect was turned off after 3 such renders). - The frame exchange file is created owner-only (0o600) on Unix: it holds the user's frames and /dev/shm is shared. Asserted in the frames unit test. - tests/it (harness = false) speaks the libtest listing protocol cargo-nextest uses: `--list --format terse` prints `helper_protocol: test` (nothing with `--ignored`), and `helper_protocol --exact` runs the checks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KxacwTpUwqyfpcLgKW8yjS * Effect helpers: bound writes to a helper, drain stderr, keep callbacks in step - Writes to a helper go through a writer thread with the request timeout: a helper that stops reading its stdin (hung with a large callback answer or render request pending) no longer blocks the render thread forever while holding the helper. - stderr is drained as bytes (lossy, in 64 KiB pieces) to the end: output that isn't UTF-8 stopped the reader, closing the pipe under the helper (EPIPE / SIGPIPE on its next write); lines were unbounded. - Callback images that can't be written are answered with `null` instead of failing the render while the helper waits (which left the protocol out of step); a frame that can't be written before the request is sent is not charged to the effect. - Mid-render calls use the render's own process: no respawn or heartbeat ping while the helper is waiting for an answer. - Waiting for a busy helper is bounded (load timeout) instead of forever; status() no longer blocks on a running render. - Reap the child when start-up fails after spawning; cap the block list read. - Scripts start helpers only with Allow Scripts to Write Files and Access Network on (they run native code), as for File.execute(). - helper-host declares the workspace's no-panic crate lints; the desktop main() keeps its doc comment. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Effect helpers: rustfmt the fake provider, build the test session in its initializer (clippy) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Plug-in Library: offer WebAssembly API 1 releases only; a helper already stopped mid-render isn't charged again - WasmPlugin loads only modules at the WebAssembly ABI version, which stays 1 while PLUGIN_API_VERSION is 2: share it as effects::plugin::WASM_API_VERSION and have the library's compatibility check use it, so it never installs an API 2 release that can't load. - with_running: a process that is already gone was stopped by what broke it (a nested render, charged there), so this render fails without charging its own effect. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * frames: plug-in effects count as reading other frames (plug-in API 2) With main's narrowed frame invalidation (#2795), an edit inside a precomp under an effect that reads its layer at other times must reach the layer's whole span. Plug-in API 2 gives every plug-in effect the input at other times, so any effect outside ec. counts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com> |
||
|
|
30d6c6e1d3 |
Frames cache: edits keep the frames they can't reach (#2795)
* frames: keep cached frames an edit can't reach Nudging, trimming or splitting a layer re-rendered the whole comp. The RAM preview now carries every frame whose stack of active layers is unchanged to the new comp identity; only the segments an edit touches re-render. * disk cache: never drop frame writes; save frames kept after edits Frames get their own write lane (bounded by bytes, written before layer buffers), so a burst of layer writes no longer drops frames and leaves gaps in the blue bar after reopening. Frames an edit kept in RAM are written to disk under the comp's new content key when idle. * frames: music and hidden layers keep the caches Layers that can't change pixels (audio-only footage, AV layers with Video off, when nothing reads them and there are no expressions) no longer count for the RAM preview's dirty segments or the disk cache's comp and footage keys. Adding, trimming or moving music keeps both caches. RENDER_VERSION bumps to effectcraft-render-3 for the new key formula. * ui-egui: background caching benchmark cache_bench fills a comp's RAM preview the way idle caching does and prints frames/s with decoder, footage-cache and disk-cache counters. * frames: soloed music and hidden layers aren't silent Solo hides every other layer, so soloing a music track or a layer with Video off changes the picture. silent_layers no longer drops soloed layers, so the RAM preview and the disk cache see Solo toggles. * disk cache: audio files a layer reads stay in the layer salt Audio-only footage left the disk layer salt so importing music keeps the keys, but Audio Spectrum and Audio Waveform draw from an audio layer. Footage another layer reads (or any, when a read layer is a precomp) counts again, so overwriting that file doesn't reuse stale buffers. * disk cache: retry frame writes the queue refused put_frame reports whether the frame is on disk or queued. persist_missing forgets the key of a refused write, so a full frame lane no longer leaves RAM frames that are never written. * ui-egui: cache_bench measures idle caching; checks --comp and --slots Frames go through request_background with one slot per frame thread, as Cache Frames When Idle does. An unknown --comp is an error (the active comp is used only without --comp), --slots is at least 1, and the disk report flushes queued writes instead of sleeping. * disk cache: RENDER_VERSION effectcraft-render-6 This branch changes the cache-key formula (silent layers, Solo, read audio in the footage salt), and upstream #2759 already took render-5 for the clip-clock change, so entries from either must not be reused. * frames: rewrite RAM frames the disk cache evicted persist_missing kept every disk key it ever queued, so a frame the disk cache evicted later was never written back. The disk cache now tracks frame writes still queued or in progress (frame_pending), and persist_missing only remembers keys while it converts them. * frames: idle passes continue while RAM frames wait for disk persist_missing reports whether there may be more to write (a content key being computed, frames being converted). Cache Frames When Idle then schedules another pass, so a refused write is retried without waiting for a UI event, and the passes stop once every frame is on disk or queued. * frames: rewrite an evicted frame once, not on every idle pass A disk cache smaller than the RAM preview, or a frame too large for it, made idle passes write, evict and rewrite the same frames every 500 ms. Each disk key is now written at most twice under one disk limit (the write and one rewrite after an eviction); a new limit resets the count. The persist test waits for the conversion to forget its keys before checking that no work is left. * frames: the work area doesn't change pixels Setting the work area (B, N) cleared every cached frame, in RAM and on disk: it was compared with the comp's pixel settings and hashed into the disk key. It is a preview and render range only, like the Resolution, so it no longer counts. * frames: audio edits keep frames unless something draws the audio Changing a clip's Audio Levels, its audio effects or its Audio switch cleared the clip's cached frames, and the comp's disk cache, although nothing drew that audio. The audio now counts only for the layers whose audio is heard (Project::heard_layers): those a layer picks in an effect (an Audio Waveform or Spectrum, even on itself), the layers of a picked precomp, and every layer while an expression (which can read Audio Levels) exists. Music in a precomp that a waveform in the outer comp picks is no longer treated as silent either. * frames: an expression only reaches the frames of its own layer One expression anywhere in the project made every edit clear the comp's whole cache, and so did editing a layer another layer reads (its parent, matte, or a layer it picks). Expressions can read anything, but they only draw in their own layer: an edit now re-renders the frames where it changed something, plus those of the layers with an expression and of the layers that read a reached layer. With expressions, an edit elsewhere (another comp, an item's name, a marker, music) reaches only the expression layers' frames. * frames: an edit inside a precomp re-renders only where the precomp shows it Editing anything inside a precomp cleared every cached frame of the comps that nest it, even a music level in it. Project::dirty_spans now follows precomps: it takes the spans their comp changed in, shifts and scales them by the precomp layer's start and stretch (its whole span under a time remap or Responsive Design timing), and keeps the outer comp's other frames. Layers that read a changed precomp (a matte, a waveform) re-render where they are active. * disk cache: keep the frames an edit can't reach The disk cache keys frames on a hash of the whole comp, so every edit lost the frames that were only on disk (the blue bar), even a rename; only those still in RAM were written again under the new key. When an edit keeps RAM frames, it now also queues a disk carry: on the frame pool, each kept frame of every disk series in use for that comp is hard linked (or copied) to its new key with DiskCache::link_frame, oldest edit first. The old entries stay, so undo finds them too. * frames: a keyframe edit re-renders only around the keys it changed Changing one keyframe (its value, interpolation or ease, in the Timeline or the Graph Editor) re-rendered the layer's whole length, so a key on a full-length layer cleared the whole cache. A layer that differs only in keyframes of its Transform properties now re-renders from two keys before the first changed key to two after the last (auto-Bezier and continuous tangents reach the neighbouring segments), to its in or out point past the first or last key. Static values, expressions, roving keys, time remap and time effects still reach the whole layer; effect keyframes do too for now (simulations and time effects read other frames). * frames: music volume keeps frames while no expression reads audio Any expression in the project (a wiggle on another layer, even a disabled one) made every layer's audio count as read, so changing a music layer's Audio Levels re-rendered every expression layer and changed the comp's disk key. Audio now counts for expressions only when one may read it (Heard::audio_exprs): its text names audio or walks properties by index. An audio-only change nothing reads no longer counts as a change elsewhere in dirty_spans either. * frames: a keyframe edit with few keys or beside a self-contained expression keeps the other frames Changing a Scale key of a layer with two or three keys re-rendered the layer's whole length: with fewer than two keys before (or after) the changed one, the span fell back to the in (or out) point instead of the first (or last) key, past which the value holds. And any layer with an expression re-rendered on every edit, so a wiggle on a full-length layer cleared the whole cache. An expression layer is now reached by edits elsewhere only when its text may read outside its layer (it names a comp, layer, footage, the project or its source); its parent and the layers it picks are still read through referenced_layers. * viewer: the caching chip stays up between uncached frames, with a spinner The "Caching frames…" chip showed only while playback waited for a frame, so during caching it flickered on and off with every frame that arrived. It now stays up 0.6 s after the last wait (and hides at once when playback stops). It is also smaller and quieter: a spinner, then "Caching frames" without the ellipsis in dimmed smaller text, on a fainter chip sized to its text. * preview: the Preview panel's cache bar counts the preview's Resolution With a preview Resolution set (Half, Third…), the frames a preview plays are cached at that scale, but the Preview panel's bar counted the series the viewer shows. When playback stopped, the viewer went back to the comp's Resolution and the bar dropped from full to the frames cached there. It now counts the preview's own series (Auto still follows the viewer). The viewer and the Timeline's cache bar are unchanged. * Contributors: add nazar-gavrylyk's name * frames: an expression reading its own layer's markers or audio re-renders that layer With expressions in the project, a layer's markers count when comparing it, and its audio counts when an expression may read audio, so a marker or volume edit re-renders the edited layer over its own span. Comp markers still count only through layers whose expressions name thisComp. * frames: a keyframe edit on a layer with an expression re-renders the layer An expression on the keyed layer can read the keyed property at other times (Rotation reading Position with valueAtTime), so the frames past the changed keys may change too. * cache_bench: read decoder counters through effectcraft-media Only crates/media, crates/export and crates/interchange may depend on the FilmCraft crates. * cache_bench: clips end where the next one starts * frames: write-back uses the worker's disk keys while a Roto Brush model is active persist_missing keyed disk frames on FrameKey::opts, which also carries the Roto Brush model stamp; the worker and disk_frames key on the render opts' hash alone. With a model active every RAM frame looked missing, was written again under keys nobody reads, and recorded a second disk series. * frames: an edit two precomps down reaches the outer comp A precomp layer whose comp Arc didn't change was skipped, but the comps nested in it may have (an edit in Inner leaves Mid's Arc as it was), so Outer kept every frame. Every nested comp is walked now, each once per edit. * cache_bench: stop when the disk cache can't be cleared * frames: one disk carry job at a time carry_disk runs on every timeline paint and idle pass, and spawned a pool job whenever carries were queued; during a drag the extra jobs waited on the carry lock and held frame-pool threads from the viewer. A scheduled flag keeps one job, which drains the queue in order until it's empty. * frames: a panicking disk carry is skipped, not left holding the job The carry job clears carry_scheduled only on its normal exit; a panic in a carry would have left it set for the session, so no later carry ran. Each carry is caught like the content key's hashing, so the job always reaches its exit. * frames: an expression reading its own in point or index re-renders its layer A trim or rename kept the layer's frames outside the cut when the layer drew the same apart from its span, but an expression on it can read its own inPoint, outPoint or name (a fade from inPoint), so every frame of the layer changes. Such a layer is no longer treated as only respanned. An expression reading `index` reads the layers above it too: a layer added or removed there shifts it, so `index` now counts as reading outside the layer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * frames: an edit of a comp reaches the expressions of the precomps it nests An expression in a precomp can read the comp that nests it (`comp("Main").layer("Controls")`), but the walk into nested comps only counted changes outside the edited top comp. Editing a hidden Controls layer there (or any layer, when the precomp is time-shifted or reads it at other times) kept every precomp frame in RAM and linked them all on disk under the new key. Nested comps now see an edit of the comp above them as an edit elsewhere. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * frames: an edit in a precomp under a time effect reaches its whole layer A precomp layer running Echo, Posterize Time, Time Displacement or another effect that reads the layer at other times shows a nested change far from where it maps, but only the mapped span (a nested frame either side) re-rendered. Such a layer now gives its whole span, like a time remap. `reads_other_frames` lists those effects (every `ec.time.` and `ec.sim.` effect and the others that call `self_at`); keyframe spans use it too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * frames: a keyframe edit beside muted keys re-renders the whole layer Muted keys are skipped when a property is evaluated, so the keys that play around an edited key can be any distance away, but the span counted two stored keys either side. A segment from an earlier playing key kept its stale frames. Muted keys now give the layer's whole span, like roving keys. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * frames: one echo in the precomp time-effect test (three saturate alike), rustfmt Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
653157532b |
Merge upstream/main into fix/exr-scene-linear-output
Conflict: crates/gpu/src/tests.rs (both sides added a test; kept both). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> |
||
|
|
389894e69b |
gpu: run Content-Aware Fill's PatchMatch and flow on the GPU (#1856)
* gpu: run Content-Aware Fill's PatchMatch on the device Content-Aware Fill's synthesis is the stage that makes the tool slow: at 1920×1080 with a 300×200 hole, PatchMatch costs ≈ 1.1 s per frame on the CPU, and it is what Object, Surface and every refining frame spend their time in. Add a `FillBackend` trait to `effectcraft-raster` whose stages (the membrane solve, the flow fields, PatchMatch) default to the CPU reference, and `Accelerator::inpaint()` on the compositor, which is how the engine asks for a device backend. `effectcraft_gpu::inpaint` implements PatchMatch: the multi-scale pyramid, the EM rounds, the propagation (a two-pass checkerboard instead of the CPU's scan order), the random search, the SSD, the histogram-based σ² and the weighted vote, as compute kernels in `shaders/inpaint.wgsl`. The fill's frames are read back to the CPU before it runs (analyses composite on the CPU by design), so the backend is about the fill's own work; the orchestration, the flow-guided propagation and the per-frame flow control stay on the CPU. Only the hole's bounds cross back to the CPU, which is what makes the offload pay. Measured on an Intel Arc (MTL) iGPU, 1920×1080, 300×200 hole: CPU 1087 ms GPU 397 ms (2.7×, 41.0 dB PSNR against the CPU's synthesis) The membrane (harmonic) solve deliberately stays on the CPU: the same measurement has it at ≈ 90 ms on the device against ≈ 55 ms on the CPU, because a fill's full-frame data movement (upload, the known pyramid, a copy) does not shrink with the hole, while the relaxation does. The device takes only the stage where its parallel search pays; `membrane_tex` still runs on the device inside PatchMatch, where it is the coarsest level's start and never crosses back. Closes #1839. * gpu: fix the fill's vote histogram and run its flow on the device Review follow-ups on the Content-Aware Fill GPU port, plus the fill's flow. Fixes: * imp_hist kept about one sample in 256: each of the 256 threads scanned a strided subset of the pixels and retained only those whose bin selected it, so the vote's sigma² came from a sample correlated with the pixel index (and a small hole usually fell back to 1.0, making the weights near-uniform). One workgroup now accumulates the whole population through shared atomic bins, with a test that pins the sample count. * imp_pm_prop and imp_pm_rand were dispatched over grow(near, 1) but with near's origin, so the ring's left column and top row were never written (NaN under test, stale pooled data in production) while a strip past it was. `over` now derives the shader's origin from the box it sizes the dispatch from, so a dispatch's region and its origin can no longer disagree. * Gpu::inpaint() requires its kernels to have built. Otherwise every stage paid an upload and a failed dispatch to fall back to the CPU, while the fill still reported the GPU path. * imp_rnd's XOR chain parenthesises its multiplicative terms: WGSL's grammar makes each XOR operand a unary expression, so `a * b ^ c * d` is rejected by spec-compliant parsers (naga accepts it). * FillBackend documents that its stages may be called concurrently. * The coarsest membrane's sweep count (64, against the CPU's 200) is now justified: matching 200 measured ~6% slower over the fill with no change in the result. And the flow: Gpu::flow runs block_flow's coarse-to-fine search, sub-pixel fit and vector median on the device. The luma is the CPU's (flow::luma_pair), so the field matches block_flow's to a float rounding and the propagation sees the same motion; the hole completion stays on the CPU with the membrane. A four-frame 1920x1080 fill goes from 1.27x to 1.42x against the CPU, and the device path is covered for holes from 16x16 to 300x200. * gpu: keep the fill vote's histogram range on HDR input imp_hist binned every distance at a fixed 256 bins per unit and clamped to 1023, so the vote's sigma2 topped out at 3.998. A 32-bpc frame's channels can exceed 1, and masked_frame forwards them unclamped: a patch distance of 1600 then reported sigma2 ~ 4, exp(-1600/(2*sigma2)) rounded to zero in f32, and imp_pm_vote kept the prior image instead of synthesising. The histogram now reports the largest finite distance it saw (atomicMax on the float bits, carried in bin 0's unused .g channel). SDR input keeps the fixed 0..4 range with identical bins; HDR input is re-binned over the observed range and percentile() maps the bin centre back through it. Nothing clamps the input. Also group over/sweeps/pm_level's arguments into small structs: the workspace denies too_many_arguments and the lint policy (#2496) does not allow new #[allow]s. No behaviour change on those paths. * engine: don't report Edge Blend fills as device fills Edge Blend calls only the membrane, which the GPU backend leaves to the CPU reference, so run_plan's "Filling (GPU)" status was wrong for it whenever a backend was present. Object and Surface still run the flow and PatchMatch on the device and keep the label. CodeRabbit's #1856 finding. * gpu: run the fill's device stages inside error scopes An allocation or validation failure inside patch_image/flow_image left an invalid texture whose later use raised an uncaptured error, retiring the shared compositor instead of falling back to the CPU for that frame. Wrap both stages in Gpu::within_memory, whose scopes are native thread-local, so the fill's rayon workers do not share them. --------- Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com> |
||
|
|
6df7eabe05 |
gpu: memory back-pressure for frames in flight (fixes OOM at 3–4 frames in flight) (#1550)
* GPU: free effect working textures after each call instead of at frame end A GPU frame keeps its encoder open while the CPU renderer runs layers with GPU effects (Gpu::effects -> run_chain). Each call's Enc handed its textures back to the shared pool, where they waited for every open encoder, so all effect intermediates of a frame stayed allocated until the frame ended (17.7 GB by layer 17 of a heavy 4K comp, then OOM). Effect calls now borrow a pool owner (Enc::lent) that is given back once the call's work is submitted or discarded; its kept textures are trimmed to the pool budget, so the rest are freed after each call. * GPU: one pool budget for idle effect calls, trimmed at the end of a render Review of #1523: each idle lent owner kept up to POOL_BUDGET of textures of its own, outside the shared accounting, and there were as many idle owners as effect calls ever ran at once (16 frame threads: ~12 GB after a 4K render). Giving a lent owner back now moves its kept textures to the shared free list, so one POOL_BUDGET bounds what the free list and every finished effect call keep. They skip Pool::release's wait for open encoders: only the lent Enc recorded work on them, and that work is submitted or discarded. Each lend is a fresh owner id, so a lease that outlives its call (an image handed to another encoder) is released through Pool::release; this does not rely on the result being read back. Accelerator::render_end (called when an export ends) drops the kept textures. * GPU: evict the pool's oldest free textures, guard escaped leases Review of #1523 (follow-up to 0a884fb6): - Pool::make_free rejected a texture when the free list was full and never evicted, so once sizes that no longer occur (per-frame blur bounds, padded layer sizes) filled the budget, every later free was a drop and every take an allocation. It now evicts the least recently freed textures until the new one fits. - A lease read by another Enc (dispatch, copy, readback, display) or handed to a cache is marked escaped and always released through the guarded Pool::release, so a lent call can no longer reuse a texture that a still open encoder reads. - Docs: POOL_BUDGET caps the free list only; textures waiting for open encoders and those of live Encs are outside it. - export_async calls render_end from a drop guard, so the pool is also trimmed when an export panics or its future is dropped. - Tests use a private context with its own pool budget instead of a global override; new tests for stale sizes and an image read by an open encoder. * GPU: evict only stale pool sizes, count evicted textures until the GPU frees them Evicting the least recently freed texture for every newcomer thrashed when a frame frees more than the budget in a repeating order (blur/glow intermediates): each free evicted the size the next call takes first, so misses came back to the old level and evict + reallocate raised the peak. The free list now evicts only sizes nobody asked for in the last 2 output frames (Accelerator::frame_end, called by the export per frame); otherwise the incoming texture is dropped and the kept set keeps hitting. Evicted bytes count against the budget until the work submitted so far is done, so evictions run at most one budget ahead of the GPU. * GPU: lower the shared pool budget to 256 MB On 4K RTX 3090 runs 256 MB matches the old code's peak reserved on d_blurglow (9204 MB) and is 256-512 MB above it on c_effects (6592 vs 6080 MB), while staying faster. * GPU: end frames on the multi-frame render paths, route recording through Enc::record Only the export pipeline called frame_end, so off export (preview, remote frames) the pool's clock stood still, no size went stale and a full free list dropped every newcomer. passes::comp_frame (exports), remote frames and the preview workers now end each frame; test without pool_frame_end. Docs: the frame clock is coarse with many frames in flight; dying only throttles eviction, approximately; no cache path escapes images. Enc::record is the single way to get the encoder outside context.rs and marks the images it names (Enc::uses). * GPU: cap in-flight frame memory on Vulkan and DX12 (#1550) wgpu destroys a dropped texture or buffer only once the submissions that used it have completed and the device is maintained. With several 4K frames in flight on an export or effect-heavy render, frames can record and drop working textures faster than the GPU retires them, accumulating freed-but-not-destroyed allocations until a 4-8 GB adapter runs out of memory (#879). Add a memory gate (MemCap) checked every 8 effect steps or layers: - Default cap of 3 GiB on discrete GPUs (low enough for 4-8 GB cards while leaving headroom above the 1.5 GiB upload cache and 256 MB shared pool) and 2 GiB on integrated/software adapters, overridable via EFFECTCRAFT_GPU_MEMCAP_MB (0 disables). - Compare total_allocated_bytes against non-reclaimable bytes (upload cache, pool free list, live leases, staging, zeros) with hysteresis and a post-wait floor so the gate only drains when a wait can free dropped allocations and does not spin when caches or other open encoders hold memory over the cap. - Submit and wait on the current Enc's own SubmissionIndex via a dedicated poll_wait helper (serialized by poll_lock) so a timeout simply carries on without retiring the context or its readback pool. * GPU: warn on invalid EFFECTCRAFT_GPU_MEMCAP_MB, register crop sources, require GPU in the mem-cap child test - memcap: an EFFECTCRAFT_GPU_MEMCAP_MB value that is not a whole number of MiB or overflows 64 bits now logs a warning and uses the default cap (it used to fall back silently, or wrap on overflow). Unset still means the default and 0 still turns the gate off. - Enc::crop records through Enc::record, so a foreign source lease is marked escaped like in copy_into. - The mem-cap child test starts its GPU through require_gpu, so EFFECTCRAFT_REQUIRE_GPU=1 fails when the GPU does not start, and prints a skip line (with the adapter name when the backend has no allocator report), which the parent passes on. * GPU: do not repeat the pool frame-end wait until evicted textures drain A wait that frees none of the evicted bytes (timeout, or a WebGPU poll that does not block) marks the pool stalled; later frames only poll until dying drops. Test covers the no-repeat and partial-progress cases; dying decrement saturates. * Remote frames: end a pending frame that a new render replaces A render arriving while a multi-pass frame waits for readbacks dropped that frame without `frame_end`, so the GPU pool's clock skipped it. Errors return before `frame_begin`. * Render: end the frame even when a comp frame panics `passes::comp_frame` called `frame_end` only after a normal return, so a frame that panicked (or whose future was dropped between passes) never aged the GPU pool. * GPU memcap: warn once when kept memory alone is over the cap; test the edge frames A frame whose non-reclaimable memory exceeds the cap never waits when reclaimable bytes stay below the hysteresis (waiting cannot free the kept set) and now logs that once. Tests cover a frame over the cap, two frames that fit one at a time, and a dropped frame giving its memory back. --------- Co-authored-by: Matej Kozlovský <MatejKozlovsky@users.noreply.github.com> Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com> |
||
|
|
08362b9c04 |
GPU: free effect working textures after each call instead of at frame end (#1523)
* GPU: free effect working textures after each call instead of at frame end A GPU frame keeps its encoder open while the CPU renderer runs layers with GPU effects (Gpu::effects -> run_chain). Each call's Enc handed its textures back to the shared pool, where they waited for every open encoder, so all effect intermediates of a frame stayed allocated until the frame ended (17.7 GB by layer 17 of a heavy 4K comp, then OOM). Effect calls now borrow a pool owner (Enc::lent) that is given back once the call's work is submitted or discarded; its kept textures are trimmed to the pool budget, so the rest are freed after each call. * GPU: one pool budget for idle effect calls, trimmed at the end of a render Review of #1523: each idle lent owner kept up to POOL_BUDGET of textures of its own, outside the shared accounting, and there were as many idle owners as effect calls ever ran at once (16 frame threads: ~12 GB after a 4K render). Giving a lent owner back now moves its kept textures to the shared free list, so one POOL_BUDGET bounds what the free list and every finished effect call keep. They skip Pool::release's wait for open encoders: only the lent Enc recorded work on them, and that work is submitted or discarded. Each lend is a fresh owner id, so a lease that outlives its call (an image handed to another encoder) is released through Pool::release; this does not rely on the result being read back. Accelerator::render_end (called when an export ends) drops the kept textures. * GPU: evict the pool's oldest free textures, guard escaped leases Review of #1523 (follow-up to 0a884fb6): - Pool::make_free rejected a texture when the free list was full and never evicted, so once sizes that no longer occur (per-frame blur bounds, padded layer sizes) filled the budget, every later free was a drop and every take an allocation. It now evicts the least recently freed textures until the new one fits. - A lease read by another Enc (dispatch, copy, readback, display) or handed to a cache is marked escaped and always released through the guarded Pool::release, so a lent call can no longer reuse a texture that a still open encoder reads. - Docs: POOL_BUDGET caps the free list only; textures waiting for open encoders and those of live Encs are outside it. - export_async calls render_end from a drop guard, so the pool is also trimmed when an export panics or its future is dropped. - Tests use a private context with its own pool budget instead of a global override; new tests for stale sizes and an image read by an open encoder. * GPU: evict only stale pool sizes, count evicted textures until the GPU frees them Evicting the least recently freed texture for every newcomer thrashed when a frame frees more than the budget in a repeating order (blur/glow intermediates): each free evicted the size the next call takes first, so misses came back to the old level and evict + reallocate raised the peak. The free list now evicts only sizes nobody asked for in the last 2 output frames (Accelerator::frame_end, called by the export per frame); otherwise the incoming texture is dropped and the kept set keeps hitting. Evicted bytes count against the budget until the work submitted so far is done, so evictions run at most one budget ahead of the GPU. * GPU: lower the shared pool budget to 256 MB On 4K RTX 3090 runs 256 MB matches the old code's peak reserved on d_blurglow (9204 MB) and is 256-512 MB above it on c_effects (6592 vs 6080 MB), while staying faster. * GPU: end frames on the multi-frame render paths, route recording through Enc::record Only the export pipeline called frame_end, so off export (preview, remote frames) the pool's clock stood still, no size went stale and a full free list dropped every newcomer. passes::comp_frame (exports), remote frames and the preview workers now end each frame; test without pool_frame_end. Docs: the frame clock is coarse with many frames in flight; dying only throttles eviction, approximately; no cache path escapes images. Enc::record is the single way to get the encoder outside context.rs and marks the images it names (Enc::uses). * GPU: wait for the empty submission while evicted pool textures still count stale_sizes_do_not_starve_a_steady_size failed with 0 hits and 20 misses on hardware adapters: pool_frame_end submitted an empty batch and then only did a non-blocking Poll, and the queue's work-done callback did not run before the next frame, so Pool::dying never drained and make_free stopped evicting stale sizes. While dying is non-zero, pool_frame_end now waits on that empty submission for up to the readback timeout (the pool lock is already released). A wait timeout just moves on and does not retire the context or its readbacks; other poll errors still retire them. Without dying textures it keeps the cheap Poll. Enc::crop records through Enc::record(&[src, &dst]) so both pooled images are registered like on every other recording path (review). * GPU: do not repeat the pool frame-end wait until evicted textures drain A wait that frees none of the evicted bytes (timeout, or a WebGPU poll that does not block) marks the pool stalled; later frames only poll until dying drops. Test covers the no-repeat and partial-progress cases; dying decrement saturates. * Remote frames: end a pending frame that a new render replaces A render arriving while a multi-pass frame waits for readbacks dropped that frame without `frame_end`, so the GPU pool's clock skipped it. Errors return before `frame_begin`. * Render: end the frame even when a comp frame panics `passes::comp_frame` called `frame_end` only after a normal return, so a frame that panicked (or whose future was dropped between passes) never aged the GPU pool. * GPU pool: log the reuse rate at render end; test reuse across effect calls and frames Log cumulative hits/misses (and reuse %) once per render before trimming. Tests cover reuse within an open frame across effect calls, and across repeated frames. --------- Co-authored-by: Matej Kozlovský <MatejKozlovsky@users.noreply.github.com> Co-authored-by: bflatastic <bombayapirat@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com> |
||
|
|
e3ab397a7e | Merge remote-tracking branch 'origin/perf/exr-decode-cache-474' into m2929-merge | ||
|
|
5b31b96cde |
perf(media): tell the media pool which EXR footage effects read (#474)
The pool learned that an effect reads a footage item's channels only at the first aux() call, so the first frame was decoded twice (picture, then in full), and the hint was never cleared after the effect was removed. The session now gives the footage source the whole set of files read by enabled 3D Channel effects (FootageSource::set_aux_footage) with every project change (Session::bump: edits, undo, redo, history jumps, rollbacks, project open), sending it only when it changed. The pool keeps that set and, as a safety net, what aux() learns; a new set drops what was learned. Refs #474 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
406c9a9648 |
EXR export: raw alpha for Alpha-only files, skip_hdr_step name, more tests
- Rename RenderOpts::scene_output to skip_hdr_step (Pipe::without_hdr_step): the consumer decodes with Pipe::output_linear or applies the step itself. - Alpha-only EXRs hold the alpha as it is in every output space (it is coverage, not encoded colour; it used to be sRGB-decoded). - Tests: a half-opacity layer over a coloured background (premultiplied and RGB-over-background paths, golden hashes taken before the change), Rec. 2100 PQ and HLG EXR outputs, raw alpha, and a visible skip of the GPU test without an adapter. - docs/architecture.md: the Working Gamma curve for unmanaged projects, and no HDR step for unmanaged projects or PQ / HLG outputs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
684e05859f |
EXR export: no compand / tone mapping, and the output space's own curve
OpenEXR renders (Render Queue sequences and Save Frame As ProEXR) received the frame after Project Settings > HDR, so Compand and Tone Map (the default of every OCIO project) compressed highlights and changed values below 1.0 before the writer decoded them, always with the sRGB curve. - RenderOpts::scene_output: the top-level comp converts to the output space without the HDR step (CPU and GPU walks, via Renderer::output_pipe). - Pipe::output_linear: the writers decode with the output space's curve (the Working Gamma's when unmanaged) instead of a fixed sRGB curve. - Render Queue EXR jobs and ProEXR set the flag; PSD layers, 8-bit stills, movies and the viewer keep the HDR step. The flag is part of the viewer frame and disk cache keys (opts_hash). Golden hashes, taken before the change, pin PNG/TIFF/Targa bytes for 8/16/32 bpc x Clip/Compand/ToneMap, Clip-mode EXR samples with an sRGB output, and the layered PSD bytes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
0af6d2aab8 |
Text animators: a later animator's Opacity can show characters an earlier one hid
Animator Opacity stacked by multiplying, so once one animator set 0 % no animator below it could raise it again. Each animator now blends the opacity left by the ones above it toward its own value. A single Opacity animator gives the same result as before. Fixes #2838. |
||
|
|
f12154cf4c | Cache: bump the renderer version so frames from the old clip clock aren't reused (#2707) | ||
|
|
474e73b85c | 3D: a model copy's cache key covers the model's Essential Property overrides (#2707) | ||
|
|
5b27de98e2 | 3D: test that a copy of an overridden model clip isn't served the keyed clip's pixels (#2707) | ||
|
|
a1ea20a52a | 3D: a model clip keyed on later starts at its activation time (#2707) | ||
|
|
4baeee63e6 | 3D: tests for keyed model clips starting at their activation time (#2707) | ||
|
|
c60b12a20f | Merge PR #2693 | ||
|
|
26a863b0f0 | Merge PR #2666 | ||
|
|
588560c368 |
Render: frame_position no longer panics on a zero or negative frame rate
A project file can carry footage with a frame rate of den 0; frame_position divided by zero while rendering it. Treat non-positive rates as having no frame to land between. |
||
|
|
e7ac2b5203 |
Disk cache: drop frames rendered before adjustment layers blended in their mode (#2618)
#2653 (#2618) changed what an adjustment layer in a mode other than Normal renders, but its cache keys are unchanged, so frames cached before it keep showing the Normal result. Bumping the render version keeps old entries from being reused. |
||
|
|
c4ed3a52ed | Merge remote-tracking branch 'origin/main' into HEAD | ||
|
|
94c8bde240 |
Adjustment layers blend in their blend mode (#2618)
An adjustment layer replaced the comp below with its effects' result whatever its blend mode. As in After Effects, a mode other than Normal now blends the adjusted comp onto the comp below in that mode, inside the footprint, track matte and opacity (an adjustment layer with no effects in Screen screens the comp with itself). CPU and GPU paths. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
ceb6cc5277 |
Keep an adjustment footprint's cache key free of its effect stack.
Noise, paint, the shutter, and the comp scene were still reaching the footprint key, so a finishing adjustment re-uploaded a full frame every frame. The key now uses EffectScope::Footprint and ignores that stack, while a moving mask still re-keys it. |
||
|
|
5522711e92 | Merge remote-tracking branch 'origin/main' into HEAD | ||
|
|
9e6332ea27 |
Merge main into #1739; keep both footage re-exports (Cineon + VIDEO_EXTENSIONS) and main's gaps.md localization row with the PR's AVI row
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
3bc1caaedb |
Shapes: per-corner roundness for Rectangle Path (#1896)
Add Per-Corner Roundness to Rectangle Path: a checkbox (off by default) and four Pixels properties (top-left, top-right, bottom-right, bottom-left). New rectangles initialise the four values from their Roundness, so ticking the box keeps the shape; the radii are animatable and expression-driven.\n\nrect_radii() builds the path clockwise from the top-left with kappa quarter-circle arcs. Overlapping radii scale down like CSS border-radius; negative and non-finite radii are square; rect() delegates to it, so uniform roundness clamps to half the shorter side as before.\n\nLottie rc has one r: four equal static radii export as that value; mixed or animated radii export roundness 0 with a warning. Import puts r into the four per-corner values so ticking the box on an imported rectangle keeps the shape.\n\nTests: path geometry and clamping, rendered per-corner pixels, Lottie equal/mixed/checkbox-off export. |
||
|
|
5c2e7ad343 | Merge remote-tracking branch 'origin/main' into HEAD | ||
|
|
5c89837f91 | Merge remote-tracking branch 'origin/main' into HEAD | ||
|
|
33ce4bbbed | Merge remote-tracking branch 'origin/main' into HEAD | ||
|
|
6c9e625a2e | Merge remote-tracking branch 'origin/main' into HEAD | ||
|
|
0039081952 | Merge main into #2567; keep both test modules, and convert four layer stacks main's new render tests assign as Vec<Layer> with .into() | ||
|
|
852f98d6a5 |
Merge enhance/filmcraft-avi-backend into enhance/avi-import
# Conflicts: # crates/export/tests/it/movie_bytes.rs |
||
|
|
56fde5c3d6 | Merge main into #1279; effects/lib.rs: keep the PR's Buf::grow_left_top/align beside main's budgeted Buf::pad, and put cover_plan's doc comment back on cover_plan (the PR's new items had split it off) | ||
|
|
335abf42c4 | Merge main into #2474; tests/it/main.rs keeps both ui_env_light_gpu and main's ui_expression_values | ||
|
|
0754a754ae | Merge main into #2300; Cineon conversion kept in footage_frame_raw (main routes proxies through footage_frame), frame-blend cache key hashes both premul colour and Cineon settings, spec strings combined | ||
|
|
cdc5fddaff |
Merge pull request #2610 from storytold/fix/text-line-join
Text: stroke Line Join — Miter, Round or Bevel (#2077) |
||
|
|
e28b43dc40 | Merge main into #2355; ROADMAP progress log keeps both entries | ||
|
|
e28612dedd |
Merge pull request #2574 from roadhong/fix/audio-level-keyframes-per-sample
Audio: keyframed Audio Levels and Stereo Mixer levels ramp within a mix block |
||
|
|
e2128a6743 |
Merge pull request #2118 from ChristopherLinnett/fix/issue-2092-camera-focus-crash
fix: bound Classic 3D depth-of-field blur and reject non-finite focus distances (#2092) |
||
|
|
de40efed7c | Merge remote-tracking branch 'origin/main' into HEAD | ||
|
|
57b76bcc88 | Merge remote-tracking branch 'origin/main' into HEAD | ||
|
|
8b846ffdf1 |
Render cache: the soloed-model key reads main's two-argument reads_other_times
#2410's dependency walk for a soloed 3D model was written before reads_other_times took the effect count; it asks about all effects (None), as the other walks do. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
661f95b901 | Merge remote-tracking branch 'origin/main' into HEAD |