1use 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
26pub trait YuvPlanes: Send + Sync + fmt::Debug {
28 fn frame(&self) -> YuvFrame<'_>;
30}
31
32pub trait RgbaPixels: Send + Sync + fmt::Debug {
34 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#[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 #[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 #[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 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#[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 encoded_identity: bool,
190 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 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 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 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
275 fn bounds(&self, size: RenderSize) -> PixelBox {
276 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 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 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 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
377fn 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
411struct Plane<'a> {
414 bytes: &'a [u8],
415 stride: usize,
416 width: u32,
417 height: u32,
418 wide: bool,
419 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 #[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
463fn srgb_eotf(value: f32) -> f32 {
465 exact_srgb_to_linear(value.clamp(0.0, 1.0))
466}
467
468const 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 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 #[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 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 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 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 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 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 #[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 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
692fn 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
769fn 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#[derive(Clone, Copy, Default)]
780struct AxisTap {
781 inside: bool,
782 luma: (u32, u32, f32),
783 chroma: (u32, u32, f32),
784}
785
786impl YuvConversion {
787 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 #[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 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#[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 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 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 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 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 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 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 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 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 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 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}