Commit Graph
183 Commits
Author SHA1 Message Date
Jonathan Cardoso aedb582eb9 fix(render): an opaque 8-bit still becomes the canvas, not a region over a zero-filled one (#4070)
An RGB PNG or a JPEG decodes to RGBA8 with alpha 255 everywhere, but only a frame with no alpha
plane counted as opaque, so each frame of a still was converted to a full-size region and mixed
over a zero-filled canvas: two 33 MB float images at 1080p instead of one, for every frame an
export renders at once. A region that is the whole picture with every alpha exactly 1 is now an
opaque full layer, which the compositor takes as the canvas. The output is the same bits.
2026-11-03 00:09:23 -05:00
Jonathan CardosoandBrandon Thomas afdb8019c1 media: say which colour values no tag gave (#4086) (#4089)
With no colour tags, FilmCraft decodes with BT.709 transfer and
primaries and the matrix by frame size, and probe and media.colorInfo
showed those values with nothing saying they were defaults, so an
untagged file looked the same as one tagged BT.709.

VideoStreamInfo gets color_assumed (optional, left out when unknown):
which of the matrix, transfer and primaries no tag in the file gave.
MP4/MOV and Matroska take it from the same sources as the colour, APV
from its colour description, and AVI, MXF and Y4M mark all three.
probe prints it as video.color_assumed and media.colorInfo as assumed,
next to the unchanged metadata.

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-11-03 00:04:13 -05:00
Jonathan Cardoso 92939f71bf #3948: Time interpolation applies when a clip's frame rate differs from the sequence's (#3949)
* #3948: time interpolation applies when a clip's frame rate differs from the sequence's

A 24 fps clip in a 29.97 sequence at 100 % rendered the same with Optical
Flow or Frame Blending as with Frame Sampling, repeating one frame in five:
interpolation_blend gave up whenever the speed was 100 %. It now gives up
at 100 % only when the media's frame rate matches the sequence's, so it
takes the sequence rate too. Clips at the sequence's own rate still never
fetch a second frame.

* #3948: a still image gets no in-between frame

A still reports 29.97 fps and returns the same picture at every time, so
in a sequence at another rate it now passed the frame rate check: Optical
Flow ran on two copies of one frame, and the GPU plan fell back to the CPU
for nothing. media_interpolation skips still sources, and the item layer,
media_image and the GPU plan all ask it.

* #3948: an in-between frame never comes from past the clip's source out

A reversed clip starts 1 tick before its source out, so on a source frame
boundary interpolation_blend paired its last frame with the frame at the
source out, at a weight near 1: the first frame showed media the trim had
cut. A forward clip could end the same way (23.976 on 29.97, 5 frames
ending on a source frame blended in 20 % of the next one). Speed changes
had this already; rate conforms at 100 % now take the same path. When the
later frame starts at or past the source out, the clip shows its last
frame alone. The later frame always starts after the source in, so the
other end needs nothing.
2026-11-03 00:03:41 -05:00
842d3ca567 lumetri.presetThumbnails: write through the host and keep the grid to the presets (#4030)
* lumetri.presetThumbnails: write through the host and keep the grid to the presets

The PNG was written with std::fs::write instead of the session's Services,
the path every other export takes (#379), so on hosts without a filesystem
it trapped and elsewhere it ignored the host's file handling. The grid was
also as wide as `columns` asked (up to 32) whatever the number of presets:
there are ten, and 32 columns of 1920 px cells made a 61 572 x 1 084 image,
over a gigabyte of f32 pixels and a 267 MB PNG for one query. Columns now
stop at the number of presets shown. Found by fuzzing commands with hostile
parameters (it took over 10 s per call in a debug build).

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

* render: the preset grid copies thumbnails with checked slices

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-11-03 00:02:50 -05:00
wweimao f3f1afd160 M10.7: fix per-character Stroke and Stroke 2 controls (#3938) 2026-11-03 00:01:43 -05:00
Arshiya RahgozarandBrandon Thomas 76cafff221 render: Luma Key can key out brights (#2648) (#3688)
Luma Key only made pixels below Cutoff transparent, so a light
background could not be keyed. Add a Key Out Brights option (default
off) that inverts the matte; projects without the param render as before.

Tested: luma_key_can_key_out_brights (default, missing param, inverted).

Fixes #2648

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-11-01 15:15:49 -05:00
Patrik LermonandClaude Opus 5.5 0ffb2b1b9d Bokeh Blur: gather through a kernel of whole-pixel weights, 5x faster on the GPU (#3768)
* render, gpu: Bokeh Blur gathers through a kernel of whole-pixel weights, 5x faster on the GPU

Each bilinear tap's four weights fall on the same offsets around every pixel (a pixel's centre is
a whole number and a half), so vfx::bokeh_kernel sums them per offset once per frame: about 330
reads at amount 10 instead of 1764. The CPU gathers through it, and the GPU reads it from a small
texture in the gather pass's aux slot, in the same order. 1080x1920, M4 Max: amount 10 hexagon
15 -> 3.2 ms on the GPU, 781 -> 114 ms on the CPU; amount 12 disc 58 -> 6.7 ms on the GPU.
New test bokeh_kernel_matches_the_bilinear_taps keeps the old gather as the reference.

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

* render: Bokeh kernel offsets stay within 2^20 pixels; the CPU gather adds them saturating

A huge amount on an image too small to halve kept its whole radius, so an offset could reach
i32::MAX and wrap when added to a pixel position (on the GPU, and on 32-bit CPU targets). An
offset past any image's width reads the edge pixel either way.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 15:15:29 -05:00
Arshiya Rahgozar e492d9e2a6 effects: give 4-Color Gradient a position for each colour (#3747)
* effects: give 4-Color Gradient a position for each colour (#3682)

The four colour points were hard-coded a quarter in from the corners, so
the gradient could not be arranged (the reporter had no position for the
fourth colour). Point 1-4 are now parameters; a point left at its NaN
default keeps the old position, and a project saved before they existed
has no such params, so both render exactly as before.

Test: a project without the points and one with the old positions set
explicitly render the same as the default; moving Point 4 shifts the
colours.

Fixes #3682

* effects: resolve the 4-Color Gradient points to the quarter positions (#3682)
2026-11-01 15:15:21 -05:00
aiongg 01608757f5 Merge integration track fx 2026-10-31 03:39:22 +00:00
aiongg d6b1265dc4 Merge integration track text 2026-10-31 03:38:55 +00:00
aiongg 1f9aff148f Merge PR #1866: perf: Warp Stabilizer works out the corrections of a clip once per set of settings 2026-10-31 02:16:04 +00:00
aiongg 99663bf5fb Merge PR #1957: render: Transform with its own shutter angle stays on the CPU (the GPU path dropped its motion blur) 2026-10-31 02:15:00 +00:00
aiongg d9a147f0a6 Merge PR #3695: Color: add a Three-Way Color Corrector 2026-10-31 02:14:52 +00:00
aiongg 42801a3df7 Merge PR #2699: Lumetri: Luma vs Sat and Sat vs Sat stop wrapping; hue curves meet exactly at red 2026-10-31 02:13:56 +00:00
aiongg 4725b076b2 Merge PR #2153: #2094: the Lumetri shadow lift no longer amplifies near-black chroma into blue specks 2026-10-31 02:12:59 +00:00
aiongg a3f66a1e47 Merge PR #2660: #2419: Bokeh Blur runs on the GPU 2026-10-31 02:12:50 +00:00
aiongg 353c55e4a3 Merge PR #1865: #1381: the Warp Stabilizer analyses a clip a block of frames at a time 2026-10-31 02:11:32 +00:00
aiongg c95672cda1 Merge PR #2505: Tests: render video in export fixtures and cover nested transforms 2026-10-31 02:10:20 +00:00
marshiyar edd37cab02 render: document and pin Responsive Design - Time scope (#3361)
Responsive Design - Time remaps only a graphic clip's layer keyframes;
the clip's own effects and masks are read at plain clip time. Applying
the remap to them would move keyframes in existing projects, so keep the
behaviour, state it in docs/graphics.md and pin it with a test
(layer keyframe remapped, clip Opacity keyframe not).

Fixes #3361
2026-10-30 14:53:15 -07:00
marshiyar b8fa438e5f render: add a Three-Way Color Corrector effect (#3465)
New Three-Way Color Corrector (Obsolete folder, like Color Balance): a
hue/balance wheel and a level shift for shadows, midtones and
highlights, tonal range thresholds with softness, and Saturation. All
parameters go through the generic param path (keyframable, undoable).
CPU (FxOp::ThreeWay) and GPU (OP_THREE_WAY) share the arithmetic; the
wheel is converted to an RGB offset on the CPU so the shader stays
simple. Defaults are the identity.

Tested: three_way_color_corrector_works_per_tonal_range (identity, each
wheel affects only its range, saturation 0 is grey) and two GPU/CPU
parity cases in fx_tests.

Fixes #3465
2026-10-30 06:11:49 -07:00
f6e329bb6c feat(text): strikethrough for graphic text (#3433)
* feat(text): strikethrough for graphic text

Text layers get a Strikethrough toggle next to Underline: per layer
(`strikethrough` parameter, Properties ▸ Text ▸ Style "S") and per
character (`graphics.setCharStyle` style `strikethrough`, stored in the
layer's style runs).

The stroke uses the font's OS/2 strikeout position and size; a font that
leaves them out gets a stroke centred on half the x-height. Strokes are
added to the layout's decoration rectangles next to underlines, so every
renderer and the path export draw them without further changes. Vertical
text gets a stroke down the middle of each cell.

generated with Claude Code

* contributors: add alexanderalber

generated with Claude Code

* graphics properties: draw the letter-button marks in a helper

Keeps the Properties scroll closure under the cognitive-complexity limit with the new
Strikethrough button.

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

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 01:45:25 -04:00
NetterSilverandClaude Opus 5.5 e009c0352b render: a monitor frame that is one opaque video picture goes straight to sRGB bytes (#3608)
On the CPU compositor every monitor frame was converted from Y'CbCr to a
premultiplied linear f32 image (16 bytes per pixel) and from that to sRGB
bytes, even when the frame is a single opaque clip shown as it is. The canvas
now keeps such a frame unconverted until something needs its pixels (another
layer, a transition, an adjustment layer, a display transform, captions).
render_sequence_rgba8, which the monitor's CPU path uses, converts a frame
that stays that picture alone directly to the bytes the float path gives,
through per-code-pair tables for R and B and one table for G, at full size and
decimated. Tests compare it bit for bit with the float path for every matrix,
range, chroma layout and decimation, and through the compositor's shortcut
scenes.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 01:42:14 -04:00
NetterSilverandClaude Opus 5.5 60d18a8174 render: convert the CPU monitor frame to sRGB bytes in vectorised batches (#3607)
over_black_rgba8 and over_black_with_alpha_rgba8 computed each value with a
table fetch, a clamp, a saturating float-to-usize cast and, for the alpha, a
libm round call, which kept the loop scalar. The table indices of a batch of
64 pixels are now computed first in a loop that vectorises and looked up
after, and the alpha is rounded without the library call. The bytes are the
same; a test checks every table and rounding boundary and the floats around
them.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 01:42:09 -04:00
Dylan M. Taylor ee985b208a Refactor render for strict Clippy checks 2026-10-28 04:36:48 +00:00
NetterSilverandClaude Opus 5.5 ef4a5f83f4 Take colour conversion off the H.264 export's critical path
- The H.264 encoder's RGBA -> 4:2:0 conversion runs on the render
  threads (VideoEncoder::prepare_ahead), which are idle while a batch is
  encoded; encode() picks the planes up by picture index and converts a
  picture itself when there are none (then on the encoder's own pool,
  not the busy global one).
- rgba_to_yuv420_8 uses per-code tables of the same products, rounds
  without roundf and works row by row so the divisions vectorise; the
  output is pinned against the float formula for all 2^24 colours.
- Full-size opaque 8-bit Y'CbCr -> linear conversion tabulates R' and
  B' per code pair with the expressions of the per-pixel loop.
- over_black_rgba8 fetches the sRGB table once per picture.

bench_export (1080p H.264 + AAC to H.264 + AAC, Ryzen 16 threads, 7
interleaved rounds) with the encoder change: median 7.86 s -> 6.14 s
(-21.9 %), min 7.78 s -> 6.02 s (-22.7 %). Same file (MD5) as
before, and the same for a ProRes export of the clip.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 17:42:52 +01:00
aiongg 7129d59aee refactor(render): move graphic_clip.rs tests to their own file 2026-10-26 18:20:21 +00:00
aiongg 3155acdb4c refactor(render): convert mod.rs files to <module>.rs 2026-10-26 18:15:17 +00:00
Dylan M. Taylor 74f2aca772 security(media): bound generator frames, imported audio dimensions, and render images (#3080)
* security(media): bound generator frames, imported audio dimensions, and render images

* fix(media): unwrap render result in demo_frames example

* fix(render, codecs): guard zero-sized images, check post-scale sequence pixels, and bound combined MXF channels

* fix(codecs): annotate channels as u32 in MXF sound track combination

* fix(render): preserve exact output_size scaling calculation for renderable_sequence validation

* fix(media,render): reject unsupported audio dimensions and invalid render scale

Sample rates above MAX_AUDIO_SAMPLE_RATE were clamped without resampling, so a
768 kHz stream was reported as 384 kHz and played at half speed. Reject them
instead, and drop the stream at the four codec call sites rather than
advertising dimensions the samples do not match.

output_size now treats a non-finite or non-positive scale as 1.0 instead of
casting it to zero and collapsing the frame to 1x1.

* fix(media): keep Matroska audio tracks and their metadata together; guard empty images

A track whose advertised sample rate or channel count is refused is dropped before the track list is built, so atracks[k] and audio_streams[k] stay the same track. over_black_with_alpha_rgba8 and Image::transformed return early for an empty image instead of chunking by zero.
2026-10-25 16:09:17 -04:00
skylar taylor-barrickandDylan M. Taylor 320cae243d Migrate FilmCraft to egui 0.37 with current editing changes preserved (#3249)
* egui 0.37: migrate FilmCraft input and rendering integration

* egui-037: fix capture failures, UTF-8 input and hidden timing

* Fix screenshot crop scale for fitted native surfaces

* egui037: simplify UI helpers and preserve pointer IME cancellation

* Translate graphics and effect control accessibility labels

* egui037: Document capture and IME lifecycle contracts

* egui-037: document UI and input integration contracts

---------

Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
2026-10-25 14:45:04 -04:00
NetterSilverandClaude Opus 5.5 19d0a78b38 Fit typewriter transition grid and caret to the layer's content (#3229)
Fixes #3086

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 09:42:31 -04:00
dotCooCoo 34f882992e Monitors: Transparency Grid shows the picture's transparent areas
The monitors flattened every frame over black before showing it, so the
transparent areas of a title, graphic or clip with an alpha channel
showed as black, and Display Mode > Alpha showed white everywhere. The
frame cache now keeps each pixel's alpha, with the same color bytes as
before, and texture_for still uploads frames opaque.

Transparency Grid, in View and in each monitor's wrench menu, paints a
white and #cccccc checkerboard of 8 pt squares behind the picture and
uploads the frame with its alpha, so the grid shows through. It is off
by default, and each monitor keeps its own setting. A magnified picture
paints only the squares in view, and in Comparison View the reference
frame gets the grid too. The GPU compositor's output and the render
previews are opaque, so while the grid is on the Program monitor and
the reference frame render the sequence live on the CPU
(Target::SequenceLive). Display Mode > Alpha now shows the frame's
alpha, and the Program monitor renders the sequence live for it too,
also in ranges that have a render preview.

The command is view.transparencyGrid; like the other view toggles it
takes {"enabled": bool} and {"monitor": "program"|"source"}, and the
setting is transparency_grid in ui.inspect and ui.set.
2026-10-24 16:15:04 -07:00
Earl Co a70cac4c23 security(render): bound embedded LUT resolution (#2901) 2026-10-24 04:50:04 -04:00
Earl Co b4792ecc06 security(render): bound burn-in text rasters (#2943) 2026-10-24 04:49:19 -04:00
aiongg 0a881266c8 CI: xtask ci in the dev profile, one test binary per crate, nextest (#2860)
* Tests: one integration-test binary per crate

Each crate with several tests/*.rs files keeps them as modules of tests/it/,
so cargo links one test executable per crate instead of one per file. Files
that change process-wide state or time a workload stay binaries of their own.

* Profiles: line tables for workspace crates, no debug info for dependencies

* Windows ARM64: nightly and on release pushes instead of every push to main

* xtask ci: clippy and tests in the dev profile

The release profile's thin LTO and single codegen unit made linking the test
binaries take most of the gate. The codec and audio crates stay at opt-level 3
in the dev profile, so their tests run at release speed.

* xtask ci: nextest, doctests, no examples, one wasm check

Tests run with cargo nextest when it is installed (cargo test otherwise) and
build with --lib --bins --tests, so examples aren't linked; doctests run
separately. The wasm check keeps going past the first failing crate. VP8
fixture generation locks across processes, since nextest runs each test in
its own.

* VP9 synthetic fixtures: write aside and rename

A test in another process could read a fixture ffmpeg was still writing.

* Fuzz tests: a third of each sweep by default, all of it with FILMCRAFT_FUZZ_ITERS

Every seed, mutation kind and thread count still runs. Header bytes each get
one overwrite value and two bit flips in rotation, so every value and bit
index still occurs across the region. FILMCRAFT_FUZZ_ITERS=N runs the whole
sweep plus N random rounds per seed, as before.

* Platform fallback tests: one failure point per hybrid path

The failure-anywhere test injected a failure at four points around every
restart point; the hybrid's paths differ only in the first GOP, around the
second and last restart points, at the end and never. The replay test needs
the first two GOPs.

* Export tests: shorter ranges with the same checks

The bitrate test measures 0.5–1.5 s, where each preset reaches its own
target (on 0–3 s the simple demo left two presets at the same rate). The
every-preset test exports three frames per preset.

* Audio tests: effects in parallel, a table-driven FFT for the Opus quality metric

The extreme-settings test keeps both settings and the 40 s tail for every
effect, one thread each. The Opus quality metric's FFT builds its twiddles
once and allocates nothing per transform (same algorithm and summation order).

* CI: dev-profile gate with nextest

xtask ci no longer builds with --release, so the release-profile overrides
go; nextest is installed for the test step.

* CI: apt packages through the cached install-apt-packages action

* Review fixes: fuzz tests with a panic hook in their own binaries, loud fixture lock

The fuzz tests of av1, h264, hevc, mpeg2v, vp8 and vp9 count panics through
a process-wide hook, so a panic in another test of the same process would
fail them; they are binaries of their own again. The VP8 fixture lock fails
the test instead of skipping it silently, and the full-sweep doc comments say
how many truncation points the full run samples.

* Fuzz tests: rotate the default subset per run (FILMCRAFT_FUZZ_SEED replays one)

* platform tests: decode-switch tests in their own binaries, one NVENC lock
2026-10-23 13:34:16 -04:00
875eec90e9 security(render): bound expanded frame plans (#2942)
* security(render): bound expanded frame plans

* render: count plan layers as they are pushed instead of re-summing every step

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

---------

Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 10:53:24 -04:00
Alexander Alber 8ce8c0d3bd M8: Luma vs Sat and Sat vs Sat stop wrapping; hue curves meet exactly at red
Luma vs Sat and Sat vs Sat went through the periodic hue-curve LUT, so a
point in the highlights reached around into the shadows: with points
(0.5, 0.5) and (0.9, 0) black got 0.04 instead of the neutral 0.5, i.e.
a highlight desaturation also desaturated the darkest pixels by about 93%.
They now use a plain (non-wrapping) curve, flat beyond the outermost points,
in the render and in the curve editor.

The hue curves were already periodic, but the LUT slice was off by a third
of a sample: entry i held hue (3i+1)/(3n-1) instead of i/(n-1), leaving a
0.23 degree gap at red. With a steep curve through red that was a step of
0.02 between hue 359.99 and 0. Sampling 3n-2 entries puts the slice on the
exact grid, so the first and last entries are both red and equal.

generated with Claude Code
2026-10-21 20:16:44 +02:00
dedandClaude Opus 5.5 034746a407 #2419: Bokeh Blur runs on the GPU
Bokeh Blur had no GPU op, so a clip carrying it left the GPU compositor's
layer path and every frame with it composited on the CPU: up to 625
bilinear taps per pixel, about 3.3 s per 1080p frame at a 12 px amount.

- render: Bokeh Blur is an FxOp (parameters evaluated in FxOp::eval, the
  CPU reference in FxOp::apply, the id in GPU_EFFECTS). The CPU result is
  unchanged; a non-finite amount is now a no-op instead of NaN.
- gpu: four passes on the top-left part of the working textures: each
  halving (Image::downsample2), the highlight boost, the iris gather
  (Image::sample_bilinear_clamped within the halved image) and the scale
  back up (Image::transformed's bilinear).

1080p, RTX 4070 Laptop vs 16 CPU threads: amount 6 947 ms -> ~9 ms,
12 3250 ms -> ~39 ms, 20 617 ms -> ~6 ms, 60 (default) 62 ms -> <1 ms.

Tests: six parity cases (no-op, every halving depth, shapes, rotation,
highlight boost) match the CPU to < 1e-6; hostile rotation / highlight stay
on the CPU, huge and NaN amounts run bounded on the GPU.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 13:08:55 +03:00
jackcanon ebbd845a90 Render visible video in export fixtures and cover nested transforms 2026-10-19 10:08:30 -07:00
00xJ0 4ff5c22f78 #2094: the lift is held only below a luma floor of 0.02; lit pixels keep the plain hue-preserving scale 2026-10-18 20:55:04 -07:00
Brandon Thomas 310f11d9b0 Merge pull request #1925 from marcoarnulfo/fix/monitor-zoom-3d-perspective
Basic 3D: Distance to Image keeps its perspective at every render scale
2026-10-18 03:20:56 -04:00
Brandon Thomas 26591777a0 Merge pull request #1907 from Guilhermeb-Ferrarezi/perf/lens-blur-disc-kernel-1871
#1871: Lens Blur works out its disc offsets once per frame, not per pixel
2026-10-18 03:20:22 -04:00
00xJ0 41da1aaf5c #2094: the Lumetri shadow lift no longer amplifies near-black chroma into blue specks 2026-10-15 20:38:04 -07:00
schnierkandClaude Opus 5.5 6184a64877 render: Transform with its own shutter angle stays on the CPU
With Use Effect Shutter Angle on and an angle above 0.5 the CPU blurs the
motion over seven samples (vfx::distort::transform_fx), but the GPU chain
accepted the effect by id and FxOp::eval placed one picture, so the
Program monitor and GPU exports silently lost the blur. eval now declines
under the same condition and the layer renders on the CPU.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 22:17:09 +02:00
Marco Arnulfo ca35e1bb9d Basic 3D: Distance to Image keeps its perspective at every render scale (fixes #1920) 2026-10-14 20:20:58 +02:00
Guilherme Barbosa Ferrarezi 98f257f56e #1871: Lens Blur works out its disc offsets once per frame, not per pixel
The Lens Blur transition blurred both pictures with a disc of golden-angle
samples and computed every sample's offset (a square root, a sine and a cosine)
again for every pixel, though the offsets depend only on the radius and the
sample count. They are now worked out once per frame (`disc_offsets`) and the
pixels come out bit for bit the same. A 1080p picture at the default radius
blurs in 50 ms instead of 95 ms (1.9x), and a Lens Blur frame blurs two.
2026-10-14 14:25:19 -03:00
Marco Arnulfo 16b458c24c Graphics: an outer stroke with Fill off draws only the ring outside the edge (fixes #1876) 2026-10-14 16:21:27 +02:00
Guilherme Barbosa Ferrarezi af9f8bd686 Warp Stabilizer works out the corrections of a clip once per set of settings
The effect smoothed the whole analysed path and scanned it for the worst border
again for every frame it rendered, so the cost of a frame grew with the length
of the clip: 0.8 ms for a 30 s clip, 6.8 ms for 2 minutes and 14.8 ms for 10
minutes, on every frame of playback and export.

The corrections, and the worst border per zoom limit, are now kept in a small
bounded cache keyed by the analysed path and the Smoothness, Result and Preserve
Scale settings. The numbers are the ones `corrections` and `cover_scale` gave
before.
2026-10-14 08:41:15 -03:00
Guilherme Barbosa Ferrarezi d016a382e1 #1381: the Warp Stabilizer analyses a clip a block of frames at a time
The analysis decoded every frame of the clip, built the tracking pyramid of each
one, and kept all of them until the steps between frames were known. Memory grew
with the length of the clip: a 10-minute clip at 30 fps is 18,000 frames, and
at the Detailed analysis size (960 px wide, about 2.8 MB per frame) that is tens
of gigabytes, which ends the process.

The frames are now decoded, prepared and tracked 64 at a time and dropped once
the steps they take part in are known; only the last frame of a block stays for
the next one. The steps, the analysis size and the handling of unreadable or
resized frames are the same as before, whatever the block size, and cancelling
still stops the analysis between blocks.
2026-10-14 08:28:43 -03:00
Brandon ThomasandClaude Opus 5.5 0cfc02a933 Merge origin/main into #1527: the Normal-blend replace moves into adjustment_layer (after #1327)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 00:49:14 -04:00
Brandon ThomasandClaude Opus 5.5 1990835de9 #1287: frame_rate in the Sharpen preview-resolution test's FxCtx (after #1361)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 00:26:53 -04:00