`Renderer::mb_on` (render options × the comp's switch × the layer's switch as the precomp
layers above limit it) now decides everywhere:
- Mask motion blur checked only the comp's switch and the layer's own switch, so Render
Settings ▸ Motion Blur ▸ Off for All Layers and a precomp layer with motion blur off still
blurred masks; it now takes the gate (`masks::MaskBlur`) and the comp's Samples Per Frame
(four in Draft) instead of a fixed eight.
- Effects that read the shutter (`EffectEnv::shutter`) get it only when the layer is motion
blurred in this render.
- Advanced 3D layers follow the precomp layers' Motion Blur switches too.
- The layer cache key of a layer with masks or effects includes whether it is blurred and the
comp's shutter and samples, so a buffer blurred for the viewer is never reused by a render
with motion blur off.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A precomp layer trimmed out (or time-remapped) past its nested comp's end kept drawing the
nested layers whose bars ran on. Like After Effects, the nested comp now shows nothing outside
its own span (`Comp::covers`), flattened, through a proxy or with Collapse Transformations, on
the CPU and the GPU (both go through `Renderer::source` / `collapse_into`).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Composition Settings ▸ Advanced has both options (they were missing from the model, the
renderer and the dialog):
- Preserve frame rate: nested in another comp, the comp shows only its own frames
(`EvalCtx::nested_time` floors the precomp layer's source time to the nested comp's frame
rate; used for the nested frame, its proxy, aux channels, collapsed transformations and the
viewer's internal wireframes), and the Render Queue renders it at its own rate whatever the
Frame Rate override (`RenderSettings::rate`).
- Preserve resolution: nested in a comp rendered at Half / Third / Quarter, it renders at full
size and the buffer's scale places it.
- `comp.settings {preserveFrameRate, preserveResolution}`, `comp.info` reports them, scripts
read and set `CompItem.preserveNestedFrameRate` / `preserveNestedResolution`, Pre-compose
copies them, and the dialog registers `dialog.comp.preserveFrameRate` /
`dialog.comp.preserveResolution`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In EPS/PostScript, `0#7` asked i64::from_str_radix for radix 0, which panics (found by
fuzzing). Radixes outside 2..=36 are not numbers now.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
roxmltree recurses per element and overflowed a 2 MiB stack at a few thousand nested
elements (aborting the app). parse() now scans the element depth first and returns
Error::Xml beyond 256 levels.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The descriptor depth limit restarted at 0 inside every nested descriptor (Objc / GlbO /
ObAr), so a crafted file of 100 000 nested descriptors (2.4 MB) recursed until the stack
overflowed and aborted the app. The depth now carries through nested descriptors.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Every purge emptied the viewers' RAM preview; Purge ▸ All Disk Cache and Snapshot now leave
it (Image Cache Memory, All Memory, All Memory & Disk Cache, All and All 3D Cache still
clear it).
- The memory purges also drop the decoded footage frames (`FootageSource::purge`, the media
pool's `clear_frames`; decoders stay open), so footage reads decode again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Composition ▸ Preview ▸ Cache Frames When Idle was only a saved checkbox. After a second
without input or edits it now renders the work area's frames (from the current time on, then
from the start) into the RAM preview, a few at a time behind the viewer's own frame, until the
work area is cached or the RAM preview budget is full.
Docs: preferences.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Viewer Resolution Auto rendered at half its scale while a preview played, so frames cached
while scrubbing were not played and frames cached by the preview were re-rendered once it
stopped. Auto now follows the zoom alike playing and paused (Fast Previews still lowers it
while dragging).
- Cache Before Playback waited for every frame of the range: a range larger than the RAM
preview budget evicted its first frames and never started. The range is now cut to the
frames that fit (90 % of Settings ▸ Memory & CPU's budget) with a message, then cached and
played.
- The Info panel shows the frame rate a playing preview achieves and whether it is real time
(`Playback::achieved_fps` / `real_time`, automation id `info.previewFps`).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- `FrameKey` carries a hash of the render options (`frames::opts_hash`): switching Fast
Previews (Draft / Fast Draft) or Realtime Shadows no longer shows frames rendered the other
way.
- Eviction drops the least recently shown frame (`Frames::get` touches it), not the oldest
inserted: a looping preview a little over budget no longer evicts the frame it shows next.
- Frames of earlier project revisions are dropped on every change (`Frames::drop_stale`); they
could never be shown again but held memory (and GPU textures) until evicted.
- The green cache bars (Timeline, Preview panel) count the frames of exactly what the viewer
shows — scale, 3D view / region of interest and options (`EffectcraftApp::shown_series`) —
instead of any frame at that scale.
- Playback loops compute the key's fixed part once (`EffectcraftApp::frame_series`).
- Docs: architecture.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Session::execute runs every command (UI, CLI, control channel, MCP, scripts; file import,
export and save are commands too) through guard::guarded, which catch_unwinds a panic that
escapes the command and returns EngineError::Other("command `x` failed with an internal error
(...); it was stopped and your project is still open"). Edits work on a copy of the project, so
a command that panics mid-edit leaves the project untouched. The desktop app also installs a
panic hook that logs every panic through the app logger (then runs the default hook).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lottie stores line cap, line join, merge mode and some text-selector enums 1-based, and the
importer subtracted 1 from whatever number the file held: -1e308 (rounded to i64::MIN)
overflowed, a panic in debug builds (found by structural fuzzing). Use saturating_sub. Denies
the panic lints in the crate.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- `Project::to_file_json` writes `savedBy` (the EffectCraft version) ahead of the project and
checks that the text reads back before anything is written: JSON has no NaN or infinity, so
such a value made a file that saved but could never be opened. Save now fails with a message
and the file on disk is left as it was.
- Opening a project saved by a newer EffectCraft warns that settings this version doesn't know
are lost on saving; `file.open` reports `savedBy`. A file that isn't a project names itself in
the error.
- Increment and Save keeps the extension (`.ecprojx` stays XML); Save a Copy writes XML for
`.ecprojx` / `.xml` paths, like Save As.
- Docs: architecture (document model), parity.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Group Shapes (Ctrl+G) / Ungroup Shapes (Ctrl+Shift+G) sit in the Layer menu itself, as in
After Effects (they were under Mask and Shape Path).
- Window ▸ Learn opens the Home screen's Learn tab.
- Composition ▸ Composition Flowchart is Ctrl+Shift+F11 and Window ▸ Flowchart Ctrl+F11 (the
flowchart had the panel's shortcut and the panel none).
- Layer ▸ Transform ▸ Center In View shows Ctrl+Home.
- "Reveal in Finder" reads "Reveal in Explorer" on Windows and "Reveal in File Manager" on
Linux (`menus::platform_label`).
- Docs: parity (View ▸ New Viewer noted as not yet).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Pre-compose makes the new composition with the original's pixel aspect, background, motion
blur and frame blending settings and 3D renderer (`Comp::nested_like`); it kept only size,
rate and background.
- File ▸ New Comp from Selection records one undo step (two per item before) and takes each
item's pixel aspect. Several items selected open the New Composition from Selection dialog:
Single / Multiple Compositions, Use Dimensions From, Still Duration (default from Settings ▸
Import ▸ Still Footage), Add to Render Queue, Sequence Layers with Overlap, Duration and
Transition. `file.newCompFromSelection {single, dimensionsFrom, duration, addToRenderQueue,
sequence, overlap, overlapDuration, transition}`; `layer.addItem {duration}` for stills.
- Docs: parity.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Locked layers can't be selected (Timeline click, Select All, Ctrl+Up/Down; shy layers while
hidden too) and are left alone by Clear/Delete, Arrange, Layer ▸ Transform and Center Anchor
Point, which fail when every target is locked (`commands::unlocked`, `commands::selectable`).
- Deleting a parent unparents its children where they are, in the same undo step: the
pick-whip compensation moved out of `layer.setParent` into `layer::parent_fixes` /
`ParentFix::apply`, which now also handles separated X/Y Position.
- Bring Forward / Send Backward move each selected layer one step (a non-contiguous selection
was squeezed into one block).
- Center In View / Fit to Comp keep a 3D layer's Z (and Fit keeps Scale/Anchor Z); Reset /
Center / Fit / Center Anchor Point work with separated dimensions (`layer::update_position`).
- Duplicate and paste keep parents and track mattes, following the copies when both were
copied; paste dropped them always.
- Docs: agents, parity.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Layer bounds come straight from the file as four i32s. Rect::width/height subtracted them in
i32 (overflow: a panic in debug builds, garbage sizes in release), and layer_pixels allocated
width x height pixels before reading any data, so a 100-byte file claiming a 2^31-square layer
hit "capacity overflow" (or an out-of-memory abort). Widths are now computed in i64 and
clamped, every decoded image is capped at 2^28 pixels (Error::Unsupported beyond that), zlib
data is inflated with a limit of the expected size, RLE count tables and UTF-16 strings
reserve no more than the remaining bytes, and the per-pixel copy only visits the part of the
layer that lands in the output. Denies the panic lints in the crate.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Layer ▸ Layer Settings on a solid or adjustment layer opens Solid Settings on the layer's
solid (it ran an empty command before): name (renames the layer and the solid), size, Pixel
Aspect Ratio, colour, and "Affect all layers that use this solid". It starts off, as in After
Effects, so changing a solid other layers share gives this layer its own copy.
`layer.settings {pixelAspect?, affectAll?}` → `{layer, solid?}`; `layer.newSolid {pixelAspect?}`.
- Nulls ask for their name; text and shape layers have no settings (the entry is disabled).
- Composition Settings / New Composition and Solid Settings register an automation id for
every control (`dialog.comp.*`, `dialog.solid.*`). Composition Settings takes any typed frame
rate besides the list, and New Composition starts from the last settings used.
- Colour buttons in both dialogs show the stored sRGB colour (`widgets::srgb_color_button`);
egui's linear picker showed it lighter.
- Docs: agents, parity.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The s, b and b* operators closed the path even when there was no current point, which kurbo
asserts against (a panic in debug builds; found by fuzzing). Close only when a subpath is open.
CFF charstrings that draw before their first rmoveto now start the subpath at the current
point, as Type 1 charstrings already did. Denies the panic lints in the crate (it had no panic
sites).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Path data like "M0 0 L10 0 Z9" repeated Z implicitly for the stray number; Z consumes no
numbers, so the parser looped forever appending close-paths until memory ran out (a 1-byte
change to a valid SVG used 24 GB and was killed). A number after Z is a syntax error, so
parsing now stops there and keeps what was read, as the spec asks. Also denies the panic lints
in the crate (it had no panic sites).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Quit and the window's close button, File ▸ New Project, Open Project, Open Recent, Close
Project, the demo project and a Home template show Save / Don't Save / Cancel when the
project has unsaved changes (Save asks for a path when untitled; cancelling that keeps the
project open). File ▸ Revert asks to discard. Before, all of them dropped unsaved work
without asking.
- The window title shows the project's path and `*` while it is modified.
- The modified mark follows undo: the session keeps the snapshot it last opened or saved
(`Session::saved_project`), so undoing, jumping in the History panel or rolling back a batch
to the saved state clears it.
- An edit that changes nothing (a value set to what it already is) records no undo step and
doesn't mark the project modified (`Project::same_content`, comparing untouched comps by
pointer).
- Agents' `engine.execute` / MCP calls are never asked; `ui.menu.invoke` shows the prompt like a
click (automation ids `dialog.unsaved.save` / `dontSave` / `revert` / `cancel`), and the
control channel's `app.quit {force: true}` skips it.
- Docs: architecture, agents, control protocol, parity.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Off macOS `Cmd` maps to Ctrl, so Enter Full Screen's Ctrl+Cmd+F parsed
to the same keys as Find's Cmd+F and both commands were bound to Ctrl+F
(ui_and_menu_shortcuts_do_not_collide fails on Linux and Windows).
Shortcuts that use both Ctrl and Cmd can't be typed distinctly there,
so they are skipped when the key bindings are built off macOS.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
warp.status solved the stabilization plan (Subspace Warp eigen-decompositions and mesh fits)
on the page the first time it saw an analysis: ~4.9 s blocking the browser's event loop in
the smoke test. The analysis job now sends the plan summary with its result, and a settings
change has a WarpPlan job solve it (status reports stabilizing meanwhile). Desktop unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
contentFill.generate sends its plan (FillPlan) to a browser job worker:
the layer's frames render in passes on the worker's WebGPU device, the PNG
sequence goes back to browser storage (Services::store_file), and the page
adds the fill layer when the plan returns (WorkerReply::Fill). The smoke
test checks it; docs updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- GPU in browser job workers: each job worker opens its own WebGPU device
(deferred readbacks); the Render Queue export and the analyses' frame loops
are futures (render::passes, offload::run_request_async) whose frames
render in passes, awaiting the device between them. GPU particles and
Advanced 3D read back under keys too. workers.jobs reports passes and
readbacks; the smoke test measures them.
- Layer buffers in the browser's OPFS disk cache: frame workers back their
layer caches with a PrefetchStore; the page plans what to fetch before a
request from recent misses (LayerPrefetch over the shared DiskIndex).
- Templates embed footage (default on, size-capped with a warning) and
creating from a template extracts it next to the new project / into
browser storage.
- ICC LUT profiles (lut8/lut16/lutAtoB/lutBtoA, from the public ICC spec) in
Simulate Output > My Custom RGB, baked into a 3D LUT for the viewer.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The geometry stays on the CPU as per-frame plans shared with the CPU effects (bolts, mask
strokes, scribbles, contour segments, audio marks, glyph strokes, waves, flood-fill region,
eyedropper sample); segments and convex polygons are tile-binned and rasterised on the GPU
with the CPU's coverage functions. CPU output unchanged (gen_golden). CC Threads' exact ties
at round parameter values are a documented exception; Fractal stays on the CPU (needs f64).
Regenerated docs/effects.md, docs/parity.md EFF-5.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GPU render passes for CC Particle World / Systems II, Particle Playground,
CC Ball Action, CC Pixel Polly, CC Scatterize and Curl Noise (fx_particles,
chunked multi-pass raster); Shatter wireframe views, Foam extras, Timewarp
with a Matte Layer, strong Warp Fisheye/Twist (CPU f64 inverse map) and
custom .ocio configs no longer fall back to the CPU. CPU refactors pinned by
particle_golden / timewarp_golden / sim_golden hashes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Inner/Outer Key's trimap and Cleanup strokes, Color Link's sample and CC Glass/Plastic's
light are shared with the CPU effects (CPU output unchanged: gen_golden). CC Plastic now
reads its Bump Layer (it read the nonexistent bumpMap id).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pin exact CPU pixel hashes of the simulation effects refactored into shared
GPU plans in M13.23 (CC Rainfall/Snowfall/Star Burst/Bubbles/Drizzle/Hair/
Mr. Mercury, Caustics, Wave World, Foam, Shatter, Card Dance, Card Wipe):
404 frames over default and non-default settings, four times incl. a seek
backwards, 8 and 32 bpc, full and half resolution. Verified bit-identical
between 7a5a93b (before M13.23) and 808ca70 (after).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FilmCraft's AV1 decoder now frame-threads by default, so a picture may
come out of a later decode call; the round-trip check wants each
packet's picture from its own call.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>