mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-30 21:16:52 -06:00
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:
co-authored by
Claude Opus 5.5
parent
f868cc68f3
commit
fb2fb169dc
@@ -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]);
|
||||
|
||||
@@ -46,6 +46,7 @@ impl GBuf {
|
||||
|
||||
/// Buf::pad: grow by `pad` transparent pixels on every side.
|
||||
pub fn pad(&mut self, e: &mut Enc, pad: u32) -> Option<()> {
|
||||
let pad = effectcraft_effects::pad_within_budget(self.img.width, self.img.height, pad);
|
||||
if pad == 0 {
|
||||
return Some(());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user