Skip to main content

alpha2_render/cpu/
source.rs

1//! CPU graph sources, and the CPU port of yuv-to-linear.wesl and rgba-present.wesl that turns a
2//! decoded frame into a source picture.
3//!
4//! Planes are not copied: a source holds the decoder's buffers and converts on first draw.
5
6use std::fmt;
7use std::sync::atomic::Ordering;
8use std::sync::{Arc, OnceLock};
9
10use alpha2_media::{ColorMetadata, YuvFormat};
11
12use super::CpuRenderer;
13use super::curves::{exact_srgb_to_linear, extended_linear_to_srgb, linear_to_srgb};
14use super::half;
15use super::image::{CpuImage, PixelBox, Pixels};
16use crate::hdr::HdrToneMap;
17use crate::yuv::{
18    HdrHandling, PlaneStorage, PresentedEncoding, hdr_handling, presentation_uniform_bytes,
19    uniform_bytes, validate_presentation,
20};
21use crate::{
22    ColorDiagnostic, ColorPolicy, RenderError, RenderSize, SourceAlpha, SourcePlane,
23    SourcePresentation, YuvFrame,
24};
25
26/// Decoded YCbCr planes owned by the decoder and kept unchanged while any source holds them.
27pub trait YuvPlanes: Send + Sync + fmt::Debug {
28    /// The frame the planes hold.
29    fn frame(&self) -> YuvFrame<'_>;
30}
31
32/// RGBA pixels a source converts from: 8-bit, or half-float bits for linear sources.
33pub trait RgbaPixels: Send + Sync + fmt::Debug {
34    /// The pixel bytes, rows `stride` bytes apart.
35    fn plane(&self) -> SourcePlane<'_>;
36}
37
38impl RgbaPixels for Vec<u8> {
39    fn plane(&self) -> SourcePlane<'_> {
40        SourcePlane {
41            bytes: self,
42            stride: 0,
43        }
44    }
45}
46
47/// A CPU graph source ([`crate::SourceView::cpu`]) holding premultiplied sRGB RGBA, like every
48/// GPU source texture. Decoded frames convert on first draw and the result is kept.
49#[derive(Clone)]
50pub struct CpuSource {
51    inner: Arc<SourceInner>,
52}
53
54struct SourceInner {
55    content: Content,
56    presented: OnceLock<Arc<CpuImage>>,
57}
58
59enum Content {
60    Image(Arc<CpuImage>),
61    Yuv {
62        planes: Arc<dyn YuvPlanes>,
63        conversion: Box<YuvConversion>,
64    },
65    Rgba {
66        pixels: Arc<dyn RgbaPixels>,
67        stride: u32,
68        size: RenderSize,
69        geometry: Geometry,
70        encoding: PresentedEncoding,
71    },
72}
73
74impl fmt::Debug for CpuSource {
75    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
76        let kind = match &self.inner.content {
77            Content::Image(_) => "image",
78            Content::Yuv { .. } => "yuv",
79            Content::Rgba { .. } => "rgba",
80        };
81        formatter
82            .debug_struct("CpuSource")
83            .field("kind", &kind)
84            .field("presented", &self.inner.presented.get().is_some())
85            .finish()
86    }
87}
88
89impl CpuSource {
90    /// A source that shows `picture` as it is.
91    #[must_use]
92    pub fn image(picture: Arc<CpuImage>) -> Self {
93        Self::new(Content::Image(picture))
94    }
95
96    fn new(content: Content) -> Self {
97        Self {
98            inner: Arc::new(SourceInner {
99                content,
100                presented: OnceLock::new(),
101            }),
102        }
103    }
104
105    /// Bytes held: the picture once presented.
106    #[must_use]
107    pub fn retained_bytes(&self) -> u64 {
108        match &self.inner.content {
109            Content::Image(image) => image.retained_bytes(),
110            _ => self
111                .inner
112                .presented
113                .get()
114                .map_or(0, |image| image.retained_bytes()),
115        }
116    }
117
118    /// The picture the source shows, converting it on first use.
119    pub(crate) fn presented(&self, renderer: &CpuRenderer) -> Arc<CpuImage> {
120        if let Content::Image(image) = &self.inner.content {
121            return Arc::clone(image);
122        }
123        if let Some(image) = self.inner.presented.get() {
124            return Arc::clone(image);
125        }
126        let image = Arc::new(match &self.inner.content {
127            Content::Image(_) => unreachable!("handled above"),
128            Content::Yuv { planes, conversion } => {
129                present_yuv(renderer, &planes.frame(), conversion)
130            }
131            Content::Rgba {
132                pixels,
133                stride,
134                size,
135                geometry,
136                encoding,
137            } => present_rgba(
138                renderer,
139                &RgbaSource {
140                    bytes: pixels.plane().bytes,
141                    stride: *stride as usize,
142                    size: *size,
143                    encoding: *encoding,
144                },
145                geometry,
146            ),
147        });
148        renderer
149            .stats
150            .presented_sources
151            .fetch_add(1, Ordering::Relaxed);
152        Arc::clone(self.inner.presented.get_or_init(|| image))
153    }
154}
155
156/// Presentation uniforms parsed from the same bytes the GPU path binds, so both paths match.
157#[derive(Clone, Copy, Debug)]
158struct Geometry {
159    output: [f32; 2],
160    source: [f32; 2],
161    visible: [f32; 4],
162    draw: [f32; 4],
163    orientation: u32,
164    max_filter_samples: f32,
165}
166
167#[derive(Clone, Debug)]
168#[allow(clippy::struct_excessive_bools)]
169struct YuvConversion {
170    rows: [[f32; 3]; 3],
171    range_scale: [f32; 3],
172    range_offset: [f32; 3],
173    transfer: u32,
174    alpha: f32,
175    alpha_plane: bool,
176    alpha_scale: f32,
177    geometry: Geometry,
178    chroma_size: [f32; 2],
179    chroma_offset: [f32; 2],
180    interleaved: bool,
181    chroma_scale: [f32; 2],
182    gamut: [[f32; 3]; 3],
183    tone: [f32; 4],
184    hlg_gamma: f32,
185    chroma_window: [f32; 4],
186    format: YuvFormat,
187    /// SDR BT.709 without an alpha plane: decoding and re-encoding with sRGB is the identity, so
188    /// the encoded value is kept.
189    encoded_identity: bool,
190    /// The HDR policy skipped the source, so it shows nothing.
191    skipped: bool,
192}
193
194fn word(bytes: &[u8], offset: usize) -> [u8; 4] {
195    bytes[offset..offset + 4].try_into().expect("four bytes")
196}
197
198fn float(bytes: &[u8], offset: usize) -> f32 {
199    f32::from_ne_bytes(word(bytes, offset))
200}
201
202fn unsigned(bytes: &[u8], offset: usize) -> u32 {
203    u32::from_ne_bytes(word(bytes, offset))
204}
205
206fn floats<const N: usize>(bytes: &[u8], offset: usize) -> [f32; N] {
207    std::array::from_fn(|index| float(bytes, offset + index * 4))
208}
209
210impl YuvConversion {
211    /// Parses `YuvUniforms` (yuv-to-linear.wesl) as `uniform_bytes` writes them.
212    fn from_uniform(bytes: &[u8], format: YuvFormat) -> Self {
213        let rows = [floats::<3>(bytes, 0), floats(bytes, 16), floats(bytes, 32)];
214        let gamut = [
215            floats::<3>(bytes, 176),
216            floats(bytes, 192),
217            floats(bytes, 208),
218        ];
219        let transfer = unsigned(bytes, 80);
220        let alpha_plane = unsigned(bytes, 88) != 0;
221        let identity = gamut.iter().enumerate().all(|(row, values)| {
222            values.iter().enumerate().all(|(column, value)| {
223                let expected = if row == column { 1.0 } else { 0.0 };
224                (value - expected).abs() <= 1.0e-6
225            })
226        });
227        let size = floats::<4>(bytes, 96);
228        Self {
229            rows,
230            range_scale: floats(bytes, 48),
231            range_offset: floats(bytes, 64),
232            transfer,
233            alpha: float(bytes, 84),
234            alpha_plane,
235            alpha_scale: float(bytes, 92),
236            geometry: Geometry {
237                output: [size[0], size[1]],
238                source: [size[2], size[3]],
239                visible: floats(bytes, 112),
240                draw: floats(bytes, 128),
241                orientation: unsigned(bytes, 160),
242                max_filter_samples: float(bytes, 272),
243            },
244            chroma_size: floats(bytes, 144),
245            chroma_offset: floats(bytes, 152),
246            interleaved: unsigned(bytes, 164) == 1,
247            chroma_scale: floats(bytes, 168),
248            gamut,
249            tone: floats(bytes, 224),
250            hlg_gamma: float(bytes, 240),
251            chroma_window: floats(bytes, 256),
252            format,
253            encoded_identity: transfer == 0 && identity && !alpha_plane,
254            skipped: false,
255        }
256    }
257}
258
259impl Geometry {
260    /// From `PresentationParams` (rgba-present.wesl).
261    fn from_uniform(bytes: &[u8]) -> Self {
262        let size = floats::<4>(bytes, 0);
263        Self {
264            output: [size[0], size[1]],
265            source: [size[2], size[3]],
266            visible: floats(bytes, 16),
267            draw: floats(bytes, 32),
268            orientation: unsigned(bytes, 48),
269            max_filter_samples: float(bytes, 56),
270        }
271    }
272
273    /// Output pixels inside the drawn rectangle; everything else is transparent.
274    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
275    fn bounds(&self, size: RenderSize) -> PixelBox {
276        // Pixel centres p + 0.5 inside [draw.x, draw.x + draw.w).
277        let first = |origin: f32| (origin - 0.5).ceil().max(0.0) as u32;
278        let end = |origin: f32, extent: f32| ((origin + extent - 0.5).ceil()).max(0.0) as u32;
279        let x = first(self.draw[0]);
280        let y = first(self.draw[1]);
281        let right = end(self.draw[0], self.draw[2]).min(size.width);
282        let bottom = end(self.draw[1], self.draw[3]).min(size.height);
283        if right <= x || bottom <= y {
284            return PixelBox::EMPTY;
285        }
286        PixelBox {
287            x,
288            y,
289            width: right - x,
290            height: bottom - y,
291        }
292    }
293
294    fn inside(&self, destination: [f32; 2]) -> bool {
295        destination[0] >= self.draw[0]
296            && destination[1] >= self.draw[1]
297            && destination[0] < self.draw[0] + self.draw[2]
298            && destination[1] < self.draw[1] + self.draw[3]
299    }
300
301    /// `source_pixel_at`: the stored pixel shown at output `destination`, clamped to the visible
302    /// rect.
303    fn source_pixel_at(&self, destination: [f32; 2]) -> [f32; 2] {
304        let local = [
305            (destination[0] - self.draw[0]) / self.draw[2],
306            (destination[1] - self.draw[1]) / self.draw[3],
307        ];
308        let mut source = match self.orientation & 3 {
309            1 => [local[1], 1.0 - local[0]],
310            2 => [1.0 - local[0], 1.0 - local[1]],
311            3 => [1.0 - local[1], local[0]],
312            _ => local,
313        };
314        if self.orientation & 4 != 0 {
315            source[0] = 1.0 - source[0];
316        }
317        let [origin_x, origin_y, width, height] = self.visible;
318        [
319            (origin_x + source[0] * width).clamp(origin_x + 0.5, origin_x + width - 0.5),
320            (origin_y + source[1] * height).clamp(origin_y + 0.5, origin_y + height - 0.5),
321        ]
322    }
323
324    /// `filter_grid`: samples per output pixel along each axis.
325    fn filter_grid(&self) -> [f32; 2] {
326        let mut extent = [self.visible[2], self.visible[3]];
327        if self.orientation & 1 != 0 {
328            extent.swap(0, 1);
329        }
330        let wanted = |extent: f32, drawn: f32| {
331            let footprint = extent / drawn;
332            if footprint > 2.0 {
333                (footprint - 0.0001).ceil().clamp(1.0, 8.0)
334            } else {
335                1.0
336            }
337        };
338        let wanted = [
339            wanted(extent[0], self.draw[2]),
340            wanted(extent[1], self.draw[3]),
341        ];
342        let budget = self.max_filter_samples.clamp(1.0, 64.0);
343        let columns = wanted[0].min(budget);
344        let rows = wanted[1].min((budget / columns).floor().max(1.0));
345        [columns, rows]
346    }
347
348    /// `filtered_stored` around `stored_sample`.
349    fn filtered(&self, destination: [f32; 2], sample: impl Fn([f32; 2]) -> [f32; 4]) -> [f32; 4] {
350        let grid = self.filter_grid();
351        #[allow(clippy::float_cmp)]
352        if grid == [1.0, 1.0] {
353            return sample(self.source_pixel_at(destination));
354        }
355        let mut sum = [0.0_f32; 4];
356        #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
357        for row in 0..grid[1] as u32 {
358            for column in 0..grid[0] as u32 {
359                #[allow(clippy::cast_precision_loss)]
360                let offset = [
361                    (column as f32 + 0.5) / grid[0] - 0.5,
362                    (row as f32 + 0.5) / grid[1] - 0.5,
363                ];
364                let value = sample(
365                    self.source_pixel_at([destination[0] + offset[0], destination[1] + offset[1]]),
366                );
367                for channel in 0..4 {
368                    sum[channel] += value[channel];
369                }
370            }
371        }
372        let count = grid[0] * grid[1];
373        sum.map(|value| value / count)
374    }
375}
376
377/// Converts the presented picture row by row with `pixel` into a stored picture.
378fn present_rows(
379    renderer: &CpuRenderer,
380    size: RenderSize,
381    geometry: &Geometry,
382    pixel: impl Fn([f32; 2]) -> [f32; 4] + Sync + Send,
383) -> CpuImage {
384    let bounds = geometry.bounds(size);
385    if bounds.is_empty() {
386        return CpuImage::transparent(size);
387    }
388    let width = bounds.width as usize;
389    let mut pixels = vec![[0_u16; 4]; bounds.pixels()];
390    renderer
391        .workers
392        .bands(&mut pixels, width, |first_row, band| {
393            for (index, line) in band.chunks_mut(width).enumerate() {
394                #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
395                let y = (bounds.y as usize + first_row + index) as f32 + 0.5;
396                for (offset, out) in line.iter_mut().enumerate() {
397                    #[allow(clippy::cast_precision_loss)]
398                    let x = (bounds.x as usize + offset) as f32 + 0.5;
399                    let destination = [x, y];
400                    *out = if geometry.inside(destination) {
401                        half::store4(pixel(destination))
402                    } else {
403                        [0; 4]
404                    };
405                }
406            }
407        });
408    CpuImage::new(size, bounds, Pixels::Half(pixels))
409}
410
411/// One plane sampled like a clamp-to-edge texture: 8-bit codes as `code / 255`, 16-bit
412/// containers as `container / 65535`.
413struct Plane<'a> {
414    bytes: &'a [u8],
415    stride: usize,
416    width: u32,
417    height: u32,
418    wide: bool,
419    /// Samples per texel: 2 for interleaved chroma.
420    components: usize,
421}
422
423impl Plane<'_> {
424    #[inline]
425    fn texel(&self, x: i64, y: i64, component: usize) -> f32 {
426        #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
427        let (x, y) = (
428            x.clamp(0, i64::from(self.width) - 1) as usize,
429            y.clamp(0, i64::from(self.height) - 1) as usize,
430        );
431        let sample = x * self.components + component;
432        if self.wide {
433            let offset = y * self.stride + sample * 2;
434            f32::from(u16::from_le_bytes([
435                self.bytes[offset],
436                self.bytes[offset + 1],
437            ])) / 65535.0
438        } else {
439            half::unorm8_value(self.bytes[y * self.stride + sample])
440        }
441    }
442
443    /// Bilinear sample at normalized `position`.
444    #[inline]
445    fn sample(&self, position: [f32; 2], component: usize) -> f32 {
446        #[allow(clippy::cast_precision_loss)]
447        let texel = [
448            position[0] * self.width as f32 - 0.5,
449            position[1] * self.height as f32 - 0.5,
450        ];
451        let origin = [texel[0].floor(), texel[1].floor()];
452        let weight = [texel[0] - origin[0], texel[1] - origin[1]];
453        #[allow(clippy::cast_possible_truncation)]
454        let (x, y) = (origin[0] as i64, origin[1] as i64);
455        let top = self.texel(x, y, component)
456            + (self.texel(x + 1, y, component) - self.texel(x, y, component)) * weight[0];
457        let bottom = self.texel(x, y + 1, component)
458            + (self.texel(x + 1, y + 1, component) - self.texel(x, y + 1, component)) * weight[0];
459        top + (bottom - top) * weight[1]
460    }
461}
462
463/// The presentation shader's sRGB EOTF, clamped first.
464fn srgb_eotf(value: f32) -> f32 {
465    exact_srgb_to_linear(value.clamp(0.0, 1.0))
466}
467
468// SMPTE ST 2084 and ARIB STD-B67 as yuv-to-linear.wesl computes them in `f32`; `libm` keeps
469// every target identical.
470const PQ_M1: f32 = 0.159_301_76;
471const PQ_M2: f32 = 78.843_75;
472const PQ_C1: f32 = 0.835_937_5;
473const PQ_C2: f32 = 18.851_563;
474const PQ_C3: f32 = 18.6875;
475
476fn pq_eotf(code: f32) -> f32 {
477    let e = libm::powf(code.clamp(0.0, 1.0).max(1.0e-10), 1.0 / PQ_M2);
478    let y = libm::powf((e - PQ_C1).max(0.0) / (PQ_C2 - PQ_C3 * e), 1.0 / PQ_M1);
479    y * 10_000.0
480}
481
482fn pq_inverse_eotf(nits: f32) -> f32 {
483    let y = libm::powf((nits / 10_000.0).clamp(0.0, 1.0).max(1.0e-10), PQ_M1);
484    libm::powf((PQ_C1 + PQ_C2 * y) / (1.0 + PQ_C3 * y), PQ_M2)
485}
486
487fn hlg_inverse_oetf(code: f32) -> f32 {
488    let e = code.clamp(0.0, 1.0);
489    if e <= 0.5 {
490        return e * e / 3.0;
491    }
492    (libm::expf((e - 0.559_910_7) / 0.178_832_77) + 0.284_668_92) / 12.0
493}
494
495impl YuvConversion {
496    fn bt2390_eetf(&self, nits: f32) -> f32 {
497        let source = self.tone[2];
498        let value = pq_inverse_eotf(nits.min(self.tone[0])) / source;
499        let max_lum = self.tone[3] / source;
500        let knee = 1.5 * max_lum - 0.5;
501        let mut mapped = value;
502        if value >= knee && knee < 1.0 {
503            let t = (value - knee) / (1.0 - knee);
504            let t2 = t * t;
505            let t3 = t2 * t;
506            mapped = (2.0 * t3 - 3.0 * t2 + 1.0) * knee
507                + (t3 - 2.0 * t2 + t) * (1.0 - knee)
508                + (-2.0 * t3 + 3.0 * t2) * max_lum;
509        }
510        pq_eotf(mapped * source)
511    }
512
513    fn to_bt709(&self, rgb: [f32; 3]) -> [f32; 3] {
514        self.gamut
515            .map(|row| row[0] * rgb[0] + row[1] * rgb[1] + row[2] * rgb[2])
516    }
517
518    fn hdr_to_working(&self, encoded: [f32; 3]) -> [f32; 3] {
519        let nits = if self.transfer == 2 {
520            let scene = encoded.map(hlg_inverse_oetf);
521            let luma = (0.2627 * scene[0] + 0.678 * scene[1] + 0.0593 * scene[2]).max(0.0);
522            let gain = self.tone[0] * libm::powf(luma.max(1.0e-10), self.hlg_gamma - 1.0);
523            scene.map(|value| value * gain)
524        } else {
525            encoded.map(pq_eotf)
526        };
527        let converted = self.to_bt709(nits);
528        let largest = converted[0].max(converted[1].max(converted[2]));
529        let scale = if largest > 0.0 {
530            self.bt2390_eetf(largest) / largest
531        } else {
532            0.0
533        };
534        converted.map(|value| (value * scale / self.tone[1]).clamp(0.0, 1.0))
535    }
536
537    /// YCbCr at a luma and chroma position, decoded by range and matrix to sRGB-encoded R'G'B'.
538    fn encoded(&self, planes: &Planes<'_>, luma: [f32; 2], chroma: [f32; 2]) -> [f32; 3] {
539        let u = planes.u.sample(chroma, 0);
540        let v = if self.interleaved {
541            planes.u.sample(chroma, 1)
542        } else {
543            planes
544                .v
545                .as_ref()
546                .map_or(0.0, |plane| plane.sample(chroma, 0))
547        };
548        self.encoded_from([planes.y.sample(luma, 0), u, v])
549    }
550
551    /// Sampled codes decoded by range and matrix to sRGB-encoded R'G'B'.
552    #[inline]
553    fn encoded_from(&self, codes: [f32; 3]) -> [f32; 3] {
554        let yuv = [
555            codes[0] * self.range_scale[0] + self.range_offset[0],
556            codes[1] * self.range_scale[1] + self.range_offset[1],
557            codes[2] * self.range_scale[2] + self.range_offset[2],
558        ];
559        self.rows
560            .map(|row| row[0] * yuv[0] + row[1] * yuv[1] + row[2] * yuv[2])
561    }
562
563    /// `linear_rgb`.
564    fn linear_rgb(&self, planes: &Planes<'_>, luma: [f32; 2], chroma: [f32; 2]) -> [f32; 3] {
565        self.linear_from_encoded(self.encoded(planes, luma, chroma))
566    }
567
568    /// Encoded R'G'B' converted to the linear working space.
569    fn linear_from_encoded(&self, encoded: [f32; 3]) -> [f32; 3] {
570        if self.transfer != 0 {
571            return self.hdr_to_working(encoded.map(|value| value.clamp(0.0, 1.0)));
572        }
573        self.to_bt709(encoded.map(srgb_eotf))
574            .map(|value| value.clamp(0.0, 1.0))
575    }
576
577    /// `coordinates_at`: luma and chroma positions of `source_pixel`.
578    fn coordinates(&self, source_pixel: [f32; 2]) -> ([f32; 2], [f32; 2]) {
579        let chroma_pixel = [
580            ((source_pixel[0] + self.chroma_offset[0]) * self.chroma_scale[0])
581                .clamp(self.chroma_window[0], self.chroma_window[2]),
582            ((source_pixel[1] + self.chroma_offset[1]) * self.chroma_scale[1])
583                .clamp(self.chroma_window[1], self.chroma_window[3]),
584        ];
585        (
586            [
587                source_pixel[0] / self.geometry.source[0],
588                source_pixel[1] / self.geometry.source[1],
589            ],
590            [
591                chroma_pixel[0] / self.chroma_size[0],
592                chroma_pixel[1] / self.chroma_size[1],
593            ],
594        )
595    }
596
597    fn alpha_at(&self, planes: &Planes<'_>, luma: [f32; 2]) -> f32 {
598        planes.alpha.as_ref().map_or(1.0, |plane| {
599            (plane.sample(luma, 0) * self.alpha_scale).clamp(0.0, 1.0)
600        })
601    }
602
603    /// `premultiplied_linear`: straight alpha premultiplied before filtering.
604    fn premultiplied_linear(&self, planes: &Planes<'_>, source_pixel: [f32; 2]) -> [f32; 4] {
605        let (luma, chroma) = self.coordinates(source_pixel);
606        let texel = [
607            luma[0] * self.geometry.source[0] - 0.5,
608            luma[1] * self.geometry.source[1] - 0.5,
609        ];
610        let origin = [texel[0].floor(), texel[1].floor()];
611        let weight = [texel[0] - origin[0], texel[1] - origin[1]];
612        let first = [self.geometry.visible[0], self.geometry.visible[1]];
613        let last = [
614            first[0] + self.geometry.visible[2] - 1.0,
615            first[1] + self.geometry.visible[3] - 1.0,
616        ];
617        let corner = |dx: f32, dy: f32| {
618            self.coordinates([
619                (origin[0] + dx).clamp(first[0], last[0]) + 0.5,
620                (origin[1] + dy).clamp(first[1], last[1]) + 0.5,
621            ])
622        };
623        let corners = [
624            corner(0.0, 0.0),
625            corner(1.0, 0.0),
626            corner(0.0, 1.0),
627            corner(1.0, 1.0),
628        ];
629        let alphas = corners.map(|(luma, _)| self.alpha_at(planes, luma));
630        if alphas.iter().all(|alpha| *alpha >= 1.0) {
631            let rgb = self.linear_rgb(planes, luma, chroma);
632            return [rgb[0], rgb[1], rgb[2], 1.0];
633        }
634        let texels: Vec<[f32; 4]> = corners
635            .iter()
636            .zip(alphas)
637            .map(|((luma, chroma), alpha)| {
638                let rgb = self.linear_rgb(planes, *luma, *chroma);
639                [rgb[0] * alpha, rgb[1] * alpha, rgb[2] * alpha, alpha]
640            })
641            .collect();
642        let mix = |a: [f32; 4], b: [f32; 4], t: f32| {
643            [0, 1, 2, 3].map(|channel| a[channel] * (1.0 - t) + b[channel] * t)
644        };
645        mix(
646            mix(texels[0], texels[1], weight[0]),
647            mix(texels[2], texels[3], weight[0]),
648            weight[1],
649        )
650    }
651
652    /// `stored_sample`: one position as the intermediate stores presented sources (sRGB-encoded,
653    /// then premultiplied).
654    fn stored_sample(&self, planes: &Planes<'_>, source_pixel: [f32; 2]) -> [f32; 4] {
655        if !self.alpha_plane {
656            let (luma, chroma) = self.coordinates(source_pixel);
657            return self.stored_opaque(self.encoded(planes, luma, chroma));
658        }
659        Self::stored_linear(self.premultiplied_linear(planes, source_pixel))
660    }
661
662    /// An opaque sample's encoded R'G'B' as stored.
663    #[inline]
664    fn stored_opaque(&self, encoded: [f32; 3]) -> [f32; 4] {
665        if self.encoded_identity {
666            return [
667                encoded[0].clamp(0.0, 1.0),
668                encoded[1].clamp(0.0, 1.0),
669                encoded[2].clamp(0.0, 1.0),
670                1.0,
671            ];
672        }
673        let rgb = self.linear_from_encoded(encoded);
674        Self::stored_linear([rgb[0], rgb[1], rgb[2], 1.0])
675    }
676
677    /// Premultiplied linear light as stored.
678    fn stored_linear(linear: [f32; 4]) -> [f32; 4] {
679        if linear[3] <= 0.0 {
680            return [0.0; 4];
681        }
682        let alpha = linear[3];
683        [
684            encode_srgb(linear[0] / alpha) * alpha,
685            encode_srgb(linear[1] / alpha) * alpha,
686            encode_srgb(linear[2] / alpha) * alpha,
687            alpha,
688        ]
689    }
690}
691
692/// `encode_srgb` of yuv-to-linear.wesl; the input is already clamped.
693fn encode_srgb(value: f32) -> f32 {
694    linear_to_srgb(value)
695}
696
697struct Planes<'a> {
698    y: Plane<'a>,
699    u: Plane<'a>,
700    v: Option<Plane<'a>>,
701    alpha: Option<Plane<'a>>,
702}
703
704fn present_yuv(
705    renderer: &CpuRenderer,
706    frame: &YuvFrame<'_>,
707    conversion: &YuvConversion,
708) -> CpuImage {
709    fn plane<'a>(
710        source: &SourcePlane<'a>,
711        (width, height): (u32, u32),
712        wide: bool,
713        components: usize,
714    ) -> Plane<'a> {
715        Plane {
716            bytes: source.bytes,
717            stride: source.stride as usize,
718            width,
719            height,
720            wide,
721            components,
722        }
723    }
724    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
725    let output = RenderSize {
726        width: conversion.geometry.output[0] as u32,
727        height: conversion.geometry.output[1] as u32,
728    };
729    if conversion.skipped {
730        return CpuImage::transparent(output);
731    }
732    let format = conversion.format;
733    let wide = format.bytes_per_sample() == 2;
734    let (chroma_width, chroma_height) = format.chroma_size(frame.size.width, frame.size.height);
735    let planes = Planes {
736        y: plane(
737            &frame.planes[0],
738            (frame.size.width, frame.size.height),
739            wide,
740            1,
741        ),
742        u: plane(
743            &frame.planes[1],
744            (chroma_width, chroma_height),
745            wide,
746            if format.interleaved_chroma { 2 } else { 1 },
747        ),
748        v: (!format.interleaved_chroma)
749            .then(|| plane(&frame.planes[2], (chroma_width, chroma_height), wide, 1)),
750        alpha: frame
751            .alpha_plane
752            .as_ref()
753            .map(|alpha| plane(alpha, (frame.size.width, frame.size.height), wide, 1)),
754    };
755    let geometry = conversion.geometry;
756    let alpha = conversion.alpha;
757    if let Some(image) = present_separable(renderer, output, &planes, conversion) {
758        return image;
759    }
760    present_rows(renderer, output, &geometry, |destination| {
761        geometry
762            .filtered(destination, |source_pixel| {
763                conversion.stored_sample(&planes, source_pixel)
764            })
765            .map(|value| value * alpha)
766    })
767}
768
769/// Filters each output column between the two vertically filtered texels its tap names.
770fn lerp_into(values: &[f32], taps: &[(usize, usize, f32)], out: &mut [f32]) {
771    for (out, &(first, second, weight)) in out.iter_mut().zip(taps) {
772        let a = values[first];
773        *out = a + (values[second] - a) * weight;
774    }
775}
776
777/// Where one output column or row of an upright presentation reads: texels either side and the
778/// weight between, for luma and chroma.
779#[derive(Clone, Copy, Default)]
780struct AxisTap {
781    inside: bool,
782    luma: (u32, u32, f32),
783    chroma: (u32, u32, f32),
784}
785
786impl YuvConversion {
787    /// Taps of output `destination` along `axis` (0 = x), using exactly the arithmetic of
788    /// `source_pixel_at`, `coordinates_at` and a bilinear sample, which are separable for upright
789    /// and half-turned presentations.
790    fn axis_tap(&self, axis: usize, destination: f32, luma_size: u32, chroma_size: u32) -> AxisTap {
791        let geometry = &self.geometry;
792        let inside = destination >= geometry.draw[axis]
793            && destination < geometry.draw[axis] + geometry.draw[axis + 2];
794        let local = (destination - geometry.draw[axis]) / geometry.draw[axis + 2];
795        let mut source = if geometry.orientation & 3 == 2 {
796            1.0 - local
797        } else {
798            local
799        };
800        if axis == 0 && geometry.orientation & 4 != 0 {
801            source = 1.0 - source;
802        }
803        let origin = geometry.visible[axis];
804        let extent = geometry.visible[axis + 2];
805        let source_pixel = (origin + source * extent).clamp(origin + 0.5, origin + extent - 0.5);
806        let chroma_pixel = ((source_pixel + self.chroma_offset[axis]) * self.chroma_scale[axis])
807            .clamp(self.chroma_window[axis], self.chroma_window[axis + 2]);
808        let taps = |position: f32, size: u32| {
809            #[allow(clippy::cast_precision_loss)]
810            let texel = position * size as f32 - 0.5;
811            let floor = texel.floor();
812            let last = i64::from(size) - 1;
813            #[allow(clippy::cast_possible_truncation)]
814            let first = floor as i64;
815            #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
816            (
817                first.clamp(0, last) as u32,
818                (first + 1).clamp(0, last) as u32,
819                texel - floor,
820            )
821        };
822        AxisTap {
823            inside,
824            luma: taps(source_pixel / geometry.source[axis], luma_size),
825            chroma: taps(chroma_pixel / self.chroma_size[axis], chroma_size),
826        }
827    }
828}
829
830impl Plane<'_> {
831    /// Normalized `component` of texel `x` in row `y`.
832    #[inline]
833    fn at(&self, x: u32, y: u32, component: usize) -> f32 {
834        let sample = x as usize * self.components + component;
835        let row = y as usize * self.stride;
836        if self.wide {
837            let offset = row + sample * 2;
838            f32::from(u16::from_le_bytes([
839                self.bytes[offset],
840                self.bytes[offset + 1],
841            ])) / 65535.0
842        } else {
843            half::unorm8_value(self.bytes[row + sample])
844        }
845    }
846
847    /// `component` of texels from `first` in the rows `row` names, filtered between them, into
848    /// `out`.
849    fn vertical(&self, row: (u32, u32, f32), first: u32, component: usize, out: &mut [f32]) {
850        if !self.wide {
851            let count = out.len();
852            let line = |y: u32| {
853                let start = y as usize * self.stride + first as usize * self.components + component;
854                self.bytes[start..]
855                    .iter()
856                    .step_by(self.components)
857                    .take(count)
858                    .map(|&code| half::unorm8_value(code))
859            };
860            if row.2 == 0.0 {
861                for (value, top) in out.iter_mut().zip(line(row.0)) {
862                    *value = top;
863                }
864            } else {
865                for ((value, top), bottom) in out.iter_mut().zip(line(row.0)).zip(line(row.1)) {
866                    *value = top + (bottom - top) * row.2;
867                }
868            }
869            return;
870        }
871        for (offset, value) in out.iter_mut().enumerate() {
872            #[allow(clippy::cast_possible_truncation)]
873            let x = first + offset as u32;
874            let top = self.at(x, row.0, component);
875            *value = if row.2 == 0.0 {
876                top
877            } else {
878                top + (self.at(x, row.1, component) - top) * row.2
879            };
880        }
881    }
882}
883
884/// Row-wise fast path for an upright or half-turned source with no alpha plane or
885/// minification: taps are computed once per column and row, planes filtered vertically once
886/// per output row, then horizontally per pixel. Same taps as the per-pixel path, summed in the
887/// other order; `None` for other presentations.
888#[allow(clippy::too_many_lines)]
889fn present_separable(
890    renderer: &CpuRenderer,
891    output: RenderSize,
892    planes: &Planes<'_>,
893    conversion: &YuvConversion,
894) -> Option<CpuImage> {
895    let geometry = &conversion.geometry;
896    if conversion.alpha_plane || geometry.orientation & 1 != 0 || {
897        #[allow(clippy::float_cmp)]
898        let unfiltered = geometry.filter_grid() == [1.0, 1.0];
899        !unfiltered
900    } {
901        return None;
902    }
903    let bounds = geometry.bounds(output);
904    if bounds.is_empty() {
905        return Some(CpuImage::transparent(output));
906    }
907    #[allow(clippy::cast_precision_loss)]
908    let columns = (bounds.x..bounds.right())
909        .map(|x| conversion.axis_tap(0, x as f32 + 0.5, planes.y.width, planes.u.width))
910        .collect::<Vec<_>>();
911    let all_inside = columns.iter().all(|column| column.inside);
912    // Column taps relative to the first texel of each plane's row.
913    let (luma_base, chroma_base) = (
914        columns.iter().map(|tap| tap.luma.0).min().unwrap_or(0),
915        columns.iter().map(|tap| tap.chroma.0).min().unwrap_or(0),
916    );
917    let relative = |(first, second, weight): (u32, u32, f32), base: u32| {
918        ((first - base) as usize, (second - base) as usize, weight)
919    };
920    let luma_taps = columns
921        .iter()
922        .map(|tap| relative(tap.luma, luma_base))
923        .collect::<Vec<_>>();
924    let chroma_taps = columns
925        .iter()
926        .map(|tap| relative(tap.chroma, chroma_base))
927        .collect::<Vec<_>>();
928    let alpha = conversion.alpha;
929    let width = bounds.width as usize;
930    let reach = |pick: fn(&AxisTap) -> (u32, u32, f32)| {
931        let first = columns.iter().map(|tap| pick(tap).0).min().unwrap_or(0);
932        let last = columns.iter().map(|tap| pick(tap).1).max().unwrap_or(0);
933        (first, last)
934    };
935    let (luma_first, luma_last) = reach(|tap| tap.luma);
936    let (chroma_first, chroma_last) = reach(|tap| tap.chroma);
937    let mut pixels = vec![[0_u16; 4]; bounds.pixels()];
938    renderer
939        .workers
940        .bands(&mut pixels, width, |first_row, band| {
941            // Filter one output row's planes vertically, then each pixel horizontally.
942            let mut luma = vec![0.0_f32; (luma_last - luma_first + 1) as usize];
943            let chroma_len = (chroma_last - chroma_first + 1) as usize;
944            let mut u = vec![0.0_f32; chroma_len];
945            let mut v = vec![0.0_f32; chroma_len];
946            let (mut sample_y, mut sample_u, mut sample_v) =
947                (vec![0.0; width], vec![0.0; width], vec![0.0; width]);
948            for (index, line) in band.chunks_mut(width).enumerate() {
949                #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
950                let y = (bounds.y as usize + first_row + index) as f32 + 0.5;
951                let row = conversion.axis_tap(1, y, planes.y.height, planes.u.height);
952                if !row.inside {
953                    continue;
954                }
955                planes.y.vertical(row.luma, luma_first, 0, &mut luma);
956                planes.u.vertical(row.chroma, chroma_first, 0, &mut u);
957                if conversion.interleaved {
958                    planes.u.vertical(row.chroma, chroma_first, 1, &mut v);
959                } else if let Some(plane) = planes.v.as_ref() {
960                    plane.vertical(row.chroma, chroma_first, 0, &mut v);
961                }
962                let between =
963                    |values: &[f32], (first, second, weight): (u32, u32, f32), base: u32| {
964                        let a = values[(first - base) as usize];
965                        a + (values[(second - base) as usize] - a) * weight
966                    };
967                if conversion.encoded_identity && all_inside {
968                    // Inlined conversion, one pass at a time: samples, then range, matrix and
969                    // clamp.
970                    let [scale_y, scale_u, scale_v] = conversion.range_scale;
971                    let [offset_y, offset_u, offset_v] = conversion.range_offset;
972                    let [red, green, blue] = conversion.rows;
973                    let alpha_bits = half::store(alpha);
974                    lerp_into(&luma, &luma_taps, &mut sample_y);
975                    lerp_into(&u, &chroma_taps, &mut sample_u);
976                    lerp_into(&v, &chroma_taps, &mut sample_v);
977                    for (((out, y), cb), cr) in
978                        line.iter_mut().zip(&sample_y).zip(&sample_u).zip(&sample_v)
979                    {
980                        let y = y * scale_y + offset_y;
981                        let cb = cb * scale_u + offset_u;
982                        let cr = cr * scale_v + offset_v;
983                        let channel = |row: [f32; 3]| {
984                            half::store(
985                                (row[0] * y + row[1] * cb + row[2] * cr).clamp(0.0, 1.0) * alpha,
986                            )
987                        };
988                        *out = [channel(red), channel(green), channel(blue), alpha_bits];
989                    }
990                    continue;
991                }
992                for (out, column) in line.iter_mut().zip(&columns) {
993                    if !column.inside {
994                        continue;
995                    }
996                    let codes = [
997                        between(&luma, column.luma, luma_first),
998                        between(&u, column.chroma, chroma_first),
999                        between(&v, column.chroma, chroma_first),
1000                    ];
1001                    let stored = conversion.stored_opaque(conversion.encoded_from(codes));
1002                    *out = half::store4(stored.map(|value| value * alpha));
1003                }
1004            }
1005        });
1006    Some(CpuImage::new(output, bounds, Pixels::Half(pixels)))
1007}
1008
1009struct RgbaSource<'a> {
1010    bytes: &'a [u8],
1011    stride: usize,
1012    size: RenderSize,
1013    encoding: PresentedEncoding,
1014}
1015
1016impl RgbaSource<'_> {
1017    /// Texel at `(x, y)` with edge repeat: 8-bit codes, or half floats for linear sources.
1018    fn texel(&self, x: i64, y: i64) -> [f32; 4] {
1019        #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
1020        let (x, y) = (
1021            x.clamp(0, i64::from(self.size.width) - 1) as usize,
1022            y.clamp(0, i64::from(self.size.height) - 1) as usize,
1023        );
1024        if self.encoding == PresentedEncoding::Linear {
1025            let offset = y * self.stride + x * 8;
1026            std::array::from_fn(|channel| {
1027                half::load(u16::from_le_bytes([
1028                    self.bytes[offset + channel * 2],
1029                    self.bytes[offset + channel * 2 + 1],
1030                ]))
1031            })
1032        } else {
1033            let offset = y * self.stride + x * 4;
1034            std::array::from_fn(|channel| half::unorm8_value(self.bytes[offset + channel]))
1035        }
1036    }
1037
1038    /// `filtered_sample` of rgba-present.wesl: bilinear premultiplied color in the filtering space.
1039    fn filtered_sample(&self, source_pixel: [f32; 2]) -> [f32; 4] {
1040        let texel = [source_pixel[0] - 0.5, source_pixel[1] - 0.5];
1041        let origin = [texel[0].floor(), texel[1].floor()];
1042        let weight = [texel[0] - origin[0], texel[1] - origin[1]];
1043        #[allow(clippy::cast_possible_truncation)]
1044        let (x, y) = (origin[0] as i64, origin[1] as i64);
1045        let corners = [
1046            self.texel(x, y),
1047            self.texel(x + 1, y),
1048            self.texel(x, y + 1),
1049            self.texel(x + 1, y + 1),
1050        ];
1051        let mix = |a: [f32; 4], b: [f32; 4], t: f32| {
1052            [0, 1, 2, 3].map(|channel| a[channel] + (b[channel] - a[channel]) * t)
1053        };
1054        let bilinear = |texels: &[[f32; 4]; 4]| {
1055            mix(
1056                mix(texels[0], texels[1], weight[0]),
1057                mix(texels[2], texels[3], weight[0]),
1058                weight[1],
1059            )
1060        };
1061        if self.encoding != PresentedEncoding::Srgb(SourceAlpha::Straight) {
1062            return bilinear(&corners);
1063        }
1064        // Straight alpha is premultiplied before filtering.
1065        let premultiplied = corners.map(|texel| {
1066            [
1067                texel[0] * texel[3],
1068                texel[1] * texel[3],
1069                texel[2] * texel[3],
1070                texel[3],
1071            ]
1072        });
1073        if premultiplied.iter().all(|texel| texel[3] >= 1.0) {
1074            let source = bilinear(&corners);
1075            return [
1076                source[0] * source[3],
1077                source[1] * source[3],
1078                source[2] * source[3],
1079                source[3],
1080            ];
1081        }
1082        bilinear(&premultiplied)
1083    }
1084
1085    /// `stored_sample`.
1086    fn stored_sample(&self, source_pixel: [f32; 2]) -> [f32; 4] {
1087        let filtered = self.filtered_sample(source_pixel);
1088        if self.encoding != PresentedEncoding::Linear {
1089            return filtered;
1090        }
1091        if filtered[3] <= 0.0 {
1092            return [0.0; 4];
1093        }
1094        let alpha = filtered[3];
1095        [
1096            extended_linear_to_srgb(filtered[0] / alpha) * alpha,
1097            extended_linear_to_srgb(filtered[1] / alpha) * alpha,
1098            extended_linear_to_srgb(filtered[2] / alpha) * alpha,
1099            alpha,
1100        ]
1101    }
1102}
1103
1104fn present_rgba(renderer: &CpuRenderer, source: &RgbaSource<'_>, geometry: &Geometry) -> CpuImage {
1105    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
1106    let output = RenderSize {
1107        width: geometry.output[0] as u32,
1108        height: geometry.output[1] as u32,
1109    };
1110    present_rows(renderer, output, geometry, |destination| {
1111        geometry.filtered(destination, |source_pixel| {
1112            source.stored_sample(source_pixel)
1113        })
1114    })
1115}
1116
1117impl CpuRenderer {
1118    /// A source showing decoded YCbCr `planes` placed by `presentation`, converted on first draw;
1119    /// the CPU counterpart of [`crate::Renderer::prepare_yuv_presented`].
1120    ///
1121    /// # Errors
1122    /// [`RenderError::InvalidImageData`] for an unsupported format or short planes, and
1123    /// [`RenderError::UnsupportedHdr`] when [`ColorPolicy::RejectHdr`] meets an HDR source.
1124    pub fn present_yuv(
1125        &self,
1126        planes: Arc<dyn YuvPlanes>,
1127        presentation: SourcePresentation,
1128        policy: ColorPolicy,
1129    ) -> Result<(CpuSource, Vec<ColorDiagnostic>), RenderError> {
1130        let frame = planes.frame();
1131        let mut diagnostics = Vec::new();
1132        let color = frame.color.unwrap_or_else(|| {
1133            diagnostics.push(ColorDiagnostic::MissingMetadataFallback);
1134            ColorMetadata::UNDECLARED
1135        });
1136        let tone: Option<HdrToneMap> = match hdr_handling(color, policy, &mut diagnostics)? {
1137            HdrHandling::Sdr => None,
1138            HdrHandling::ToneMap(tone) => Some(tone),
1139            HdrHandling::Skip => {
1140                return Ok((
1141                    CpuSource::image(Arc::new(CpuImage::transparent(presentation.output_size))),
1142                    diagnostics,
1143                ));
1144            }
1145        };
1146        let format = frame.format;
1147        if !frame.alpha.is_finite()
1148            || !(0.0..=1.0).contains(&frame.alpha)
1149            || !format.is_supported()
1150            || format.alpha != frame.alpha_plane.is_some()
1151        {
1152            return Err(RenderError::InvalidImageData);
1153        }
1154        validate_presentation(frame.size, presentation)?;
1155        let (chroma_width, chroma_height) = format.chroma_size(frame.size.width, frame.size.height);
1156        let pairs = if format.interleaved_chroma { 2 } else { 1 };
1157        let specs = [
1158            (frame.size.width, frame.size.height, 1_u32),
1159            (chroma_width, chroma_height, pairs),
1160            (chroma_width, chroma_height, 1),
1161        ];
1162        for (plane, (width, height, samples)) in frame
1163            .planes
1164            .iter()
1165            .zip(specs)
1166            .take(format.color_plane_count())
1167            .chain(
1168                frame
1169                    .alpha_plane
1170                    .iter()
1171                    .map(|plane| (plane, (frame.size.width, frame.size.height, 1))),
1172            )
1173        {
1174            let row_bytes = width
1175                .checked_mul(samples)
1176                .and_then(|row| row.checked_mul(format.bytes_per_sample()))
1177                .ok_or(RenderError::InvalidImageData)?;
1178            crate::yuv::validate_plane(plane.bytes, plane.stride, row_bytes, height)?;
1179        }
1180        let storage = if format.bytes_per_sample() == 1 {
1181            PlaneStorage::Unorm8
1182        } else {
1183            PlaneStorage::Unorm16
1184        };
1185        #[allow(clippy::cast_precision_loss)]
1186        let bytes = uniform_bytes(
1187            color,
1188            tone,
1189            frame.alpha,
1190            frame.size,
1191            presentation,
1192            format,
1193            storage.code_scale(format),
1194            self.quality_controls().max_filter_samples as f32,
1195        );
1196        let conversion = Box::new(YuvConversion::from_uniform(&bytes, format));
1197        Ok((
1198            CpuSource::new(Content::Yuv { planes, conversion }),
1199            diagnostics,
1200        ))
1201    }
1202
1203    /// A source showing RGBA8 `pixels` placed by `presentation`; the CPU counterpart of
1204    /// [`crate::Renderer::prepare_rgba_presented`]. A zero `stride` means packed rows.
1205    ///
1206    /// # Errors
1207    /// [`RenderError::InvalidImageData`] for short pixels or an invalid presentation.
1208    pub fn present_rgba8(
1209        &self,
1210        pixels: Arc<dyn RgbaPixels>,
1211        size: RenderSize,
1212        stride: u32,
1213        presentation: SourcePresentation,
1214        alpha: SourceAlpha,
1215    ) -> Result<CpuSource, RenderError> {
1216        self.present_rgba(
1217            pixels,
1218            size,
1219            stride,
1220            4,
1221            presentation,
1222            PresentedEncoding::Srgb(alpha),
1223        )
1224    }
1225
1226    /// A source showing premultiplied linear BT.709 RGBA half-float bits (`1.0` = SDR white); the
1227    /// CPU counterpart of [`crate::Renderer::prepare_linear_presented`]. A zero `stride` means
1228    /// packed rows.
1229    ///
1230    /// # Errors
1231    /// [`RenderError::InvalidImageData`] for short pixels or an invalid presentation.
1232    pub fn present_linear_rgba16f(
1233        &self,
1234        pixels: Arc<dyn RgbaPixels>,
1235        size: RenderSize,
1236        stride: u32,
1237        presentation: SourcePresentation,
1238    ) -> Result<CpuSource, RenderError> {
1239        self.present_rgba(
1240            pixels,
1241            size,
1242            stride,
1243            8,
1244            presentation,
1245            PresentedEncoding::Linear,
1246        )
1247    }
1248
1249    fn present_rgba(
1250        &self,
1251        pixels: Arc<dyn RgbaPixels>,
1252        size: RenderSize,
1253        stride: u32,
1254        bytes_per_pixel: u32,
1255        presentation: SourcePresentation,
1256        encoding: PresentedEncoding,
1257    ) -> Result<CpuSource, RenderError> {
1258        validate_presentation(size, presentation)?;
1259        let row_bytes = size
1260            .width
1261            .checked_mul(bytes_per_pixel)
1262            .ok_or(RenderError::InvalidImageData)?;
1263        let stride = if stride == 0 { row_bytes } else { stride };
1264        crate::yuv::validate_plane(pixels.plane().bytes, stride, row_bytes, size.height)?;
1265        #[allow(clippy::cast_precision_loss)]
1266        let bytes = presentation_uniform_bytes(
1267            size,
1268            presentation,
1269            encoding,
1270            self.quality_controls().max_filter_samples as f32,
1271        );
1272        Ok(CpuSource::new(Content::Rgba {
1273            pixels,
1274            stride,
1275            size,
1276            geometry: Geometry::from_uniform(&bytes),
1277            encoding,
1278        }))
1279    }
1280}