Commit Graph
4481 Commits
Author SHA1 Message Date
lisn0 3694f07600 Preserve frozen Object Mattes with pre-0.23 footage keys (#3430) 2026-11-11 00:37:34 +00:00
Dylan M. Taylor c30fb9aeed Sonar: [[ in the plugin-catalog check, Number.* in the boa runtimes, no Invoke-Expression (S7688, S7773, S8659) (#3427)
Clears all 29 open SonarCloud issues.

- `examples/plugin-catalog/scripts/validate.sh`: `[[ ]]` in place of `[ ]` for bash tests (SonarCloud shell:S7688). Every script has a bash shebang; no test uses `-a`/`-o`, escaped parentheses or an unquoted pattern on the right of `=`.
- `crates/expr/src/prelude.js`, `crates/script/src/prelude.js`, `crates/script/src/scriptui.js`: `Number.isFinite` / `Number.isNaN` / `Number.parseInt` (javascript:S7773). Every call already has a number (a `typeof` check or `Number()` before it), and `Number.parseInt === parseInt`, so nothing changes in boa.
- `packaging/windows/test-legacy-fixture.ps1`: dot-sources `[scriptblock]::Create($block)` in place of `Invoke-Expression $block` (powershelldre:S8659). It runs in the same scope, so `$LegacyWxs` is still set after it.
- `sonar-project.properties`: S7773 and S8786 are not raised on `*.jsx`. Those are After Effects ExtendScript (ES3), which also runs in After Effects and has no `Number.isFinite` or `String.prototype.trim`. The ignore covers 7 issues in `Ease Presets.jsx` and `Rename Layers.jsx`.
2026-11-10 19:04:34 -05:00
Dylan M. Taylor 27fa43fd52 text: caret and line lookups by bisection (#3029) 2026-11-10 18:00:52 -05:00
Dylan M. Taylor e2afb4b17c Timewarp: hold Block Size to its declared range and the matching window to a cap (#3024) 2026-11-10 18:00:27 -05:00
Dylan M. Taylor 8d46e1df7f Gradients: bound the stops sampled per pixel (#2950)
* Gradients: bound the stops sampled per pixel

Gradient sampling scans the stops linearly and shape painting samples once
per pixel, so a gradient with a huge stop list multiplied every frame's
work. Sampling now looks at the first 256 colour and opacity stops, and the
plain JSON form (coerce_json) refuses gradients with more than 256.

* address review: bound tagged gradients to the stop limit
2026-11-10 17:59:59 -05:00
Dylan M. Taylor e1ea71f540 Text: merge variable font axes by tag and cap their number (#2949)
* Text: merge variable font axes by tag and cap their number

Each tag in a variations object scanned the style's growing axis list, so a
large object cost quadratic time, twice (validation probe and apply). The
object is now merged in one keyed pass, refused when it names more than 64
axes, and a merge that would leave a style with more than 64 is refused
without changing the style.

* cargo fmt

* address review: refuse variation over every run before changing any

* address review: empty-document range cannot overflow in set_attr
2026-11-10 17:59:33 -05:00
Dylan M. Taylor 2c95408777 Auto-trace: bound the nesting, simplification and fitting work (#2945)
* Auto-trace: bound the nesting, simplification and fitting work

Many separate regions made the nesting depth test every pair of contours,
Douglas-Peucker could rescan a span once per point (and recurse as deep as
the loop is long), and the curve fit searched the raw contour from its start
for every simplified vertex.

- Depth: each loop's bounding box goes into a coarse grid; a loop's first
  point is tested only against loops whose box holds it. Same depths as
  testing every pair.
- Douglas-Peucker: spans wait on an explicit stack, with a budget of 64
  distance evaluations per point; spans still waiting when it runs out keep
  all their points, so the outline stays exact.
- Fitting: uses the raw indices the simplifier kept instead of searching.

* cargo fmt; clippy manual_is_multiple_of

* address review: keep wide contour boxes out of the nesting grid
2026-11-10 17:59:07 -05:00
Dylan M. Taylor 497a056c07 Box blur: count repeated edge pixels instead of visiting them, and bound the radius (#2944)
* Box blur: count repeated edge pixels instead of visiting them, and bound the radius

* address review: box blur first mean ignores an unused edge term (inf * 0 gave NaN)
2026-11-10 17:58:43 -05:00
Dylan M. Taylor ed085bd483 ICC: interpolate only the CLUT axes that need it (#2985)
* fix(color): interpolate only the CLUT axes that need it

* address review: CLUT upper grid points are not interpolated, stride arithmetic saturates
2026-11-10 17:58:17 -05:00
Dylan M. Taylor c6411266eb Render Queue: keep Output Module crop padding within the largest output frame (#2984)
* fix(render-queue): keep Output Module crop padding within the largest output frame

* address review: crop padding is taken back only when the side would pass the cap
2026-11-10 17:57:53 -05:00
Dylan M. Taylor 4020edb8cd PDF fonts and fax: bound charstring work, ToUnicode ranges and reference search (#2964)
* PDF fonts and fax: bound charstring work, ToUnicode ranges and reference search

Type 1 and CFF charstrings bounded subroutine depth but not total work:
subroutines that call each other several times multiplied a small font into
unbounded instructions and geometry. Each glyph now has a shared budget of
100,000 instructions. The Type 1 Subrs parser looked up /CharStrings again
for every entry; it is found once. A ToUnicode bfrange expanded a long
destination string for each of up to 65,536 codes; destinations past 256
bytes are skipped and a CMap keeps at most 2^20 mappings. CCITT 2-D decoding
scanned the reference row from its start for every code; it bisects.

* address review: cap bfchar and array bfrange unicode mappings

* address review: seac components share the glyph instruction budget (CFF and Type 1)
2026-11-10 17:57:27 -05:00
Dylan M. Taylor 684c53f1b1 Segment checkpoints: charge memo copies and string bytes; refuse impossible zip sizes; bounded inflate errors (#3006)
* Segment checkpoints: charge memo copies and string bytes; refuse impossible zip sizes; bounded inflate errors

The pickle reader charged memo reads but not memo writes, and counted a
string as one object whatever its length, so memoizing one long string
many times kept unbounded copies; writes are charged too and strings count
one node per 64 bytes. A zip entry stating more than deflate can expand
its data to is refused before inflating, and inflate errors keep only the
status, not the partial output.

* address review: charge every owned field of Global, Storage and Tensor to the pickle clone budget
2026-11-10 17:56:41 -05:00
Dylan M. Taylor 48694df715 Lottie export: embed only files that are PNG, JPEG, GIF or WebP images (#3008)
* fix(lottie): export embeds only files that are PNG, JPEG, GIF or WebP images

* address review: match full PNG and GIF signatures
2026-11-10 17:56:16 -05:00
Dylan M. Taylor f541597d51 fix(export): encode WebM audio packets in place instead of draining each from the front (#2982) 2026-11-10 17:55:32 -05:00
FredApps d5a8eb064a Merge pull request #3407 from storytold/sonar/analysis-scope
sonar: leave out the AEP corpus generators and document the expression eval wrappers
2026-11-10 19:31:29 +01:00
FredApps 5bb98f5e9c Merge branch 'main' into sonar/analysis-scope 2026-11-10 18:38:03 +01:00
FredApps f85f18e0b4 Merge pull request #3401 from nazar-gavrylyk/fix/first-playback-lag
Preview: smoother first playback of uncached comps
2026-11-10 18:36:23 +01:00
FredApps 37f2017664 Merge branch 'main' into sonar/analysis-scope 2026-11-10 18:23:19 +01:00
FredApps 9eb9ac003b Preview: end the small-RAM-preview pacing test with a newline (cargo fmt) 2026-11-10 17:56:26 +01:00
FredApps 114fbaed63 Preview: the pace measures its lead over no more frames than the RAM preview holds
The pace counted cached frames among the next ahead x 2, but the prefetch never requests more than the RAM preview holds (offsets 0..fit-1 from the current frame). With room for only a few frames the lead could never fill, so a preview that rendered every frame at once was slowed to a fraction of its frame rate, with Lower Resolution When Playback Can't Keep Up off (the default). The lead is now counted over at most the frames the prefetch keeps after the current one, and with none of those (room for at most two frames) the clock runs in real time as before pacing.
2026-11-10 17:22:44 +01:00
ash feaa1138d3 Merge pull request #3421 from arnav-hiwarkar/fix/xtask-non-utf8-macos
xtask: skip the non-UTF-8 changed-file test on macOS, where APFS can't hold such names
2026-11-10 21:25:42 +05:30
arnav-hiwarkarandClaude Opus 5.5 c330262ef2 xtask: run the non-UTF-8 changed-file test off macOS only (#1224)
APFS refuses file names that aren't UTF-8 (EILSEQ, "Illegal byte sequence"), so the test's
fixture write panicked on the macOS gate before reaching changed_files. Such a file can't
exist on macOS; Linux keeps running the test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 20:48:05 +05:30
nazar-gavrylyk e356e4adf5 Preview: a stall after playing at full rate still steps down a resolution
The step-down was skipped while the achieved frame rate read as keeping up, and that rate comes from the frames shown in the last second they were shown: through a stall it kept the rate from before it, so a preview that played a cached stretch and then stalled never stepped down. A rate whose last frame is more than a second old no longer counts.
2026-11-10 15:58:42 +01:00
ash b23bbf927f Merge pull request #3412 from arnav-hiwarkar/fix/1828-timeline-layer-snapping
Timeline: Shift-dragging a layer bar snaps to layer ends, the current time, markers and keys (#1828)
2026-11-10 20:10:39 +05:30
FredApps 9143393ce5 #945: play preview audio at the selected frame rate (#3416) 2026-11-10 15:26:21 +01:00
nazar-gavrylyk a10c7fa2cd Footage: note that 8-bit and float frames of one movie decode apart
In an 8 bpc comp, a movie used by both a plain and a masked layer has an 8-bit and a float entry in the media pool, decoded separately. Rare and only a speed cost, so it is noted where the 8-bit path starts rather than changed.
2026-11-10 15:10:35 +01:00
nazar-gavrylyk 513d5eaea7 Preview: Lower Resolution When Playback Can't Keep Up is off by default
It never steps back up during a play, so a looping preview kept playing at a quarter of its resolution after its first loop. Without it the paced first loop caches the viewer's resolution and the next loops play in real time, as in After Effects. The setting stays for whoever prefers a lower first pass.
2026-11-10 15:10:24 +01:00
nazar-gavrylyk b18bf9186a Preview: GPU admission's reasoning holds for lanes four threads wide
Only a lane's drain thread waits for a GPU slot. A lane runs one drain at a time (it is idle, and started again, only once its drain found no job), and its other threads only help with that frame's parallel work, as Rayon helpers that never wait. The comments said lanes had one thread; the test now runs on a lane LANE_WIDTH wide and checks that its other threads decline while the drain thread waits.
2026-11-10 15:09:40 +01:00
nazar-gavrylyk db21c733b4 Auto: a frame's ticket ends it on every exit
AutoPick::ticket chooses a side and reports the frame's time once; dropped without one it ends the frame unmeasured. The viewer's frames, the renderer's top-level frames and frames rendered in passes use it, so an Auto CPU frame turned away by GPU admission (#3028), a cancelled export frame or any later exit no longer leaves its frame counted as running. The frame server keeps its choice across calls and still ends a replaced frame itself.
2026-11-10 15:07:57 +01:00
nazar-gavrylyk e8b94271c4 Footage: the 8-bit levels test checks the movie is in the frame's files
The test source notes its file while a read is recorded, as the media pool does, and the test checks the render's read log lists it.
2026-11-10 15:04:01 +01:00
nazar-gavrylyk 21d54b2a1a Gaussian Blur: the bit-identity test covers tiles past the first
A 300-pixel-wide case runs the row-by-row pass over tiles at x 128 and 256 and a partial last tile.
2026-11-10 15:04:01 +01:00
nazar-gavrylyk e4c49f9bc0 Disk cache: queued layer writes hold at most 1 GB
Layer jobs keep their float buffer until the writer compresses it, so the 64-job limit could hold ~8.5 GB of evicted 4K buffers. Layer and matte writes now also have a byte budget, like frames; a single bigger buffer is still taken when nothing else is queued.
2026-11-10 15:04:01 +01:00
nazar-gavrylyk bbc5f9889d Preview: a cached end of a preview that doesn't loop plays in real time
The paced clock counted the cached lead out of twice the prefetch depth even when fewer frames were left before the end, so a fully cached preview that doesn't loop slowed down over its last frames, and one shorter than that depth played slow throughout (and could step down). The lead now counts out of the frames left.
2026-11-10 15:04:01 +01:00
nazar-gavrylyk dd1aff17e5 Footage: 8-bit levels are read while the render records its reads
footage_buf8 asked for a movie's 8-bit levels outside Renderer::read, so the media pool noted the movie in no recording: a layer buffer made of it listed no files and was still marked complete, and could be served stale after the movie changed on disk.
2026-11-10 15:04:01 +01:00
nazar-gavrylyk 396495dfa0 Auto: frames that end without a time stop counting as running
A GPU frame that failed or failed its health check, and a remote frame replaced before it finished, stayed counted as running on its side, so every later frame there was timed as one of several side by side and that side looked cheaper. AutoPick::abandoned releases them.
2026-11-10 15:04:01 +01:00
nazar-gavrylyk b3000aac70 Preview: playback runs at the pace frames arrive when it can't keep up 2026-11-10 15:03:10 +01:00
nazar-gavrylyk 3709cccedf Preview: frame lanes are four threads wide, and cache_bench measures the cadence 2026-11-10 15:03:10 +01:00
nazar-gavrylyk 893d9a0b04 Preview: playback steps down a resolution when it can't keep up
Settings > Previews > Lower Resolution When Playback Can't Keep Up (on by
default): a preview at Resolution Auto that stalls on uncached frames and
shows under 80 % of its frame rate for two seconds plays on at half the
scale, and two seconds later at a quarter if still slow, no lower than the
Adaptive Resolution Limit. Stopping goes back to the viewer's resolution
and Cache Frames When Idle fills it in. An explicit Resolution, the comp's
or the Preview panel's, is kept as set.

First playback of a 1080p comp with a blur (cache_bench --play): 9.0 to
22.1 frames/s on the CPU (about 27 once at Half), 4.1 to 16.1 with Mercury
Auto on an integrated GPU; plain clips that keep up stay at Full.
2026-11-10 15:03:10 +01:00
nazar-gavrylyk cc4f834a94 cache_bench: --play measures a first playback in the app
--play SECS runs the app headless (egui_kittest) on the bench comp, with
the viewer at 100 %, Resolution Auto, nothing cached and Cache Frames When
Idle off, presses play and steps it for SECS seconds of wall time. It
prints the frames shown per second, the time the clock stalled on uncached
frames, the frame reached and the share of passes at each viewer scale.
--gpu composites on the GPU (Mercury Auto) as the desktop app does;
--res N sets the comp's Resolution to every N-th pixel.
2026-11-10 15:03:10 +01:00
nazar-gavrylyk 76e4008151 Footage: 8 bpc renders keep plain movie frames as 8-bit levels
An 8 bpc render at Full quantises plain footage straight away, so the
media pool now keeps those movie frames as the 8-bit levels it is
quantised to (a quarter of the memory) and the renderer expands them
into the layer buffer: the same pixels to the bit. Other depths,
reduced renders, masks, colour profiles, fields, frame blending,
Cineon, Invert Alpha, linear light and 10/12-bit or float movies keep
the float path.

First playback of plain 1080p clips at Full caches ~43% faster on the
CPU, and the footage cache holds 4x the frames.
2026-11-10 15:03:10 +01:00
nazar-gavrylyk 50a889737e Auto: frames rendered side by side no longer all warm up the GPU
A preview renders up to 12 frames at once, and Auto only learns a frame's
time when it is done. A starting preview sent every lane to the GPU for
its warm-up, and on an integrated GPU 2–4× slower than the CPU each first
playback stalled. Now at most one GPU warm-up frame (and one re-probe frame
per side) runs at once, a frame's time counts per frame running on its side
at once, and the warm-up starts on the CPU, so the frames on screen come
first. Iris Xe, 1080p: Auto went from 13–34% slower than CPU only to 0–17%.
2026-11-10 15:03:10 +01:00
nazar-gavrylyk 6bb52b1cb1 cache_bench: --gpu measures the GPU compositor
--gpu renders on a headless GPU of its own and reads the frames back, as the
desktop viewer does; --backend cpu|gpu|auto picks the compositor (default
auto, the viewer's Mercury GPU Acceleration). With auto the bench prints
Auto's per-comp timing history: how many frames each side rendered and how
long they took.
2026-11-10 15:03:10 +01:00
nazar-gavrylyk b3dc49a0cf Render: plain footage is copied, converted and quantised in one pass
A footage layer's frame was copied from the footage cache, converted from
its colour profile, then quantised to the project depth in masked_source:
three passes over a 33 MB 1080p float frame. Footage without masks now
comes quantised from one fused pass (Pipe::copy_converted); other source()
callers still get unquantised pixels. Bit-identical (tested at 8/16/32 bpc).
2026-11-10 15:03:10 +01:00
nazar-gavrylyk 42989edd8e Gaussian Blur: the exact kernel for small blurs runs row by row
The sampled Gaussian (sigma < 3) read every tap through a bounds-checked
get, and its vertical pass walked a column per pixel. Taps inside a row are
now one slice, and the vertical pass adds whole input rows in 128-pixel
tiles. Same f64 sums in the same order: bit-identical output (tested
against the per-pixel kernel). Each pass is 15–25% faster.
2026-11-10 15:03:10 +01:00
nazar-gavrylyk 87bfaa8f1c Preview: Cache Frames When Idle is on by default, as in After Effects
A comp left alone for a second fills its RAM preview from the current time,
so the first playback is cached instead of rendering frame by frame. Saved
settings keep their value. Two UI tests that assumed the old default set it
explicitly.
2026-11-10 15:03:10 +01:00
nazar-gavrylyk 38dffd4aea Disk cache: frames and layer buffers are compressed on the writer thread
A render thread handed each frame to the disk cache already copied pixel by
pixel and LZ4-compressed (~9 ms per 1080p frame) before it could start the
next one, and layer buffers were encoded even when the queue then dropped
them. The queue now holds raw RGBA (one memcpy via ColorImage::as_raw) and
Arc<Buf>s; the writer thread encodes them. Every frame of a 120-frame bench
run now reaches disk (113–119 before). cache_bench prints the flush time.
2026-11-10 15:03:10 +01:00
13470ca161 Add affected CI and replayable crash repros (#1224)
* AG4: xtask ci --affected

cargo xtask ci --affected [--base REV] [--dry-run] runs fmt, clippy, tests (nextest + doctests when available), layers, assets (only on asset changes) and wasm (only for affected L0-L4/web crates) for the packages a change can affect: changed files map to the owning package and to packages whose sources read them via ../ paths, expanded to reverse dependents. Workspace-wide inputs (root Cargo.toml, Cargo.lock, .cargo/, xtask/, toolchain, clippy.toml) fall back to full CI. Documented in docs/testing.md, AGENTS.md and CLAUDE.md.

(cherry picked from commit f3ca84fc2ee3948582cbc94e4c99ee510e829540)

(cherry picked from commit 8a2cdcadf447eafa57ab14016beae4420f002af0)
(cherry picked from commit d8a4301bc1)

(cherry picked from commit 36402c61ac2f7e29ecd76a6cfc89365cc2572e0a)

* AO1: fast CI profile, looser timing tests

- [profile.ci] (release without LTO, 16 codegen units); `xtask ci` runs clippy
  and tests with --profile ci instead of --release.
- Wall-clock asserts in engine memory_tick, gpu OpenGL decline and script
  socket tests keep their intent with bounds that survive a loaded machine.
- docs/testing.md and CONTRIBUTING.md mention the profile.

(cherry picked from commit b45c8a379351ebdfda0d7076ad4b740baf6f2087)

(cherry picked from commit fa1ea6a107c4c1936cde89b518a05adc73f2b4b0)
(cherry picked from commit e5d5ed80e9)

(cherry picked from commit d2c67660c70764f9c56880a642ad7f0d004a7f92)

* AG6: journal replay repros

Bug reports become replay scripts and crash regression tests become data files:
- engine::replay: a JSON-lines repro format (start line, command steps with
  expect_error, expect queries with JSON pointer + equals, expect_no_error),
  a never-panicking runner that reports per-step ok/error and the first failure,
  and journal export (from the last project open; saves left out; size-capped).
- journal.export {path?} and help.exportRepro {path?} (not journaled).
- Crash capture: when the last-resort guard catches a panic in the outermost
  command, the journal plus that command is saved to Repros/crash-*.jsonl in the
  settings store (newest 20 kept) and the error names the file.
- Commands run by a journaled command (script steps) are no longer journaled twice.
- effectcraft-cli replay SCRIPT.jsonl [--json] [--keep-going]: exits 1 on a failed
  step, unmet expectation or panic.
- crates/host/tests/repros.rs replays every tests/repros/*.jsonl; three samples
  from past crash fixes (zero frame rate, Layer Settings with no selection,
  New Comp from Selection with hostile overlap).
- Docs: docs/agents.md "Repro scripts"; AGENTS.md: crash fix regression test =
  a .jsonl in tests/repros.

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

(cherry picked from commit c38509d7dafe507856c48c78db65e00b94b0cebc)

(cherry picked from commit e55b56dbf76716adfcbfe4cc773df19a74edc419)
(cherry picked from commit 96a63855a9)

(cherry picked from commit c59351ab3bd697f0868e41230dd883960d94a540)

* xtask: ci and ci --affected run cargo test and nextest with --no-fail-fast

One run now lists every failing crate instead of stopping at the first.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019qXL3GPNnY9BSFF2XoU2Yu

(cherry picked from commit e46c7debe738eca62b35bf7392ab77637f6a33ae)

* Agents: confinement write-checks journal.export, help.exportRepro and file.exportTimeline

Review on #859 (split into this PR): their output paths were only read-checked (relative ones
not at all). Test: confinement_covers_every_command_that_writes_a_file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014d65YdNxQmjRkbXRtP2NwH

* #1224: handle bin-only CI targets and BOM repros without weakening timing tests

* #1224: run the full gate when nextest policy changes

---------

Co-authored-by: Elias <hakifx91@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: FredApps <FredApps@users.noreply.github.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
2026-11-10 14:57:32 +01:00
arnav-hiwarkarandClaude Opus 5.5 d0eb89904c Timeline: note that a snapped bar edge off the frame grid lands within half a frame (#1828)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 19:26:51 +05:30
arnav-hiwarkarandClaude Opus 5.5 3c6abe3606 Timeline: a Shift-drag on an unselected bar moves the whole selection (#1828)
A Shift-press on an unselected bar adds it to the selection, so the drag now moves that
selection and its layers are no snap targets. Tests cover the selection drag and that a
moved layer's own marker is no target.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 18:44:24 +05:30
FredApps f88e7980ae Merge pull request #2162 from MarekSpodBiedry/issue-2104
Make the audio preview follow mute changes made during playback (#2104)
2026-11-10 14:10:00 +01:00