1use crate::{
4 shader::{BindGroupLayoutBuilder, ShaderCompiler, ShaderSource},
5 GpuDevice, Result,
6};
7use bytemuck::{Pod, Zeroable};
8use once_cell::sync::OnceCell;
9use wgpu::{BindGroup, BindGroupLayout, ComputePipeline};
10
11use super::utils;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum ColorSpace {
16 BT601,
18 BT709,
20 BT2020,
22}
23
24impl ColorSpace {
25 fn to_format_id(self) -> u32 {
26 match self {
27 Self::BT601 => 0,
28 Self::BT709 => 1,
29 Self::BT2020 => 2,
30 }
31 }
32}
33
34#[repr(C)]
35#[derive(Copy, Clone, Pod, Zeroable)]
36struct ConversionParams {
37 width: u32,
38 height: u32,
39 stride: u32,
40 format: u32,
41}
42
43pub struct ColorSpaceConversion;
45
46impl ColorSpaceConversion {
47 #[allow(clippy::too_many_arguments)]
62 pub fn rgb_to_yuv(
63 device: &GpuDevice,
64 input: &[u8],
65 output: &mut [u8],
66 width: u32,
67 height: u32,
68 color_space: ColorSpace,
69 ) -> Result<()> {
70 utils::validate_dimensions(width, height)?;
71 utils::validate_buffer_size(input, width, height, 4)?;
72 utils::validate_buffer_size(output, width, height, 4)?;
73
74 let pipeline = Self::get_rgb_to_yuv_pipeline(device)?;
75 let layout = Self::get_bind_group_layout(device)?;
76
77 Self::execute_conversion(
78 device,
79 pipeline,
80 layout,
81 input,
82 output,
83 width,
84 height,
85 color_space,
86 )
87 }
88
89 #[allow(clippy::too_many_arguments)]
104 pub fn yuv_to_rgb(
105 device: &GpuDevice,
106 input: &[u8],
107 output: &mut [u8],
108 width: u32,
109 height: u32,
110 color_space: ColorSpace,
111 ) -> Result<()> {
112 utils::validate_dimensions(width, height)?;
113 utils::validate_buffer_size(input, width, height, 4)?;
114 utils::validate_buffer_size(output, width, height, 4)?;
115
116 let pipeline = Self::get_yuv_to_rgb_pipeline(device)?;
117 let layout = Self::get_bind_group_layout(device)?;
118
119 Self::execute_conversion(
120 device,
121 pipeline,
122 layout,
123 input,
124 output,
125 width,
126 height,
127 color_space,
128 )
129 }
130
131 #[allow(clippy::too_many_arguments)]
132 fn execute_conversion(
133 device: &GpuDevice,
134 pipeline: &ComputePipeline,
135 layout: &BindGroupLayout,
136 input: &[u8],
137 output: &mut [u8],
138 width: u32,
139 height: u32,
140 color_space: ColorSpace,
141 ) -> Result<()> {
142 let input_buffer = utils::create_storage_buffer(device, input.len() as u64)?;
144 let output_buffer = utils::create_storage_buffer(device, output.len() as u64)?;
145
146 device.queue().write_buffer(input_buffer.buffer(), 0, input);
148
149 let params = ConversionParams {
151 width,
152 height,
153 stride: width,
154 format: color_space.to_format_id(),
155 };
156 let params_bytes = bytemuck::bytes_of(¶ms);
157 let params_buffer = utils::create_uniform_buffer(device, params_bytes)?;
158
159 let compiler = ShaderCompiler::new(device);
161 let bind_group = compiler.create_bind_group(
162 "ColorSpace Bind Group",
163 layout,
164 &[
165 wgpu::BindGroupEntry {
166 binding: 0,
167 resource: input_buffer.buffer().as_entire_binding(),
168 },
169 wgpu::BindGroupEntry {
170 binding: 1,
171 resource: output_buffer.buffer().as_entire_binding(),
172 },
173 wgpu::BindGroupEntry {
174 binding: 2,
175 resource: params_buffer.buffer().as_entire_binding(),
176 },
177 ],
178 );
179
180 Self::dispatch_compute(device, pipeline, &bind_group, width, height)?;
182
183 let readback_buffer = utils::create_readback_buffer(device, output.len() as u64)?;
185 let mut encoder = device
186 .device()
187 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
188 label: Some("ColorSpace Copy Encoder"),
189 });
190
191 output_buffer.copy_to(&mut encoder, &readback_buffer, 0, 0, output.len() as u64)?;
192
193 device.queue().submit(Some(encoder.finish()));
194 device.wait();
195
196 let result = readback_buffer.read(device, 0, output.len() as u64)?;
197 output.copy_from_slice(&result);
198
199 Ok(())
200 }
201
202 fn dispatch_compute(
203 device: &GpuDevice,
204 pipeline: &ComputePipeline,
205 bind_group: &BindGroup,
206 width: u32,
207 height: u32,
208 ) -> Result<()> {
209 let mut encoder = device
210 .device()
211 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
212 label: Some("ColorSpace Compute Encoder"),
213 });
214
215 {
216 let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
217 label: Some("ColorSpace Compute Pass"),
218 timestamp_writes: None,
219 });
220
221 compute_pass.set_pipeline(pipeline);
222 compute_pass.set_bind_group(0, bind_group, &[]);
223
224 let (dispatch_x, dispatch_y) = utils::calculate_dispatch_size(width, height, (16, 16));
225 compute_pass.dispatch_workgroups(dispatch_x, dispatch_y, 1);
226 }
227
228 device.queue().submit(Some(encoder.finish()));
229 Ok(())
230 }
231
232 fn get_bind_group_layout(device: &GpuDevice) -> Result<&'static BindGroupLayout> {
233 static LAYOUT: OnceCell<BindGroupLayout> = OnceCell::new();
234
235 Ok(LAYOUT.get_or_init(|| {
236 let compiler = ShaderCompiler::new(device);
237 let entries = BindGroupLayoutBuilder::new()
238 .add_storage_buffer_read_only(0) .add_storage_buffer(1) .add_uniform_buffer(2) .build();
242
243 compiler.create_bind_group_layout("ColorSpace Bind Group Layout", &entries)
244 }))
245 }
246
247 fn init_pipeline(
248 device: &GpuDevice,
249 name: &str,
250 entry_point: &str,
251 ) -> std::result::Result<ComputePipeline, String> {
252 let compiler = ShaderCompiler::new(device);
253 let shader = compiler
254 .compile(
255 "ColorSpace Shader",
256 ShaderSource::Embedded(crate::shader::embedded::COLORSPACE_SHADER),
257 )
258 .map_err(|e| format!("Failed to compile colorspace shader: {e}"))?;
259
260 let layout = Self::get_bind_group_layout(device)
261 .map_err(|e| format!("Failed to create bind group layout: {e}"))?;
262
263 compiler
264 .create_pipeline(name, &shader, entry_point, layout)
265 .map_err(|e| format!("Failed to create pipeline: {e}"))
266 }
267
268 fn get_rgb_to_yuv_pipeline(device: &GpuDevice) -> Result<&'static ComputePipeline> {
269 static PIPELINE: OnceCell<std::result::Result<ComputePipeline, String>> = OnceCell::new();
270
271 PIPELINE
272 .get_or_init(|| {
273 ColorSpaceConversion::init_pipeline(
274 device,
275 "RGB to YUV Pipeline",
276 "rgb_to_yuv_main",
277 )
278 })
279 .as_ref()
280 .map_err(|e| crate::GpuError::PipelineCreation(e.clone()))
281 }
282
283 fn get_yuv_to_rgb_pipeline(device: &GpuDevice) -> Result<&'static ComputePipeline> {
284 static PIPELINE: OnceCell<std::result::Result<ComputePipeline, String>> = OnceCell::new();
285
286 PIPELINE
287 .get_or_init(|| {
288 ColorSpaceConversion::init_pipeline(
289 device,
290 "YUV to RGB Pipeline",
291 "yuv_to_rgb_main",
292 )
293 })
294 .as_ref()
295 .map_err(|e| crate::GpuError::PipelineCreation(e.clone()))
296 }
297}
298
299#[must_use]
308pub fn bt601_rgb_to_ycbcr(r: u8, g: u8, b: u8) -> (u8, u8, u8) {
309 let r = f64::from(r);
310 let g = f64::from(g);
311 let b = f64::from(b);
312 let y = 16.0 + (65.481 * r + 128.553 * g + 24.966 * b) / 255.0;
313 let cb = 128.0 + (-37.797 * r - 74.203 * g + 112.0 * b) / 255.0;
314 let cr = 128.0 + (112.0 * r - 93.786 * g - 18.214 * b) / 255.0;
315 (
316 y.round().clamp(0.0, 255.0) as u8,
317 cb.round().clamp(0.0, 255.0) as u8,
318 cr.round().clamp(0.0, 255.0) as u8,
319 )
320}
321
322#[must_use]
326pub fn bt601_ycbcr_to_rgb(y: u8, cb: u8, cr: u8) -> (u8, u8, u8) {
327 let y = f64::from(y) - 16.0;
328 let cb = f64::from(cb) - 128.0;
329 let cr = f64::from(cr) - 128.0;
330 let r = 255.0 * (1.164 * y + 1.596 * cr) / 255.0;
331 let g = 255.0 * (1.164 * y - 0.392 * cb - 0.813 * cr) / 255.0;
332 let b = 255.0 * (1.164 * y + 2.017 * cb) / 255.0;
333 (
334 r.round().clamp(0.0, 255.0) as u8,
335 g.round().clamp(0.0, 255.0) as u8,
336 b.round().clamp(0.0, 255.0) as u8,
337 )
338}
339
340#[must_use]
345pub fn bt709_rgb_to_ycbcr(r: u8, g: u8, b: u8) -> (u8, u8, u8) {
346 let r_n = f64::from(r) / 255.0;
349 let g_n = f64::from(g) / 255.0;
350 let b_n = f64::from(b) / 255.0;
351 let y = 16.0 + 219.0 * (0.2126 * r_n + 0.7152 * g_n + 0.0722 * b_n);
352 let cb = 128.0 + 224.0 * (-0.2126 / 1.8556 * r_n - 0.7152 / 1.8556 * g_n + 0.5 * b_n);
353 let cr = 128.0 + 224.0 * (0.5 * r_n - 0.7152 / 1.5748 * g_n - 0.0722 / 1.5748 * b_n);
354 (
355 y.round().clamp(0.0, 255.0) as u8,
356 cb.round().clamp(0.0, 255.0) as u8,
357 cr.round().clamp(0.0, 255.0) as u8,
358 )
359}
360
361#[must_use]
365pub fn bt709_ycbcr_to_rgb(y: u8, cb: u8, cr: u8) -> (u8, u8, u8) {
366 let y_n = (f64::from(y) - 16.0) / 219.0;
367 let cb_n = (f64::from(cb) - 128.0) / 224.0;
368 let cr_n = (f64::from(cr) - 128.0) / 224.0;
369 let r = y_n + 1.5748 * cr_n;
370 let g = y_n - 0.2126 / 0.7152 * 1.5748 * cr_n - 0.0722 / 0.7152 * 1.8556 * cb_n;
371 let b = y_n + 1.8556 * cb_n;
372 (
373 (r * 255.0).round().clamp(0.0, 255.0) as u8,
374 (g * 255.0).round().clamp(0.0, 255.0) as u8,
375 (b * 255.0).round().clamp(0.0, 255.0) as u8,
376 )
377}
378
379#[must_use]
387pub fn bt2020_rgb_to_ycbcr(r: u8, g: u8, b: u8) -> (u8, u8, u8) {
388 let r_n = f64::from(r) / 255.0;
389 let g_n = f64::from(g) / 255.0;
390 let b_n = f64::from(b) / 255.0;
391 let kr = 0.2627_f64;
393 let kb = 0.0593_f64;
394 let kg = 1.0 - kr - kb; let y = 16.0 + 219.0 * (kr * r_n + kg * g_n + kb * b_n);
396 let cb = 128.0
397 + 224.0 * ((-kr / (2.0 * (1.0 - kb))) * r_n + (-kg / (2.0 * (1.0 - kb))) * g_n + 0.5 * b_n);
398 let cr = 128.0
399 + 224.0 * (0.5 * r_n + (-kg / (2.0 * (1.0 - kr))) * g_n + (-kb / (2.0 * (1.0 - kr))) * b_n);
400 (
401 y.round().clamp(0.0, 255.0) as u8,
402 cb.round().clamp(0.0, 255.0) as u8,
403 cr.round().clamp(0.0, 255.0) as u8,
404 )
405}
406
407#[must_use]
411pub fn bt2020_ycbcr_to_rgb(y: u8, cb: u8, cr: u8) -> (u8, u8, u8) {
412 let y_n = (f64::from(y) - 16.0) / 219.0;
413 let cb_n = (f64::from(cb) - 128.0) / 224.0;
414 let cr_n = (f64::from(cr) - 128.0) / 224.0;
415 let kr = 0.2627_f64;
417 let kb = 0.0593_f64;
418 let kg = 1.0 - kr - kb;
419 let r_cr = 2.0 * (1.0 - kr); let b_cb = 2.0 * (1.0 - kb); let g_cr = -2.0 * kr * (1.0 - kr) / kg;
422 let g_cb = -2.0 * kb * (1.0 - kb) / kg;
423 let r = y_n + r_cr * cr_n;
424 let g = y_n + g_cr * cr_n + g_cb * cb_n;
425 let b = y_n + b_cb * cb_n;
426 (
427 (r * 255.0).round().clamp(0.0, 255.0) as u8,
428 (g * 255.0).round().clamp(0.0, 255.0) as u8,
429 (b * 255.0).round().clamp(0.0, 255.0) as u8,
430 )
431}
432
433#[must_use]
438pub fn pq_oetf(l: f64) -> f64 {
439 const M1: f64 = 0.159_301_758_5;
441 const M2: f64 = 78.843_75;
442 const C1: f64 = 0.835_937_5;
443 const C2: f64 = 18.851_563;
444 const C3: f64 = 18.687_5;
445 let l_m1 = l.abs().powf(M1);
446 ((C1 + C2 * l_m1) / (1.0 + C3 * l_m1)).powf(M2)
447}
448
449#[must_use]
454pub fn pq_eotf(e: f64) -> f64 {
455 const M1: f64 = 0.159_301_758_5;
456 const M2: f64 = 78.843_75;
457 const C1: f64 = 0.835_937_5;
458 const C2: f64 = 18.851_563;
459 const C3: f64 = 18.687_5;
460 let e_m2 = e.abs().powf(1.0 / M2);
461 let num = (e_m2 - C1).max(0.0);
462 let den = C2 - C3 * e_m2;
463 (num / den).powf(1.0 / M1)
464}
465
466#[must_use]
471pub fn hlg_oetf(l: f64) -> f64 {
472 const A: f64 = 0.178_832_77;
473 const B: f64 = 0.284_668_92;
474 const C: f64 = 0.559_910_73;
475 if l <= 1.0 / 12.0 {
476 (3.0 * l).sqrt()
477 } else {
478 A * (12.0 * l - B).ln() + C
479 }
480}
481
482#[must_use]
487pub fn hlg_eotf(e: f64) -> f64 {
488 const A: f64 = 0.178_832_77;
489 const B: f64 = 0.284_668_92;
490 const C: f64 = 0.559_910_73;
491 if e <= 0.5 {
492 e * e / 3.0
493 } else {
494 ((e - C) / A).exp() / 12.0 + B / 12.0
495 }
496}
497
498#[cfg(test)]
503mod tests {
504 use super::*;
505
506 fn approx_eq(a: u8, b: u8, tol: u8, label: &str) {
508 assert!(
509 (a as i32 - b as i32).unsigned_abs() as u8 <= tol,
510 "{label}: got {a}, expected ~{b} (tol={tol})"
511 );
512 }
513
514 #[test]
517 fn test_bt601_white_rgb_to_ycbcr() {
518 let (y, cb, cr) = bt601_rgb_to_ycbcr(255, 255, 255);
520 approx_eq(y, 235, 2, "Y for white");
521 approx_eq(cb, 128, 2, "Cb for white");
522 approx_eq(cr, 128, 2, "Cr for white");
523 }
524
525 #[test]
526 fn test_bt601_black_rgb_to_ycbcr() {
527 let (y, cb, cr) = bt601_rgb_to_ycbcr(0, 0, 0);
529 approx_eq(y, 16, 2, "Y for black");
530 approx_eq(cb, 128, 2, "Cb for black");
531 approx_eq(cr, 128, 2, "Cr for black");
532 }
533
534 #[test]
535 fn test_bt601_red_rgb_to_ycbcr() {
536 let (y, cb, cr) = bt601_rgb_to_ycbcr(255, 0, 0);
538 approx_eq(y, 82, 3, "Y for red");
539 approx_eq(cb, 90, 4, "Cb for red");
540 approx_eq(cr, 240, 4, "Cr for red");
541 }
542
543 #[test]
544 fn test_bt601_green_rgb_to_ycbcr() {
545 let (y, cb, cr) = bt601_rgb_to_ycbcr(0, 255, 0);
547 approx_eq(y, 145, 3, "Y for green");
548 approx_eq(cb, 54, 4, "Cb for green");
549 approx_eq(cr, 34, 4, "Cr for green");
550 }
551
552 #[test]
553 fn test_bt601_blue_rgb_to_ycbcr() {
554 let (y, cb, cr) = bt601_rgb_to_ycbcr(0, 0, 255);
556 approx_eq(y, 41, 3, "Y for blue");
557 approx_eq(cb, 240, 4, "Cb for blue");
558 approx_eq(cr, 110, 4, "Cr for blue");
559 }
560
561 #[test]
562 fn test_bt601_roundtrip_white() {
563 let (y, cb, cr) = bt601_rgb_to_ycbcr(255, 255, 255);
564 let (r, g, b) = bt601_ycbcr_to_rgb(y, cb, cr);
565 approx_eq(r, 255, 3, "R roundtrip white");
566 approx_eq(g, 255, 3, "G roundtrip white");
567 approx_eq(b, 255, 3, "B roundtrip white");
568 }
569
570 #[test]
571 fn test_bt601_roundtrip_black() {
572 let (y, cb, cr) = bt601_rgb_to_ycbcr(0, 0, 0);
573 let (r, g, b) = bt601_ycbcr_to_rgb(y, cb, cr);
574 approx_eq(r, 0, 3, "R roundtrip black");
575 approx_eq(g, 0, 3, "G roundtrip black");
576 approx_eq(b, 0, 3, "B roundtrip black");
577 }
578
579 #[test]
580 fn test_bt601_roundtrip_grey128() {
581 let (y, cb, cr) = bt601_rgb_to_ycbcr(128, 128, 128);
582 let (r, g, b) = bt601_ycbcr_to_rgb(y, cb, cr);
583 approx_eq(r, 128, 4, "R roundtrip grey");
584 approx_eq(g, 128, 4, "G roundtrip grey");
585 approx_eq(b, 128, 4, "B roundtrip grey");
586 }
587
588 #[test]
591 fn test_bt709_white_rgb_to_ycbcr() {
592 let (y, cb, cr) = bt709_rgb_to_ycbcr(255, 255, 255);
593 approx_eq(y, 235, 2, "Y for white BT.709");
594 approx_eq(cb, 128, 2, "Cb for white BT.709");
595 approx_eq(cr, 128, 2, "Cr for white BT.709");
596 }
597
598 #[test]
599 fn test_bt709_black_rgb_to_ycbcr() {
600 let (y, cb, cr) = bt709_rgb_to_ycbcr(0, 0, 0);
601 approx_eq(y, 16, 2, "Y for black BT.709");
602 approx_eq(cb, 128, 2, "Cb for black BT.709");
603 approx_eq(cr, 128, 2, "Cr for black BT.709");
604 }
605
606 #[test]
607 fn test_bt709_red_rgb_to_ycbcr() {
608 let (y, _cb, _cr) = bt709_rgb_to_ycbcr(255, 0, 0);
610 approx_eq(y, 63, 3, "Y for red BT.709");
612 }
613
614 #[test]
615 fn test_bt709_roundtrip_white() {
616 let (y, cb, cr) = bt709_rgb_to_ycbcr(255, 255, 255);
617 let (r, g, b) = bt709_ycbcr_to_rgb(y, cb, cr);
618 approx_eq(r, 255, 4, "R roundtrip white BT.709");
619 approx_eq(g, 255, 4, "G roundtrip white BT.709");
620 approx_eq(b, 255, 4, "B roundtrip white BT.709");
621 }
622
623 #[test]
624 fn test_bt709_roundtrip_black() {
625 let (y, cb, cr) = bt709_rgb_to_ycbcr(0, 0, 0);
626 let (r, g, b) = bt709_ycbcr_to_rgb(y, cb, cr);
627 approx_eq(r, 0, 4, "R roundtrip black BT.709");
628 approx_eq(g, 0, 4, "G roundtrip black BT.709");
629 approx_eq(b, 0, 4, "B roundtrip black BT.709");
630 }
631
632 #[test]
633 fn test_bt709_roundtrip_colour() {
634 let (y, cb, cr) = bt709_rgb_to_ycbcr(100, 150, 200);
636 let (r, g, b) = bt709_ycbcr_to_rgb(y, cb, cr);
637 approx_eq(r, 100, 5, "R roundtrip colour BT.709");
638 approx_eq(g, 150, 5, "G roundtrip colour BT.709");
639 approx_eq(b, 200, 5, "B roundtrip colour BT.709");
640 }
641
642 #[test]
645 fn test_bt2020_white_rgb_to_ycbcr() {
646 let (y, cb, cr) = bt2020_rgb_to_ycbcr(255, 255, 255);
647 approx_eq(y, 235, 2, "Y for white BT.2020");
648 approx_eq(cb, 128, 2, "Cb for white BT.2020");
649 approx_eq(cr, 128, 2, "Cr for white BT.2020");
650 }
651
652 #[test]
653 fn test_bt2020_black_rgb_to_ycbcr() {
654 let (y, cb, cr) = bt2020_rgb_to_ycbcr(0, 0, 0);
655 approx_eq(y, 16, 2, "Y for black BT.2020");
656 approx_eq(cb, 128, 2, "Cb for black BT.2020");
657 approx_eq(cr, 128, 2, "Cr for black BT.2020");
658 }
659
660 #[test]
661 fn test_bt2020_red_luma() {
662 let (y, _, _) = bt2020_rgb_to_ycbcr(255, 0, 0);
664 approx_eq(y, 74, 3, "Y for red BT.2020");
665 }
666
667 #[test]
668 fn test_bt2020_roundtrip_white() {
669 let (y, cb, cr) = bt2020_rgb_to_ycbcr(255, 255, 255);
670 let (r, g, b) = bt2020_ycbcr_to_rgb(y, cb, cr);
671 approx_eq(r, 255, 4, "R roundtrip white BT.2020");
672 approx_eq(g, 255, 4, "G roundtrip white BT.2020");
673 approx_eq(b, 255, 4, "B roundtrip white BT.2020");
674 }
675
676 #[test]
677 fn test_bt2020_roundtrip_colour() {
678 let (y, cb, cr) = bt2020_rgb_to_ycbcr(100, 150, 200);
679 let (r, g, b) = bt2020_ycbcr_to_rgb(y, cb, cr);
680 approx_eq(r, 100, 5, "R roundtrip colour BT.2020");
681 approx_eq(g, 150, 5, "G roundtrip colour BT.2020");
682 approx_eq(b, 200, 5, "B roundtrip colour BT.2020");
683 }
684
685 #[test]
688 fn test_pq_oetf_zero() {
689 let v = pq_oetf(0.0);
691 assert!(v.abs() < 1e-6, "pq_oetf(0) = {v}");
692 }
693
694 #[test]
695 fn test_pq_oetf_one() {
696 let v = pq_oetf(1.0);
698 assert!((v - 1.0).abs() < 1e-4, "pq_oetf(1) = {v}");
699 }
700
701 #[test]
702 fn test_pq_roundtrip() {
703 for nits_norm in [0.0, 0.01, 0.1, 0.5, 0.9, 1.0_f64] {
704 let encoded = pq_oetf(nits_norm);
705 let decoded = pq_eotf(encoded);
706 assert!(
707 (decoded - nits_norm).abs() < 1e-5,
708 "PQ roundtrip failed at {nits_norm}: got {decoded}"
709 );
710 }
711 }
712
713 #[test]
714 fn test_hlg_oetf_zero() {
715 let v = hlg_oetf(0.0);
716 assert!(v.abs() < 1e-6, "hlg_oetf(0) = {v}");
717 }
718
719 #[test]
720 fn test_hlg_oetf_range() {
721 for i in 0..=20 {
723 let l = i as f64 / 20.0;
724 let e = hlg_oetf(l);
725 assert!((0.0..=1.0).contains(&e), "hlg_oetf({l}) = {e} out of [0,1]");
726 }
727 }
728
729 #[test]
730 fn test_hlg_roundtrip() {
731 for l in [0.0, 0.01, 0.05, 0.1, 0.25, 0.5, 0.75, 1.0_f64] {
732 let encoded = hlg_oetf(l);
733 let decoded = hlg_eotf(encoded);
734 assert!(
735 (decoded - l).abs() < 1e-6,
736 "HLG roundtrip failed at {l}: got {decoded}"
737 );
738 }
739 }
740
741 #[test]
746 fn test_bt601_cpu_vs_reference_batch() {
747 let colours = [
748 (255u8, 0u8, 0u8), (0u8, 255u8, 0u8), (0u8, 0u8, 255u8), (255u8, 255u8, 0u8), (128u8, 128u8, 128u8), ];
754 let expected_y: &[u8] = &[82, 145, 41, 210, 126];
756 for (i, ((r, g, b), &ey)) in colours.iter().zip(expected_y.iter()).enumerate() {
757 let (y, _, _) = bt601_rgb_to_ycbcr(*r, *g, *b);
758 assert!(
759 (y as i32 - ey as i32).unsigned_abs() <= 3,
760 "BT.601 Y mismatch for colour {i}: got {y}, expected ~{ey}"
761 );
762 }
763 }
764
765 #[test]
768 fn test_bt2020_vs_bt601_luma_differ_for_red() {
769 let (y601, _, _) = bt601_rgb_to_ycbcr(255, 0, 0);
770 let (y2020, _, _) = bt2020_rgb_to_ycbcr(255, 0, 0);
771 assert_ne!(y601, y2020, "BT.601 and BT.2020 Y for red must differ");
773 }
774
775 #[test]
778 fn test_grey_axis_chroma_neutral_all_standards() {
779 for v in [0u8, 64, 128, 192, 255] {
780 let (_, cb601, cr601) = bt601_rgb_to_ycbcr(v, v, v);
781 let (_, cb709, cr709) = bt709_rgb_to_ycbcr(v, v, v);
782 let (_, cb2020, cr2020) = bt2020_rgb_to_ycbcr(v, v, v);
783 approx_eq(cb601, 128, 2, &format!("Cb BT.601 grey {v}"));
784 approx_eq(cr601, 128, 2, &format!("Cr BT.601 grey {v}"));
785 approx_eq(cb709, 128, 2, &format!("Cb BT.709 grey {v}"));
786 approx_eq(cr709, 128, 2, &format!("Cr BT.709 grey {v}"));
787 approx_eq(cb2020, 128, 2, &format!("Cb BT.2020 grey {v}"));
788 approx_eq(cr2020, 128, 2, &format!("Cr BT.2020 grey {v}"));
789 }
790 }
791
792 #[test]
797 fn test_bt601_reference_vectors() {
798 let cases: &[((u8, u8, u8), (u8, u8, u8))] = &[
800 ((255, 0, 0), (82, 90, 240)), ((0, 255, 0), (145, 54, 34)), ((0, 0, 255), (41, 240, 110)), ((255, 255, 255), (235, 128, 128)), ((0, 0, 0), (16, 128, 128)), ((128, 128, 128), (126, 128, 128)), ];
807 for &((r, g, b), (ey, ecb, ecr)) in cases {
808 let (y, cb, cr) = bt601_rgb_to_ycbcr(r, g, b);
809 approx_eq(y, ey, 3, &format!("Y for ({r},{g},{b}) BT.601"));
810 approx_eq(cb, ecb, 4, &format!("Cb for ({r},{g},{b}) BT.601"));
811 approx_eq(cr, ecr, 4, &format!("Cr for ({r},{g},{b}) BT.601"));
812 }
813 }
814
815 #[test]
817 fn test_bt709_reference_vectors() {
818 let cases: &[((u8, u8, u8), (u8, u8, u8))] = &[
820 ((255, 255, 255), (235, 128, 128)), ((0, 0, 0), (16, 128, 128)), ((255, 0, 0), (63, 102, 240)), ((0, 255, 0), (173, 42, 26)), ((0, 0, 255), (32, 240, 118)), ];
826 for &((r, g, b), (ey, ecb, ecr)) in cases {
827 let (y, cb, cr) = bt709_rgb_to_ycbcr(r, g, b);
828 approx_eq(y, ey, 4, &format!("Y for ({r},{g},{b}) BT.709"));
829 approx_eq(cb, ecb, 5, &format!("Cb for ({r},{g},{b}) BT.709"));
830 approx_eq(cr, ecr, 5, &format!("Cr for ({r},{g},{b}) BT.709"));
831 }
832 }
833
834 #[test]
837 fn test_bt601_vs_bt709_differ_for_primaries() {
838 let test_colours = [(255u8, 0, 0), (0, 255, 0), (0, 0, 255)];
839 for (r, g, b) in test_colours {
840 let (y601, _, _) = bt601_rgb_to_ycbcr(r, g, b);
841 let (y709, _, _) = bt709_rgb_to_ycbcr(r, g, b);
842 assert_ne!(
843 y601, y709,
844 "BT.601 and BT.709 Y should differ for ({r},{g},{b})"
845 );
846 }
847 }
848
849 #[test]
851 fn test_bt601_deterministic() {
852 let (y1, cb1, cr1) = bt601_rgb_to_ycbcr(100, 150, 200);
853 let (y2, cb2, cr2) = bt601_rgb_to_ycbcr(100, 150, 200);
854 assert_eq!(y1, y2);
855 assert_eq!(cb1, cb2);
856 assert_eq!(cr1, cr2);
857 }
858
859 #[test]
861 fn test_bt709_batch_roundtrip_within_tolerance() {
862 let colours = [
863 (10u8, 20u8, 30u8),
864 (200, 100, 50),
865 (64, 128, 192),
866 (0, 255, 128),
867 (255, 128, 0),
868 (77, 77, 77),
869 ];
870 for (r, g, b) in colours {
871 let (y, cb, cr) = bt709_rgb_to_ycbcr(r, g, b);
872 let (ro, go, bo) = bt709_ycbcr_to_rgb(y, cb, cr);
873 let dr = (r as i32 - ro as i32).unsigned_abs();
874 let dg = (g as i32 - go as i32).unsigned_abs();
875 let db = (b as i32 - bo as i32).unsigned_abs();
876 assert!(
877 dr <= 5 && dg <= 5 && db <= 5,
878 "BT.709 roundtrip ({r},{g},{b}) → ({ro},{go},{bo}): diff=({dr},{dg},{db})"
879 );
880 }
881 }
882}