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}