collective

Browse / github.com/storytold/effectcraft / patch

av1enc: reuse scratch buffers in the transform path, the coefficient writer and the level search

av1enc: per-block heap allocations and a plane clone in the loop-filter search (-2..3 %) ## Problem Heap allocation in per-block paths: - `decide.rs` `try_tx` / `reconstruct` allocate six Vecs per candidate (res, coef, levels, deq, resid, recon). - `writer.rs:504` allocates `quant` per coded transform block; `writer.rs:184` has a `to_vec` per edge block. - The loop-filter level search (`loopfilter.rs`, about 1.7 % of the time in total) clones the whole luma plane for every candidate level (about 1 %). calloc + free + malloc are about 1.5 % and memset about 2 % of the encode (libc). ## Proposal Scratch buffers owned by the `Decider` and the `TileWriter` (reused Vec fields or fixed

Status
open
Score
0.00
Adopters
0 other operators
Author
vangel76 (imported from pull request #2405, open)
Base
991904888a20b7da72b2a4880b21c7d912154736
Size
+113 −50 in 3 files
Review
pending
Shared
2026-10-28
Id
336f4bedb9dcee600d3d6b82

How it went

No other operator has reported using it yet.

Diff

raw · crates/av1enc/src/decide.rs crates/av1enc/src/lib.rs crates/av1enc/src/writer.rs

diff --git a/crates/av1enc/src/decide.rs b/crates/av1enc/src/decide.rs--- a/crates/av1enc/src/decide.rs+++ b/crates/av1enc/src/decide.rs@@ -107,6 +107,31 @@ struct TxResult {     bits: f64, } +/// Outcome of trying one transform block; the levels and the reconstruction stay in `TxScratch`.+#[derive(Clone, Copy)]+struct TxEval {+    nonzero: bool,+    dist: f64,+    bits: f64,+}++/// Buffers of the transform path, sized for the largest coded transform block (32x32). Only the+/// first `n * n` entries are used; they are reused across candidates instead of being allocated.+struct TxScratch {+    res: [i32; 1024],+    coef: [f32; 1024],+    levels: [i32; 1024],+    deq: [i32; 1024],+    resid: [i32; 1024],+    recon: [u16; 1024],+}++impl Default for TxScratch {+    fn default() -> Self {+        TxScratch { res: [0; 1024], coef: [0.0; 1024], levels: [0; 1024], deq: [0; 1024], resid: [0; 1024], recon: [0; 1024] }+    }+}+ /// Saved encoder state of a block area (reconstruction, BlockDecoded flags). struct Snapshot {     planes: [Vec<u16>; 3],@@ -131,6 +156,8 @@ pub(crate) struct Decider<'a> {     lambda_sad: f64,     /// Half-sample interpolations of the reference luma (visible area): (8, 0), (0, 8), (8, 8).     half: [Vec<u16>; 3],+    /// Transform buffers, taken out while a block is being tried (see `code_tx`).+    scratch: Option<Box<TxScratch>>, }  const MODE_BITS_INTRA: f64 = 3.0;@@ -166,6 +193,7 @@ impl<'a> Decider<'a> {                 Some(rf) => [(8, 0), (0, 8), (8, 8)].map(|(fx, fy)| subpel_plane(&rf.planes[0], g.width, g.height, fx, fy, bd)),                 None => Default::default(),             },+            scratch: None,         }     } @@ -324,8 +352,10 @@ impl<'a> Decider<'a> {         let log2 = (n4 * 4).trailing_zeros();         let (sbr, sbc) = ((r & 15) as isize, (c & 15) as isize);         let bd = self.bd;-        let mut pred = vec![0u16; 32 * 32];-        let mut best_pred = vec![0u16; 32 * 32];+        let mut pred = [0u16; 32 * 32];+        let mut best_pred = [0u16; 32 * 32];+        let mut best_levels = [0i32; 32 * 32];+        let mut best_recon = [0u16; 32 * 32];         let n = 1usize << log2;         // luma         let e = IntraEdge {@@ -343,26 +373,26 @@ impl<'a> Decider<'a> {         let ed = edges(&self.rec[0], &e);         // modes ranked by SATD, then a rate-distortion choice of mode and transform type among         // the best three-        let mut ranked: Vec<(f64, usize)> = INTRA_MODES_USED-            .iter()-            .map(|&mode| {-                predict_intra(&ed, &e, mode, &mut pred);-                let sad = self.satd(0, c * 4, r * 4, n, &pred[..n * n]) as f64;-                (sad + self.lambda_sad * if mode == DC_PRED { 1.0 } else { MODE_BITS_INTRA }, mode)-            })-            .collect();+        let mut ranked: [(f64, usize); INTRA_MODES_USED.len()] = INTRA_MODES_USED.map(|mode| {+            predict_intra(&ed, &e, mode, &mut pred);+            let sad = self.satd(0, c * 4, r * 4, n, &pred[..n * n]) as f64;+            (sad + self.lambda_sad * if mode == DC_PRED { 1.0 } else { MODE_BITS_INTRA }, mode)+        });         ranked.sort_by(|a, b| a.0.total_cmp(&b.0));         let tx_y = tx_size_for(log2);-        let mut best: Option<(f64, usize, usize, TxResult, Option<Vec<u16>>)> = None;+        let mut best: Option<(f64, usize, usize, TxEval, bool)> = None;+        let mut sc = self.scratch.take().unwrap_or_default();         for &(_, mode) in ranked.iter().take(3) {             predict_intra(&ed, &e, mode, &mut pred);-            let mut types = vec![DCT_DCT];+            let mut types = [DCT_DCT; 2];+            let mut n_types = 1;             let alt = MODE_TO_TXFM[mode] as usize;             if alt != DCT_DCT && tx_in_set(tx_set(tx_y, false), false, alt) {-                types.push(alt);+                types[1] = alt;+                n_types = 2;             }-            for &t in &types {-                let (res, recon) = self.try_tx(0, c * 4, r * 4, log2, t, &pred[..n * n], true);+            for &t in &types[..n_types] {+                let (res, has_recon) = self.try_tx(&mut sc, 0, c * 4, r * 4, log2, t, &pred[..n * n], true);                 let type_bits = if !res.nonzero || tx_set(tx_y, false) == TX_SET_DCTONLY {                     0.0                 } else if t == DCT_DCT {@@ -374,18 +404,24 @@ impl<'a> Decider<'a> {                 let cost = res.dist + self.lambda * (res.bits + type_bits + mode_bits);                 if best.as_ref().is_none_or(|b| cost < b.0) {                     best_pred[..n * n].copy_from_slice(&pred[..n * n]);-                    best = Some((cost, mode, t, res, recon));+                    best_levels[..n * n].copy_from_slice(&sc.levels[..n * n]);+                    if has_recon {+                        best_recon[..n * n].copy_from_slice(&sc.recon[..n * n]);+                    }+                    best = Some((cost, mode, t, res, has_recon));                 }             }         }+        self.scratch = Some(sc);         // Every ranked mode tries DCT_DCT, so `best` is always set; without a candidate the block         // can't be coded intra (an infinite cost keeps it from being chosen).-        let Some((_, y_mode, tx_type_y, ty, recon)) = best else {+        let Some((_, y_mode, tx_type_y, ty, has_recon)) = best else {             let leaf =                 Leaf { bsize, is_inter: false, y_mode: DC_PRED, uv_mode: DC_PRED, tx_type_y: DCT_DCT, mv: [0, 0], skip: true, coefs: Default::default() };             return (leaf, f64::INFINITY);         };-        self.put(0, c * 4, r * 4, n, recon.as_deref().unwrap_or(&best_pred[..n * n]));+        self.put(0, c * 4, r * 4, n, if has_recon { &best_recon[..n * n] } else { &best_pred[..n * n] });+        let ty = TxResult { levels: best_levels[..n * n].to_vec(), nonzero: ty.nonzero, dist: ty.dist, bits: ty.bits };         let tx_type_y = if ty.nonzero { tx_type_y } else { DCT_DCT };         self.mark_decoded(0, r, c, n4);         // chroma (one mode for both planes)@@ -772,31 +808,35 @@ impl<'a> Decider<'a> {     /// Transform, quantise and reconstruct one square transform block of `plane` at (x, y)     /// over the prediction `pred`; the reconstruction is written to `rec`.     fn code_tx(&mut self, p: usize, x: usize, y: usize, log2: u32, tx_type: usize, pred: &[u16], intra: bool) -> TxResult {-        let (res, recon) = self.try_tx(p, x, y, log2, tx_type, pred, intra);+        let mut sc = self.scratch.take().unwrap_or_default();+        let (res, has_recon) = self.try_tx(&mut sc, p, x, y, log2, tx_type, pred, intra);         let n = 1usize << log2;-        self.put(p, x, y, n, recon.as_deref().unwrap_or(pred));-        res+        self.put(p, x, y, n, if has_recon { &sc.recon[..n * n] } else { pred });+        let levels = sc.levels[..n * n].to_vec();+        self.scratch = Some(sc);+        TxResult { levels, nonzero: res.nonzero, dist: res.dist, bits: res.bits }     } -    /// Transform and quantise one block; returns the result and its reconstruction (None: the-    /// prediction) without writing it.-    fn try_tx(&self, p: usize, x: usize, y: usize, log2: u32, tx_type: usize, pred: &[u16], intra: bool) -> (TxResult, Option<Vec<u16>>) {+    /// Transform and quantise one block without writing it. The levels are left in `sc.levels`;+    /// the flag tells whether `sc.recon` holds the reconstruction (false: it is the prediction).+    fn try_tx(&self, sc: &mut TxScratch, p: usize, x: usize, y: usize, log2: u32, tx_type: usize, pred: &[u16], intra: bool) -> (TxEval, bool) {         let n = 1usize << log2;+        let nn = n * n;         let tx_sz = tx_size_for(log2);         let bd = self.bd;         let src = &self.src[p];-        let mut res = vec![0i32; n * n];+        let TxScratch { res, coef, levels, deq, resid, recon } = sc;+        let (res, coef, levels) = (&mut res[..nn], &mut coef[..nn], &mut levels[..nn]);         for i in 0..n {             let row = &src.row(y + i)[x..x + n];             for j in 0..n {                 res[i * n + j] = row[j] as i32 - pred[i * n + j] as i32;             }         }-        let mut coef = vec![0f32; n * n];-        forward_2d(&res, &mut coef, n, tx_type);+        forward_2d(res, coef, n, tx_type);         // dead-zone quantiser: rounding offset below one half         let rnd = if intra { 0.38f32 } else { 0.30f32 };-        let mut levels = vec![0i32; n * n];+        levels.fill(0);         let mut nonzero = false;         for (k, (&cf, l)) in coef.iter().zip(levels.iter_mut()).enumerate() {             let q = if k == 0 { self.dc_q } else { self.ac_q };@@ -808,23 +848,36 @@ impl<'a> Decider<'a> {         }         let zero_dist = self.sse(p, x, y, n, pred);         if !nonzero {-            return (TxResult { levels, nonzero, dist: zero_dist, bits: 1.0 }, None);+            return (TxEval { nonzero, dist: zero_dist, bits: 1.0 }, false);         }-        let recon = self.reconstruct(&levels, tx_sz, tx_type, pred, n, bd);-        let dist = self.sse(p, x, y, n, &recon);-        let bits = estimate_bits(&levels, tx_sz);+        let recon = &mut recon[..nn];+        self.reconstruct(levels, &mut deq[..nn], &mut resid[..nn], recon, tx_sz, tx_type, pred, n, bd);+        let dist = self.sse(p, x, y, n, recon);+        let bits = estimate_bits(levels, tx_sz);         if dist + self.lambda * bits >= zero_dist + self.lambda {-            levels.iter_mut().for_each(|l| *l = 0);-            return (TxResult { levels, nonzero: false, dist: zero_dist, bits: 1.0 }, None);-        }-        (TxResult { levels, nonzero: true, dist, bits }, Some(recon))-    }--    /// Dequantisation (7.12.3) and the inverse transform, added to the prediction.-    fn reconstruct(&self, levels: &[i32], tx_sz: usize, tx_type: usize, pred: &[u16], n: usize, bd: u32) -> Vec<u16> {+            levels.fill(0);+            return (TxEval { nonzero: false, dist: zero_dist, bits: 1.0 }, false);+        }+        (TxEval { nonzero: true, dist, bits }, true)+    }++    /// Dequantisation (7.12.3) and the inverse transform, added to the prediction; the result is+    /// written to `recon`. `deq` and `resid` are scratch buffers of `n * n` entries.+    fn reconstruct(+        &self,+        levels: &[i32],+        deq: &mut [i32],+        resid: &mut [i32],+        recon: &mut [u16],+        tx_sz: usize,+        tx_type: usize,+        pred: &[u16],+        n: usize,+        bd: u32,+    ) {         let denom: i64 = if tx_sz == TX_32X32 { 2 } else { 1 };         let lim = 1i64 << (7 + bd);-        let mut deq = vec![0i32; n * n];+        deq.fill(0);         for (k, (&l, d)) in levels.iter().zip(deq.iter_mut()).enumerate() {             if l == 0 {                 continue;@@ -834,10 +887,12 @@ impl<'a> Decider<'a> {             let mag = (dq.abs() & 0xFF_FFFF) / denom;             *d = (if dq < 0 { -mag } else { mag }).clamp(-lim, lim - 1) as i32;         }-        let mut resid = vec![0i32; n * n];-        inverse_2d(&deq, &mut resid, tx_sz, tx_type, bd);+        inverse_2d(deq, resid, tx_sz, tx_type, bd);         let max = (1i32 << bd) - 1;-        pred.iter().zip(&resid).map(|(&p, &r)| (p as i32 + r).clamp(0, max) as u16).collect()+        for ((o, &p), &r) in recon.iter_mut().zip(pred).zip(resid.iter()) {+            *o = (p as i32 + r).clamp(0, max) as u16;+        }+        debug_assert_eq!(recon.len(), n * n);     } } diff --git a/crates/av1enc/src/lib.rs b/crates/av1enc/src/lib.rs--- a/crates/av1enc/src/lib.rs+++ b/crates/av1enc/src/lib.rs@@ -409,11 +409,13 @@ impl Encoder {         };         let bd = self.cfg.bit_depth as u32;         let mut cache: Vec<(u32, u64)> = vec![(0, sse(&rec[0]))];+        // one working copy of the luma plane for all candidate levels+        let mut planes = [rec[0].clone(), Plane::new(0, 0), Plane::new(0, 0)];         let mut eval = |lvl: u32| -> u64 {             if let Some(&(_, e)) = cache.iter().find(|(l, _)| *l == lvl) {                 return e;             }-            let mut planes = [rec[0].clone(), Plane::new(0, 0), Plane::new(0, 0)];+            planes[0].data.copy_from_slice(&rec[0].data);             loop_filter(&mut planes, info, [lvl; 4], bd, true, sb_row_step);             let e = sse(&planes[0]);             cache.push((lvl, e));diff --git a/crates/av1enc/src/writer.rs b/crates/av1enc/src/writer.rs--- a/crates/av1enc/src/writer.rs+++ b/crates/av1enc/src/writer.rs@@ -62,6 +62,8 @@ pub(crate) struct TileWriter<'a> {     left_dc: [Vec<u8>; 3],     /// LoopfilterTxSizes per plane (row stride mi_cols + 32).     lf_tx: [Vec<u8>; 3],+    /// Scratch for the levels coded so far in `coeffs`.+    quant: Vec<i32>,     // current block     r: usize,     c: usize,@@ -114,6 +116,7 @@ impl<'a> TileWriter<'a> {             left_level: std::array::from_fn(|_| vec![0; g.mi_rows + 64]),             left_dc: std::array::from_fn(|_| vec![0; g.mi_rows + 64]),             lf_tx: std::array::from_fn(|_| vec![0; (g.mi_cols + 32) * (g.mi_rows + 32)]),+            quant: Vec::new(),             r: 0,             c: 0,             bsize: 0,@@ -181,10 +184,10 @@ impl<'a> TileWriter<'a> {             self.sw.symbol(partition, cdf);         } else if has_cols || has_rows {             assert!(split, "blocks crossing the frame edge are split");-            let pcdf: Vec<u16> = match bsl {-                2 => self.cdf.partition_w16[ctx].to_vec(),-                3 => self.cdf.partition_w32[ctx].to_vec(),-                _ => self.cdf.partition_w64[ctx].to_vec(),+            let pcdf: &[u16] = match bsl {+                2 => &self.cdf.partition_w16[ctx],+                3 => &self.cdf.partition_w32[ctx],+                _ => &self.cdf.partition_w64[ctx],             };             let pr = |k: usize| pcdf[k] as i32 - if k == 0 { 0 } else { pcdf[k - 1] as i32 };             let psum = if has_cols {@@ -501,7 +504,9 @@ impl<'a> TileWriter<'a> {             let height = TX_HEIGHT[tx_sz] as usize;             let width = 1usize << bwl;             // Quant[] as the decoder sees it while parsing: the levels coded so far (capped)-            let mut quant = vec![0i32; width * height];+            let mut quant = std::mem::take(&mut self.quant);+            quant.clear();+            quant.resize(width * height, 0);             for c in (0..eob).rev() {                 let pos = scan[c] as usize;                 let a = levels[pos].unsigned_abs().min(15) as i32;@@ -526,6 +531,7 @@ impl<'a> TileWriter<'a> {                 }                 quant[pos] = a;             }+            self.quant = quant;             for c in 0..eob {                 let pos = scan[c] as usize;                 let v = levels[pos];