- parity: the status note covers the M3.9–M4.12 passes (feature counts unchanged: they fixed
behaviour inside features already counted done) and the GPU effect count matches EFF-5 (280);
the Layers, Compositions, Interface, Project and Preview rows name what is left and what
landed; landed items in "Highest-value gaps" are struck through with where they landed, the
gaps the passes left are items 16–18, and the closing paragraph no longer says 69 menu entries
are disabled.
- ROADMAP: what is left includes the preview gaps.
- web: a restored session with unsaved changes stays modified and asks before closing.
- preferences: Adaptive Resolution Limit applies while dragging, not to playing previews.
- agents: nested comp settings and New Comp from Selection for several items.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The web app reopens the last visit's project and, when it had unsaved changes, marked it
modified with `bump()`. Since M3.9 the modified mark follows the saved snapshot, and a bump on
the very project just opened counts as "back at the saved state", so restored changes looked
saved (no `*`, no unsaved-changes prompt). `Session::mark_unsaved` forgets the saved snapshot
instead: the project stays modified until saved, whatever is undone. Regression test included.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Following AGENTS.md's Never crash rules for the M3 / M4 additions:
- Cache Frames When Idle panicked (`clamp` with min > max) when the work area was inverted
(start set past the end); an empty work area now has nothing to cache.
- `file.newCompFromSelection`: the sequence overlap times the layer count overflowed for a huge
`overlapDuration` (wrapping to a wrong comp length without overflow checks); negative or
non-finite values fall back to 1 s and the product saturates. The lead item is read with
`get()` instead of indexing.
- Layer Settings' enablement reads the first selected layer with `first()`.
- Regression tests for both (each failed before the fix).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
`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 <[email protected]>
These crates have no production panic sites: add the deny attribute so they stay clean
(geom, color, model, automation, host, export, plugin, and the desktop, CLI and web apps). The
plugin's wasm-only test module gets a separate cfg(test) attribute so clippy recognises it as
test code.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
ui-egui without expect()/unreachable!(): the frame pool is optional (rayon's global pool if the
OS refuses threads) and gets a panic_handler, because a panic in a rayon spawn() job aborts the
whole process by default; render-queue progress, the text-edit anchor and the GPU viewer
texture are bound where they're checked; the settings panel's Bool arm no longer panics.
Denies the panic lints in the crate.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
render: the debug-only quantisation check uses debug_assert! instead of panic!. gpu: the
test-only panic on a wgpu validation error is cfg(test) with a narrow allow, and release
builds only log. Test-only unreachable!() calls become panic!(). Denies the panic lints in both
crates.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Tick arithmetic saturates, seconds-to-tick conversion clamps to Tick::MIN..=Tick::MAX (NaN is
zero), zero rates/timebases no longer divide by zero, and timecode / feet+frames parsing and
formatting use checked or saturating math. `layer.timing {"delta": -1e308}` over the control
channel overflowed FrameRate::snap_nearest (found by fuzzing every command), and a timecode
field holding 99999999999999:00:00:00 overflowed parse_timecode. Denies the panic lints.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Effects without expect(): nested parameter groups are looked up with ? (falling back to the
effect's top level), the OCIO effects use named constants for the built-in ACES spaces instead
of expect()ing a table lookup, curve fitting and Path Text no longer expect on just-pushed
vectors. Test-only unreachable!() becomes panic!(). Denies the panic lints in the crate.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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 <[email protected]>
FCP 7 XML / FCPXML nested about 1 000 elements deep overflowed an 8 MiB stack in the XML
parser (aborting the app). Import now scans XML and OTIO JSON nesting first and returns
Error::TooDeep beyond 256 levels.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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 <[email protected]>
- 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 <[email protected]>
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 <[email protected]>
- 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 <[email protected]>
- `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 <[email protected]>
raster: the inpainting pyramid stops at a missing level, and video inpainting falls back to a
plain patch fill if the previous frame is missing, instead of expect(). path: curve fitting of
an empty point list returns a degenerate cubic instead of indexing/unwrapping, and Merge Paths
no longer has an unreachable!() arm. Denies the panic lints in both crates.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Removes the engine's production expect() calls: background-job polling (camera, mask, roto,
point and warp tracking, render queue) uses let-else / if-let instead of take().expect("job"),
command helpers bind what they checked instead of re-reading it with expect, the menu bar falls
back to an empty tree (logged) instead of expect()ing that a constant parses, and
autotrace/text animator group lookups no longer expect a just-inserted group. perf.rs, the
benchmark harness behind `effectcraft-cli bench --ops`, opts out with a module-level allow.
Test-only unreachable!() calls become panic!() (clippy.toml allows panics in tests). Denies the
panic lints in the crate.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Text layout and font resolution without expect(): an empty line list keeps the default bounds,
the line breaker stops instead of expect()ing a checked option, and font resolution falls back
to the first bundled face. The one remaining expect (the bundled-font database is never empty)
carries the documented narrow allow. Denies the panic lints in the crate.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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 <[email protected]>