1#![forbid(unsafe_code)]
8#![allow(clippy::unreadable_literal)]
9#![allow(clippy::items_after_statements)]
10#![allow(clippy::unnecessary_wraps)]
11#![allow(clippy::struct_excessive_bools)]
12#![allow(clippy::identity_op)]
13#![allow(clippy::range_plus_one)]
14#![allow(clippy::needless_range_loop)]
15#![allow(clippy::useless_conversion)]
16#![allow(clippy::redundant_closure_for_method_calls)]
17#![allow(clippy::single_match_else)]
18#![allow(dead_code)]
19#![allow(clippy::doc_markdown)]
20#![allow(clippy::unused_self)]
21#![allow(clippy::trivially_copy_pass_by_ref)]
22#![allow(clippy::cast_possible_truncation)]
23#![allow(clippy::cast_sign_loss)]
24#![allow(clippy::cast_possible_wrap)]
25#![allow(clippy::missing_errors_doc)]
26#![allow(clippy::too_many_arguments)]
27#![allow(clippy::similar_names)]
28#![allow(clippy::cast_precision_loss)]
29#![allow(clippy::cast_lossless)]
30
31use super::pipeline::FrameContext;
32use super::{FrameBuffer, PlaneBuffer, PlaneType, ReconstructResult};
33
34pub const MAX_AR_COEFFS_Y: usize = 24;
40
41pub const MAX_AR_COEFFS_UV: usize = 25;
43
44pub const MAX_AR_LAG: usize = 3;
46
47pub const GRAIN_BLOCK_SIZE: usize = 32;
49
50pub const MAX_LUMA_POINTS: usize = 14;
52
53pub const MAX_CHROMA_POINTS: usize = 10;
55
56pub const GRAIN_SEED: u16 = 0xB524;
58
59pub const GRAIN_LUT_SIZE: usize = 82;
61
62#[derive(Clone, Copy, Debug, Default)]
68pub struct ScalingPoint {
69 pub value: u8,
71 pub scaling: u8,
73}
74
75impl ScalingPoint {
76 #[must_use]
78 pub const fn new(value: u8, scaling: u8) -> Self {
79 Self { value, scaling }
80 }
81}
82
83#[derive(Clone, Debug)]
85pub struct FilmGrainParams {
86 pub apply_grain: bool,
88 pub grain_seed: u16,
90 pub update_grain: bool,
92 pub num_y_points: usize,
94 pub y_points: [ScalingPoint; MAX_LUMA_POINTS],
96 pub chroma_scaling_from_luma: bool,
98 pub num_cb_points: usize,
100 pub cb_points: [ScalingPoint; MAX_CHROMA_POINTS],
102 pub num_cr_points: usize,
104 pub cr_points: [ScalingPoint; MAX_CHROMA_POINTS],
106 pub grain_scaling_minus_8: u8,
108 pub ar_coeff_lag: u8,
110 pub ar_coeffs_y: [i8; MAX_AR_COEFFS_Y],
112 pub ar_coeffs_cb: [i8; MAX_AR_COEFFS_UV],
114 pub ar_coeffs_cr: [i8; MAX_AR_COEFFS_UV],
116 pub ar_coeff_shift_minus_6: u8,
118 pub grain_scale_shift: u8,
120 pub cb_mult: u8,
122 pub cb_luma_mult: u8,
124 pub cb_offset: u16,
126 pub cr_mult: u8,
128 pub cr_luma_mult: u8,
130 pub cr_offset: u16,
132 pub overlap_flag: bool,
134 pub clip_to_restricted_range: bool,
136}
137
138impl Default for FilmGrainParams {
139 fn default() -> Self {
140 Self {
141 apply_grain: false,
142 grain_seed: 0,
143 update_grain: false,
144 num_y_points: 0,
145 y_points: [ScalingPoint::default(); MAX_LUMA_POINTS],
146 chroma_scaling_from_luma: false,
147 num_cb_points: 0,
148 cb_points: [ScalingPoint::default(); MAX_CHROMA_POINTS],
149 num_cr_points: 0,
150 cr_points: [ScalingPoint::default(); MAX_CHROMA_POINTS],
151 grain_scaling_minus_8: 0,
152 ar_coeff_lag: 0,
153 ar_coeffs_y: [0; MAX_AR_COEFFS_Y],
154 ar_coeffs_cb: [0; MAX_AR_COEFFS_UV],
155 ar_coeffs_cr: [0; MAX_AR_COEFFS_UV],
156 ar_coeff_shift_minus_6: 0,
157 grain_scale_shift: 0,
158 cb_mult: 0,
159 cb_luma_mult: 0,
160 cb_offset: 0,
161 cr_mult: 0,
162 cr_luma_mult: 0,
163 cr_offset: 0,
164 overlap_flag: false,
165 clip_to_restricted_range: false,
166 }
167 }
168}
169
170impl FilmGrainParams {
171 #[must_use]
173 pub fn new() -> Self {
174 Self::default()
175 }
176
177 #[must_use]
179 pub const fn is_enabled(&self) -> bool {
180 self.apply_grain && (self.num_y_points > 0 || self.chroma_scaling_from_luma)
181 }
182
183 #[must_use]
185 pub const fn grain_scaling(&self) -> u8 {
186 self.grain_scaling_minus_8 + 8
187 }
188
189 #[must_use]
191 pub const fn ar_coeff_shift(&self) -> u8 {
192 self.ar_coeff_shift_minus_6 + 6
193 }
194
195 #[must_use]
197 pub fn num_ar_coeffs_y(&self) -> usize {
198 let lag = self.ar_coeff_lag as usize;
199 2 * lag * (lag + 1)
200 }
201
202 #[must_use]
204 pub fn num_ar_coeffs_uv(&self) -> usize {
205 let lag = self.ar_coeff_lag as usize;
206 2 * lag * (lag + 1) + 1
207 }
208
209 pub fn add_y_point(&mut self, value: u8, scaling: u8) {
211 if self.num_y_points < MAX_LUMA_POINTS {
212 self.y_points[self.num_y_points] = ScalingPoint::new(value, scaling);
213 self.num_y_points += 1;
214 }
215 }
216
217 pub fn add_cb_point(&mut self, value: u8, scaling: u8) {
219 if self.num_cb_points < MAX_CHROMA_POINTS {
220 self.cb_points[self.num_cb_points] = ScalingPoint::new(value, scaling);
221 self.num_cb_points += 1;
222 }
223 }
224
225 pub fn add_cr_point(&mut self, value: u8, scaling: u8) {
227 if self.num_cr_points < MAX_CHROMA_POINTS {
228 self.cr_points[self.num_cr_points] = ScalingPoint::new(value, scaling);
229 self.num_cr_points += 1;
230 }
231 }
232}
233
234#[derive(Clone, Debug)]
240pub struct GrainBlock {
241 values: Vec<i16>,
243 width: usize,
245 height: usize,
247}
248
249impl GrainBlock {
250 #[must_use]
252 pub fn new(width: usize, height: usize) -> Self {
253 Self {
254 values: vec![0; width * height],
255 width,
256 height,
257 }
258 }
259
260 #[must_use]
262 pub fn get(&self, x: usize, y: usize) -> i16 {
263 if x < self.width && y < self.height {
264 self.values[y * self.width + x]
265 } else {
266 0
267 }
268 }
269
270 pub fn set(&mut self, x: usize, y: usize, value: i16) {
272 if x < self.width && y < self.height {
273 self.values[y * self.width + x] = value;
274 }
275 }
276
277 #[must_use]
279 pub fn values(&self) -> &[i16] {
280 &self.values
281 }
282
283 pub fn values_mut(&mut self) -> &mut [i16] {
285 &mut self.values
286 }
287}
288
289struct GrainRng {
295 state: u16,
296}
297
298impl GrainRng {
299 fn new(seed: u16) -> Self {
301 Self { state: seed }
302 }
303
304 fn next(&mut self) -> i16 {
306 let bit =
308 ((self.state >> 0) ^ (self.state >> 1) ^ (self.state >> 3) ^ (self.state >> 12)) & 1;
309 self.state = (self.state >> 1) | (bit << 15);
310 (self.state as i16) >> 5 }
312
313 fn gaussian(&mut self) -> i16 {
315 let mut sum: i32 = 0;
317 for _ in 0..4 {
318 sum += i32::from(self.next());
319 }
320 (sum / 4) as i16
321 }
322}
323
324#[derive(Debug)]
330pub struct FilmGrainSynthesizer {
331 params: FilmGrainParams,
333 bit_depth: u8,
335 luma_grain: Vec<i16>,
337 cb_grain: Vec<i16>,
339 cr_grain: Vec<i16>,
341 luma_scaling: Vec<u8>,
343 cb_scaling: Vec<u8>,
345 cr_scaling: Vec<u8>,
347}
348
349impl FilmGrainSynthesizer {
350 #[must_use]
352 pub fn new(bit_depth: u8) -> Self {
353 let lut_size = GRAIN_LUT_SIZE * GRAIN_LUT_SIZE;
354 let scaling_size = 1 << bit_depth.min(8);
355
356 Self {
357 params: FilmGrainParams::default(),
358 bit_depth,
359 luma_grain: vec![0; lut_size],
360 cb_grain: vec![0; lut_size],
361 cr_grain: vec![0; lut_size],
362 luma_scaling: vec![0; scaling_size],
363 cb_scaling: vec![0; scaling_size],
364 cr_scaling: vec![0; scaling_size],
365 }
366 }
367
368 pub fn set_params(&mut self, params: FilmGrainParams) {
370 self.params = params;
371 if self.params.is_enabled() {
372 self.generate_grain_luts();
373 self.generate_scaling_luts();
374 }
375 }
376
377 #[must_use]
379 pub fn params(&self) -> &FilmGrainParams {
380 &self.params
381 }
382
383 fn generate_grain_luts(&mut self) {
385 let mut rng = GrainRng::new(self.params.grain_seed ^ GRAIN_SEED);
386
387 for val in &mut self.luma_grain {
389 *val = rng.gaussian();
390 }
391
392 for val in &mut self.cb_grain {
394 *val = rng.gaussian();
395 }
396
397 for val in &mut self.cr_grain {
399 *val = rng.gaussian();
400 }
401
402 if self.params.ar_coeff_lag > 0 {
404 self.apply_ar_filter();
405 }
406 }
407
408 fn apply_ar_filter(&mut self) {
410 let lag = self.params.ar_coeff_lag as usize;
411 let shift = self.params.ar_coeff_shift();
412
413 for y in lag..GRAIN_LUT_SIZE {
415 for x in lag..(GRAIN_LUT_SIZE - lag) {
416 let mut sum: i32 = 0;
417 let mut coeff_idx = 0;
418
419 for dy in 0..=lag {
420 for dx in 0..(2 * lag + 1) {
421 if dy == 0 && dx >= lag {
422 break;
423 }
424 let coeff = i32::from(self.params.ar_coeffs_y[coeff_idx]);
425 let grain_idx = (y - lag + dy) * GRAIN_LUT_SIZE + (x - lag + dx);
426 sum += coeff * i32::from(self.luma_grain[grain_idx]);
427 coeff_idx += 1;
428 }
429 }
430
431 let idx = y * GRAIN_LUT_SIZE + x;
432 self.luma_grain[idx] = (i32::from(self.luma_grain[idx]) + (sum >> shift)) as i16;
433 }
434 }
435 }
436
437 fn generate_scaling_luts(&mut self) {
439 let y_points: Vec<_> = self.params.y_points[..self.params.num_y_points].to_vec();
441 let cb_points: Vec<_> = self.params.cb_points[..self.params.num_cb_points].to_vec();
442 let cr_points: Vec<_> = self.params.cr_points[..self.params.num_cr_points].to_vec();
443 let chroma_from_luma = self.params.chroma_scaling_from_luma;
444
445 interpolate_scaling_points(&y_points, &mut self.luma_scaling);
447
448 if chroma_from_luma {
450 self.cb_scaling.copy_from_slice(&self.luma_scaling);
451 } else {
452 interpolate_scaling_points(&cb_points, &mut self.cb_scaling);
453 }
454
455 if chroma_from_luma {
457 self.cr_scaling.copy_from_slice(&self.luma_scaling);
458 } else {
459 interpolate_scaling_points(&cr_points, &mut self.cr_scaling);
460 }
461 }
462
463 pub fn apply(
469 &mut self,
470 frame: &mut FrameBuffer,
471 _context: &FrameContext,
472 ) -> ReconstructResult<()> {
473 if !self.params.is_enabled() {
474 return Ok(());
475 }
476
477 let bd = frame.bit_depth();
478
479 self.apply_to_plane(frame.y_plane_mut(), PlaneType::Y, bd);
481
482 if let Some(u) = frame.u_plane_mut() {
484 self.apply_to_plane(u, PlaneType::U, bd);
485 }
486 if let Some(v) = frame.v_plane_mut() {
487 self.apply_to_plane(v, PlaneType::V, bd);
488 }
489
490 Ok(())
491 }
492
493 fn apply_to_plane(&self, plane: &mut PlaneBuffer, plane_type: PlaneType, bd: u8) {
495 let width = plane.width() as usize;
496 let height = plane.height() as usize;
497 let max_val = (1i32 << bd) - 1;
498
499 let (grain_lut, scaling_lut) = match plane_type {
500 PlaneType::Y => (&self.luma_grain, &self.luma_scaling),
501 PlaneType::U => (&self.cb_grain, &self.cb_scaling),
502 PlaneType::V => (&self.cr_grain, &self.cr_scaling),
503 };
504
505 let grain_scale = self.params.grain_scaling();
506 let grain_shift = self.params.grain_scale_shift;
507
508 for y in 0..height {
510 for x in 0..width {
511 let pixel = plane.get(x as u32, y as u32);
512
513 let scaling_idx = (pixel as usize).min(scaling_lut.len() - 1);
515 let scaling = i32::from(scaling_lut[scaling_idx]);
516
517 let grain_x = x % GRAIN_LUT_SIZE;
519 let grain_y = y % GRAIN_LUT_SIZE;
520 let grain_idx = grain_y * GRAIN_LUT_SIZE + grain_x;
521 let grain = i32::from(grain_lut[grain_idx]);
522
523 let scaled_grain = (grain * scaling) >> grain_scale;
525 let adjusted_grain = scaled_grain >> grain_shift;
526 let result = (i32::from(pixel) + adjusted_grain).clamp(0, max_val);
527
528 plane.set(x as u32, y as u32, result as i16);
529 }
530 }
531 }
532
533 #[must_use]
535 pub fn generate_block(&self, x: usize, y: usize, plane: PlaneType) -> GrainBlock {
536 let mut block = GrainBlock::new(GRAIN_BLOCK_SIZE, GRAIN_BLOCK_SIZE);
537
538 let grain_lut = match plane {
539 PlaneType::Y => &self.luma_grain,
540 PlaneType::U => &self.cb_grain,
541 PlaneType::V => &self.cr_grain,
542 };
543
544 for by in 0..GRAIN_BLOCK_SIZE {
545 for bx in 0..GRAIN_BLOCK_SIZE {
546 let grain_x = (x + bx) % GRAIN_LUT_SIZE;
547 let grain_y = (y + by) % GRAIN_LUT_SIZE;
548 let grain_idx = grain_y * GRAIN_LUT_SIZE + grain_x;
549 block.set(bx, by, grain_lut[grain_idx]);
550 }
551 }
552
553 block
554 }
555}
556
557fn interpolate_scaling_points(points: &[ScalingPoint], lut: &mut [u8]) {
559 if points.is_empty() {
560 lut.fill(0);
561 return;
562 }
563
564 let lut_size = lut.len();
565
566 let first_scaling = points[0].scaling;
568 for val in lut.iter_mut().take(points[0].value as usize) {
569 *val = first_scaling;
570 }
571
572 for i in 0..points.len().saturating_sub(1) {
574 let p0 = &points[i];
575 let p1 = &points[i + 1];
576
577 for x in p0.value as usize..=p1.value as usize {
578 if x < lut_size {
579 let t = (x - p0.value as usize) as f32 / (p1.value - p0.value).max(1) as f32;
580 lut[x] = ((1.0 - t) * p0.scaling as f32 + t * p1.scaling as f32).round() as u8;
581 }
582 }
583 }
584
585 let last_point = points.last().expect("points is non-empty by construction");
587 for val in lut.iter_mut().skip(last_point.value as usize + 1) {
588 *val = last_point.scaling;
589 }
590}
591
592#[cfg(test)]
597mod tests {
598 use super::*;
599 use crate::reconstruct::ChromaSubsampling;
600
601 #[test]
602 fn test_scaling_point() {
603 let point = ScalingPoint::new(128, 64);
604 assert_eq!(point.value, 128);
605 assert_eq!(point.scaling, 64);
606 }
607
608 #[test]
609 fn test_film_grain_params_default() {
610 let params = FilmGrainParams::default();
611 assert!(!params.apply_grain);
612 assert!(!params.is_enabled());
613 assert_eq!(params.num_y_points, 0);
614 }
615
616 #[test]
617 fn test_film_grain_params_enabled() {
618 let mut params = FilmGrainParams::new();
619 params.apply_grain = true;
620 params.add_y_point(0, 32);
621 params.add_y_point(255, 64);
622
623 assert!(params.is_enabled());
624 assert_eq!(params.num_y_points, 2);
625 }
626
627 #[test]
628 fn test_film_grain_params_scaling_values() {
629 let mut params = FilmGrainParams::new();
630 params.grain_scaling_minus_8 = 2;
631 params.ar_coeff_shift_minus_6 = 3;
632
633 assert_eq!(params.grain_scaling(), 10);
634 assert_eq!(params.ar_coeff_shift(), 9);
635 }
636
637 #[test]
638 fn test_film_grain_params_ar_coeffs() {
639 let mut params = FilmGrainParams::new();
640
641 params.ar_coeff_lag = 0;
642 assert_eq!(params.num_ar_coeffs_y(), 0);
643
644 params.ar_coeff_lag = 1;
645 assert_eq!(params.num_ar_coeffs_y(), 4);
646
647 params.ar_coeff_lag = 2;
648 assert_eq!(params.num_ar_coeffs_y(), 12);
649
650 params.ar_coeff_lag = 3;
651 assert_eq!(params.num_ar_coeffs_y(), 24);
652 }
653
654 #[test]
655 fn test_grain_block() {
656 let mut block = GrainBlock::new(32, 32);
657 block.set(10, 20, 100);
658 assert_eq!(block.get(10, 20), 100);
659 assert_eq!(block.get(0, 0), 0);
660 }
661
662 #[test]
663 fn test_grain_rng() {
664 let mut rng = GrainRng::new(12345);
665
666 for _ in 0..100 {
668 let val = rng.next();
669 assert!(val >= -2048 && val < 2048);
670 }
671 }
672
673 #[test]
674 fn test_grain_rng_gaussian() {
675 let mut rng = GrainRng::new(12345);
676
677 let mut sum: i32 = 0;
679 for _ in 0..1000 {
680 sum += i32::from(rng.gaussian());
681 }
682 let mean = sum / 1000;
683 assert!(mean.abs() < 100);
684 }
685
686 #[test]
687 fn test_film_grain_synthesizer_creation() {
688 let synth = FilmGrainSynthesizer::new(8);
689 assert_eq!(synth.bit_depth, 8);
690 assert!(!synth.params().is_enabled());
691 }
692
693 #[test]
694 fn test_film_grain_synthesizer_set_params() {
695 let mut synth = FilmGrainSynthesizer::new(8);
696
697 let mut params = FilmGrainParams::new();
698 params.apply_grain = true;
699 params.grain_seed = 12345;
700 params.add_y_point(0, 32);
701 params.add_y_point(255, 64);
702
703 synth.set_params(params);
704 assert!(synth.params().is_enabled());
705 }
706
707 #[test]
708 fn test_film_grain_apply_disabled() {
709 let mut frame = FrameBuffer::new(64, 64, 8, ChromaSubsampling::Cs420);
710 let context = FrameContext::new(64, 64);
711
712 let mut synth = FilmGrainSynthesizer::new(8);
713 let result = synth.apply(&mut frame, &context);
714 assert!(result.is_ok());
715 }
716
717 #[test]
718 fn test_film_grain_apply_enabled() {
719 let mut frame = FrameBuffer::new(64, 64, 8, ChromaSubsampling::Cs420);
720
721 for y in 0..64 {
723 for x in 0..64 {
724 frame.y_plane_mut().set(x, y, 128);
725 }
726 }
727
728 let context = FrameContext::new(64, 64);
729
730 let mut synth = FilmGrainSynthesizer::new(8);
731 let mut params = FilmGrainParams::new();
732 params.apply_grain = true;
733 params.grain_seed = 12345;
734 params.add_y_point(0, 16);
735 params.add_y_point(255, 32);
736 synth.set_params(params);
737
738 let result = synth.apply(&mut frame, &context);
739 assert!(result.is_ok());
740 }
741
742 #[test]
743 fn test_generate_block() {
744 let mut synth = FilmGrainSynthesizer::new(8);
745
746 let mut params = FilmGrainParams::new();
747 params.apply_grain = true;
748 params.grain_seed = 12345;
749 params.add_y_point(0, 32);
750 params.add_y_point(255, 64);
751 synth.set_params(params);
752
753 let block = synth.generate_block(0, 0, PlaneType::Y);
754 assert_eq!(block.width, GRAIN_BLOCK_SIZE);
755 assert_eq!(block.height, GRAIN_BLOCK_SIZE);
756 }
757
758 #[test]
759 fn test_constants() {
760 assert_eq!(MAX_AR_COEFFS_Y, 24);
761 assert_eq!(MAX_AR_COEFFS_UV, 25);
762 assert_eq!(MAX_AR_LAG, 3);
763 assert_eq!(GRAIN_BLOCK_SIZE, 32);
764 assert_eq!(MAX_LUMA_POINTS, 14);
765 assert_eq!(MAX_CHROMA_POINTS, 10);
766 assert_eq!(GRAIN_LUT_SIZE, 82);
767 }
768}