Skip to main content

alpha2_render/cpu/
pack.rs

1//! Packs the picture into an encoder's planar YUV420 frame exactly as rgb-to-yuv420.wesl does,
2//! writing straight into the caller's planes.
3
4use alpha2_media::YuvEncoding;
5
6use super::half;
7use super::image::CpuImage;
8use super::workers::Workers;
9use crate::{ReadbackFailure, RenderError, Yuv420pTargets};
10
11/// The pack's coefficients, as the shader's `YuvParams`.
12#[derive(Copy, Clone)]
13struct Coding {
14    kr: f32,
15    kg: f32,
16    kb: f32,
17    luma_black: f32,
18    luma_span: f32,
19    chroma_center: f32,
20    chroma_span: f32,
21    cosited: [bool; 2],
22}
23
24impl Coding {
25    fn new(encoding: YuvEncoding) -> Self {
26        let values = crate::readback::yuv_uniform(encoding);
27        Self {
28            kr: values[0],
29            kg: values[1],
30            kb: values[2],
31            luma_black: values[4],
32            luma_span: values[5],
33            chroma_center: values[6],
34            chroma_span: values[7],
35            cosited: [values[8] != 0.0, values[9] != 0.0],
36        }
37    }
38
39    #[inline]
40    fn luma(self, rgb: [f32; 3]) -> f32 {
41        rgb[0] * self.kr + rgb[1] * self.kg + rgb[2] * self.kb
42    }
43
44    #[inline]
45    fn yuv(self, rgb: [f32; 3]) -> [f32; 3] {
46        let luma = self.luma(rgb);
47        [
48            self.luma_black + self.luma_span * luma,
49            self.chroma_center + self.chroma_span * (rgb[2] - luma) / (2.0 * (1.0 - self.kb)),
50            self.chroma_center + self.chroma_span * (rgb[0] - luma) / (2.0 * (1.0 - self.kr)),
51        ]
52    }
53}
54
55/// Weight of the luma sample `offset` from a chroma sample's even column or row: 1:2:1 when
56/// co-sited, the straddling pair when centred.
57fn tap_weight(offset: i64, cosited: bool) -> f32 {
58    if cosited {
59        if offset == 0 { 0.5 } else { 0.25 }
60    } else if offset >= 0 {
61        0.5
62    } else {
63        0.0
64    }
65}
66
67/// Luma taps a chroma sample sums, in summation order: line of `lines` (rows 2j - 1 to 2j + 1),
68/// column offset from the even column plus one, and weight.
69fn chroma_taps(coding: Coding) -> Vec<(usize, usize, f32)> {
70    let mut taps = Vec::with_capacity(9);
71    for dy in -1..=1_i64 {
72        let row_weight = tap_weight(dy, coding.cosited[1]);
73        if row_weight == 0.0 {
74            continue;
75        }
76        for dx in -1..=1_i64 {
77            let column_weight = tap_weight(dx, coding.cosited[0]);
78            if column_weight == 0.0 {
79                continue;
80            }
81            #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
82            taps.push((
83                (dy + 1) as usize,
84                (dx + 1) as usize,
85                row_weight * column_weight,
86            ));
87        }
88    }
89    taps
90}
91
92/// One chroma row filtered from the surrounding picture rows as the shader does; columns left
93/// of the picture repeat the edge.
94fn chroma_row(
95    lines: &[Vec<[f32; 3]>; 3],
96    coding: Coding,
97    taps: &[(usize, usize, f32)],
98    u: &mut [u8],
99    v: &mut [u8],
100) {
101    for (column, (u, v)) in u.iter_mut().zip(v.iter_mut()).enumerate() {
102        let x = column * 2;
103        let mut rgb = [0.0_f32; 3];
104        if !coding.cosited[0] && !coding.cosited[1] {
105            for sample in [lines[1][x], lines[1][x + 1], lines[2][x], lines[2][x + 1]] {
106                for channel in 0..3 {
107                    rgb[channel] += sample[channel];
108                }
109            }
110            rgb = rgb.map(|value| value * 0.25);
111        } else {
112            for &(line, offset, weight) in taps {
113                // The width is even, so only the left edge repeats.
114                let sample = lines[line][(x + offset).saturating_sub(1)];
115                for channel in 0..3 {
116                    rgb[channel] += sample[channel] * weight;
117                }
118            }
119        }
120        let yuv = coding.yuv(rgb);
121        *u = half::unorm8(yuv[1]);
122        *v = half::unorm8(yuv[2]);
123    }
124}
125
126/// Packs `picture` into `targets` with `encoding`.
127///
128/// # Errors
129/// [`RenderError::TextureReadback`] for an odd-sized picture or a target too small.
130pub(crate) fn pack_yuv420p(
131    workers: &Workers,
132    picture: &CpuImage,
133    encoding: YuvEncoding,
134    targets: &mut Yuv420pTargets<'_>,
135) -> Result<(), RenderError> {
136    let size = picture.size();
137    if !size.width.is_multiple_of(2) || !size.height.is_multiple_of(2) {
138        return Err(RenderError::TextureReadback(
139            ReadbackFailure::UnsupportedTarget,
140        ));
141    }
142    let (width, height) = (size.width as usize, size.height as usize);
143    let (chroma_width, chroma_height) = (width / 2, height / 2);
144    targets.y.fits(width, height)?;
145    targets.u.fits(chroma_width, chroma_height)?;
146    targets.v.fits(chroma_width, chroma_height)?;
147    let coding = Coding::new(encoding);
148    let taps = chroma_taps(coding);
149    let (y_stride, u_stride, v_stride) = (targets.y.stride, targets.u.stride, targets.v.stride);
150    // Each band writes one chroma row and its two luma rows.
151    let mut lumas = targets.y.bytes.chunks_mut(y_stride);
152    let mut rows = Vec::with_capacity(chroma_height);
153    for (u, v) in targets
154        .u
155        .bytes
156        .chunks_mut(u_stride)
157        .zip(targets.v.bytes.chunks_mut(v_stride))
158        .take(chroma_height)
159    {
160        let top = lumas.next().expect("the luma plane fits");
161        let bottom = lumas.next().expect("the luma plane fits");
162        rows.push((top, bottom, u, v));
163    }
164    workers.bands(&mut rows, 1, |first, band| {
165        // Picture rows around this chroma row, edge-clamped like the shader's loads.
166        let mut lines = [
167            vec![[0.0_f32; 3]; width],
168            vec![[0.0_f32; 3]; width],
169            vec![[0.0_f32; 3]; width],
170        ];
171        let mut decoded = vec![[0.0_f32; 4]; width];
172        for (index, (top, bottom, u, v)) in band.iter_mut().enumerate() {
173            let row = first + index;
174            // Rows 2j - 1 (co-sited only), 2j and 2j + 1, edge repeated.
175            let first_line = usize::from(!coding.cosited[1]);
176            for (offset, line) in lines.iter_mut().enumerate().skip(first_line) {
177                let y = (row * 2 + offset).saturating_sub(1).min(height - 1);
178                #[allow(clippy::cast_possible_truncation)]
179                picture.decode_row(y as u32, 0, &mut decoded);
180                for (rgb, texel) in line.iter_mut().zip(&decoded) {
181                    *rgb = [
182                        texel[0].clamp(0.0, 1.0),
183                        texel[1].clamp(0.0, 1.0),
184                        texel[2].clamp(0.0, 1.0),
185                    ];
186                }
187            }
188            for (luma_row, line) in [(&mut **top, &lines[1]), (&mut **bottom, &lines[2])] {
189                for (out, rgb) in luma_row[..width].iter_mut().zip(line) {
190                    *out = half::unorm8(coding.luma_black + coding.luma_span * coding.luma(*rgb));
191                }
192            }
193            chroma_row(
194                &lines,
195                coding,
196                &taps,
197                &mut u[..chroma_width],
198                &mut v[..chroma_width],
199            );
200        }
201    });
202    Ok(())
203}
204
205/// Packs `picture` into `layout`, one target per plane in storage order
206/// ([`alpha2_media::PixelLayout::planes`]), as the GPU texel readback does
207/// ([`crate::Yuv420pReadback::finish_into_planes`]).
208///
209/// # Errors
210/// [`RenderError::TextureReadback`] for a size the chroma cannot subsample, a wrong target
211/// count, or a target too small.
212pub fn pack_cpu_image(
213    picture: &CpuImage,
214    encoding: YuvEncoding,
215    layout: alpha2_media::PixelLayout,
216    targets: &mut [crate::PlaneTarget<'_>],
217) -> Result<(), RenderError> {
218    let size = picture.size();
219    let row = |y: usize, out: &mut [[f32; 4]]| {
220        #[allow(clippy::cast_possible_truncation)]
221        picture.decode_row(y as u32, 0, out);
222    };
223    let rows = crate::layout_pack::Picture {
224        width: size.width as usize,
225        height: size.height as usize,
226        row: &row,
227    };
228    crate::layout_pack::pack(&rows, encoding, layout, targets)
229}