Skip to main content

flashraw_gpu/
run.rs

1//! Running kernels: upload once, one fused dispatch, GPU-resident result.
2
3use crate::context::{GpuContext, Kernel, PipelineKey};
4use crate::image::{GpuImage, GpuPixelFormat, GpuTile};
5use crate::kernels::{self, Params};
6use flashraw_core::{Error, FrameInfo, Orientation, PackOrder, PackedPayload, Rect, Result};
7use std::sync::Arc;
8
9/// Input raster.
10#[derive(Debug, Clone, Copy)]
11pub enum GpuInput<'a> {
12    /// Uniformly bit-packed payload (unpacked on the GPU).
13    Packed {
14        data: &'a [u8],
15        bits: u32,
16        order: PackOrder,
17        /// Bytes from one row start to the next.
18        row_stride: usize,
19    },
20    /// Decoded u16 samples, `raster_width * cpp` per row.
21    U16(&'a [u16]),
22    /// Decoded f32 samples, `raster_width * cpp` per row.
23    F32(&'a [f32]),
24}
25
26/// A frame to process: an input raster plus the window and coordinate anchors.
27#[derive(Debug, Clone, Copy)]
28pub struct GpuFrame<'a> {
29    pub input: GpuInput<'a>,
30    /// Size of the uploaded raster in pixels.
31    pub raster_width: u32,
32    pub raster_height: u32,
33    pub cpp: u32,
34    /// Window of the raster to process (region decodes); full raster by default.
35    pub window: Rect,
36    /// Sensor coordinates of the window origin (anchors the CFA and black patterns).
37    pub origin: (u32, u32),
38    /// Optional linearisation curve applied to integer samples (`v -> lut[min(v, len-1)]`).
39    pub linearization: Option<&'a [u16]>,
40}
41
42impl<'a> GpuFrame<'a> {
43    pub fn from_packed(p: &PackedPayload<'a>) -> Self {
44        GpuFrame {
45            input: GpuInput::Packed { data: p.data, bits: p.bits, order: p.order, row_stride: p.row_stride },
46            raster_width: p.width,
47            raster_height: p.height,
48            cpp: 1,
49            window: Rect::full(p.width, p.height),
50            origin: (0, 0),
51            linearization: None,
52        }
53    }
54    pub fn from_u16(data: &'a [u16], width: u32, height: u32, cpp: u32) -> Self {
55        Self::plain(GpuInput::U16(data), width, height, cpp)
56    }
57    pub fn from_f32(data: &'a [f32], width: u32, height: u32, cpp: u32) -> Self {
58        Self::plain(GpuInput::F32(data), width, height, cpp)
59    }
60    fn plain(input: GpuInput<'a>, width: u32, height: u32, cpp: u32) -> Self {
61        GpuFrame {
62            input,
63            raster_width: width,
64            raster_height: height,
65            cpp,
66            window: Rect::full(width, height),
67            origin: (0, 0),
68            linearization: None,
69        }
70    }
71    /// Process only `r` (raster coordinates); the origin moves with it.
72    pub fn with_window(mut self, r: Rect) -> Self {
73        self.origin = (self.origin.0 + r.x - self.window.x, self.origin.1 + r.y - self.window.y);
74        self.window = r;
75        self
76    }
77    pub fn with_origin(mut self, origin: (u32, u32)) -> Self {
78        self.origin = origin;
79        self
80    }
81    pub fn with_linearization(mut self, lut: Option<&'a [u16]>) -> Self {
82        self.linearization = lut.filter(|l| !l.is_empty());
83        self
84    }
85    /// Bytes uploaded for this frame.
86    pub fn upload_bytes(&self) -> usize {
87        match self.input {
88            GpuInput::Packed { data, .. } => data.len(),
89            GpuInput::U16(d) => d.len() * 2,
90            GpuInput::F32(d) => d.len() * 4,
91        }
92    }
93}
94
95/// Output of the develop kernel.
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97pub enum DevelopOutput {
98    /// sRGB RGBA8 with EXIF orientation applied.
99    Rgba8,
100    /// Linear sRGB-primaries f32 RGB (no orientation), like `OutputFormat::LinearRgbF32`.
101    LinearRgbF32,
102    /// sRGB 16-bit with EXIF orientation applied, like `OutputFormat::Rgb16`, stored as
103    /// RGBA (alpha 65535): a [`GpuPixelFormat::U16`] image with 4 channels.
104    Rgb16,
105    /// Linear 16-bit integers (no orientation), like `OutputFormat::LinearRgb16`, stored
106    /// as RGBA (alpha 65535).
107    LinearRgb16,
108    /// Linear half floats (no orientation), like `OutputFormat::LinearRgbF16`, stored as
109    /// RGBA bit patterns (alpha 1.0) in a [`GpuPixelFormat::U16`] image with 4 channels.
110    LinearRgbF16,
111}
112
113/// Parameters of the fused develop pass (mirror of `flashraw::process::DevelopParams`).
114#[derive(Debug, Clone, Copy)]
115pub struct DevelopSpec {
116    /// Region to develop, in window coordinates.
117    pub crop: Rect,
118    /// 2x2 superpixel (half resolution) instead of bilinear.
119    pub half_size: bool,
120    /// White-balance multipliers per CFA colour (R, G, B, 4th).
121    pub wb: [f32; 4],
122    pub cam_to_rgb: [[f32; 3]; 3],
123    pub orientation: Orientation,
124    pub output: DevelopOutput,
125    /// Exposure and black/white points after the colour conversion: `v * scale - offset`
126    /// (`[scale, offset]`; `None` for none).
127    pub levels: Option<[f32; 2]>,
128    /// Transfer-curve tables of the display outputs; `None`: sRGB.
129    pub curve: Option<GpuCurve>,
130}
131
132/// Encoder tables of a non-sRGB output curve (the CPU `CurveTables`).
133#[derive(Debug, Clone, Copy)]
134pub struct GpuCurve {
135    /// 8-bit codes at `lut8.len() - 1` segments of `[0, 1]` (RGBA8 output).
136    pub lut8: &'static [u8],
137    /// 16-bit codes (as `f32`) at 16385 points of `[0, 1]` (16-bit display output).
138    pub lut16: &'static [f32],
139}
140
141/// What to compute.
142#[derive(Debug, Clone, Copy)]
143pub enum GpuOp {
144    /// u16 samples (packed unpack and/or linearisation).
145    Unpack,
146    /// Black-subtracted, white-normalised f32 samples (sensor layout).
147    Normalize,
148    Develop(DevelopSpec),
149}
150
151/// Per-stage timings of one run, in milliseconds.
152#[derive(Debug, Clone, Copy, Default, PartialEq)]
153pub struct GpuTimings {
154    /// Host → device: time the host spent staging input (summed over bands; with
155    /// several bands it overlaps the kernels of earlier bands).
156    pub upload_ms: f64,
157    /// The rest of the run until the result is complete (wall clock).
158    pub compute_ms: f64,
159    /// Kernel execution measured with GPU timestamps (summed over bands), when supported.
160    pub kernel_ms: Option<f64>,
161    /// Number of bands the frame was processed in (1 = untiled).
162    pub bands: u32,
163}
164
165/// Result of [`GpuContext::run`]: a GPU-resident image plus timings.
166#[derive(Debug)]
167pub struct GpuRun {
168    pub image: GpuImage,
169    pub timings: GpuTimings,
170}
171
172pub(crate) fn buffer_init(device: &wgpu::Device, label: &str, data: &[u8], usage: wgpu::BufferUsages) -> wgpu::Buffer {
173    let size = (data.len() as u64).div_ceil(4).max(1) * 4;
174    let buf =
175        device.create_buffer(&wgpu::BufferDescriptor { label: Some(label), size, usage, mapped_at_creation: true });
176    {
177        let mut view = buf.get_mapped_range_mut(..).expect("buffer mapped at creation");
178        let mut padded = data.to_vec();
179        padded.resize(size as usize, 0);
180        view.copy_from_slice(&padded);
181    }
182    buf.unmap();
183    buf
184}
185
186/// Write `data` at offset 0, padding the final partial word with zeros
187/// (`write_buffer` requires 4-byte multiples) without copying the bulk.
188fn write_padded(queue: &wgpu::Queue, buf: &wgpu::Buffer, data: &[u8]) {
189    let bulk = data.len() & !3;
190    if bulk > 0 {
191        queue.write_buffer(buf, 0, &data[..bulk]);
192    }
193    if bulk < data.len() {
194        let mut tail = [0u8; 4];
195        tail[..data.len() - bulk].copy_from_slice(&data[bulk..]);
196        queue.write_buffer(buf, bulk as u64, &tail);
197    }
198}
199
200/// 2-D grid of 256-wide workgroups covering `n` invocations (respects the 65535 limit).
201fn grid_1d(n: u64) -> (u32, u32) {
202    let groups = n.div_ceil(256).max(1);
203    let x = groups.min(32768);
204    (x as u32, groups.div_ceil(x) as u32)
205}
206
207fn u32_checked(v: u64, what: &str) -> Result<u32> {
208    u32::try_from(v).map_err(|_| Error::LimitExceeded(format!("{what} too large for GPU addressing")))
209}
210
211fn round4(v: u64) -> u64 {
212    v.div_ceil(4).max(1) * 4
213}
214
215/// How the output is laid out across bands.
216#[derive(Debug, Clone, Copy, PartialEq, Eq)]
217enum OutLayout {
218    /// One buffer, bound whole by every band (fits one storage binding).
219    Single,
220    /// One buffer (fits `max_buffer_size`), each band binds the sub-range it writes.
221    SubRange,
222    /// One buffer per band ([`GpuImage::tiles`]).
223    Tiled,
224}
225
226/// One band: rows `[y0, y1)` of the computed image (frame rows for unpack/normalise,
227/// developed rows for develop) and the frame rows `[in0, in1)` it reads.
228#[derive(Debug, Clone, Copy, PartialEq, Eq)]
229pub(crate) struct Band {
230    pub y0: u32,
231    pub y1: u32,
232    pub in0: u32,
233    pub in1: u32,
234}
235
236/// Geometry of a banded run, independent of the device (unit-testable).
237#[derive(Debug, Clone, Copy)]
238pub(crate) struct BandGeometry {
239    /// Rows of the computed image.
240    pub rows: u32,
241    /// Bytes uploaded per frame row (packed: max(stride, row bytes)).
242    pub in_row_bytes: u64,
243    /// Output bytes per computed row.
244    pub out_row_bytes: u64,
245    /// Frame rows read for computed rows `[y0, y1)` (unclamped above).
246    pub kind: BandKind,
247    /// Frame height (clamps the input rows).
248    pub frame_h: u32,
249    /// Computed rows must start at a multiple of this (unpack of odd-length rows: 2).
250    pub row_align: u32,
251    /// Preferred band granularity (develop: the 16-row workgroup tile).
252    pub tile_rows: u32,
253    pub max_rows: u32,
254}
255
256#[derive(Debug, Clone, Copy, PartialEq, Eq)]
257pub(crate) enum BandKind {
258    /// Same rows (unpack, normalise).
259    Direct,
260    /// 3x3 neighbourhood: rows `crop_y + y - 1 ..= crop_y + y + 1`.
261    Bilinear { crop_y: u32 },
262    /// 2x2 superpixel: rows `crop_y + 2y ..= crop_y + 2y + 1`.
263    Half { crop_y: u32 },
264}
265
266impl BandGeometry {
267    fn input_rows(&self, y0: u32, y1: u32) -> (u32, u32) {
268        let h = self.frame_h;
269        match self.kind {
270            BandKind::Direct => (y0, y1.min(h)),
271            BandKind::Bilinear { crop_y } => ((crop_y + y0).saturating_sub(1), (crop_y + y1 + 1).min(h)),
272            BandKind::Half { crop_y } => ((crop_y + 2 * y0).min(h), (crop_y + 2 * y1).min(h)),
273        }
274    }
275
276    /// Split into bands whose input (+ `in_slack` bytes) fits `budget` and, unless the
277    /// output is a single binding, whose output (+ `out_slack`) fits it too.
278    pub fn plan(&self, budget: u64, in_slack: u64, out_whole: bool, out_slack: u64) -> Result<Vec<Band>> {
279        let rows_in = budget.saturating_sub(in_slack) / self.in_row_bytes.max(1);
280        let n_in = match self.kind {
281            BandKind::Direct => rows_in,
282            BandKind::Bilinear { .. } => rows_in.saturating_sub(2),
283            BandKind::Half { .. } => rows_in / 2,
284        };
285        let n_out = if out_whole { u64::MAX } else { budget.saturating_sub(out_slack) / self.out_row_bytes.max(1) };
286        let mut n = n_in.min(n_out).min(self.max_rows as u64).min(self.rows as u64) as u32;
287        if n < self.rows {
288            if n >= self.tile_rows {
289                n -= n % self.tile_rows;
290            }
291            n -= n % self.row_align;
292        }
293        if n == 0 {
294            return Err(Error::LimitExceeded(format!(
295                "one band of {} rows exceeds the GPU buffer limit ({:.1} MB)",
296                self.row_align,
297                budget as f64 / 1e6
298            )));
299        }
300        let mut bands = Vec::with_capacity(self.rows.div_ceil(n) as usize);
301        let mut y0 = 0;
302        while y0 < self.rows {
303            let y1 = (y0 + n).min(self.rows);
304            let (in0, in1) = self.input_rows(y0, y1);
305            bands.push(Band { y0, y1, in0, in1 });
306            y0 = y1;
307        }
308        Ok(bands)
309    }
310}
311
312/// Output rectangle written by developed rows `[y0, y1)` (`dw` x `dh` developed image)
313/// under `orientation` (EXIF 1..8): a row band, or a column band when rotated.
314pub(crate) fn band_rect(y0: u32, y1: u32, dw: u32, dh: u32, orientation: u32) -> Rect {
315    let n = y1 - y0;
316    match orientation {
317        3 | 4 => Rect::new(0, dh - y1, dw, n),
318        5 | 8 => Rect::new(y0, 0, n, dw),
319        6 | 7 => Rect::new(dh - y1, 0, n, dw),
320        _ => Rect::new(0, y0, dw, n),
321    }
322}
323
324/// Input bytes of a band: the slice to upload plus `src_offset` / `src_len`.
325struct InputSlice<'a> {
326    bytes: &'a [u8],
327    offset: u32,
328    len: u32,
329}
330
331/// Double-buffered upload slot.
332struct Slot {
333    src: wgpu::Buffer,
334    staging: Option<wgpu::Buffer>,
335    last: Option<wgpu::SubmissionIndex>,
336}
337
338impl GpuContext {
339    /// Upload `frame`, run `op` and return the GPU-resident result.
340    ///
341    /// Frames whose input or output exceed one storage binding
342    /// ([`GpuContext::max_binding_bytes`]) are processed in horizontal bands: each band
343    /// uploads only the rows it needs (plus a one-row halo for the demosaic), the upload
344    /// of band N+1 overlaps the kernel of band N, and the output is either one buffer
345    /// (bound per band) or a tiled [`GpuImage`].
346    pub async fn run(self: &Arc<Self>, frame: &GpuFrame<'_>, info: &FrameInfo, op: &GpuOp) -> Result<GpuRun> {
347        self.run_banded(frame, info, op, None).await
348    }
349
350    /// [`GpuContext::run`] with an explicit per-binding budget (bytes) instead of the
351    /// device limit (used by calibration to measure the per-band cost).
352    pub(crate) async fn run_banded(
353        self: &Arc<Self>,
354        frame: &GpuFrame<'_>,
355        info: &FrameInfo,
356        op: &GpuOp,
357        budget: Option<u64>,
358    ) -> Result<GpuRun> {
359        let (w, h, cpp) = (frame.window.width, frame.window.height, frame.cpp.max(1));
360        let win = frame.window;
361        if w == 0 || h == 0 || win.right() > frame.raster_width || win.bottom() > frame.raster_height {
362            return Err(Error::InvalidArgument(format!(
363                "window {win} outside {}x{} raster",
364                frame.raster_width, frame.raster_height
365            )));
366        }
367        let mut curve: Option<GpuCurve> = None;
368        let mut p = Params {
369            srgb_n: 4096,
370            width: w,
371            height: h,
372            cpp,
373            src_x: win.x,
374            origin_x: frame.origin.0,
375            origin_y: frame.origin.1,
376            orientation: 1,
377            wb: [1.0; 4],
378            ..Default::default()
379        };
380        // ---- Input description ----
381        let raster_samples = frame.raster_width as u64 * frame.raster_height as u64 * cpp as u64;
382        let raster_row = frame.raster_width as u64 * cpp as u64;
383        let (input_mode, order, in_row_bytes): (u32, u32, u64) = match frame.input {
384            GpuInput::Packed { bits, order, row_stride, .. } => {
385                if !(1..=16).contains(&bits) {
386                    return Err(Error::InvalidArgument(format!("unsupported packed bit depth {bits}")));
387                }
388                let row_bytes = (raster_row * bits as u64).div_ceil(8);
389                p.bits = bits;
390                p.src_stride = u32_checked(row_stride as u64, "row stride")?;
391                p.row_bytes = u32_checked(row_bytes, "row size")?;
392                (2, kernels::order_index(order), (row_stride as u64).max(row_bytes))
393            }
394            GpuInput::U16(d) => {
395                if (d.len() as u64) < raster_samples {
396                    return Err(Error::InvalidArgument("u16 raster shorter than its dimensions".into()));
397                }
398                p.src_stride = u32_checked(raster_row, "row")?;
399                (0, 0, raster_row * 2)
400            }
401            GpuInput::F32(d) => {
402                if (d.len() as u64) < raster_samples {
403                    return Err(Error::InvalidArgument("f32 raster shorter than its dimensions".into()));
404                }
405                p.src_stride = u32_checked(raster_row, "row")?;
406                (1, 0, raster_row * 4)
407            }
408        };
409        let use_lut = frame.linearization.is_some() && input_mode != 1;
410        let mut calib = kernels::calib_table(info, cpp as usize);
411        p.pat_w = calib.pat_w;
412        p.pat_h = calib.pat_h;
413        p.off_scale = calib.off_scale;
414        p.off_rowd = calib.off_rowd;
415        p.off_cold = calib.off_cold;
416        p.n_rowd = calib.n_rowd;
417        p.n_cold = calib.n_cold;
418        let (cfa_w, cfa_h, sites) = kernels::cfa_sites(info);
419        p.cfa_w = cfa_w;
420        p.cfa_h = cfa_h;
421
422        // ---- Output description ----
423        let row_samples = w as u64 * cpp as u64;
424        let mut geo = BandGeometry {
425            rows: h,
426            in_row_bytes,
427            out_row_bytes: 0,
428            kind: BandKind::Direct,
429            frame_h: h,
430            row_align: 1,
431            tile_rows: 1,
432            max_rows: u32::MAX,
433        };
434        let (key, out_w, out_h, channels, format) = match op {
435            GpuOp::Unpack => {
436                if input_mode == 1 {
437                    return Err(Error::InvalidArgument("unpack needs integer input".into()));
438                }
439                geo.out_row_bytes = row_samples * 2;
440                // A band must start on a whole output word (two u16 samples).
441                geo.row_align = if row_samples % 2 == 1 { 2 } else { 1 };
442                let key = PipelineKey { kernel: Kernel::Unpack, input_mode, order, use_lut, layout: 0, out_mode: 0 };
443                (key, w, h, cpp, GpuPixelFormat::U16)
444            }
445            GpuOp::Normalize => {
446                geo.out_row_bytes = row_samples * 4;
447                let key = PipelineKey { kernel: Kernel::Normalize, input_mode, order, use_lut, layout: 0, out_mode: 0 };
448                (key, w, h, cpp, GpuPixelFormat::F32)
449            }
450            GpuOp::Develop(d) => {
451                let crop = d.crop.clamp_to(w, h);
452                let (dw, dh) = if d.half_size { (crop.width / 2, crop.height / 2) } else { (crop.width, crop.height) };
453                if dw == 0 || dh == 0 {
454                    return Err(Error::InvalidArgument("develop region too small".into()));
455                }
456                p.crop_x = crop.x;
457                p.crop_y = crop.y;
458                p.dev_w = dw;
459                p.dev_h = dh;
460                p.wb = d.wb;
461                curve = d.curve;
462                let m = d.cam_to_rgb;
463                p.m0 = [m[0][0], m[0][1], m[0][2], 0.0];
464                p.m1 = [m[1][0], m[1][1], m[1][2], 0.0];
465                p.m2 = [m[2][0], m[2][1], m[2][2], 0.0];
466                if let Some([scale, offset]) = d.levels {
467                    p.levels = [scale, offset, 1.0, 0.0];
468                }
469                let layout = if cpp == 1 && info.cfa.is_some_and(|c| c.is_bayer()) {
470                    3
471                } else if cpp == 1 && info.cfa.is_some() {
472                    1
473                } else if cpp >= 3 {
474                    2
475                } else {
476                    0
477                };
478                let (out_mode, ow, oh, ch, fmt) = match d.output {
479                    DevelopOutput::Rgba8 => {
480                        p.orientation = d.orientation.exif();
481                        let (ow, oh) = if d.orientation.swaps_dimensions() { (dh, dw) } else { (dw, dh) };
482                        (0, ow, oh, 4, GpuPixelFormat::Rgba8)
483                    }
484                    DevelopOutput::LinearRgbF32 => (1, dw, dh, 3, GpuPixelFormat::F32),
485                    DevelopOutput::Rgb16 => {
486                        p.orientation = d.orientation.exif();
487                        let (ow, oh) = if d.orientation.swaps_dimensions() { (dh, dw) } else { (dw, dh) };
488                        (2, ow, oh, 4, GpuPixelFormat::U16)
489                    }
490                    DevelopOutput::LinearRgb16 => (3, dw, dh, 4, GpuPixelFormat::U16),
491                    DevelopOutput::LinearRgbF16 => (4, dw, dh, 4, GpuPixelFormat::U16),
492                };
493                p.out_w = ow;
494                p.out_h = oh;
495                geo.rows = dh;
496                geo.out_row_bytes = dw as u64 * ch as u64 * fmt.bytes_per_sample() as u64;
497                geo.kind =
498                    if d.half_size { BandKind::Half { crop_y: crop.y } } else { BandKind::Bilinear { crop_y: crop.y } };
499                geo.tile_rows = 16;
500                geo.max_rows = 16 * self.device.limits().max_compute_workgroups_per_dimension;
501                let key = PipelineKey {
502                    kernel: if d.half_size { Kernel::DevelopHalf } else { Kernel::Develop },
503                    input_mode,
504                    order,
505                    use_lut,
506                    layout,
507                    out_mode,
508                };
509                (key, ow, oh, ch, fmt)
510            }
511        };
512        if key.out_mode == 2 {
513            p.off_srgb16 = calib.tab.len() as u32;
514            match curve {
515                Some(c) if c.lut16.len() == kernels::srgb16_table().len() => calib.tab.extend_from_slice(c.lut16),
516                _ => calib.tab.extend_from_slice(kernels::srgb16_table()),
517            }
518        }
519        let bps = format.bytes_per_sample() as u64;
520        let out_bytes = out_w as u64 * out_h as u64 * channels as u64 * bps;
521        // Rotated RGBA8: a band of developed rows is a column band of the output.
522        let columns = p.orientation >= 5;
523
524        // ---- Band plan ----
525        let limits = self.device.limits();
526        let max_buffer = self.max_buffer_bytes();
527        let binding = budget.unwrap_or_else(|| self.max_binding_bytes()).min(self.max_binding_bytes()) & !3;
528        let align = limits.min_storage_buffer_offset_alignment.max(4) as u64;
529        let layout = if round4(out_bytes) <= binding {
530            OutLayout::Single
531        } else if out_bytes <= max_buffer && !columns {
532            OutLayout::SubRange
533        } else {
534            OutLayout::Tiled
535        };
536        let bands = geo.plan(binding, 8, layout == OutLayout::Single, align + 4)?;
537
538        // ---- Tables ----
539        let mut lut: Vec<u32> = Vec::new();
540        if let Some(l) = frame.linearization.filter(|_| use_lut) {
541            lut.extend(l.iter().map(|&v| v as u32));
542            p.lut_len = l.len() as u32;
543        }
544        p.off_srgb = lut.len() as u32;
545        match curve {
546            Some(c) if key.out_mode == 0 && c.lut8.len() > 1 => {
547                p.srgb_n = (c.lut8.len() - 1) as u32;
548                lut.extend(c.lut8.iter().map(|&v| v as u32));
549            }
550            _ => lut.extend_from_slice(&self.srgb),
551        }
552        p.off_cfa = lut.len() as u32;
553        lut.extend_from_slice(&sites);
554
555        // ---- Per-band input slices and parameters ----
556        let slice_of = |b: &Band| -> InputSlice<'_> {
557            let (r0, r1) = ((win.y + b.in0) as u64, (win.y + b.in1) as u64);
558            match frame.input {
559                GpuInput::Packed { data, row_stride, .. } => {
560                    let len = data.len() as u64;
561                    let start = (r0 * row_stride as u64).min(len);
562                    let end = if r1 > r0 { ((r1 - 1) * row_stride as u64 + in_row_bytes).min(len) } else { start };
563                    let al = start & !3;
564                    InputSlice {
565                        bytes: &data[al as usize..end as usize],
566                        offset: (start - al) as u32,
567                        len: (end - start) as u32,
568                    }
569                }
570                GpuInput::U16(d) => {
571                    let s = &d[(r0 * raster_row) as usize..(r1 * raster_row) as usize];
572                    InputSlice { bytes: bytemuck::cast_slice(s), offset: 0, len: 0 }
573                }
574                GpuInput::F32(d) => {
575                    let s = &d[(r0 * raster_row) as usize..(r1 * raster_row) as usize];
576                    InputSlice { bytes: bytemuck::cast_slice(s), offset: 0, len: 0 }
577                }
578            }
579        };
580        let slices: Vec<InputSlice<'_>> = bands.iter().map(slice_of).collect();
581        let in_max = slices.iter().map(|s| round4(s.bytes.len() as u64)).max().unwrap_or(4);
582        let develop = matches!(op, GpuOp::Develop(_));
583        // Output binding of each band: (output rectangle, byte offset, byte size) and the
584        // `dst_*` parameters.
585        let mut params = Vec::with_capacity(bands.len());
586        let mut out_ranges = Vec::with_capacity(bands.len());
587        for (b, s) in bands.iter().zip(&slices) {
588            let mut q = p;
589            q.src_y = b.in0;
590            q.src_offset = s.offset;
591            q.src_len = s.len;
592            q.band_y = b.y0;
593            q.band_end = b.y1;
594            q.total = u32_checked((b.y1 - b.y0) as u64 * row_samples, "band")?;
595            let rect = if develop {
596                band_rect(b.y0, b.y1, p.dev_w, p.dev_h, p.orientation)
597            } else {
598                Rect::new(0, b.y0, w, b.y1 - b.y0)
599            };
600            let row_out = out_w as u64 * channels as u64 * bps;
601            let (offset, size) = match layout {
602                OutLayout::Single => {
603                    q.dst_stride = out_w;
604                    q.dst_off = if develop { 0 } else { u32_checked(rect.y as u64 * row_out / 4, "output")? };
605                    (0, round4(out_bytes))
606                }
607                OutLayout::SubRange => {
608                    let start = rect.y as u64 * row_out;
609                    let a = start / align * align;
610                    let end = round4(rect.bottom() as u64 * row_out);
611                    q.dst_y = rect.y;
612                    q.dst_stride = out_w;
613                    q.dst_off = ((start - a) / 4) as u32;
614                    (a, end - a)
615                }
616                OutLayout::Tiled => {
617                    q.dst_x = rect.x;
618                    q.dst_y = rect.y;
619                    q.dst_stride = rect.width;
620                    (0, round4(rect.width as u64 * rect.height as u64 * channels as u64 * bps))
621                }
622            };
623            params.push(q);
624            out_ranges.push((rect, offset, size));
625        }
626
627        let dev = &self.device;
628        let validation = dev.push_error_scope(wgpu::ErrorFilter::Validation);
629        let oom = dev.push_error_scope(wgpu::ErrorFilter::OutOfMemory);
630
631        // ---- Buffers ----
632        let ustride = (std::mem::size_of::<Params>() as u64)
633            .div_ceil(limits.min_uniform_buffer_offset_alignment as u64)
634            * limits.min_uniform_buffer_offset_alignment as u64;
635        let mut ubytes = vec![0u8; ustride as usize * params.len()];
636        for (i, q) in params.iter().enumerate() {
637            let o = i * ustride as usize;
638            ubytes[o..o + std::mem::size_of::<Params>()].copy_from_slice(bytemuck::bytes_of(q));
639        }
640        let ubuf = buffer_init(dev, "flashraw.params", &ubytes, wgpu::BufferUsages::UNIFORM);
641        let tab = buffer_init(dev, "flashraw.calib", bytemuck::cast_slice(&calib.tab), wgpu::BufferUsages::STORAGE);
642        let lutb = buffer_init(dev, "flashraw.lut", bytemuck::cast_slice(&lut), wgpu::BufferUsages::STORAGE);
643        let mut dst_usage = wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC;
644        if self.uma {
645            dst_usage |= wgpu::BufferUsages::MAP_READ;
646        }
647        let outputs: Vec<wgpu::Buffer> = match layout {
648            OutLayout::Single | OutLayout::SubRange => {
649                vec![self.acquire("flashraw.output", round4(out_bytes), dst_usage)]
650            }
651            OutLayout::Tiled => {
652                out_ranges.iter().map(|&(_, _, size)| self.acquire("flashraw.output", size, dst_usage)).collect()
653            }
654        };
655        // Upload path: unified memory writes straight into the storage buffer; otherwise
656        // a reused mapped staging buffer + device copy, or `write_buffer` for small inputs.
657        let staged = !self.uma && in_max >= 1 << 20;
658        let nslots = bands.len().min(2);
659        let mut slots: Vec<Slot> = (0..nslots)
660            .map(|_| {
661                let src_usage = if self.uma {
662                    wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::MAP_WRITE
663                } else {
664                    wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST
665                };
666                Slot {
667                    src: self.acquire("flashraw.src", in_max, src_usage),
668                    staging: staged.then(|| {
669                        self.acquire(
670                            "flashraw.upload",
671                            in_max,
672                            wgpu::BufferUsages::MAP_WRITE | wgpu::BufferUsages::COPY_SRC,
673                        )
674                    }),
675                    last: None,
676                }
677            })
678            .collect();
679        let ts = (self.info.timestamps && bands.len() * 2 <= wgpu::QUERY_SET_MAX_QUERIES as usize).then(|| {
680            let n = bands.len() as u32 * 2;
681            let qs = dev.create_query_set(&wgpu::QuerySetDescriptor {
682                label: Some("flashraw.timestamps"),
683                ty: wgpu::QueryType::Timestamp,
684                count: n,
685            });
686            let resolve = dev.create_buffer(&wgpu::BufferDescriptor {
687                label: Some("flashraw.ts_resolve"),
688                size: n as u64 * 8,
689                usage: wgpu::BufferUsages::QUERY_RESOLVE | wgpu::BufferUsages::COPY_SRC,
690                mapped_at_creation: false,
691            });
692            let read = dev.create_buffer(&wgpu::BufferDescriptor {
693                label: Some("flashraw.ts_read"),
694                size: n as u64 * 8,
695                usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
696                mapped_at_creation: false,
697            });
698            (qs, resolve, read)
699        });
700        let pipeline = self.pipeline(key);
701
702        // ---- Bands: upload N+1 while N computes ----
703        let t0 = flashraw_core::profile::now_ms();
704        let mut upload_ms = 0.0;
705        for (i, (b, s)) in bands.iter().zip(&slices).enumerate() {
706            let slot = &mut slots[i % nslots];
707            let in_size = round4(s.bytes.len() as u64);
708            // Only the host copy counts as upload; waiting for the slot to be released by
709            // band N-2 is part of the (overlapped) compute.
710            upload_ms += if self.uma {
711                self.write_mapped(&slot.src, s.bytes, slot.last.take()).await?
712            } else if let Some(st) = &slot.staging {
713                self.write_mapped(st, s.bytes, slot.last.take()).await?
714            } else {
715                let tu = flashraw_core::profile::now_ms();
716                write_padded(&self.queue, &slot.src, s.bytes);
717                flashraw_core::profile::now_ms() - tu
718            };
719            let (_, out_offset, out_size) = out_ranges[i];
720            let out_buf = if layout == OutLayout::Tiled { &outputs[i] } else { &outputs[0] };
721            let bg = dev.create_bind_group(&wgpu::BindGroupDescriptor {
722                label: Some("flashraw.kernel"),
723                layout: &self.layout,
724                entries: &[
725                    wgpu::BindGroupEntry {
726                        binding: 0,
727                        resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
728                            buffer: &ubuf,
729                            offset: i as u64 * ustride,
730                            size: wgpu::BufferSize::new(std::mem::size_of::<Params>() as u64),
731                        }),
732                    },
733                    wgpu::BindGroupEntry {
734                        binding: 1,
735                        resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
736                            buffer: &slot.src,
737                            offset: 0,
738                            size: wgpu::BufferSize::new(in_size),
739                        }),
740                    },
741                    wgpu::BindGroupEntry { binding: 2, resource: tab.as_entire_binding() },
742                    wgpu::BindGroupEntry { binding: 3, resource: lutb.as_entire_binding() },
743                    wgpu::BindGroupEntry {
744                        binding: 4,
745                        resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
746                            buffer: out_buf,
747                            offset: out_offset,
748                            size: wgpu::BufferSize::new(out_size),
749                        }),
750                    },
751                ],
752            });
753            let groups = match op {
754                GpuOp::Unpack => grid_1d((params[i].total as u64).div_ceil(2)),
755                GpuOp::Normalize => grid_1d(params[i].total as u64),
756                GpuOp::Develop(_) => (p.dev_w.div_ceil(16), (b.y1 - b.y0).div_ceil(16)),
757            };
758            let mut enc = dev.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("flashraw.band") });
759            if let Some(st) = &slot.staging {
760                enc.copy_buffer_to_buffer(st, 0, &slot.src, 0, in_size);
761            }
762            {
763                let mut pass = enc.begin_compute_pass(&wgpu::ComputePassDescriptor {
764                    label: Some("flashraw.kernel"),
765                    timestamp_writes: ts.as_ref().map(|(qs, _, _)| wgpu::ComputePassTimestampWrites {
766                        query_set: qs,
767                        beginning_of_pass_write_index: Some(2 * i as u32),
768                        end_of_pass_write_index: Some(2 * i as u32 + 1),
769                    }),
770                });
771                pass.set_pipeline(&pipeline);
772                pass.set_bind_group(0, &bg, &[]);
773                pass.dispatch_workgroups(groups.0, groups.1, 1);
774            }
775            if let Some((qs, resolve, read)) = ts.as_ref().filter(|_| i + 1 == bands.len()) {
776                let n = bands.len() as u32 * 2;
777                enc.resolve_query_set(qs, 0..n, resolve, 0);
778                enc.copy_buffer_to_buffer(resolve, 0, read, 0, n as u64 * 8);
779            }
780            slot.last = Some(self.queue.submit([enc.finish()]));
781        }
782        self.wait_idle().await?;
783        let compute_ms = (flashraw_core::profile::now_ms() - t0 - upload_ms).max(0.0);
784        let kernel_ms = match &ts {
785            Some((_, _, read)) => {
786                let n = bands.len() * 2;
787                let ticks = self
788                    .read_mapped(read, n as u64 * 8, |b| {
789                        b.chunks_exact(16)
790                            .map(|c| {
791                                let t0 = bytemuck::pod_read_unaligned::<u64>(&c[0..8]);
792                                let t1 = bytemuck::pod_read_unaligned::<u64>(&c[8..16]);
793                                t1.saturating_sub(t0)
794                            })
795                            .sum::<u64>()
796                    })
797                    .await?;
798                let ns = ticks as f64 * self.queue.get_timestamp_period() as f64;
799                // Some drivers report zeros when sampling is unsupported mid-pass.
800                (ns > 0.0).then_some(ns / 1e6)
801            }
802            None => None,
803        };
804        for s in slots {
805            self.release(s.src);
806            if let Some(st) = s.staging {
807                self.release(st);
808            }
809        }
810        let e2 = oom.pop().await;
811        let e1 = validation.pop().await;
812        if let Some(e) = e2.or(e1) {
813            return Err(Error::GpuUnavailable(format!("GPU error: {e}")));
814        }
815        if let Some(e) = self.take_error() {
816            return Err(Error::GpuUnavailable(format!("GPU error: {e}")));
817        }
818        let tiled = layout == OutLayout::Tiled;
819        let parts: Vec<GpuTile> = if tiled {
820            outputs.into_iter().zip(&out_ranges).map(|(buffer, &(rect, _, _))| GpuTile { rect, buffer }).collect()
821        } else {
822            outputs.into_iter().map(|buffer| GpuTile { rect: Rect::full(out_w, out_h), buffer }).collect()
823        };
824        Ok(GpuRun {
825            image: GpuImage { ctx: self.clone(), parts, tiled, width: out_w, height: out_h, channels, format },
826            timings: GpuTimings { upload_ms, compute_ms, kernel_ms, bands: bands.len() as u32 },
827        })
828    }
829
830    /// Blocking [`GpuContext::run`] (native only).
831    #[cfg(not(target_arch = "wasm32"))]
832    pub fn run_blocking(self: &Arc<Self>, frame: &GpuFrame<'_>, info: &FrameInfo, op: &GpuOp) -> Result<GpuRun> {
833        pollster::block_on(self.run(frame, info, op))
834    }
835}
836
837#[cfg(test)]
838mod tests {
839    use super::*;
840
841    fn geo(rows: u32, kind: BandKind, frame_h: u32) -> BandGeometry {
842        BandGeometry {
843            rows,
844            in_row_bytes: 100,
845            out_row_bytes: 400,
846            kind,
847            frame_h,
848            row_align: 1,
849            tile_rows: 16,
850            max_rows: u32::MAX,
851        }
852    }
853
854    fn check_cover(bands: &[Band], rows: u32) {
855        assert_eq!(bands.first().unwrap().y0, 0);
856        assert_eq!(bands.last().unwrap().y1, rows);
857        assert!(bands.windows(2).all(|w| w[0].y1 == w[1].y0));
858    }
859
860    #[test]
861    fn plan_untiled_when_it_fits() {
862        let g = geo(100, BandKind::Bilinear { crop_y: 0 }, 100);
863        let b = g.plan(1 << 20, 8, true, 0).unwrap();
864        assert_eq!(b, vec![Band { y0: 0, y1: 100, in0: 0, in1: 100 }]);
865    }
866
867    #[test]
868    fn plan_bilinear_halo_and_tile_rounding() {
869        // 40 input rows fit: 38 developed rows, rounded down to 32.
870        let g = geo(100, BandKind::Bilinear { crop_y: 3 }, 110);
871        let b = g.plan(4008, 8, true, 0).unwrap();
872        check_cover(&b, 100);
873        assert_eq!(b[0], Band { y0: 0, y1: 32, in0: 2, in1: 36 });
874        assert_eq!(b[1], Band { y0: 32, y1: 64, in0: 34, in1: 68 });
875        assert_eq!(b.last().unwrap().in1, 104);
876        // Output-bound: 400 bytes per row, 10 rows per band (below the 16-row tile).
877        let b = g.plan(4000 + 8, 8, false, 8).unwrap();
878        assert!(b.iter().all(|b| b.y1 - b.y0 <= 10));
879        check_cover(&b, 100);
880    }
881
882    #[test]
883    fn plan_half_and_alignment() {
884        let g = geo(50, BandKind::Half { crop_y: 1 }, 101);
885        let b = g.plan(2008, 8, true, 0).unwrap();
886        check_cover(&b, 50);
887        assert_eq!(b[0], Band { y0: 0, y1: 10, in0: 1, in1: 21 });
888        let mut g = geo(9, BandKind::Direct, 9);
889        g.row_align = 2;
890        g.tile_rows = 1;
891        let b = g.plan(308, 8, true, 0).unwrap();
892        assert!(b.iter().all(|b| b.y0 % 2 == 0));
893        check_cover(&b, 9);
894        assert!(g.plan(108, 8, true, 0).is_err());
895    }
896
897    #[test]
898    fn band_rects_cover_every_orientation() {
899        let (dw, dh) = (7u32, 10u32);
900        for o in 1..=8u32 {
901            let (ow, oh) = if o >= 5 { (dh, dw) } else { (dw, dh) };
902            let mut seen = vec![0u8; (ow * oh) as usize];
903            for (y0, y1) in [(0, 3), (3, 4), (4, 10)] {
904                let r = band_rect(y0, y1, dw, dh, o);
905                assert!(r.right() <= ow && r.bottom() <= oh, "o {o} {r}");
906                // Every developed pixel of the band maps inside its rectangle.
907                for dy in y0..y1 {
908                    for dx in 0..dw {
909                        let (ox, oy) = match o {
910                            2 => (dw - 1 - dx, dy),
911                            3 => (dw - 1 - dx, dh - 1 - dy),
912                            4 => (dx, dh - 1 - dy),
913                            5 => (dy, dx),
914                            6 => (dh - 1 - dy, dx),
915                            7 => (dh - 1 - dy, dw - 1 - dx),
916                            8 => (dy, dw - 1 - dx),
917                            _ => (dx, dy),
918                        };
919                        assert!(r.contains_rect(&Rect::new(ox, oy, 1, 1)), "o {o} ({ox},{oy}) outside {r}");
920                        seen[(oy * ow + ox) as usize] += 1;
921                    }
922                }
923            }
924            assert!(seen.iter().all(|&s| s == 1), "o {o}");
925        }
926    }
927}