Commit Graph

48 Commits

Author SHA1 Message Date
Brandon Thomas f953cd2855
Release prep: 0.8.0 and a Linux riscv64 release build (#669)
* Release prep: bump to 0.8.0 and add a Linux riscv64 release build

- release.yml: new `linux-riscv64` job, ported from GridCraft. It cross-compiles on
  ubuntu-24.04 against riscv64 libraries from ports.ubuntu.com (the Linux job's X11/Wayland/GL
  -dev packages plus ALSA), ships effectcraft-<v>-linux-riscv64.tar.gz, and smoke-tests
  effectcraft-cli under qemu-riscv64. The draft-release job now needs it, so its tarball is
  uploaded and covered by SHA256SUMS.txt.
- packaging/linux/package.sh: cross-compiles when CROSS_ARCH/CROSS_TARGET/CROSS_COMPILE are set
  (cargo --target, the cross strip, the EMULATOR smoke test), as GridCraft's does.
- Version 0.7.0 -> 0.8.0 (cargo xtask version set), Cargo.lock refreshed.
- README and docs/releasing.md list the riscv64 tarball.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Fix FreeBSD CI: pointer_wrap test harness, wasm32 target, clippy as_chunks

- pointer_wrap tests: the harnesses dropped run_ui's FullOutput with the font atlas upload still
  in textures_delta, and epaint 0.36's drop check panics on that in debug builds. Every
  pointer_wrap test failed on every platform; FreeBSD is the only CI that runs them. Clear the
  delta, as the other UI tests do.
- freebsd.yml: install wasm32-unknown-unknown in the VM. Without it the host plug-in test's build
  of examples/plugins/posterize-bands failed (E0463, no std) and the test skipped silently; now
  it builds and runs.
- media/convert.rs: chunks_exact(2) -> as_chunks::<2>() (clippy chunks_exact_to_as_chunks,
  new in Rust 1.99); same pairs, same dropped remainder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 21:08:18 -04:00
Claude faffad3a6e
Media: a multi-layer EXR's picture and its EXtractoR / Cryptomatte channels share one decode (#474)
A multi-layer OpenEXR file without an unnamed RGB layer (Blender's compositor File Output)
was decoded twice per frame: `layered_image` decoded every channel to find the picture,
then `MediaPool::aux` decoded the same bytes again into the aux-channel cache for
EXtractoR and Cryptomatte. The first EXtractoR layer doubled the frame time.

The footage path now reads the picture from the aux-channel cache (`cached_layered_image`),
so the decode it makes is the one EXtractoR and Cryptomatte reuse. (Step 1 of the issue;
reading only the picture's channels and keeping half channels as f16 are still to do.)

Test: after a frame of a layered file is decoded, its aux channels come from the cache
without reading the file again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
2026-10-10 20:40:50 +00:00
Claude da06c42de1
Media: OpenEXR files with HTJ2K compression say it isn't supported (#482)
The exr crate reads HTJ2K (OpenEXR 3.4's High-Throughput JPEG 2000) headers
but can't decompress the pixels, and the image paths turned that into a
misleading "no non-deep rgb channels" (multi-layer files) or imported the
file and showed a transparent frame (single-layer files).

`exr_channels::unsupported_compression` reads the headers; import (probe)
and frame decoding fail with "<file>: OpenEXR HTJ2K compression isn't
supported yet: re-save the file with ZIP, PIZ or DWAA compression".
Decoding HTJ2K itself needs a pure-Rust JPEG 2000 Part 15 decoder and is
left for later.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
2026-10-10 18:51:12 +00:00
Claude 34f2955585
Media: an audio file that fails to decode no longer stays silent for good (#324)
With the Conformed Audio folder on (the default), the conform thread wrote
whatever `decode_audio` returned, and that returned silence when the source
couldn't be opened or a read failed. The conformed file is found by the
source's path, size and date, so one failed decode (an MP3 the decoder
stumbled on once) kept the footage silent in previews and exports in every
later session, while a renamed copy played.

The conform path now decodes with `try_decode_audio`, which fails instead of
returning silence; a failed conform removes its temporary file, writes
nothing and isn't retried this session (reads go to the decoder). Playback
reads still fall back to silence. CONFORM_VERSION goes to 3 so the silent
files earlier builds wrote are ignored and conformed again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
2026-10-10 18:51:07 +00:00
Claude 616574439e
Integrate community PRs: relink's params doc and menu rows, pt-BR in the language list, MCP docs, test fixes
- file.relinkFootage (#461): close the params doc so checked execution
  (MCP, control channel) rejects unknown keys; translate its menu entry.
- general.language lists Português (Brasil) (#217).
- docs/agents.md: set_property / add_keyframe take timeBase (#522).
- #299's MCP test no longer sends get_property a value.
- The Automatically Reload Footage sequence test uses PNG frames.
- h264_frames_at_reduced_sizes (#507): the bar depends on the fixture's
  encoder; 50 dB with ffmpeg 6.1's x264.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
2026-10-10 15:15:11 +00:00
Claude 57bd68c86e
Import: Automatically Reload Footage is Off / Non-Sequence Footage / All Footage, as in After Effects; a reload never shows disk-cached frames from before (#578 follow-up)
Image sequences are watched only with All Footage (the default,
Non-Sequence Footage, leaves them alone). Purge also forgets the viewer's
per-revision disk keys, which hash footage files' size and time, so a
file reloaded without a new revision is keyed again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
2026-10-10 12:53:09 +00:00
Claude d020715b1e
Merge pull request #487 from MarekSpodBiedry/issue-481 2026-10-10 12:51:15 +00:00
Claude 5e28ecd53b
Merge pull request #578 from KattKnight/footage-reload 2026-10-10 12:50:18 +00:00
Claude ab09f219d4
Import: Purge frees decoded GIFs; a browser tab keeps less of one GIF (#567 follow-up)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
2026-10-10 12:50:12 +00:00
Claude e625ef3bef
Merge pull request #567 from Limestudio90/import-animated-gifs
# Conflicts:
#	crates/media/src/pool.rs
2026-10-10 12:50:00 +00:00
Katt e6d37c2fa6 Reload Footage shows new pixels and keeps Photoshop layers; Reload Footage Changed on Disk
Reload Footage (Cmd+Alt+L) updated an item's metadata but left the media
pool's decoded frames and the layer cache in place, so the old image kept
rendering until Edit > Purge. It also replaced the footage with a probe of
the merged document, so a Photoshop layer item lost its layer and its
Retain Layer Sizes bounds.

Reload now re-reads only what the file decides (size, a movie's duration,
streams and frame rate, codec, a data file's text), keeps the layer or
page and the interpretation, finds a Photoshop layer again by name, and
makes the media layer forget the file (FootageSource::forget, which also
drops the EXR aux-channel cache) and clears the layer cache.

Settings > Import > Reload Footage Changed on Disk (on by default): the
desktop app stamps footage files (size + modification time) on a worker
thread every two seconds and when the window comes back to the front, and
reloads what changed. Not an undo step and it doesn't modify the project;
a file still being written is retried on the next check.
footage.reloadChanged runs one check (CLI, MCP, agents).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 16:36:50 +05:30
Limestudio90 bfbdadd34a Import animated GIFs as movies
GIFs went through the still-image path, so an animated GIF came in as a
single frame with zero duration. A GIF with two or more frames is now
probed as a movie: its frame rate is chosen so every per-frame delay is
a whole number of frames (uneven timing is kept exactly), delays of 0
or 1 hundredths count as 10 like in browsers, and the pool decodes the
frame shown at the requested time. A single-frame GIF stays a still.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-10 11:38:00 +02:00
daijro 74b151cedc Footage: reduced-resolution renders get movie frames converted at their size
Cause: at Half, Third and Quarter resolution (the viewer's Auto resolution
in most windows) every movie frame was converted from Y'CbCr to f32 RGBA at
full size and then resampled down. In the browser (wasm, one thread) that
was about 8 ms converting a 1080p frame, 6 ms resampling it and 2 ms
zeroing a 33 MB buffer, per layer and frame, for a frame shown at a
quarter or a ninth of its pixels; the footage frame cache held the
full-size frames.

Fix: FootageSource::frame_at_size asks for a frame shown at a given size
(default: the full frame, as before). The renderer asks for it where it
resampled footage (below 3/4 scale; not for proxies, fields or Interpret
Footage's pixel options, which need the full frame). MediaPool makes opaque
4:2:0 movie frames of the footage's own size there: it box-filters them to
half size in Y'CbCr before converting (each 2x2 block of luma, and the
[1 6 1]/8 neighbourhood of its chroma sample, which is the mean of the
upsampled chroma its pixels get), resamples the rest of the way and caches
the result per size. Conversion is linear until colours clip, so this is
the RGB box filter resample starts with, except in blocks where some
colours clip. Other frames convert at full size and resample as before.
In the browser the conversion and resampling take about 4 ms instead of
16. Docs (architecture).

Tests:
- convert::scaled_conversion_matches_resampling_the_converted_frame: half,
  third, quarter and odd sizes of an unclipped 4:2:0 frame are within 1e-5
  of resampling the converted frame; larger sizes, odd frame sizes and
  4:4:4 convert at full size.
- tests_footage_size::reduced_renders_take_footage_resampled_by_the_source:
  a source that resamples frames itself renders the same frames as one
  that doesn't, at 8 and 32 bpc and every resolution, and is asked only
  below 3/4 scale, and not with Invert Alpha.
- footage::h264_frames_at_reduced_sizes (ffmpeg fixture): 1080p H.264
  testsrc2 frames, whose saturated colours clip, at 960x540, 640x360,
  480x270 and 323x182 are 58-62 dB from the resampled full frames, and
  are cached per size.
- convert's ignored 1080p bench also times the conversion at a third.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 16:44:49 -05:00
MarekSpodBiedry 6c31247ceb EXR: do not double the part name of Blender 5 per-pass parts (#481) 2026-10-09 21:53:30 +02:00
bflatastic 09a5dc357a Audio: a FLAC opens once and off the UI thread; conforming is bounded (#408)
The freeze and quit during a FLAC preview did not reproduce on current
main (Windows 11): a 4-minute stereo and a 10-minute 24-bit 6-channel
96 kHz FLAC played through a RAM preview with a real-time audio pull,
after trimming and zooming, with no stall, no growing queue and memory
bounded by the RAM preview budget; 150 cpal output start/stop cycles
ran clean. Three defects in that path were found and fixed:

- FilmCraft decodes FLAC, MP3, Ogg and AIFF files whole when they are
  opened (8 s and 1.4 GB for the 10-minute 6-channel file). MediaPool
  opened a file outside its lock, so every caller asking before the
  first open finished decoded the whole file too: a preview's frame
  workers, audio feeder, conform and waveform threads at once (8 reads
  at once: 8 decodes). Each path now opens once; the others wait for it.
- AudioPlayback::start mixed its first 50 ms on the calling (UI)
  thread, so the first read of such a file froze the UI for the whole
  decode (audio-only previews start the sound right away). All mixing
  is on the feeder thread now; the device plays silence until the first
  block arrives, and the clock (frames actually played) waits for it.
- Conforming (Settings > Disk > Conformed Audio Folder, on by default)
  held the whole conformed track in memory before writing it, sized by
  the footage duration from the file's header: a header claiming a
  month of audio aborted the app on a failed 995 GB allocation. The
  conformed file is now written a chunk at a time, and footage longer
  than 24 hours is read from its decoder instead of conformed.

Tests: media concurrent_first_reads_open_a_file_once (8 threads: 8
opens before, 1 now; PoolStats::opened), pool
a_header_claiming_a_month_of_audio_is_not_conformed (aborted before),
ui-egui audio starting_audio_does_not_wait_for_slow_footage (start took
the footage's 2 s before), conformed_flac_audio_reads_back_like_the_
decoder, and the robustness test flac_audio_of_any_shape_reads_in_
device_blocks (2 minutes of 24-bit 6-channel 96 kHz, mono 8 kHz,
11.025 kHz, 192 kHz and 8-channel FLAC read in 1024-frame blocks at
48 kHz through and past the end: level, silence after the end, opened
once). FLAC fixtures come from ffmpeg (skipped without it).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 00:53:21 +07:00
bflatastic d6de68b20d Import: Interpret Footage ▸ Preserve RGB keeps the file's values (#411)
Cause: float footage (OpenEXR, float TIFF) was always sRGB-encoded on
decode, and Interpret Footage could not turn that off, so an OCIO effect
on the layer converted values that were already encoded.

Fix: After Effects' Interpret Footage ▸ Color ▸ Preserve RGB
(Footage::preserve_rgb; file.interpretFootage {"preserveRgb": true}; a
checkbox in the Interpret Footage form; Remember / Apply Interpretation
carry it). The decoder keeps float values linear, and the working-space
conversion from the colour profile and Interpret As Linear Light don't
apply, so the file's values enter the composition unconverted. The media
cache key includes the flag, so both interpretations decode separately.

Tests: pool::preserve_rgb_reads_float_exr_values_unconverted,
tests_color::preserve_rgb_footage_is_not_converted (managed, linearized
and not), tests_proxy::guess_alpha_invert_and_linear_light (command,
render, Apply Interpretation).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 23:57:12 +07:00
bflatastic 698ade94ac Import: layered EXRs show the beauty pass, never Cryptomatte (#412)
Cause: a multi-layer OpenEXR without an unnamed RGB layer showed its
first colour layer unless one was named combined / beauty / rgba. A
Blender compositor File Output names the beauty pass "Image", and its
Cryptomatte layers (lowercase r, g, b, a) sort first, so the picture was
CryptoMaterial00.

Fix: the picture is the unnamed RGB layer, else the beauty pass by the
last part of its name (Combined, Beauty, Image, RGBA, RGB, Color), else
the first colour layer that isn't a data pass (Cryptomatte, depth, mist,
normals, vectors, positions, IDs). A layer without R, G and B shows its
own first channel as grey. After Effects shows a file's main RGBA and
leaves the other layers to EXtractoR, which is unchanged.

Tests: exr_channels::layered_exr_shows_the_beauty_pass_not_cryptomatte
(the reported Blender layout, ViewLayer.Combined, no beauty name,
data-only files).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 23:57:12 +07:00
bflatastic efd348335f Import: missing sequence frames show a placeholder, as in After Effects (#297)
After Effects shows a missing frame of an image sequence as a
placeholder (colour bars) and has no Hold Previous Frame or Skip choice
in Interpret Footage, so EffectCraft doesn't either.

Interpret Footage loses its Missing Frames field, file.interpretFootage
its missingFrames parameter, and Footage its missing_frames setting. A
gap in a numbered sequence's numbering always shows colour bars. A
sequence imported with Force Alphabetical Order still plays its files
one after another (its numbering doesn't count), as in After Effects;
that is now an internal Footage.alphabetical flag, set on import and
kept by Reload. The Interpret Footage note lists the missing frames.

Projects saved before this have no such field and open with the After
Effects behaviour: a sequence with gaps shows placeholders where it
used to close them up.

Test: project sequence unit tests (placeholders, alphabetical order,
projects saved without the flag), tests_sequence (Start Frame instead
of Hold / Skip), sequence_gaps_show_placeholders (media) and the
Interpret Footage fields in ui_drag_drop.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 18:52:19 +07:00
bflatastic 50c9549529 Import: image sequences with a Sequence option, ranges, folders and missing frames (#297)
Image sequences were found only by the desktop media prober: one picked
numbered still brought its whole run, with no way to import just that
frame, picking every frame made one sequence item per frame, the browser
couldn't make sequences at all, and a gap in the numbering closed up
silently.

The engine now groups numbered stills itself (engine/src/sequence.rs):
one picked file brings its run from its folder, several picked files of
one run import as that range, a picked or dropped folder imports its
files with each run as a sequence, and Force Alphabetical Order takes
every image of that type in the folder by name. `file.import` takes
`sequence` (default on), `alphabetical` and `frameRate`; Replace Footage,
Reload, proxies and render outputs probe the same way. The media layer
probes one file; the browser lists its file table for the run and
registers every frame with its media pool.

File > Import > File... asks, like After Effects' "PNG Sequence"
checkbox, whether picked numbered stills import as a sequence, with
Force Alphabetical Order and the frame rate. Sequences are named
`shot_[0001-0250].png`.

Interpret Footage gains Missing Frames (Show Placeholder, colour bars as
in After Effects, the new default; Hold Previous Frame; Skip) and Start
Frame (the frame number at the footage's first frame: trims or pads the
head). Saved projects keep Skip, so their sequences play as before.

Form notes keep a readable width and the form fits its rows, so OK stays
in view. docs/footage.md explains importing footage and sequences.

Test: tests_sequence (engine: run, picked range, every frame, Sequence
off, folder, alphabetical, Interpret Footage Missing Frames / Start
Frame, Replace Footage and Reload), project sequence unit tests,
sequence_gaps_show_placeholders_hold_or_close_up (media),
import_dialog_offers_the_image_sequence_option and
dropped_frames_import_as_one_sequence_with_sequence_interpretation (UI).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 18:00:07 +07:00
bflatastic 27cabcb36b Effects: EXtractoR picks layers and channels of multi-layer EXRs (#295)
EXtractoR's Red, Green, Blue and Alpha are text parameters naming
channels, and Effect Controls drew nothing for text parameters: there
was no way to choose a layer or channel. And a multi-layer OpenEXR file
whose colour is only in named layers (`diffuse.R`, Blender's
`ViewLayer.Combined.R`) didn't import at all: the image decoder wants an
unnamed RGB layer ("does not contain non-deep rgb channels").

- Effect Controls: EXtractoR gets a Layer popup listing the footage's
  OpenEXR layers. Picking one sets red, green, blue and alpha to its
  channels in one undo step, as After Effects' EXtractoR dialog does (a
  one-channel layer such as depth goes in all three colours). Red /
  Green / Blue / Alpha are popups of the layer's own R, G, B, A, every
  channel of the file, and (none). The lists are read only while open,
  so playing an EXR sequence doesn't parse files on the UI thread.
- `layer.channels {layer}` lists a layer's EXR layers and channels and
  what each layer shows in red, green, blue and alpha, for agents.
- Media: an EXR without an unnamed RGB layer imports and shows its
  colour layer (one named combined, beauty or rgba first); its channels
  stay available to EXtractoR and the 3D Channel effects.
- Raster: AuxChannels::layers / layer_rgba and split_channel.

Test: extractor_shows_the_chosen_layer_and_channels_of_a_layered_exr
(host: a layered EXR written with the exr crate imports, layer.channels,
EXtractoR renders spec and depth; it failed to import before),
extractor_offers_the_exr_layers_and_channels (UI: Layer popup, one undo
step, channel popup), layered_exr_without_an_unnamed_rgb_layer_imports
(media).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 17:32:09 +07:00
bflatastic fc5771f00f Media: resampling footage audio guards a zero mix rate (#274)
A requested rate of 0 would divide by zero in the resampler; it now
reads through the plain path, which returns silence for it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 16:43:21 +07:00
Brandon Thomas cae67546dd Media: checked offset and as_chunks_mut in the stereo interleave (#305 follow-up)
clippy 1.99 flags chunks_exact_mut with a constant size; the skip offset
was also an unchecked slice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 00:45:44 -04:00
bflatastic ec892ef145 Media: footage audio at another sample rate no longer stutters (#274)
AAC audio in an MP4/MOV whose sample rate differs from the mix rate
(a 44.1 kHz clip in the 48 kHz preview or export, or the reverse)
stuttered and sounded low-pitched in previews and in exported movies.

Cause: FilmCraft's MP4 reader, asked for audio at a rate other than
the file's, applied the edit list's priming offset (1024 samples for
AAC) twice when it resampled: each read started about 1024 source
samples early and extrapolated a ramp over its first ~940 samples
(peaks near 100 against a 0.5 tone). Export and the preview mix ask
for audio a frame or a device buffer at a time, so the ramp repeated
dozens of times a second. Same-rate reads take FilmCraft's copy path
and were exact. FilmCraft fixed this upstream in 5bf2203, 87 commits
past the pinned revision.

Fix: MediaPool reads footage audio at the source's own rate and
resamples it itself (linear, by absolute sample position, so a range
comes out the same however it is split into reads), for every
container.

Test: resampled_audio_read_in_frame_pieces_matches_ffmpeg (44.1 kHz
AAC read at 48 kHz and 48 kHz at 44.1 kHz in 29.97 fps frame pieces,
against ffmpeg's decode: max error 44 before, 0.002 after) and
movies_carry_resampled_footage_audio (a 29.97 fps comp with 44.1 kHz
AAC footage exported to MP4/AAC and MOV/PCM, decoded by ffmpeg: max
error 8.7 before, 0.03 / 0.002 after).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 22:16:35 +07:00
bflatastic 86b6a8b6b9 Media: conformed audio keeps its sound past 12:36 (#172)
Conforming a footage file's audio (Settings ▸ Disk ▸ Conformed Audio
Folder, which preview playback reads) timed each 65,536-frame chunk as
`frame index × ticks per second / rate` in i64, which overflows past
36,310,516 frames: 12:36.47 at 48 kHz (25:13 at 24 kHz, hence the
workaround). Chunks after the overflow decoded at a wrapped, negative
time and came out silent, so preview audio stopped at 12:37.76 (the end
of the last chunk that started in time) in every long file.

Chunk times now come from Tick::from_units (exact, 128-bit). The
conformed files' names include a format version, so files written by
earlier builds are ignored and conformed again.

Test: conform_chunks_keep_their_times_on_long_files (every chunk start
of a 2-hour file at 24–96 kHz); it failed before (multiply overflow).
Checked end to end: a 13-minute file's conformed audio has sound at 760 s
and 790 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:20:47 +07:00
Dexter 39e6907b5f G2: improve editing reliability and Windows rendering fidelity 2026-10-06 00:00:31 -05:00
Brandon Thomas 010cb8129f
Merge pull request #17 from storytold/pr/never-crash-media
Never crash: media crate without expect()
2026-10-04 15:29:59 -04:00
bflatastic fa27d621e0 M4.8: Edit ▸ Purge clears what it names: disk and snapshot purges keep the RAM preview, memory purges drop decoded footage
- 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>
2026-10-05 01:03:47 +07:00
Brandon Thomas 37c9b449c6 Never crash: media crate without expect()
The smart-object branch of the layered-file decoder bound the footage layer twice and
re-read it with expect("embedded implies a layer"); bind it once with a let-chain. Denies the
panic lints in the crate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 12:18:36 -04:00
Brandon Thomas 7b0a449c5f M13.6: PSD smart objects apply perspective quads and placed-layer warps
nonAffineTransform and the warp descriptor (named styles with bend / distortion, custom
envelope quilt meshes via ObAr meshPoints) are baked as placed over the placed bounds
(SourceLayer::placed), at up to 4x resolution.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 07:21:00 -04:00
Brandon Thomas 7d944ca494 M13.6: PDF/AI text, images, tiling patterns, blend modes, soft masks, pages; clip groups to masks / Merge Paths
Text with embedded TrueType, CFF (Type1C / CIDFontType0C / OpenType), Type 1 and Type 3 fonts,
standard-14 fallback to the bundled OFL fonts; Flate / DCT / inline / stencil images with SMask,
Indexed and ICC alternates; luminosity / alpha soft masks and the 16 blend modes in the SVG
render tree; tiling patterns; calculator functions; file.import page and Footage::page;
Create Shapes from Vector Layer keeps clips (page clips as layer masks, nested clips as Merge
Paths Intersect).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 05:49:36 -04:00
Brandon Thomas 7ac5317ce9 Merge main into M13.2 2026-10-03 04:19:01 -04:00
Brandon Thomas 7352b624d4 M13.2: PSD smart objects (SoLd/PlLd + lnk2 embedded files) import as placed footage of the embedded document or image
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 04:18:05 -04:00
Brandon Thomas da0ec526f3 M13.2: PDF / Illustrator (.ai) / EPS vector footage (effectcraft-pdf): continuous rasterisation, layered composition import, Create Shapes from Vector Layer; SVG render tree clip groups
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 04:12:21 -04:00
Brandon Thomas b17d2c05e6 Merge main into M14.5 (settings and menus)
Reconciled with main's keyframe labels (same Keyframe::label field; keys.setLabel and
keys.selectLabelGroup from main are kept, M14.5's duplicates removed; Edit ▸ Label on
selected keys delegates to the label helper), proxies (RenderOpts::proxy next to
nested_switches/draft_shadows), offloaded renders (Work carries nested_switches), wasm
audio pump (preview mix rate), variable font axes and mask feather points.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 03:11:45 -04:00
Brandon Thomas f855a4b55b M14.5: wire the remaining settings; keyframe assistants and labels; AE menu entries
Settings: 56 of 59 not-yet-wired settings now change behaviour (nested-comp switches,
constant vertex count, related-item time sync, match-name pick whip, split-layer order,
Home on open, motion path span, internal wireframes, zoom quality, label-coloured tabs,
mask colour cycling, gradients, grid/guide styles, text engine, font preview/recent/native
names, missing frames, unlabeled alpha, drag import, default output folder, segmenting,
bit depth names, preview sample rate, disk/media/conformed-audio caches, RAM reserve and
low-memory reduction, Video Preview window, 3D reference axes/extended viewer/draft
shadows, script file gate + File.execute warning, expression editor options and error
banner). Three stay display-only and are documented.

Features: Convert Audio to Keyframes, RPF Camera Import (JSON/CSV camera data),
keyframe colour labels + Select Keyframe Label Group, Recent Animation Presets.
Menus: Edit > History, File > Import Recent Footage, Replace Footage > With Layered
Comp, Save a Copy As XML (.ecprojx, opens again), Composition > VR (Create VR
Environment, Extract Cubemap), Assign Shortcut to 3D view / workspace, Window viewer
labels + open viewers, Help > System Compatibility Report / Enable Logging / Reveal
Logging File. Dynamic menu nodes (@dynamic:…) shared by the egui and native menus.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 02:45:44 -04:00
Brandon Thomas dbcda2eba9 Merge main into CMP-7/ANM-5/PRJ-8/PRJ-3 work
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 23:43:41 -04:00
Brandon Thomas abd39f5fd8 CMP-7: Essential Graphics controls, master properties, .ectemplate templates, Responsive Design — Time
Comps expose controls (properties typed as text/color/slider/checkbox/point/angle/dropdown,
Media Replacement, groups, comments); precomp layers get an Essential Properties group whose
edits override per instance (Push to Comp / Revert); templates export/import as an open ZIP
(manifest.json + project subset + media + poster). Stretched precomps keep protected regions
at their original speed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 22:44:06 -04:00
Brandon Thomas 36683d95ee Merge main into IO-2/SHP-5/OUT-2/PRV-2 work
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 22:39:47 -04:00
Brandon Thomas 406706e3d5 OUT-2: WebM export (VP9 intra encoder + alpha, Opus), WAV/AIFF audio-only outputs
- effectcraft-vp9enc (L0): VP9 profile 0 key-frame encoder from the VP9 bitstream spec
  (8x8 intra blocks, DC/V/H/TM, 4x4 DCT/ADST, lossless WHT at quality 100, tile columns);
  round-trips exactly through FilmCraft's VP9 decoder and ffmpeg.
- export: WebM muxer (EBML/Matroska spec) with VP9, VP9 alpha in BlockAdditional
  (AlphaMode 1), Opus (CodecDelay = pre-skip); WAV and AIFF outputs.
- OutputFormat WebM/Wav/Aiff in the Render Queue, renderQueue.add formats, CLI --format.
- Tests: FilmCraft decode-back (frames, size, pixels, audio RMS), ffprobe/ffmpeg oracles.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 22:28:15 -04:00
Brandon Thomas 94e1c86d89 IO-2/SHP-5/PRV-2: PSD import, SVG import + vector/text conversions, persistent disk cache
- effectcraft-psd (L1): PSD/PSB reader from Adobe's published file format spec (layers, groups,
  masks, blend modes, text engine data, lfx2 effects, fill/adjustment layers, vector masks;
  1/8/16/32-bit, RGB/CMYK/Gray/Lab/Indexed, raw/RLE/ZIP) and a minimal writer for fixtures.
- File > Import: importAs footage|composition|compositionLayerSizes, layer (Choose Layer);
  groups -> precomps (pass-through collapses), text -> editable text, styles -> Layer Styles,
  adjustments -> adjustment layers with effects; UI asks the Import Kind for Photoshop files.
- effectcraft-svg (L2): SVG parser (shapes, paths, transforms, use, CSS, gradients) and
  rasteriser; .svg imports as footage, continuously rasterised when the switch is on.
- layer.create: shapesFromVector, shapesFromText, masksFromText, editableText (stub removed).
- Disk cache (render::disk_cache): LZ4 content-keyed frames and layer buffers, LRU size limit
  from prefs, atomic writes, restart persistence; edit.purge clears it, cache.diskStats query;
  viewer frames read/write it and the timeline shows disk-only frames in blue.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 22:10:48 -04:00
Brandon Thomas a51f439d34 Merge branch 'main' into worktree-agent-a758de7c5b6efcc3d
# Conflicts:
#	crates/effects/src/lib.rs
#	crates/render/src/lib.rs
#	docs/parity.md
2026-10-02 21:48:49 -04:00
Brandon Thomas 9a11c577f3 M9.11: 3D Channel effects on auxiliary channels
Aux-channel model (effectcraft_raster::channels3d: depth, object/material
ID, normals, UV, coverage, Cryptomatte ranks + manifests, MurmurHash3);
EffectHost::aux; multi-layer OpenEXR channels via the exr crate
(FootageSource::aux); the compositor's depth / layer-ID / Cryptomatte pass
(Renderer::comp_aux) so the effects work on precomps of 3D comps.
3D Channel Extract, Cryptomatte, Depth Matte, Depth of Field, EXtractoR,
Fog 3D, ID Matte, IDentifier.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 21:28:37 -04:00
Brandon Thomas 83c7e14b94 M7.4: model import
New crate effectcraft-model (L2): glTF 2.0 (.gltf/.glb, data URIs, external buffers and
images, PBR metallic-roughness with base/MR/normal/occlusion/emissive textures, node
hierarchy, skins, step/linear/cubic animations) and OBJ/MTL import, parametric primitives,
extruded/bevelled outline meshes and polygon triangulation with holes.

Project: FootageKind::Model, LayerSource::Model/Primitive, Layer::environment, Geometry and
Material Options builders, Environment light Source. Media probes and loads models
(FootageSource::model). Engine: layer.newModel, layer.new3dPrimitive/newPrimitive,
comp.renderer, layer.environment, material.set/reset/duplicateAssign/revealSource (stubs
replaced); File > Import and layer.addItem pick up models. Hand-written fixtures with
attribution sidecars.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 21:04:47 -04:00
Brandon Thomas 029c0da5e5 M2.4: bit depth + linear blending + working space
8/16 bpc projects clamp and quantise layer pixels after source/masks and
every effect, and the comp after every layer (16 bpc = 0..32768); 32 bpc
keeps over-range values. Blend modes without HDR support clamp their inputs
in 32 bpc. Working space (sRGB, Rec. 709, Rec. 2020, Display P3; matrices
derived from the published primaries), footage colour profiles from stream
metadata or Interpret Footage, Linearize Working Space and Blend Colors Using
1.0 Gamma. Layer cache keys include the colour settings. Project panel depth
button: click cycles, Alt-click opens Project Settings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:03:36 -04:00
Brandon Thomas 47f7712364 M14.2: preferences
A real Settings model in the engine (`engine::prefs`): serde, versioned, dotted keys
(`general.undoLevels`, `labels.3.name`), migration of older layouts, unknown keys kept,
persisted through a `ConfigStore` trait (platform config dir on the desktop; MemoryConfig
for tests; the web app can back it with localStorage).

- After Effects 2026 pages (General … Scripting & Expressions) as a schema shared by the
  dialog, `prefs.pages` and docs/preferences.md (rows not wired yet are listed there and
  kept in step by a test).
- Commands: prefs.get, prefs.set {key, value | values}, prefs.reset {page?}, prefs.open
  {page?}, prefs.pages.
- Wired settings: undo levels, path point size, tool tips, layers at comp start / CTI,
  default spatial interpolation linear, Home screen on launch, new project template,
  rendering progress in Info, adaptive resolution limit, cache when idle, fast previews,
  theme + UI brightness, label names/colours everywhere (Label menu, timeline, project,
  render queue, viewer), label colour for handles, grid colour/spacing/subdivisions, guide
  colour, safe margins, still footage duration, sequence fps, audio output device and
  output mapping, layer / footage / preview cache budgets, default 3D renderer.
- Settings dialog: AE's left page list + right content, OK / Cancel (restores) / Previous /
  Next, every widget with an automation id.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 09:14:45 -04:00
Brandon Thomas fa21ce2cb4 M6.8: fix media pool deadlock (rayon worker blocking on an in-flight decode; prefetch on its own thread)
A rayon worker running frame_at could wait on the in-flight condvar for a
frame whose decode sat lower on its own stack (work stealing during the
decoder's parallel join), hanging footage playback and the media tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 00:37:39 -04:00
Brandon Thomas c91af194f2 M9.3: effect env (masks, layer params, audio, comp timing), sim cache, polyline helpers; module stubs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 21:43:48 -04:00
Brandon Thomas d867178137 M6.8: media import via FilmCraft codecs (probe, frame cache, stills, sequences)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 14:28:14 -04:00