1use super::{Format, Plane};
16use crate::kernels::pool::SyncPtr;
17use std::sync::atomic::{AtomicUsize, Ordering};
18use std::sync::OnceLock;
19
20#[derive(Clone, Copy, PartialEq, Eq, Debug)]
22pub enum Transfer {
23 Pq,
24 Hlg,
25}
26
27#[derive(Clone, Copy, PartialEq, Eq, Debug)]
29pub enum Matrix {
30 Bt709,
31 Bt601,
32 Bt2020,
33}
34
35#[derive(Clone, Copy, PartialEq, Eq, Debug)]
38pub enum Packing {
39 Rgba8,
40 Rgb10a2,
41}
42
43pub const ABOVE_WHITE: f32 = 1.5;
45
46#[allow(clippy::too_many_arguments)]
49pub fn planes_to_rgba(
50 src: &[u8],
51 format: Format,
52 planes: &[Plane],
53 width: usize,
54 height: usize,
55 matrix: Matrix,
56 full: bool,
57 transfer: Transfer,
58 out: &mut [u8],
59) -> usize {
60 let (kr, kb) = match matrix {
61 Matrix::Bt709 => (0.2126f32, 0.0722f32),
62 Matrix::Bt601 => (0.299, 0.114),
63 Matrix::Bt2020 => (0.2627, 0.0593),
64 };
65 let kg = 1.0 - kr - kb;
66 let (ys, yo, cs) = if full {
68 (1.0 / 255.0, 0.0, 1.0 / 255.0)
69 } else {
70 (1.0 / 219.0, 16.0, 1.0 / 224.0)
71 };
72 let (rv, bu) = (2.0 * (1.0 - kr) * cs, 2.0 * (1.0 - kb) * cs);
73 let (gu, gv) = (
74 2.0 * kb * (1.0 - kb) / kg * cs,
75 2.0 * kr * (1.0 - kr) / kg * cs,
76 );
77 let luma: [f32; 256] = std::array::from_fn(|y| (y as f32 - yo) * ys);
78 let (eotf, oetf) = (eotf_table(transfer), srgb_table());
79 let grey: [([u8; 4], bool); 256] = std::array::from_fn(|y| pixel([luma[y]; 3], eotf, oetf));
82 let (shift, chroma_rows) = match format {
83 Format::I444 => (0, 1),
84 Format::I422 => (1, 1),
85 Format::I420 | Format::Nv12 => (1, 2),
86 };
87 rows(width, height, out, |y, row, memo| {
88 let line =
89 |i: usize, r: usize, len: usize| &src[planes[i].offset + r * planes[i].stride..][..len];
90 let cw = if shift == 0 { width } else { width.div_ceil(2) };
91 let cy = y / chroma_rows;
92 let ls = line(0, y, width);
93 let (us, vs, interleaved) = if format == Format::Nv12 {
94 (line(1, cy, cw * 2), &[][..], true)
95 } else {
96 (line(1, cy, cw), line(2, cy, cw), false)
97 };
98 let mut above = 0;
99 for (x, px) in row.chunks_exact_mut(4).enumerate() {
100 let c = x >> shift;
101 let (u, v) = if interleaved {
102 (us[c * 2], us[c * 2 + 1])
103 } else {
104 (us[c], vs[c])
105 };
106 let (p, bright) = if u == 128 && v == 128 {
107 grey[ls[x] as usize]
108 } else {
109 let key = (ls[x] as u32) << 16 | (u as u32) << 8 | v as u32;
110 memo.get(key, || {
111 let (yy, u, v) = (luma[ls[x] as usize], u as f32 - 128.0, v as f32 - 128.0);
112 pixel([yy + rv * v, yy - gu * u - gv * v, yy + bu * u], eotf, oetf)
113 })
114 };
115 px.copy_from_slice(&p);
116 above += bright as usize;
117 }
118 above
119 })
120}
121
122pub fn rgb_to_rgba(
125 pixels: &mut [u8],
126 packing: Packing,
127 width: usize,
128 height: usize,
129 transfer: Transfer,
130) -> usize {
131 let (eotf, oetf) = (eotf_table(transfer), srgb_table());
132 let top = match packing {
134 Packing::Rgba8 => 255,
135 Packing::Rgb10a2 => 1023,
136 };
137 let linear: Vec<f32> = (0..=top)
140 .map(|code| linear_value(code as f32 / top as f32, eotf))
141 .collect();
142 let grey: Vec<([u8; 4], bool)> = (0..=top)
143 .map(|c| pixel([c as f32 / top as f32; 3], eotf, oetf))
144 .collect();
145 rows(width, height, pixels, |_, row, _| {
146 let mut above = 0;
147 for px in row.chunks_exact_mut(4) {
148 let s = [px[0], px[1], px[2], px[3]];
149 let codes = match packing {
150 Packing::Rgba8 => [s[0], s[1], s[2]].map(u32::from),
151 Packing::Rgb10a2 => {
152 let w = u32::from_le_bytes([s[0], s[1], s[2], s[3]]);
153 [w, w >> 10, w >> 20].map(|v| v & 1023)
154 }
155 };
156 let (p, bright) = if codes[0] == codes[1] && codes[1] == codes[2] {
157 grey[codes[0] as usize]
158 } else {
159 pixel_linear(codes.map(|v| linear[v as usize]), oetf)
160 };
161 px.copy_from_slice(&p);
162 above += bright as usize;
163 }
164 above
165 })
166}
167
168const EOTF_SIZE: usize = 4096;
172const SRGB_SIZE: usize = 16384;
173
174const BT2020_TO_709: [[f32; 3]; 3] = [
176 [1.660_491, -0.587_641, -0.072_850],
177 [-0.124_550, 1.132_9, -0.008_349],
178 [-0.018_151, -0.100_579, 1.118_73],
179];
180
181fn eotf_table(transfer: Transfer) -> &'static [f32] {
183 static PQ: OnceLock<Vec<f32>> = OnceLock::new();
184 static HLG: OnceLock<Vec<f32>> = OnceLock::new();
185 let (cell, f): (_, fn(f64) -> f64) = match transfer {
186 Transfer::Pq => (&PQ, |e| pq_eotf(e) / PQ_WHITE),
187 Transfer::Hlg => (&HLG, |e| hlg_inverse_oetf(e) / hlg_inverse_oetf(0.75)),
188 };
189 cell.get_or_init(|| {
190 (0..EOTF_SIZE)
191 .map(|i| f(i as f64 / (EOTF_SIZE - 1) as f64) as f32)
192 .collect()
193 })
194}
195
196fn srgb_table() -> &'static [u8] {
198 static SRGB: OnceLock<Vec<u8>> = OnceLock::new();
199 SRGB.get_or_init(|| {
200 (0..SRGB_SIZE)
201 .map(|i| {
202 let l = i as f64 / (SRGB_SIZE - 1) as f64;
203 let v = if l <= 0.003_130_8 {
204 12.92 * l
205 } else {
206 1.055 * libm::pow(l, 1.0 / 2.4) - 0.055
207 };
208 (v * 255.0).round() as u8
209 })
210 .collect()
211 })
212}
213
214const PQ_WHITE: f64 = 100.0;
216
217fn pq_eotf(e: f64) -> f64 {
219 let (m1, m2, c1, c2, c3) = (
220 0.159_301_757_812_5,
221 78.843_75,
222 0.835_937_5,
223 18.851_562_5,
224 18.687_5,
225 );
226 let p = libm::pow(e.clamp(0.0, 1.0), 1.0 / m2);
227 10000.0 * libm::pow((p - c1).max(0.0) / (c2 - c3 * p), 1.0 / m1)
228}
229
230fn hlg_inverse_oetf(e: f64) -> f64 {
232 let (a, b) = (0.178_832_77f64, 1.0 - 4.0 * 0.178_832_77);
233 let c = 0.5 - a * libm::log(4.0 * a);
234 let e = e.clamp(0.0, 1.0);
235 if e <= 0.5 {
236 e * e / 3.0
237 } else {
238 (libm::exp((e - c) / a) + b) / 12.0
239 }
240}
241
242#[inline]
244fn pixel(rgb: [f32; 3], eotf: &[f32], srgb: &[u8]) -> ([u8; 4], bool) {
245 pixel_linear(rgb.map(|v| linear_value(v, eotf)), srgb)
246}
247
248fn linear_value(v: f32, eotf: &[f32]) -> f32 {
250 let at = v.clamp(0.0, 1.0) * (EOTF_SIZE - 1) as f32;
251 let i = (at as usize).min(EOTF_SIZE - 2);
252 eotf[i] + (at - i as f32) * (eotf[i + 1] - eotf[i])
253}
254
255fn pixel_linear(lin: [f32; 3], srgb: &[u8]) -> ([u8; 4], bool) {
256 let out = BT2020_TO_709.map(|m| {
257 let l = m[0] * lin[0] + m[1] * lin[1] + m[2] * lin[2];
258 srgb[(l.clamp(0.0, 1.0) * (SRGB_SIZE - 1) as f32 + 0.5) as usize]
259 });
260 (
261 [out[0], out[1], out[2], 255],
262 lin[0].max(lin[1]).max(lin[2]) > ABOVE_WHITE,
263 )
264}
265
266const MEMO_SIZE: usize = 4096;
268
269pub(crate) struct Memo([u64; MEMO_SIZE]);
275
276impl Memo {
277 #[inline]
278 fn get(&mut self, key: u32, compute: impl FnOnce() -> ([u8; 4], bool)) -> ([u8; 4], bool) {
279 let slot = (key.wrapping_mul(0x9e37_79b1) >> (32 - MEMO_SIZE.trailing_zeros())) as usize;
280 let e = self.0[slot];
281 if (e >> 32) as u32 == key + 1 {
282 let [r, g, b, bright] = (e as u32).to_le_bytes();
283 return ([r, g, b, 255], bright != 0);
284 }
285 let (p, bright) = compute();
286 self.0[slot] =
287 ((key + 1) as u64) << 32 | u32::from_le_bytes([p[0], p[1], p[2], bright as u8]) as u64;
288 (p, bright)
289 }
290}
291
292fn rows<F: Fn(usize, &mut [u8], &mut Memo) -> usize + Sync>(
295 width: usize,
296 height: usize,
297 out: &mut [u8],
298 row: F,
299) -> usize {
300 let chunks = crate::kernels::pool::chunks_for(width * height, 64 * 1024);
301 let dst = SyncPtr(out.as_mut_ptr());
302 let above = AtomicUsize::new(0);
303 crate::kernels::pool::par_for(chunks, |c| {
304 let mut count = 0;
305 let mut memo = Memo([0; MEMO_SIZE]);
306 for y in crate::kernels::pool::split(height, chunks, c)
307 ..crate::kernels::pool::split(height, chunks, c + 1)
308 {
309 let line =
311 unsafe { std::slice::from_raw_parts_mut(dst.get().add(y * width * 4), width * 4) };
312 count += row(y, line, &mut memo);
313 }
314 above.fetch_add(count, Ordering::Relaxed);
315 });
316 above.into_inner()
317}
318
319#[cfg(test)]
320mod tests {
321 use super::*;
322
323 #[test]
334 fn memo_returns_what_the_computation_returned_for_every_key() {
335 let colour = |key: u32| {
336 let h = key.wrapping_mul(0x85eb_ca6b) ^ key >> 7;
337 (
338 [h as u8, (h >> 8) as u8, (h >> 16) as u8, 255],
339 h >> 24 & 1 == 1,
340 )
341 };
342 let mut memo = Memo([0; MEMO_SIZE]);
343 let mut seed = 0x2545_f491u32;
344 let mut keys = vec![0, 0, 0xff_ffff, 0xff_ffff, 0x80_8080];
345 for _ in 0..200_000 {
346 seed ^= seed << 13;
347 seed ^= seed >> 17;
348 seed ^= seed << 5;
349 keys.push((seed % 6000 * 2797) & 0xff_ffff);
351 }
352 for key in keys {
353 assert_eq!(memo.get(key, || colour(key)), colour(key), "{key:06x}");
354 }
355 }
356
357 const KR_KB: [(f64, f64); 3] = [(0.2126, 0.0722), (0.299, 0.114), (0.2627, 0.0593)];
358 const M709_TO_2020: [[f64; 3]; 3] = [
359 [0.627_404, 0.329_282, 0.043_314],
360 [0.069_097, 0.919_540, 0.011_361],
361 [0.016_392, 0.088_013, 0.895_595],
362 ];
363
364 fn pq_oetf(nits: f64) -> f64 {
365 let (m1, m2, c1, c2, c3) = (
366 0.159_301_757_812_5,
367 78.843_75,
368 0.835_937_5,
369 18.851_562_5,
370 18.687_5,
371 );
372 let p = libm::pow(nits.max(0.0) / 10000.0, m1);
373 libm::pow((c1 + c2 * p) / (1.0 + c3 * p), m2)
374 }
375 fn pq_eotf(e: f64) -> f64 {
376 let (m1, m2, c1, c2, c3) = (
377 0.159_301_757_812_5,
378 78.843_75,
379 0.835_937_5,
380 18.851_562_5,
381 18.687_5,
382 );
383 let p = libm::pow(e.clamp(0.0, 1.0), 1.0 / m2);
384 10000.0 * libm::pow((p - c1).max(0.0) / (c2 - c3 * p), 1.0 / m1)
385 }
386 const HLG_A: f64 = 0.178_832_77;
387 const HLG_B: f64 = 1.0 - 4.0 * HLG_A;
388 fn hlg_c() -> f64 {
389 0.5 - HLG_A * libm::log(4.0 * HLG_A)
390 }
391 fn hlg_oetf(e: f64) -> f64 {
392 if e <= 1.0 / 12.0 {
393 (3.0 * e).sqrt()
394 } else {
395 HLG_A * libm::log(12.0 * e - HLG_B) + hlg_c()
396 }
397 }
398 fn hlg_inverse(v: f64) -> f64 {
399 let v = v.clamp(0.0, 1.0);
400 if v <= 0.5 {
401 v * v / 3.0
402 } else {
403 (libm::exp((v - hlg_c()) / HLG_A) + HLG_B) / 12.0
404 }
405 }
406 fn hlg_white() -> f64 {
408 hlg_inverse(0.75)
409 }
410 fn srgb_eotf(v: f64) -> f64 {
411 if v <= 0.040_45 {
412 v / 12.92
413 } else {
414 libm::pow((v + 0.055) / 1.055, 2.4)
415 }
416 }
417 fn srgb_oetf(l: f64) -> f64 {
418 let l = l.clamp(0.0, 1.0);
419 if l <= 0.003_130_8 {
420 12.92 * l
421 } else {
422 1.055 * libm::pow(l, 1.0 / 2.4) - 0.055
423 }
424 }
425 fn encode(l: f64, t: Transfer) -> f64 {
427 match t {
428 Transfer::Pq => pq_oetf(l * 100.0),
429 Transfer::Hlg => hlg_oetf(l * hlg_white()),
430 }
431 }
432 fn decode(e: f64, t: Transfer) -> f64 {
433 match t {
434 Transfer::Pq => pq_eotf(e) / 100.0,
435 Transfer::Hlg => hlg_inverse(e) / hlg_white(),
436 }
437 }
438 fn invert(m: [[f64; 3]; 3]) -> [[f64; 3]; 3] {
439 let det = m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1])
440 - m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0])
441 + m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0]);
442 let c = |r: usize, k: usize| {
443 let (r0, r1) = ((r + 1) % 3, (r + 2) % 3);
444 let (k0, k1) = ((k + 1) % 3, (k + 2) % 3);
445 m[r0][k0] * m[r1][k1] - m[r0][k1] * m[r1][k0]
446 };
447 std::array::from_fn(|i| std::array::from_fn(|j| c(j, i) / det))
448 }
449 fn mul(m: [[f64; 3]; 3], v: [f64; 3]) -> [f64; 3] {
450 std::array::from_fn(|i| m[i][0] * v[0] + m[i][1] * v[1] + m[i][2] * v[2])
451 }
452 fn expected(rgb: [f64; 3], t: Transfer) -> [u8; 3] {
454 let lin = rgb.map(|e| decode(e, t));
455 mul(invert(M709_TO_2020), lin).map(|l| (srgb_oetf(l) * 255.0).round() as u8)
456 }
457 fn coded(srgb: [u8; 3], t: Transfer) -> [f64; 3] {
459 let lin = srgb.map(|v| srgb_eotf(v as f64 / 255.0));
460 mul(M709_TO_2020, lin).map(|l| encode(l, t))
461 }
462 fn ycbcr(rgb: [f64; 3], m: Matrix, full: bool) -> [u8; 3] {
464 let (kr, kb) = KR_KB[m as usize];
465 let y = kr * rgb[0] + (1.0 - kr - kb) * rgb[1] + kb * rgb[2];
466 let (cb, cr) = (
467 (rgb[2] - y) / (2.0 * (1.0 - kb)),
468 (rgb[0] - y) / (2.0 * (1.0 - kr)),
469 );
470 let q = |v: f64| v.round().clamp(0.0, 255.0) as u8;
471 if full {
472 [q(y * 255.0), q(128.0 + cb * 255.0), q(128.0 + cr * 255.0)]
473 } else {
474 [
475 q(16.0 + y * 219.0),
476 q(128.0 + cb * 224.0),
477 q(128.0 + cr * 224.0),
478 ]
479 }
480 }
481 fn rgb_of(yuv: [u8; 3], m: Matrix, full: bool) -> [f64; 3] {
483 let (kr, kb) = KR_KB[m as usize];
484 let (y, cb, cr) = if full {
485 (
486 yuv[0] as f64 / 255.0,
487 (yuv[1] as f64 - 128.0) / 255.0,
488 (yuv[2] as f64 - 128.0) / 255.0,
489 )
490 } else {
491 (
492 (yuv[0] as f64 - 16.0) / 219.0,
493 (yuv[1] as f64 - 128.0) / 224.0,
494 (yuv[2] as f64 - 128.0) / 224.0,
495 )
496 };
497 let r = y + 2.0 * (1.0 - kr) * cr;
498 let b = y + 2.0 * (1.0 - kb) * cb;
499 let g = (y - kr * r - kb * b) / (1.0 - kr - kb);
500 [r, g, b].map(|v| v.clamp(0.0, 1.0))
501 }
502
503 fn rgb8(pixels: &[[f64; 3]]) -> Vec<u8> {
504 pixels
505 .iter()
506 .flat_map(|p| {
507 [p[0], p[1], p[2], 1.0].map(|v| (v.clamp(0.0, 1.0) * 255.0).round() as u8)
508 })
509 .collect()
510 }
511 fn rgb10(pixels: &[[f64; 3]]) -> Vec<u8> {
512 pixels
513 .iter()
514 .flat_map(|p| {
515 let q = p.map(|v| (v.clamp(0.0, 1.0) * 1023.0).round() as u32);
516 (q[0] | q[1] << 10 | q[2] << 20 | 3 << 30).to_le_bytes()
517 })
518 .collect()
519 }
520 fn convert_rgb(src: &[u8], packing: Packing, t: Transfer) -> (Vec<u8>, usize) {
521 let mut out = src.to_vec();
522 let above = rgb_to_rgba(&mut out, packing, src.len() / 4, 1, t);
523 (out, above)
524 }
525 fn near(actual: &[u8], expected: [u8; 3], tolerance: i32, what: &str) {
526 for c in 0..3 {
527 assert!(
528 (actual[c] as i32 - expected[c] as i32).abs() <= tolerance,
529 "{what}: got {:?}, expected {expected:?}",
530 &actual[..4]
531 );
532 }
533 assert_eq!(actual[3], 255, "{what}: alpha");
534 }
535
536 #[test]
537 fn sdr_white_is_255_and_black_is_0() {
538 for t in [Transfer::Pq, Transfer::Hlg] {
539 let white = [encode(1.0, t); 3];
540 for packing in [Packing::Rgba8, Packing::Rgb10a2] {
541 let src = if packing == Packing::Rgba8 {
542 rgb8(&[white, [0.0; 3]])
543 } else {
544 rgb10(&[white, [0.0; 3]])
545 };
546 let (out, above) = convert_rgb(&src, packing, t);
547 near(
549 &out[..4],
550 [255; 3],
551 if packing == Packing::Rgba8 { 2 } else { 0 },
552 &format!("{t:?} {packing:?} white"),
553 );
554 near(&out[4..], [0; 3], 0, &format!("{t:?} {packing:?} black"));
555 assert_eq!(
556 above, 0,
557 "{t:?} {packing:?}: white and black are not above white"
558 );
559 }
560 }
561 }
562
563 #[test]
564 fn grey_levels_come_back_as_themselves() {
565 for t in [Transfer::Pq, Transfer::Hlg] {
566 let greys: Vec<[f64; 3]> = (0..=255u8).map(|v| coded([v; 3], t)).collect();
567 let (out, _) = convert_rgb(&rgb10(&greys), Packing::Rgb10a2, t);
568 for v in 0..=255u8 {
569 near(
570 &out[v as usize * 4..],
571 [v; 3],
572 1,
573 &format!("{t:?} grey {v} (10-bit)"),
574 );
575 }
576 let codes: Vec<[f64; 3]> = (0..=255u8).map(|c| [c as f64 / 255.0; 3]).collect();
578 let (out, _) = convert_rgb(&rgb8(&codes), Packing::Rgba8, t);
579 for (c, rgb) in codes.iter().enumerate() {
580 near(
581 &out[c * 4..],
582 expected(*rgb, t),
583 1,
584 &format!("{t:?} code {c} (8-bit)"),
585 );
586 }
587 }
588 }
589
590 #[test]
591 fn bt709_colours_in_the_bt2020_container_come_back() {
592 let colours = [
593 [255, 0, 0],
594 [0, 255, 0],
595 [0, 0, 255],
596 [0, 255, 255],
597 [255, 0, 255],
598 [255, 255, 0],
599 [229, 115, 115],
600 [30, 144, 255],
601 [12, 160, 50],
602 ];
603 for t in [Transfer::Pq, Transfer::Hlg] {
604 let coded: Vec<[f64; 3]> = colours.iter().map(|&c| coded(c, t)).collect();
605 let (out, _) = convert_rgb(&rgb10(&coded), Packing::Rgb10a2, t);
606 for (i, &c) in colours.iter().enumerate() {
607 let quantised = coded[i].map(|v| (v * 1023.0).round() / 1023.0);
610 near(
611 &out[i * 4..],
612 expected(quantised, t),
613 1,
614 &format!("{t:?} {c:?} exact"),
615 );
616 near(&out[i * 4..], c, 16, &format!("{t:?} {c:?} round trip"));
617 }
618 }
619 }
620
621 #[test]
622 fn highlights_clip_to_white_and_are_counted() {
623 for t in [Transfer::Pq, Transfer::Hlg] {
624 let levels = [1.0, 1.4, 1.6, 3.0, 10.0];
625 let pixels: Vec<[f64; 3]> = levels.iter().map(|&l| [encode(l, t); 3]).collect();
626 let (out, above) = convert_rgb(&rgb10(&pixels), Packing::Rgb10a2, t);
627 for i in 0..levels.len() {
628 near(&out[i * 4..], [255; 3], 0, &format!("{t:?} {}", levels[i]));
629 }
630 assert_eq!(above, 3, "{t:?}: 1.6, 3 and 10 are above {ABOVE_WHITE}");
632 }
633 }
634
635 #[test]
636 fn rgb10a2_reads_red_from_the_low_bits() {
637 let t = Transfer::Pq;
638 let red = coded([255, 0, 0], t);
639 let (out, _) = convert_rgb(&rgb10(&[red]), Packing::Rgb10a2, t);
640 near(&out, [255, 0, 0], 2, "red");
641 }
642
643 fn planes_of(
647 format: Format,
648 w: usize,
649 h: usize,
650 yuv: &[[u8; 3]],
651 pad: usize,
652 ) -> (Vec<u8>, Vec<Plane>) {
653 let (sx, sy) = match format {
654 Format::I444 => (1, 1),
655 Format::I422 => (2, 1),
656 Format::I420 | Format::Nv12 => (2, 2),
657 };
658 let mut layout = Vec::new();
659 let mut len = 0;
660 for (row_bytes, rows) in format.planes(w, h) {
661 layout.push(Plane {
662 offset: len,
663 stride: row_bytes + pad,
664 });
665 len += (row_bytes + pad) * rows;
666 }
667 let mut src = vec![7u8; len];
668 for y in 0..h {
669 for x in 0..w {
670 src[layout[0].offset + y * layout[0].stride + x] = yuv[y * w + x][0];
671 }
672 }
673 for cy in 0..h.div_ceil(sy) {
674 for cx in 0..w.div_ceil(sx) {
675 let p = yuv[cy * sy * w + cx * sx];
676 if format == Format::Nv12 {
677 let at = layout[1].offset + cy * layout[1].stride + cx * 2;
678 src[at] = p[1];
679 src[at + 1] = p[2];
680 } else {
681 src[layout[1].offset + cy * layout[1].stride + cx] = p[1];
682 src[layout[2].offset + cy * layout[2].stride + cx] = p[2];
683 }
684 }
685 }
686 (src, layout)
687 }
688
689 #[test]
690 fn every_layout_matrix_and_range_matches_the_formulas() {
691 let mut seed = 0x2545_f491u32;
692 let mut next = move || {
693 seed ^= seed << 13;
694 seed ^= seed >> 17;
695 seed ^= seed << 5;
696 seed
697 };
698 for format in [Format::I420, Format::I422, Format::I444, Format::Nv12] {
699 for (w, h) in [(1, 1), (3, 2), (7, 5), (33, 9), (640, 120)] {
700 for t in [Transfer::Pq, Transfer::Hlg] {
701 for (mi, m) in [Matrix::Bt709, Matrix::Bt601, Matrix::Bt2020]
702 .into_iter()
703 .enumerate()
704 {
705 for full in [false, true] {
706 let colours: Vec<[u8; 3]> = (0..w * h)
708 .map(|_| [next() as u8, next() as u8, next() as u8])
709 .collect();
710 let mut yuv: Vec<[u8; 3]> = colours
711 .iter()
712 .map(|&c| ycbcr(coded(c, t), m, full))
713 .collect();
714 let (sx, sy) = match format {
715 Format::I444 => (1, 1),
716 Format::I422 => (2, 1),
717 _ => (2, 2),
718 };
719 for y in 0..h {
720 for x in 0..w {
721 let anchor = yuv[(y / sy * sy) * w + x / sx * sx];
722 yuv[y * w + x][1] = anchor[1];
723 yuv[y * w + x][2] = anchor[2];
724 }
725 }
726 let (src, layout) = planes_of(format, w, h, &yuv, 3);
727 let mut out = vec![0u8; w * h * 4];
728 planes_to_rgba(&src, format, &layout, w, h, m, full, t, &mut out);
729 for (i, p) in yuv.iter().enumerate() {
730 near(
731 &out[i * 4..],
732 expected(rgb_of(*p, m, full), t),
733 1,
734 &format!("{w}x{h} matrix {mi} full {full} {t:?} pixel {i}"),
735 );
736 }
737 }
738 }
739 }
740 }
741 }
742 }
743
744 #[test]
748 fn neutral_pixels_match_the_formulas_whichever_path_converts_them() {
749 for t in [Transfer::Pq, Transfer::Hlg] {
750 for full in [false, true] {
751 let yuv: Vec<[u8; 3]> = (0..=255u8)
752 .flat_map(|y| [[y, 128, 128], [y, 129, 128], [y, 128, 127]])
753 .collect();
754 let (src, layout) = planes_of(Format::I444, yuv.len(), 1, &yuv, 0);
755 let mut out = vec![0u8; yuv.len() * 4];
756 planes_to_rgba(
757 &src,
758 Format::I444,
759 &layout,
760 yuv.len(),
761 1,
762 Matrix::Bt2020,
763 full,
764 t,
765 &mut out,
766 );
767 for (i, p) in yuv.iter().enumerate() {
768 near(
769 &out[i * 4..],
770 expected(rgb_of(*p, Matrix::Bt2020, full), t),
771 1,
772 &format!("{t:?} full {full} {p:?}"),
773 );
774 }
775 }
776 for packing in [Packing::Rgba8, Packing::Rgb10a2] {
777 let top = if packing == Packing::Rgba8 {
778 255.0
779 } else {
780 1023.0
781 };
782 let codes: Vec<[f64; 3]> = (0..=top as u32)
783 .flat_map(|c| {
784 let v = c as f64 / top;
785 [[v; 3], [v, v, (v + 1.0 / top).min(1.0)]]
786 })
787 .collect();
788 let src = if packing == Packing::Rgba8 {
789 rgb8(&codes)
790 } else {
791 rgb10(&codes)
792 };
793 let (out, _) = convert_rgb(&src, packing, t);
794 for (i, c) in codes.iter().enumerate() {
795 near(
796 &out[i * 4..],
797 expected(*c, t),
798 1,
799 &format!("{t:?} {packing:?} {c:?}"),
800 );
801 }
802 }
803 }
804 }
805
806 #[test]
807 fn limited_and_full_range_planes_give_the_same_picture() {
808 let t = Transfer::Pq;
809 let colours: Vec<[u8; 3]> = (0..=255u8).step_by(5).map(|v| [v, v, v]).collect();
812 let picture = |full: bool| {
813 let yuv: Vec<[u8; 3]> = colours
814 .iter()
815 .map(|&c| ycbcr(coded(c, t), Matrix::Bt2020, full))
816 .collect();
817 let (src, layout) = planes_of(Format::I444, yuv.len(), 1, &yuv, 0);
818 let mut out = vec![0u8; yuv.len() * 4];
819 planes_to_rgba(
820 &src,
821 Format::I444,
822 &layout,
823 yuv.len(),
824 1,
825 Matrix::Bt2020,
826 full,
827 t,
828 &mut out,
829 );
830 out
831 };
832 let (limited, full) = (picture(false), picture(true));
833 for i in 0..colours.len() {
834 near(
835 &limited[i * 4..],
836 [full[i * 4], full[i * 4 + 1], full[i * 4 + 2]],
837 4,
838 &format!("grey {:?}", colours[i]),
839 );
840 near(
841 &full[i * 4..],
842 colours[i],
843 2,
844 &format!("full-range round trip {:?}", colours[i]),
845 );
846 }
847 }
848
849 #[test]
850 fn planes_and_rgb_entry_points_both_match_the_formulas_on_one_picture() {
851 let t = Transfer::Pq;
852 let yuv: Vec<[u8; 3]> = (0..=255u8)
853 .step_by(3)
854 .map(|v| [v, 255 - v, v / 2 + 64])
855 .collect();
856 let (src, layout) = planes_of(Format::I444, yuv.len(), 1, &yuv, 0);
857 let mut from_planes = vec![0u8; yuv.len() * 4];
858 planes_to_rgba(
859 &src,
860 Format::I444,
861 &layout,
862 yuv.len(),
863 1,
864 Matrix::Bt2020,
865 true,
866 t,
867 &mut from_planes,
868 );
869 let rgb: Vec<[f64; 3]> = yuv
870 .iter()
871 .map(|&p| rgb_of(p, Matrix::Bt2020, true))
872 .collect();
873 let (from_rgb, _) = convert_rgb(&rgb10(&rgb), Packing::Rgb10a2, t);
874 for (i, coded) in rgb.iter().enumerate() {
875 let quantised = coded.map(|v| (v * 1023.0).round() / 1023.0);
876 near(
877 &from_planes[i * 4..],
878 expected(*coded, t),
879 1,
880 &format!("planes {:?}", yuv[i]),
881 );
882 near(
883 &from_rgb[i * 4..],
884 expected(quantised, t),
885 1,
886 &format!("rgb {:?}", yuv[i]),
887 );
888 }
889 }
890
891 #[test]
892 fn a_frame_split_across_threads_counts_every_pixel_above_white_once() {
893 let (w, h) = (1024, 300);
894 let bright = [encode(4.0, Transfer::Pq); 3];
895 let pixels: Vec<[f64; 3]> = (0..w * h)
896 .map(|i| if i % 7 == 0 { bright } else { [0.4; 3] })
897 .collect();
898 let mut out = rgb10(&pixels);
899 let above = rgb_to_rgba(&mut out, Packing::Rgb10a2, w, h, Transfer::Pq);
900 assert_eq!(above, (0..w * h).filter(|i| i % 7 == 0).count());
901 assert!(out
902 .chunks_exact(4)
903 .enumerate()
904 .all(|(i, p)| p[0] == if i % 7 == 0 { 255 } else { out[4] }));
905 }
906}