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