Effects: padded buffers stay within a pixel budget (#2261)

Effects that grow their buffer (CC Radial Fast Blur, CC Lens, Magnify,
the distorts and blurs) could pad a 20 x 20 layer by 4096 px a side, a
~8200 x 8200 f32 buffer plus its clone. Buf::pad (CPU and GPU) now cuts
the pad so the grown buffer stays within 16 Mpx or four times its own
area, whichever is larger: small layers are bounded at 256 MiB, big
layers still grow as before in practice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bflatastic
2026-10-30 04:32:17 +07:00
co-authored by Claude Opus 5.5
parent f868cc68f3
commit fb2fb169dc
2 changed files with 35 additions and 1 deletions
+34 -1
View File
@@ -312,8 +312,9 @@ impl Buf {
self.offset[0] += l as f64;
self.offset[1] += t as f64;
}
/// Grow by `pad` transparent pixels on every side.
/// Grow by `pad` transparent pixels on every side, within [`pad_within_budget`].
pub fn pad(&mut self, pad: u32) {
let pad = pad_within_budget(self.img.width, self.img.height, pad);
if pad == 0 {
return;
}
@@ -323,6 +324,21 @@ impl Buf {
}
}
/// `pad` on every side of a `w` × `h` effect buffer, cut so the padded buffer stays within
/// 16 Mpx (256 MiB of f32 RGBA) or four times its own area, whichever is larger: a small layer
/// far from an effect's centre can't ask for gigabytes (#2261). Shared with the GPU path.
pub fn pad_within_budget(w: u32, h: u32, pad: u32) -> u32 {
const MIN_BUDGET: f64 = 16_777_216.0;
let (w, h) = (f64::from(w), f64::from(h));
let budget = MIN_BUDGET.max(4.0 * w * h);
if (w + 2.0 * f64::from(pad)) * (h + 2.0 * f64::from(pad)) <= budget {
return pad;
}
// (w + 2p)(h + 2p) = budget, solved for p.
let p = (((w - h).powi(2) + 4.0 * budget).sqrt() - (w + h)) / 4.0;
(p.floor().max(0.0) as u32).min(pad)
}
/// Motion Tile's output frame for a buffer with layer origin `offset` (pixels) at `scale`:
/// `(width, height, offset)`. Output Width / Height grow (or crop) the layer rectangle about its
/// centre, on adjustment layers too, where the rectangle is the comp's and starts at the comp
@@ -1344,6 +1360,23 @@ pub(crate) fn run_fx(id: &str, vals: &[(&str, Value)], img: Image, time: f64, en
mod tests {
use super::*;
/// #2261: a 20 × 20 layer with CC Radial Fast Blur's Center far away grows to at most 16 Mpx;
/// big layers may still grow to four times their area, and small pads are untouched.
#[test]
fn padded_buffers_stay_within_the_budget() {
let area = |w: u32, h: u32, p: u32| (u64::from(w) + 2 * u64::from(p)) * (u64::from(h) + 2 * u64::from(p));
let far = radial_fast_pad((6500.0, 6500.0), 20, 20, 0.8);
assert_eq!(far, 4096, "the effect asks for its full pad");
let mut b = Buf { img: Image::new(20, 20), offset: [0.0; 2], scale: 1.0 };
b.pad(far);
assert!(area(20, 20, 0) < area(b.img.width, b.img.height, 0) && area(b.img.width, b.img.height, 0) <= 1 << 24, "{}x{}", b.img.width, b.img.height);
assert_eq!(b.offset, [f64::from((b.img.width - 20) / 2); 2], "the content keeps its place");
let p = pad_within_budget(3840, 2160, 4096);
assert!(p > 1000 && area(3840, 2160, p) <= 4 * 3840 * 2160 && area(3840, 2160, p + 1) > 4 * 3840 * 2160);
assert_eq!(pad_within_budget(640, 360, 200), 200);
assert_eq!(pad_within_budget(20, 20, 0), 0);
}
#[test]
fn run_fx_passthrough() {
let img = Image::filled(4, 3, [0.5, 0.25, 0.125, 1.0]);