Repository navigation
perf: faster effects, expressions, text, caches, movie reading, project open, export and UI playback - #856
perf: faster effects, expressions, text, caches, movie reading, project open, export and UI playback#856eliasx45 wants to merge 47 commits into
Conversation
Used twice per box_plane/gauss_plane vertical pass; bit-identical to the column-by-column version (tested against it). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HDewEVE9bAYgXzvLq94jnb (cherry picked from commit 8c70c77) (cherry picked from commit 49971b6)
Replaces the per-sample &mut dyn FnMut; bit-identical to the old code (kept as a test oracle). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HDewEVE9bAYgXzvLq94jnb (cherry picked from commit 2b3d2b6) (cherry picked from commit 95ea456)
Pixel counts were re-summed for every output pixel; output bit-identical. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HDewEVE9bAYgXzvLq94jnb (cherry picked from commit d84d1b1) (cherry picked from commit 3383520)
Bit-identical (oracle test against the per-sample computation); HQ ~7x faster. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013MFSUedLFmbuKi4EawpizQ (cherry picked from commit 89f485e) (cherry picked from commit 32281ff)
(cherry picked from commit b7bb2dba1a62405f5712432acf05a6726b713820) (cherry picked from commit 3e3fe2d)
(cherry picked from commit eb10f4ce5518fbed2e8530e2e22340da3b3c93d4) (cherry picked from commit c32fcb5)
… pixel progressive_blur blurred the whole image at up to eight radii and kept them all; iris kernels now gather just the two levels around each pixel's radius from one set of row prefix sums (Fast Rectangle's O(1) box levels are unchanged). Bit-exact against the old code (test oracle); ~4x faster at 640x480, 0..24 px. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H2Vw4ifTRhxWcLfvzW3G9b (cherry picked from commit f157946) (cherry picked from commit e109c7a)
… shadows Both compositors drop them unused when RenderOpts::shadows() is false (draft without Realtime Shadows); output unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H2Vw4ifTRhxWcLfvzW3G9b (cherry picked from commit dd4e94f) (cherry picked from commit c03b345)
… answer the last run read A per-thread memo keyed by script, comp, layer, property, comp time and the __begin inputs. A hit answers the recorded requests at the evaluation's own depth and returns the stored result only when every answer is bit-identical, so edits can never serve a stale result; a slider read by N layers runs once per frame. Scripts using Math.random/Date are not stored. Tests compare against fresh runs on a thread with the memo off. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VzreW5cLMczbUzegSKxcUv (cherry picked from commit ff5e217) (cherry picked from commit 2f03926)
…ares memo off/on Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VzreW5cLMczbUzegSKxcUv (cherry picked from commit de244f3) (cherry picked from commit 2d3f690)
…upstream) (cherry picked from commit e9b042e9fe6f6f52b9ec92c9a084f169a75148f0) (cherry picked from commit 5e1cc23)
Glyph outlines are read under a RwLock and transformed straight from the shared Arc (no clone). layout_doc caches its finished TextLayout keyed by the shaped layout, styles and box, and both that cache and the shaping cache are byte-bounded LRUs (64 / 32 MiB) instead of 512 entries. Output is identical to the previous path (oracle test over varied documents). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TZCme2FE1XWV7FWbjyAxaW (cherry picked from commit 65bfe27) (cherry picked from commit 4dbb636)
(cherry picked from commit f61830766970fa32780dce700b792e006e8dfd7a) (cherry picked from commit d9befa8)
…s branch) (cherry picked from commit 1b4f841f1408ba2db9dd438f28ad7d0ca58b3e98) (cherry picked from commit bfb97cd)
`Node` was an internally tagged serde enum, so serde buffered every node subtree into its private `Content` tree before converting it. A hand-written `Deserialize` reads the `"node"` tag when it comes first (serialization always writes it first) and streams the rest of the map into `Property`/`PropGroup`; a tag found later falls back to buffering. Serialization is unchanged (still derived). Parsing a 20.9 MB synthetic layer array (200 layers x 5 animated props x 200 keys) drops from 226 ms to 72 ms in release. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0123prcPx5zRkZvD52XuVKJE (cherry picked from commit cd480a0) (cherry picked from commit 5614422)
migrate::is_current is a read-only check mirroring upgrade_instance; current instances return early instead of being deep-cloned for the comparison. A test checks it never skips a needed upgrade over per-parameter variants of every registered effect. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kqn7byVXrwKLfhCti44RYL (cherry picked from commit ca89dc7) (cherry picked from commit ff825b7)
…torytold#514), so opening a project upgrades old Apply Color LUT / OCIO instances Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YAHHERF9trzfV2uSdYVBc6 (cherry picked from commit 0b083de) (cherry picked from commit 2536391)
…e, WebM VP9/AV1) Cx::batches hands batches in order from a scoped renderer thread through a rendezvous channel (at most the batch being encoded + the one rendered next). Serial in passes, on wasm and on rayon workers. Output bytes unchanged. Rebased for upstream: keeps upstream's audio start-tick fix and the budget-sized batch_size(job, comp); tests/pipelined.rs is its own test binary. (cherry picked from commit 6159698) Co-Authored-By: Claude <noreply@anthropic.com> (cherry picked from commit fa3f7993be048105866a8bfb8710875eeffffeb0) (cherry picked from commit bca439c)
…vious module's layer cache A Render Queue item with N output modules exported each module as a fresh job: a project clone and an empty layer cache per module, so static and transform-only layers rendered N times. A finished export now keeps its overridden project and layer cache (one slot, dropped after 3 s or when the next job takes it); the next export takes it only when it renders the same frames: equal project, same comp, Render Settings equal but for the time span, same Switches Affect Nested Comps, memory budget, footage source, expression host and accelerator. Layer buffers are content-keyed, so files are byte-identical to each module exported alone (new output_modules test: H.264, PNG sequence, ProRes). Failed or cancelled exports keep nothing; the web keeps nothing (no timer to drop it). Progress, cancellation and error semantics of the queue are unchanged and now documented at the module loop. tests/output_modules.rs is its own test binary upstream (no tests/export/main.rs). (cherry picked from commit f3f683c) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ldtkj4yRfYi6j6CwRrde11 (cherry picked from commit 508f9b983694724031519126685a4f8e0c83a12a) (cherry picked from commit 4733a7c)
export_modules / export_modules_async export one Render Queue item's output modules: modules rendering the same frames (same project, comp, Render Settings incl. time span, session objects, memory budget, Crop and Resize) export at once from one render of each frame, each with its own encoder and muxer on a thread of its own; the rest export alone. Batches keep the per-module partition (batch_size) so files stay byte-identical, the next batch renders while modules encode, and a batch goes to one module at a time. Results, progress, stop and failure behave as modules exported one after another. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011kdPxhtproheZmiPj5p3ML (cherry picked from commit b697b25) (cherry picked from commit ccb2002)
Exporter::export_modules_async takes every output module of a queue item; the default exports them one after another as before, so an exporter can render the frames they share once. The item's progress shows the module furthest behind; result, stop and failure handling are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011kdPxhtproheZmiPj5p3ML (cherry picked from commit 6c73d53) (cherry picked from commit 335c8de)
…dules FileExporter overrides Exporter::export_modules_async with effectcraft_export::export_modules_async (same job construction, progress, cancellation and error mapping as export_async), so the app fans one render of each frame out to every output module that shares it. Tests: byte identity with each module alone and one footage request per frame for 2 and 3 modules, a stop mid-way, a failing module, and an ignored timing test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fwx5KNPtB2EbV1TCwq4puC (cherry picked from commit 49ca5eb) (cherry picked from commit f3e743c)
(cherry picked from commit c534678c3034f68a1bd49574f9f36f0d480b8010) (cherry picked from commit 4a05bd3)
(cherry picked from commit 026bce565b8a841ff0587125bebb9425babbb4da) (cherry picked from commit 0134eac)
(cherry picked from commit 7a08a4f5fee6e4dbe885c51325c9e660476ae464) (cherry picked from commit 1133015)
… (A9) cached_frames and disk_frames scanned the RAM cache and hashed a disk key per comp frame (with mutex locks) on every UI frame. RAM and disk caches now count changes, and the lists are kept per series until the frames, project or caches change. (cherry picked from commit 8c0b997) (cherry picked from commit c8bf68c)
… to the panel size, keyed on item + frame + interpretation Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Gh8SBi8MuyzgBSJ2f5n7W (cherry picked from commit f3e5438) (cherry picked from commit 6b053c6)
…orrow) (cherry picked from commit 9ff7208ba18657fa813aa7bbcfb3f8de5f2e29e8) (cherry picked from commit 3848d2e)
…e spec changes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Gh8SBi8MuyzgBSJ2f5n7W (cherry picked from commit 7c4e305) (cherry picked from commit d4703e6)
…rytold#859 storytold#860; fidelity harness waits for agents-1
# Conflicts: # crates/export/src/encode.rs # crates/expr/src/lib.rs # crates/expr/src/runtime.rs # crates/media/src/layered.rs # crates/media/src/pool.rs # crates/media/src/probe.rs # crates/media/tests/footage.rs # crates/ui-egui/src/frames.rs # crates/ui-egui/src/panels/viewer.rs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MMnX8KooFZRPByLioEuyso
|
Thank you for this, @eliasx45! We're holding this for the maintainers: it's a 45-commit grab-bag, so they'll decide whether to take it whole or ask for a split. Points to settle either way: it adds If you are not on our Discord yet, please join us: https://discord.gg/artcraft |
…owever it reaches them Review on storytold#856: the memo skipped scripts only by text (Math.random, Date), so Math["random"]() or an alias got around it. Math.random and Date (construct, call, Date.now, and through Date.prototype.constructor) are now wrapped at start-up so a call marks the evaluation impure, and impure runs aren't remembered; the text check stays as a cheap first filter. Test: impure_calls_are_caught_however_they_are_spelled. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014d65YdNxQmjRkbXRtP2NwH
|
@echelon Thanks. Fixed: the expression memo now catches Math.random/Date however they're reached (Math["random"], aliases, Date.prototype.constructor). Impure runs are never cached. Open question: is the symphonia dependency OK, or should it go? |
|
@echelon Updated in 98db5fe: removed the direct Symphonia dependency (standalone audio stays through FilmCraft), closed the Date.now property-descriptor memo bypass, and integrated current main while preserving #688’s upload-cache budget alongside the layer-cache budget. Also corrected two merge mismatches in expression fallback handling and the property-parser reference tests. Formatting and strict full-workspace all-target Clippy pass. Exact clock-wrapper probes pass in Node and Boa. Full runtime CI was stopped during compilation; no full-suite or fresh performance claim. The PR description distinguishes earlier measurements from current checks. The bundle’s scope and overlap with open #898 remain maintainer decisions. |
What
Effects
Plane::transposeis cache-blocked in 32×32 tiles (twiceper vertical pass of
box_plane/gauss_plane); Median updates its histogram inline through ageneric column visitor; Camera Lens Blur sums iris span counts once per iris, not per pixel; Cell
Pattern computes each cell's feature point once per frame (about 7× faster at High Quality).
3D
one set of row prefix sums, instead of keeping up to eight full-image blurs (about 4× faster).
Shadows).
Layer cache
Under budget pressure, quantised 8 and 16 bpc entries are packed as
u8/u16(PackedBuf)instead of evicted; packing checks every value round-trips bit for bit and declines otherwise. The
cache holds 2–4× as many 8/16 bpc layers at the same budget.
More caches follow the Memory & CPU budget: the GPU upload cache drops freed layers' textures on
every upload and evicts least recently used when over budget, with the texture pool sized from the
layer-cache budget; Roto Brush segmentations get half the layer-cache budget (at most 768 MB); the
disk-cache write queue is bounded by bytes.
Media
Standalone audio stays on the existing FilmCraft path. The direct Symphonia dependency and its
standalone-audio probing shortcut have been removed to follow ADR 0001.
Expressions
keyframe lists are read lazily;
sourceRectAtTimeon shape layers is geometric; a layer's own pathis memoised.
recorded host answer is bit-identical, never stores evaluations that read randomness or the clock (including aliases and property descriptors), and is bounded
by bytes. A 100-layer expression rig drops from 23.4 to 14.7 ms per frame.
Text
RwLockand transformed straight from the sharedArc;layout_doccaches its finishedTextLayout; the layout and shaping caches are byte-bounded LRUs(64 / 32 MiB); a
.ttcfile is read once for all of its faces; the UI shares the engine's font bytes.Project open
Deserializereads the"node"tagfirst instead of buffering every subtree);
migrate::is_currentavoids deep-cloning every effectto compare it with its upgrade. A 20.9 MB layer array parses in 72 ms instead of 226 ms.
Export
JobOptions::memory_budget(default 1 GiB) sizes thelayer cache and bounds the frames in flight (one per core, 2–16, fewer when they'd exceed it).
batches held); serial on wasm and rayon workers.
same frames share one render, each with its own encoder and muxer thread, and files stay
byte-identical; others reuse the previous module's layer cache when the frames match.
UI
instead of cloning it every frame, a playing preview repaints when its next frame is due (not at
the display rate), and the channel / exposure transform runs in parallel.
per cache generation instead of scanning the caches under locks every UI frame; Lumetri Scopes
share their cached scope; the Footage panel decodes on a worker thread at panel size; the native
menu catches up after a drag and updates at most 4 times a second.
Why
These were the hottest paths in effect renders, expression rigs, text layers, project open and
exports and the UI thread: rendering and encoding alternated, opening a long movie read the whole
file, playback copied every frame and repainted at up to 120 Hz, multi-module items rendered every frame N times, and a
slider read by N layers ran N times per frame.
Earlier validation (before this review update)
rustfmt --edition 2024 --checkon every changed.rsfile: clean.cargo clippy --all-targets -- -D warningsfor effects, engine, export, expr, gpu, host, media,project, render, text, ui-egui and effectcraft: clean.
cargo check --target wasm32-unknown-unknownfor effects, engine, export, expr, gpu, media,project, render and text: clean.
cargo testfor the same twelve packages: 2,055 passed (the perf changes keep outputbit-identical where their tests say so: kernels, packed cache, export files).
mainon this runner:theme::script_font_tests::arabic_words_are_joined_and_read_right_to_left,and in
effectcraft-gpu's lib tests on llvmpipe two CPU/GPU mismatches (CC HexTile, CC Threshold RGB)and a segfault later in the suite.
cargo xtask ci. Tests ran on a 4-core Linux runner without a GPU (wgpu onllvmpipe).
Cherry-picked with
-xtrailers from topic branches already adapted to currentmain.Review update
Merged current upstream main, preserved both GPU cache budgets, removed the direct codec dependency,
and fixed clock reads reached through property descriptors so they cannot be memoized.
Current-tree validation: formatting and strict full-workspace all-target release Clippy passed. The descriptor-bypass fix passed exact-wrapper Node and Boa probes. Full runtime CI was intentionally stopped during compilation; the full suite has not passed on this tree. Runtime tests use RUST_TEST_THREADS=1; the shared process-global cache collision found in the upstream baseline is fixed separately in #855. Earlier benchmark numbers above have not been remeasured.
Maintainer decisions remain on the bundle's scope and its overlap with open #898. #688 is already merged upstream; this integration preserves its upload-cache budget alongside the layer-cache budget.