Skip to main content

flashraw_core/
par.rs

1//! Parallel execution helpers that degrade to sequential execution when the
2//! `rayon` feature is disabled (e.g. single-threaded WASM) or parallelism is off.
3
4/// Minimum number of samples before splitting work across threads pays off.
5pub const PAR_MIN_SAMPLES: usize = 1 << 18;
6
7/// Run `f(first_row, rows_chunk)` over `data` split into chunks of `rows_per_chunk`
8/// rows of `row_len` elements each.
9pub fn for_each_row_chunk<T, F>(data: &mut [T], row_len: usize, rows_per_chunk: usize, parallel: bool, f: F)
10where
11    T: Send,
12    F: Fn(usize, &mut [T]) + Sync + Send,
13{
14    if row_len == 0 || data.is_empty() {
15        return;
16    }
17    let chunk = row_len * rows_per_chunk.max(1);
18    #[cfg(feature = "rayon")]
19    if parallel && data.len() >= PAR_MIN_SAMPLES && threads() > 1 {
20        use rayon::prelude::*;
21        data.par_chunks_mut(chunk).enumerate().for_each(|(i, c)| f(i * rows_per_chunk.max(1), c));
22        return;
23    }
24    let _ = parallel;
25    for (i, c) in data.chunks_mut(chunk).enumerate() {
26        f(i * rows_per_chunk.max(1), c);
27    }
28}
29
30/// Workers worth using for memory-bound kernels (copies, byte swaps, packed unpack,
31/// lookup curves). Measured on a 16-thread x86 server: such loops peak at ~4 threads and
32/// get 1.7-1.9x slower at 16 from memory-controller and page-fault contention. Machines
33/// with more bandwidth per core scale further, so the cap grows with the thread count.
34pub fn memory_bound_workers() -> usize {
35    (threads() / 4).clamp(2, 8)
36}
37
38/// Like [`for_each_row_chunk`] but split into at most `max_workers` tasks (used for
39/// memory-bound kernels where more threads only add contention).
40pub fn for_each_row_chunk_bounded<T, F>(data: &mut [T], row_len: usize, max_workers: usize, parallel: bool, f: F)
41where
42    T: Send,
43    F: Fn(usize, &mut [T]) + Sync + Send,
44{
45    if row_len == 0 || data.is_empty() {
46        return;
47    }
48    let rows = data.len().div_ceil(row_len);
49    let workers = max_workers.clamp(1, threads().max(1));
50    for_each_row_chunk(data, row_len, rows.div_ceil(workers), parallel && workers > 1, f)
51}
52
53/// Map `f` over `items` in parallel (when enabled), preserving order.
54pub fn map<I, O, F>(items: Vec<I>, parallel: bool, f: F) -> Vec<O>
55where
56    I: Send,
57    O: Send,
58    F: Fn(I) -> O + Sync + Send,
59{
60    #[cfg(feature = "rayon")]
61    if parallel && items.len() > 1 && threads() > 1 {
62        use rayon::prelude::*;
63        return items.into_par_iter().map(f).collect();
64    }
65    let _ = parallel;
66    items.into_iter().map(f).collect()
67}
68
69/// Run `f(i)` for `i in 0..n` in parallel when enabled.
70pub fn for_each_index<F>(n: usize, parallel: bool, f: F)
71where
72    F: Fn(usize) + Sync + Send,
73{
74    #[cfg(feature = "rayon")]
75    if parallel && n > 1 && threads() > 1 {
76        use rayon::prelude::*;
77        (0..n).into_par_iter().for_each(f);
78        return;
79    }
80    let _ = parallel;
81    (0..n).for_each(f);
82}
83
84#[cfg(feature = "rayon")]
85std::thread_local! {
86    /// Set while [`sequential`] runs on this thread.
87    static SEQUENTIAL: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
88}
89
90/// Run `f` on the calling thread with every parallel helper sequential: inside `f`,
91/// [`threads`] returns 1 and no work is handed to a thread pool. This is how a
92/// single-threaded decode runs, so it stays on the caller's thread (and on the CPU core
93/// class the operating system picked for it) instead of moving to a pool worker.
94pub fn sequential<R>(f: impl FnOnce() -> R) -> R {
95    #[cfg(feature = "rayon")]
96    {
97        struct Restore(bool);
98        impl Drop for Restore {
99            fn drop(&mut self) {
100                SEQUENTIAL.with(|s| s.set(self.0));
101            }
102        }
103        let _restore = Restore(SEQUENTIAL.with(|s| s.replace(true)));
104        f()
105    }
106    #[cfg(not(feature = "rayon"))]
107    {
108        f()
109    }
110}
111
112/// Number of worker threads available to parallel helpers (1 inside [`sequential`]).
113pub fn threads() -> usize {
114    #[cfg(feature = "rayon")]
115    {
116        if SEQUENTIAL.with(|s| s.get()) { 1 } else { rayon::current_num_threads() }
117    }
118    #[cfg(not(feature = "rayon"))]
119    {
120        1
121    }
122}
123
124/// Geometry of a [`wavefront`] pass over a row-major buffer.
125#[derive(Clone, Copy, Debug)]
126pub struct Wavefront {
127    /// Samples per row.
128    pub row_len: usize,
129    /// Rows above a row that it reads (the band above must be at least this tall).
130    pub halo: usize,
131    /// Columns left of a block that its first samples read from rows above.
132    pub margin: usize,
133    /// Block width in samples (block boundaries are multiples of it).
134    pub block: usize,
135}
136
137/// Process a buffer whose sample `(r, x)` depends only on samples left of it in row `r`
138/// and on columns `<= x` of the `halo` rows above it. Rows are cut into bands (one per
139/// thread) and columns into blocks; block `j` of a band runs once block `j - 1` of the
140/// same band and block `j` of the band above are done, so the bands advance as a
141/// wavefront without waiting for each other row by row.
142///
143/// `f(first_row, band, above, lo, a, b)` must process columns `a..b` of every row of
144/// `band` (whole rows, `first_row` being the index of its first row in `data`) in row
145/// order. `above` holds the rows directly above the band (up to `halo`, oldest first),
146/// restricted to columns `lo..b` (`lo = a - margin`, clamped at 0); for the first band
147/// they come from `top` (which may hold fewer than `halo` rows, or none).
148#[allow(clippy::type_complexity)]
149pub fn wavefront<T, F>(data: &mut [T], top: &[T], geo: Wavefront, parallel: bool, f: F)
150where
151    T: Copy + Default + Send + Sync,
152    F: Fn(usize, &mut [T], &[T], usize, usize, usize) + Sync,
153{
154    use std::sync::Mutex;
155    use std::sync::atomic::{AtomicU8, Ordering};
156
157    let Wavefront { row_len: rl, halo, block, .. } = geo;
158    if rl == 0 || block == 0 || data.len() < rl {
159        return;
160    }
161    let rows = data.len() / rl;
162    let halo = halo.max(1);
163    let workers = if parallel { threads() } else { 1 };
164    let nbands = workers.min(rows / halo).max(1);
165    let nblocks = rl.div_ceil(block);
166    let bounds: Vec<(usize, usize)> =
167        (0..nbands).map(|k| (rows * k / nbands * rl, rows * (k + 1) / nbands * rl)).collect();
168    if nbands == 1 {
169        // One band: whole rows, no blocks.
170        let top_rows = (top.len() / rl).min(halo);
171        f(0, &mut data[..rows * rl], &top[top.len() - top_rows * rl..], 0, 0, rl);
172        return;
173    }
174    let Some(parts) = split_disjoint(&mut data[..rows * rl], &bounds) else { return };
175
176    struct Ctx<'a, T, F> {
177        /// A band is used by one task at a time (its blocks run in order); the locks only
178        /// make that visible to the compiler and are never contended.
179        bands: Vec<Mutex<&'a mut [T]>>,
180        first_rows: Vec<usize>,
181        /// The rows above each band, filled block by block as the band above finishes.
182        edges: Vec<Mutex<Vec<T>>>,
183        /// Unfinished dependencies of each block (`band * nblocks + block`).
184        pending: Vec<AtomicU8>,
185        nblocks: usize,
186        geo: Wavefront,
187        halo: usize,
188        f: F,
189    }
190
191    impl<T: Copy + Default + Send + Sync, F: Fn(usize, &mut [T], &[T], usize, usize, usize) + Sync> Ctx<'_, T, F> {
192        /// Run block `j` of band `k`; returns the blocks it made ready.
193        fn block(&self, k: usize, j: usize) -> impl Iterator<Item = (usize, usize)> + '_ {
194            let (rl, block, halo) = (self.geo.row_len, self.geo.block, self.halo);
195            let (a, b) = (j * block, ((j + 1) * block).min(rl));
196            let lo = a.saturating_sub(self.geo.margin);
197            let above: Vec<T> = {
198                let e = self.edges[k].lock().unwrap_or_else(|e| e.into_inner());
199                e.chunks_exact(rl).flat_map(|row| row[lo..b].iter().copied()).collect()
200            };
201            {
202                let mut band = self.bands[k].lock().unwrap_or_else(|e| e.into_inner());
203                (self.f)(self.first_rows[k], &mut band, &above, lo, a, b);
204                if let Some(next) = self.edges.get(k + 1) {
205                    let mut e = next.lock().unwrap_or_else(|e| e.into_inner());
206                    let n = band.len() / rl;
207                    for (h, dst) in e.chunks_exact_mut(rl).enumerate() {
208                        let src = &band[(n - halo + h) * rl..][..rl];
209                        dst[lo..b].copy_from_slice(&src[lo..b]);
210                    }
211                }
212            }
213            // The last dependency to finish starts a block; AcqRel orders this block's
214            // writes before it (the locks order the data as well).
215            [(k + 1, j), (k, j + 1)].into_iter().filter(move |&(k2, j2)| {
216                k2 < self.bands.len()
217                    && j2 < self.nblocks
218                    && self.pending[k2 * self.nblocks + j2].fetch_sub(1, Ordering::AcqRel) == 1
219            })
220        }
221    }
222
223    let first_rows: Vec<usize> = bounds.iter().map(|&(s, _)| s / rl).collect();
224    let top_rows = (top.len() / rl).min(halo);
225    let edges = (0..nbands)
226        .map(|k| {
227            Mutex::new(if k == 0 { top[top.len() - top_rows * rl..].to_vec() } else { vec![T::default(); halo * rl] })
228        })
229        .collect();
230    let pending =
231        (0..nbands * nblocks).map(|i| AtomicU8::new((i / nblocks > 0) as u8 + (i % nblocks > 0) as u8)).collect();
232    let ctx =
233        Ctx { bands: parts.into_iter().map(Mutex::new).collect(), first_rows, edges, pending, nblocks, geo, halo, f };
234
235    #[cfg(feature = "rayon")]
236    if nbands > 1 && threads() > 1 {
237        fn run<'s, T, F>(s: &rayon::Scope<'s>, ctx: &'s Ctx<'s, T, F>, k: usize, j: usize)
238        where
239            T: Copy + Default + Send + Sync,
240            F: Fn(usize, &mut [T], &[T], usize, usize, usize) + Sync,
241        {
242            for (k2, j2) in ctx.block(k, j) {
243                s.spawn(move |s| run(s, ctx, k2, j2));
244            }
245        }
246        rayon::scope(|s| run(s, &ctx, 0, 0));
247        return;
248    }
249    // In order, band by band: every block's dependencies are done before it runs.
250    for k in 0..nbands {
251        for j in 0..nblocks {
252            ctx.block(k, j).for_each(drop);
253        }
254    }
255}
256
257/// Split a mutable slice into disjoint sub-slices at the given ascending offsets.
258/// `bounds` are `(start, end)` pairs that must not overlap and be sorted. Returns
259/// `None` if they are not valid for `data`.
260pub fn split_disjoint<'a, T>(mut data: &'a mut [T], bounds: &[(usize, usize)]) -> Option<Vec<&'a mut [T]>> {
261    let mut out = Vec::with_capacity(bounds.len());
262    let mut consumed = 0usize;
263    for &(s, e) in bounds {
264        if s < consumed || e < s {
265            return None;
266        }
267        let rest = std::mem::take(&mut data);
268        if e - consumed > rest.len() {
269            return None;
270        }
271        let (_, tail) = rest.split_at_mut(s - consumed);
272        let (mid, tail) = tail.split_at_mut(e - s);
273        out.push(mid);
274        data = tail;
275        consumed = e;
276    }
277    Some(out)
278}
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283
284    #[test]
285    fn sequential_runs_everything_on_the_calling_thread() {
286        let me = std::thread::current().id();
287        let outer = threads();
288        let r = sequential(|| {
289            assert_eq!(threads(), 1);
290            let ids = map((0..64).collect::<Vec<_>>(), true, |_| std::thread::current().id());
291            assert!(ids.iter().all(|&i| i == me));
292            for_each_index(64, true, |_| assert_eq!(std::thread::current().id(), me));
293            let mut v = vec![0u8; PAR_MIN_SAMPLES * 2];
294            for_each_row_chunk(&mut v, 1024, 1, true, |_, _| assert_eq!(std::thread::current().id(), me));
295            // Nested calls keep the setting and restore it on the way out.
296            sequential(|| assert_eq!(threads(), 1));
297            assert_eq!(threads(), 1);
298            7
299        });
300        assert_eq!(r, 7);
301        assert_eq!(threads(), outer);
302    }
303
304    #[test]
305    fn row_chunks_cover_everything() {
306        let mut v = vec![0u32; 1 << 19];
307        for_each_row_chunk(&mut v, 1000, 7, true, |first, c| {
308            for (i, x) in c.iter_mut().enumerate() {
309                *x = (first * 1000 + i) as u32;
310            }
311        });
312        assert!(v.iter().enumerate().all(|(i, &x)| x == i as u32));
313    }
314
315    /// `v[r][x] += v[r][x-1] + 3 v[r-1][x] + 5 v[r-1][x-1] + 7 v[r-2][x]` (missing terms 0).
316    fn recurrence(get: impl Fn(isize, isize) -> u32, own: u32) -> u32 {
317        own.wrapping_add(get(0, -1))
318            .wrapping_add(get(-1, 0).wrapping_mul(3))
319            .wrapping_add(get(-1, -1).wrapping_mul(5))
320            .wrapping_add(get(-2, 0).wrapping_mul(7))
321    }
322
323    #[test]
324    fn wavefront_matches_row_order() {
325        for (rl, rows, block) in [(1000usize, 97usize, 64usize), (130, 9, 16), (64, 3, 64), (50, 40, 7)] {
326            let seed: Vec<u32> = (0..rl * rows).map(|i| (i as u32).wrapping_mul(2654435761) >> 7).collect();
327            let mut want = seed.clone();
328            for r in 0..rows {
329                for x in 0..rl {
330                    let v = recurrence(
331                        |dr, dx| {
332                            let (rr, xx) = (r as isize + dr, x as isize + dx);
333                            if rr < 0 || xx < 0 { 0 } else { want[rr as usize * rl + xx as usize] }
334                        },
335                        want[r * rl + x],
336                    );
337                    want[r * rl + x] = v;
338                }
339            }
340            for parallel in [false, true] {
341                let mut got = seed.clone();
342                let geo = Wavefront { row_len: rl, halo: 2, margin: 1, block };
343                wavefront(&mut got, &[], geo, parallel, |first, band, above, lo, a, b| {
344                    let w = b - lo;
345                    let na = above.len() / w;
346                    for i in 0..band.len() / rl {
347                        for x in a..b {
348                            let get = |dr: isize, dx: isize| {
349                                let xx = x as isize + dx;
350                                if xx < 0 || (first + i) as isize + dr < 0 {
351                                    return 0;
352                                }
353                                let xx = xx as usize;
354                                let ri = i as isize + dr;
355                                if ri >= 0 {
356                                    band[ri as usize * rl + xx]
357                                } else {
358                                    above[(na as isize + ri) as usize * w + xx - lo]
359                                }
360                            };
361                            let v = recurrence(get, band[i * rl + x]);
362                            band[i * rl + x] = v;
363                        }
364                    }
365                });
366                assert_eq!(got, want, "{rl}x{rows} block {block} parallel {parallel}");
367            }
368        }
369    }
370
371    #[test]
372    fn disjoint_split() {
373        let mut v: Vec<u8> = (0..10).collect();
374        let parts = split_disjoint(&mut v, &[(1, 3), (5, 6), (6, 10)]).unwrap();
375        assert_eq!(parts[0], &[1, 2]);
376        assert_eq!(parts[1], &[5]);
377        assert_eq!(parts[2], &[6, 7, 8, 9]);
378        assert!(split_disjoint(&mut v, &[(3, 5), (4, 6)]).is_none());
379        assert!(split_disjoint(&mut v, &[(3, 50)]).is_none());
380    }
381}
382
383/// Concurrent writer for a buffer partitioned into a regular tile grid.
384///
385/// Tiles of a grid share buffer rows, so `chunks_mut` cannot hand them out to
386/// threads. `GridWriter` lets each thread [`claim`](GridWriter::claim) a tile and
387/// write its rows directly into the final image. Soundness rests on two checked
388/// invariants: (1) every claim is recorded in an atomic flag, so a tile can be
389/// claimed at most once; (2) grid tiles cover pairwise-disjoint rectangles, and
390/// every row slice handed out is clipped to its tile, so no two claims alias.
391///
392/// The buffer may cover a sub-rectangle (`origin`, `width x height`) of the grid's
393/// coordinate space, which is how region decoding writes only intersecting tiles.
394pub struct GridWriter<'a, T> {
395    ptr: *mut T,
396    len: usize,
397    width: u32,
398    height: u32,
399    cpp: usize,
400    origin: (u32, u32),
401    tile_w: u32,
402    tile_h: u32,
403    tiles_x: u32,
404    tiles_y: u32,
405    claimed: Vec<std::sync::atomic::AtomicBool>,
406    _marker: std::marker::PhantomData<&'a mut [T]>,
407}
408
409// SAFETY: access to the underlying buffer is partitioned by claims (see type docs);
410// `T: Send` values may be written from any thread.
411unsafe impl<T: Send> Send for GridWriter<'_, T> {}
412// SAFETY: shared access only hands out disjoint, exclusively-claimed tiles.
413unsafe impl<T: Send> Sync for GridWriter<'_, T> {}
414
415/// Exclusive access to one claimed tile's area within the buffer.
416pub struct TileMut<'w, 'a, T> {
417    writer: &'w GridWriter<'a, T>,
418    /// Clipped area in buffer coordinates.
419    x0: usize,
420    y0: usize,
421    w: usize,
422    h: usize,
423    /// Offset of the clipped area relative to the tile's own origin.
424    pub skip_x: u32,
425    pub skip_y: u32,
426}
427
428impl<'a, T: Send> GridWriter<'a, T> {
429    /// `buf` holds `width x height` pixels of `cpp` samples, covering grid coordinates
430    /// starting at `origin`. The grid has `tile_w x tile_h` tiles anchored at (0, 0) and
431    /// spans `grid_w x grid_h` pixels.
432    #[allow(clippy::too_many_arguments)]
433    pub fn new(
434        buf: &'a mut [T],
435        width: u32,
436        height: u32,
437        cpp: u32,
438        origin: (u32, u32),
439        tile_w: u32,
440        tile_h: u32,
441        grid_w: u32,
442        grid_h: u32,
443    ) -> Option<Self> {
444        if tile_w == 0 || tile_h == 0 || buf.len() < width as usize * height as usize * cpp as usize {
445            return None;
446        }
447        let tiles_x = grid_w.div_ceil(tile_w);
448        let tiles_y = grid_h.div_ceil(tile_h);
449        let n = tiles_x as usize * tiles_y as usize;
450        Some(GridWriter {
451            ptr: buf.as_mut_ptr(),
452            len: buf.len(),
453            width,
454            height,
455            cpp: cpp as usize,
456            origin,
457            tile_w,
458            tile_h,
459            tiles_x,
460            tiles_y,
461            claimed: (0..n).map(|_| std::sync::atomic::AtomicBool::new(false)).collect(),
462            _marker: std::marker::PhantomData,
463        })
464    }
465
466    pub fn tiles_x(&self) -> u32 {
467        self.tiles_x
468    }
469    pub fn tiles_y(&self) -> u32 {
470        self.tiles_y
471    }
472
473    /// Claim tile `(tx, ty)`. Returns `None` if out of range, already claimed, or if
474    /// the tile does not intersect the buffer area.
475    pub fn claim(&self, tx: u32, ty: u32) -> Option<TileMut<'_, 'a, T>> {
476        if tx >= self.tiles_x || ty >= self.tiles_y {
477            return None;
478        }
479        let idx = ty as usize * self.tiles_x as usize + tx as usize;
480        if self.claimed[idx].swap(true, std::sync::atomic::Ordering::AcqRel) {
481            return None;
482        }
483        // Tile rect in grid coordinates, intersected with the buffer area.
484        let (gx, gy) = (tx as u64 * self.tile_w as u64, ty as u64 * self.tile_h as u64);
485        let (ox, oy) = (self.origin.0 as u64, self.origin.1 as u64);
486        let l = gx.max(ox);
487        let t = gy.max(oy);
488        let r = (gx + self.tile_w as u64).min(ox + self.width as u64);
489        let b = (gy + self.tile_h as u64).min(oy + self.height as u64);
490        if r <= l || b <= t {
491            return None;
492        }
493        Some(TileMut {
494            writer: self,
495            x0: (l - ox) as usize,
496            y0: (t - oy) as usize,
497            w: (r - l) as usize,
498            h: (b - t) as usize,
499            skip_x: (l - gx) as u32,
500            skip_y: (t - gy) as u32,
501        })
502    }
503}
504
505impl<T> TileMut<'_, '_, T> {
506    /// Width (pixels) and height of the clipped area.
507    pub fn size(&self) -> (usize, usize) {
508        (self.w, self.h)
509    }
510    /// Mutable samples of row `y` of the clipped area (`w * cpp` samples).
511    pub fn row(&mut self, y: usize) -> &mut [T] {
512        assert!(y < self.h, "tile row out of range");
513        let wr = self.writer;
514        let start = ((self.y0 + y) * wr.width as usize + self.x0) * wr.cpp;
515        let n = self.w * wr.cpp;
516        assert!(start + n <= wr.len);
517        // SAFETY: `start..start+n` lies within the buffer (asserted) and within this
518        // tile's clipped rectangle, which is disjoint from every other tile's; the tile
519        // was claimed exactly once (atomic flag) and `&mut self` prevents aliasing
520        // between rows handed out by this same TileMut.
521        unsafe { std::slice::from_raw_parts_mut(wr.ptr.add(start), n) }
522    }
523}
524
525#[cfg(test)]
526mod grid_tests {
527    use super::*;
528
529    #[test]
530    fn grid_writes_and_single_claim() {
531        let mut buf = vec![0u32; 10 * 7];
532        {
533            let g = GridWriter::new(&mut buf, 10, 7, 1, (0, 0), 4, 3, 10, 7).unwrap();
534            assert_eq!((g.tiles_x(), g.tiles_y()), (3, 3));
535            for_each_index(9, true, |i| {
536                let (tx, ty) = (i as u32 % 3, i as u32 / 3);
537                let mut t = g.claim(tx, ty).unwrap();
538                let (_, h) = t.size();
539                for y in 0..h {
540                    for v in t.row(y).iter_mut() {
541                        *v = i as u32 + 1;
542                    }
543                }
544            });
545            assert!(g.claim(0, 0).is_none());
546        }
547        for y in 0..7 {
548            for x in 0..10 {
549                assert_eq!(buf[y * 10 + x], (y / 3 * 3 + x / 4) as u32 + 1);
550            }
551        }
552    }
553
554    #[test]
555    fn region_buffer() {
556        // Buffer covers grid rect (3,2) 5x4; tile 4x4.
557        let mut buf = vec![0u8; 5 * 4];
558        let g = GridWriter::new(&mut buf, 5, 4, 1, (3, 2), 4, 4, 16, 16).unwrap();
559        let t = g.claim(0, 0).unwrap();
560        assert_eq!((t.x0, t.y0, t.size(), t.skip_x, t.skip_y), (0, 0, (1, 2), 3, 2));
561        let t = g.claim(1, 1).unwrap();
562        assert_eq!((t.x0, t.y0, t.size(), t.skip_x, t.skip_y), (1, 2, (4, 2), 0, 0));
563        assert!(g.claim(3, 3).is_none());
564    }
565}