architecture.md and web.md still said every job renders on the project's
renderer (#414); since #3335 a GPU job times both compositors on its first
frames and keeps the faster.
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`.
* 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
* 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
* 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
* 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)
* 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
* 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)
* 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
Reuse the existing diff() helper so a non-finite pixel fails the test
(f32::max ignored NaN). Keep exact equality on llvmpipe, which still
fails without the row-block fix; other adapters use the usual Bpc32
tolerance, because a 2e-6 bound can fail on unmeasured hardware and
does not catch the regression there.
Horizontal GPU box passes restarted the running sum every 256 pixels, while
the CPU keeps one sum per row; use the full row width as the block so f32
rounding matches. The regression test requires exact equality on llvmpipe
(CI); other adapters only get an absolute 2e-6 sanity bound.
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.
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>
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
--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.