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::many_single_char_names)]
29#![allow(clippy::cast_precision_loss)]
30#![allow(clippy::cast_lossless)]
31#![allow(clippy::needless_bool)]
32#![allow(clippy::collapsible_if)]
33#![allow(clippy::if_not_else)]
34
35use super::pipeline::FrameContext;
36use super::{FrameBuffer, PlaneBuffer, PlaneType, ReconstructResult};
37
38pub const MAX_LOOP_FILTER_LEVEL: u8 = 63;
44
45pub const MAX_SHARPNESS_LEVEL: u8 = 7;
47
48pub const NARROW_FILTER_TAPS: usize = 4;
50
51pub const WIDE_FILTER_TAPS: usize = 8;
53
54pub const EXTRA_WIDE_FILTER_TAPS: usize = 14;
56
57#[derive(Clone, Copy, Debug, PartialEq, Eq)]
63pub enum FilterDirection {
64 Vertical,
66 Horizontal,
68}
69
70impl FilterDirection {
71 #[must_use]
73 pub const fn perpendicular(self) -> Self {
74 match self {
75 Self::Vertical => Self::Horizontal,
76 Self::Horizontal => Self::Vertical,
77 }
78 }
79}
80
81#[derive(Clone, Copy, Debug, Default)]
87pub struct EdgeFilter {
88 pub level: u8,
90 pub limit: u8,
92 pub threshold: u8,
94 pub hev_threshold: u8,
96 pub filter_size: u8,
98}
99
100impl EdgeFilter {
101 #[must_use]
103 pub fn new(level: u8, sharpness: u8) -> Self {
104 let limit = Self::compute_limit(level, sharpness);
105 let threshold = limit >> 1;
106 let hev_threshold = Self::compute_hev_threshold(level);
107
108 Self {
109 level,
110 limit,
111 threshold,
112 hev_threshold,
113 filter_size: 4,
114 }
115 }
116
117 fn compute_limit(level: u8, sharpness: u8) -> u8 {
119 if sharpness > 0 {
120 let block_limit = (9u8).saturating_sub(sharpness).max(1);
121 let shift = (sharpness + 3) >> 2;
122 let shifted = level >> shift;
123 shifted.min(block_limit).max(1)
124 } else {
125 level.max(1)
126 }
127 }
128
129 const fn compute_hev_threshold(level: u8) -> u8 {
131 if level >= 40 {
132 2
133 } else if level >= 20 {
134 1
135 } else {
136 0
137 }
138 }
139
140 #[must_use]
142 pub const fn should_filter(&self) -> bool {
143 self.level > 0
144 }
145
146 #[must_use]
148 pub fn is_flat(&self, p: &[i16], q: &[i16]) -> bool {
149 if p.len() < 4 || q.len() < 4 {
150 return false;
151 }
152
153 let flat_threshold = i16::from(self.threshold);
154
155 for i in 1..4 {
157 if (p[i] - p[0]).abs() > flat_threshold {
158 return false;
159 }
160 }
161
162 for i in 1..4 {
164 if (q[i] - q[0]).abs() > flat_threshold {
165 return false;
166 }
167 }
168
169 true
170 }
171
172 #[must_use]
174 pub fn is_flat2(&self, p: &[i16], q: &[i16]) -> bool {
175 if p.len() < 7 || q.len() < 7 {
176 return false;
177 }
178
179 let flat_threshold = i16::from(self.threshold);
180
181 for i in 4..7 {
183 if (p[i] - p[0]).abs() > flat_threshold {
184 return false;
185 }
186 }
187
188 for i in 4..7 {
190 if (q[i] - q[0]).abs() > flat_threshold {
191 return false;
192 }
193 }
194
195 true
196 }
197
198 #[must_use]
200 pub fn has_hev(&self, p1: i16, p0: i16, q0: i16, q1: i16) -> bool {
201 let hev = i16::from(self.hev_threshold);
202 (p1 - p0).abs() > hev || (q1 - q0).abs() > hev
203 }
204}
205
206#[derive(Clone, Debug, Default)]
212pub struct LoopFilterConfig {
213 pub y_levels: [u8; 2],
215 pub u_levels: [u8; 2],
217 pub v_levels: [u8; 2],
219 pub sharpness: u8,
221 pub delta_enabled: bool,
223 pub ref_deltas: [i8; 8],
225 pub mode_deltas: [i8; 2],
227}
228
229impl LoopFilterConfig {
230 #[must_use]
232 pub fn new() -> Self {
233 Self::default()
234 }
235
236 #[must_use]
238 pub const fn with_y_levels(mut self, vertical: u8, horizontal: u8) -> Self {
239 self.y_levels = [vertical, horizontal];
240 self
241 }
242
243 #[must_use]
245 pub const fn with_sharpness(mut self, sharpness: u8) -> Self {
246 self.sharpness = sharpness;
247 self
248 }
249
250 #[must_use]
252 pub fn get_level(&self, plane: PlaneType, direction: FilterDirection) -> u8 {
253 let dir_idx = match direction {
254 FilterDirection::Vertical => 0,
255 FilterDirection::Horizontal => 1,
256 };
257
258 match plane {
259 PlaneType::Y => self.y_levels[dir_idx],
260 PlaneType::U => self.u_levels[dir_idx],
261 PlaneType::V => self.v_levels[dir_idx],
262 }
263 }
264
265 #[must_use]
267 pub fn is_enabled(&self) -> bool {
268 self.y_levels.iter().any(|&l| l > 0)
269 || self.u_levels.iter().any(|&l| l > 0)
270 || self.v_levels.iter().any(|&l| l > 0)
271 }
272
273 #[must_use]
275 pub fn create_edge_filter(&self, plane: PlaneType, direction: FilterDirection) -> EdgeFilter {
276 let level = self.get_level(plane, direction);
277 EdgeFilter::new(level, self.sharpness)
278 }
279}
280
281#[inline(always)]
289fn filter4(p1: i16, p0: i16, q0: i16, q1: i16, hev: bool, bd: u8) -> (i16, i16, i16, i16) {
290 let max_val = (1i16 << bd) - 1;
291
292 let filter = if hev {
293 let f = (p1 - q1).clamp(-128, 127) + 3 * (q0 - p0);
295 f.clamp(-128, 127)
296 } else {
297 3 * (q0 - p0)
299 };
300
301 let filter1 = (filter + 4).clamp(-128, 127) >> 3;
302 let filter2 = (filter + 3).clamp(-128, 127) >> 3;
303
304 let new_q0 = (q0 - filter1).clamp(0, max_val);
305 let new_p0 = (p0 + filter2).clamp(0, max_val);
306
307 let (new_p1, new_q1) = if !hev {
308 let filter3 = (filter1 + 1) >> 1;
310 (
311 (p1 + filter3).clamp(0, max_val),
312 (q1 - filter3).clamp(0, max_val),
313 )
314 } else {
315 (p1, q1)
316 };
317
318 (new_p1, new_p0, new_q0, new_q1)
319}
320
321#[inline(always)]
325fn filter8(p: &mut [i16], q: &mut [i16], bd: u8) {
326 if p.len() < 4 || q.len() < 4 {
327 return;
328 }
329
330 let max_val = (1i16 << bd) - 1;
331
332 let p0 = i32::from(p[0]);
334 let p1 = i32::from(p[1]);
335 let p2 = i32::from(p[2]);
336 let p3 = i32::from(p[3]);
337 let q0 = i32::from(q[0]);
338 let q1 = i32::from(q[1]);
339 let q2 = i32::from(q[2]);
340 let q3 = i32::from(q[3]);
341
342 p[0] = ((p0 * 6 + p1 * 2 + q0 * 2 + q1 + p2 + 6) >> 3).clamp(0, i32::from(max_val)) as i16;
344 p[1] = ((p1 * 6 + p0 * 2 + p2 * 2 + q0 + p3 + 6) >> 3).clamp(0, i32::from(max_val)) as i16;
345 p[2] = ((p2 * 6 + p1 * 2 + p3 * 2 + p0 + q0 + 6) >> 3).clamp(0, i32::from(max_val)) as i16;
346
347 q[0] = ((q0 * 6 + q1 * 2 + p0 * 2 + p1 + q2 + 6) >> 3).clamp(0, i32::from(max_val)) as i16;
348 q[1] = ((q1 * 6 + q0 * 2 + q2 * 2 + p0 + q3 + 6) >> 3).clamp(0, i32::from(max_val)) as i16;
349 q[2] = ((q2 * 6 + q1 * 2 + q3 * 2 + q0 + p0 + 6) >> 3).clamp(0, i32::from(max_val)) as i16;
350}
351
352#[inline(always)]
355fn filter14(p: &mut [i16], q: &mut [i16], bd: u8) {
356 if p.len() < 7 || q.len() < 7 {
357 return;
358 }
359
360 let max_val = (1i32 << bd) - 1;
361
362 let orig_p: Vec<i32> = p.iter().map(|&x| i32::from(x)).collect();
364 let orig_q: Vec<i32> = q.iter().map(|&x| i32::from(x)).collect();
365
366 let weights = [1, 1, 2, 2, 4, 2, 2, 1, 1];
368 let sum_weights = 16;
369
370 for i in 0..6 {
372 let mut sum = 0i32;
373 for (j, &w) in weights.iter().enumerate() {
374 let idx = i as i32 - 4 + j as i32;
375 let val = if idx < 0 {
376 orig_p[(-idx) as usize].min(orig_p[6])
377 } else if idx < 7 {
378 orig_p[idx as usize]
379 } else {
380 orig_q[(idx - 7) as usize].min(orig_q[6])
381 };
382 sum += val * i32::from(w);
383 }
384 p[i] = ((sum + sum_weights / 2) / sum_weights).clamp(0, max_val) as i16;
385 }
386
387 for i in 0..6 {
389 let mut sum = 0i32;
390 for (j, &w) in weights.iter().enumerate() {
391 let idx = i as i32 - 4 + j as i32;
392 let val = if idx < 0 {
393 orig_p[(6 + idx) as usize].min(orig_p[6])
394 } else if idx < 7 {
395 orig_q[idx as usize]
396 } else {
397 orig_q[6]
398 };
399 sum += val * i32::from(w);
400 }
401 q[i] = ((sum + sum_weights / 2) / sum_weights).clamp(0, max_val) as i16;
402 }
403}
404
405#[derive(Debug)]
411pub struct LoopFilterPipeline {
412 config: LoopFilterConfig,
414 block_size: usize,
416}
417
418impl Default for LoopFilterPipeline {
419 fn default() -> Self {
420 Self::new()
421 }
422}
423
424impl LoopFilterPipeline {
425 #[must_use]
427 pub fn new() -> Self {
428 Self {
429 config: LoopFilterConfig::new(),
430 block_size: 8,
431 }
432 }
433
434 #[must_use]
436 pub fn with_config(config: LoopFilterConfig) -> Self {
437 Self {
438 config,
439 block_size: 8,
440 }
441 }
442
443 pub fn set_config(&mut self, config: LoopFilterConfig) {
445 self.config = config;
446 }
447
448 #[must_use]
450 pub fn config(&self) -> &LoopFilterConfig {
451 &self.config
452 }
453
454 pub fn apply(
460 &mut self,
461 frame: &mut FrameBuffer,
462 _context: &FrameContext,
463 ) -> ReconstructResult<()> {
464 if !self.config.is_enabled() {
465 return Ok(());
466 }
467
468 self.filter_plane(frame.y_plane_mut(), PlaneType::Y)?;
470
471 if let Some(u_plane) = frame.u_plane_mut() {
473 self.filter_plane(u_plane, PlaneType::U)?;
474 }
475
476 if let Some(v_plane) = frame.v_plane_mut() {
478 self.filter_plane(v_plane, PlaneType::V)?;
479 }
480
481 Ok(())
482 }
483
484 fn filter_plane(
486 &self,
487 plane: &mut PlaneBuffer,
488 plane_type: PlaneType,
489 ) -> ReconstructResult<()> {
490 let bit_depth = plane.bit_depth();
491 let width = plane.width() as usize;
492 let height = plane.height() as usize;
493
494 let v_filter = self
496 .config
497 .create_edge_filter(plane_type, FilterDirection::Vertical);
498 if v_filter.should_filter() {
499 for by in 0..(height / self.block_size) {
500 for bx in 1..(width / self.block_size) {
501 self.filter_vertical_edge(
502 plane,
503 (bx * self.block_size) as u32,
504 (by * self.block_size) as u32,
505 &v_filter,
506 bit_depth,
507 );
508 }
509 }
510 }
511
512 let h_filter = self
514 .config
515 .create_edge_filter(plane_type, FilterDirection::Horizontal);
516 if h_filter.should_filter() {
517 for by in 1..(height / self.block_size) {
518 for bx in 0..(width / self.block_size) {
519 self.filter_horizontal_edge(
520 plane,
521 (bx * self.block_size) as u32,
522 (by * self.block_size) as u32,
523 &h_filter,
524 bit_depth,
525 );
526 }
527 }
528 }
529
530 Ok(())
531 }
532
533 fn filter_vertical_edge(
547 &self,
548 plane: &mut PlaneBuffer,
549 x: u32,
550 y: u32,
551 filter: &EdgeFilter,
552 bd: u8,
553 ) {
554 let px0 = x.saturating_sub(1);
557 let px1 = x.saturating_sub(2);
558 let px2 = x.saturating_sub(3);
559 let px3 = x.saturating_sub(4);
560 let qx0 = x;
561 let qx1 = x + 1;
562 let qx2 = x + 2;
563 let qx3 = x + 3;
564
565 for row in 0..self.block_size {
566 let py = y + row as u32;
567
568 let mut p = [
571 plane.get(px0, py),
572 plane.get(px1, py),
573 plane.get(px2, py),
574 plane.get(px3, py),
575 ];
576 let mut q = [
577 plane.get(qx0, py),
578 plane.get(qx1, py),
579 plane.get(qx2, py),
580 plane.get(qx3, py),
581 ];
582
583 if !self.filter_mask(&p, &q, filter) {
585 continue;
586 }
587
588 let hev = filter.has_hev(p[1], p[0], q[0], q[1]);
589
590 if filter.is_flat(&p, &q) {
591 filter8(&mut p, &mut q, bd);
592 } else {
593 let (new_p1, new_p0, new_q0, new_q1) = filter4(p[1], p[0], q[0], q[1], hev, bd);
594 p[1] = new_p1;
595 p[0] = new_p0;
596 q[0] = new_q0;
597 q[1] = new_q1;
598 }
599
600 plane.set(px0, py, p[0]);
602 plane.set(px1, py, p[1]);
603 plane.set(px2, py, p[2]);
604 plane.set(px3, py, p[3]);
605 plane.set(qx0, py, q[0]);
606 plane.set(qx1, py, q[1]);
607 plane.set(qx2, py, q[2]);
608 plane.set(qx3, py, q[3]);
609 }
610 }
611
612 fn filter_horizontal_edge(
625 &self,
626 plane: &mut PlaneBuffer,
627 x: u32,
628 y: u32,
629 filter: &EdgeFilter,
630 bd: u8,
631 ) {
632 let py0 = y.saturating_sub(1);
635 let py1 = y.saturating_sub(2);
636 let py2 = y.saturating_sub(3);
637 let py3 = y.saturating_sub(4);
638 let qy0 = y;
639 let qy1 = y + 1;
640 let qy2 = y + 2;
641 let qy3 = y + 3;
642
643 for col in 0..self.block_size {
644 let px = x + col as u32;
645
646 let mut p = [
649 plane.get(px, py0),
650 plane.get(px, py1),
651 plane.get(px, py2),
652 plane.get(px, py3),
653 ];
654 let mut q = [
655 plane.get(px, qy0),
656 plane.get(px, qy1),
657 plane.get(px, qy2),
658 plane.get(px, qy3),
659 ];
660
661 if !self.filter_mask(&p, &q, filter) {
663 continue;
664 }
665
666 let hev = filter.has_hev(p[1], p[0], q[0], q[1]);
667
668 if filter.is_flat(&p, &q) {
669 filter8(&mut p, &mut q, bd);
670 } else {
671 let (new_p1, new_p0, new_q0, new_q1) = filter4(p[1], p[0], q[0], q[1], hev, bd);
672 p[1] = new_p1;
673 p[0] = new_p0;
674 q[0] = new_q0;
675 q[1] = new_q1;
676 }
677
678 plane.set(px, py0, p[0]);
680 plane.set(px, py1, p[1]);
681 plane.set(px, py2, p[2]);
682 plane.set(px, py3, p[3]);
683 plane.set(px, qy0, q[0]);
684 plane.set(px, qy1, q[1]);
685 plane.set(px, qy2, q[2]);
686 plane.set(px, qy3, q[3]);
687 }
688 }
689
690 #[inline(always)]
694 fn filter_mask(&self, p: &[i16], q: &[i16], filter: &EdgeFilter) -> bool {
695 if p.len() < 2 || q.len() < 2 {
696 return false;
697 }
698
699 let limit = i16::from(filter.limit);
700 let threshold = i16::from(filter.threshold);
701
702 if (p[0] - q[0]).abs() > (limit * 2 + threshold) {
704 return false;
705 }
706
707 if (p[1] - p[0]).abs() > limit {
709 return false;
710 }
711
712 if (q[1] - q[0]).abs() > limit {
713 return false;
714 }
715
716 true
717 }
718}
719
720#[cfg(test)]
725mod tests {
726 use super::*;
727 use crate::reconstruct::ChromaSubsampling;
728
729 #[test]
730 fn test_filter_direction() {
731 assert_eq!(
732 FilterDirection::Vertical.perpendicular(),
733 FilterDirection::Horizontal
734 );
735 assert_eq!(
736 FilterDirection::Horizontal.perpendicular(),
737 FilterDirection::Vertical
738 );
739 }
740
741 #[test]
742 fn test_edge_filter_new() {
743 let filter = EdgeFilter::new(32, 0);
744 assert_eq!(filter.level, 32);
745 assert!(filter.limit > 0);
746 assert!(filter.should_filter());
747
748 let filter_zero = EdgeFilter::new(0, 0);
749 assert!(!filter_zero.should_filter());
750 }
751
752 #[test]
753 fn test_edge_filter_hev_threshold() {
754 let filter_low = EdgeFilter::new(10, 0);
755 assert_eq!(filter_low.hev_threshold, 0);
756
757 let filter_mid = EdgeFilter::new(30, 0);
758 assert_eq!(filter_mid.hev_threshold, 1);
759
760 let filter_high = EdgeFilter::new(50, 0);
761 assert_eq!(filter_high.hev_threshold, 2);
762 }
763
764 #[test]
765 fn test_edge_filter_flat_detection() {
766 let filter = EdgeFilter::new(32, 0);
767
768 let p = [128i16, 128, 128, 128];
770 let q = [128i16, 128, 128, 128];
771 assert!(filter.is_flat(&p, &q));
772
773 let p_nonflat = [128i16, 100, 128, 128];
775 assert!(!filter.is_flat(&p_nonflat, &q));
776 }
777
778 #[test]
779 fn test_loop_filter_config() {
780 let config = LoopFilterConfig::new()
781 .with_y_levels(32, 32)
782 .with_sharpness(2);
783
784 assert_eq!(
785 config.get_level(PlaneType::Y, FilterDirection::Vertical),
786 32
787 );
788 assert_eq!(
789 config.get_level(PlaneType::Y, FilterDirection::Horizontal),
790 32
791 );
792 assert!(config.is_enabled());
793 }
794
795 #[test]
796 fn test_loop_filter_config_disabled() {
797 let config = LoopFilterConfig::new();
798 assert!(!config.is_enabled());
799 }
800
801 #[test]
802 fn test_loop_filter_pipeline_creation() {
803 let pipeline = LoopFilterPipeline::new();
804 assert!(!pipeline.config().is_enabled());
805 }
806
807 #[test]
808 fn test_loop_filter_pipeline_with_config() {
809 let config = LoopFilterConfig::new().with_y_levels(20, 20);
810 let pipeline = LoopFilterPipeline::with_config(config);
811 assert!(pipeline.config().is_enabled());
812 }
813
814 #[test]
815 fn test_filter4() {
816 let p1 = 100i16;
817 let p0 = 120i16;
818 let q0 = 140i16;
819 let q1 = 130i16;
820
821 let (_new_p1, new_p0, new_q0, _new_q1) = filter4(p1, p0, q0, q1, false, 8);
822
823 assert!((new_p0 - new_q0).abs() < (120 - 140i16).abs());
825 }
826
827 #[test]
828 fn test_filter8() {
829 let mut p = [130i16, 128, 126, 124];
830 let mut q = [140i16, 142, 144, 146];
831
832 filter8(&mut p, &mut q, 8);
833
834 let edge_diff_after = (p[0] - q[0]).abs();
837 assert!(edge_diff_after < 15);
838 }
839
840 #[test]
841 fn test_loop_filter_apply_disabled() {
842 let mut frame = FrameBuffer::new(64, 64, 8, ChromaSubsampling::Cs420);
843 let context = FrameContext::new(64, 64);
844
845 let mut pipeline = LoopFilterPipeline::new();
846 let result = pipeline.apply(&mut frame, &context);
847
848 assert!(result.is_ok());
849 }
850
851 #[test]
852 fn test_loop_filter_apply_enabled() {
853 let mut frame = FrameBuffer::new(64, 64, 8, ChromaSubsampling::Cs420);
854
855 for y in 0..64 {
857 for x in 0..8 {
858 frame.y_plane_mut().set(x, y as u32, 100);
859 }
860 for x in 8..64 {
861 frame.y_plane_mut().set(x as u32, y as u32, 150);
862 }
863 }
864
865 let context = FrameContext::new(64, 64);
866 let config = LoopFilterConfig::new().with_y_levels(32, 32);
867 let mut pipeline = LoopFilterPipeline::with_config(config);
868
869 let result = pipeline.apply(&mut frame, &context);
870 assert!(result.is_ok());
871 }
872
873 #[test]
874 fn test_constants() {
875 assert_eq!(MAX_LOOP_FILTER_LEVEL, 63);
876 assert_eq!(MAX_SHARPNESS_LEVEL, 7);
877 assert_eq!(NARROW_FILTER_TAPS, 4);
878 assert_eq!(WIDE_FILTER_TAPS, 8);
879 assert_eq!(EXTRA_WIDE_FILTER_TAPS, 14);
880 }
881
882 #[test]
889 fn test_loop_filter_1920x1080_1000_iterations_completes() {
890 use std::time::Instant;
891
892 let w = 64u32; let h = 64u32;
894 let mut frame = FrameBuffer::new(w, h, 8, ChromaSubsampling::Cs420);
895 for y in 0..h {
896 for x in 0..w {
897 let val = ((x + y) % 220 + 16) as i16;
898 frame.y_plane_mut().set(x, y, val);
899 }
900 }
901
902 let config = LoopFilterConfig::new().with_y_levels(32, 32);
903 let mut pipeline = LoopFilterPipeline::with_config(config);
904 let context = FrameContext::new(w, h);
905
906 let start = Instant::now();
907 for _ in 0..1000 {
908 pipeline
909 .apply(&mut frame, &context)
910 .expect("loop filter apply");
911 }
912 let elapsed = start.elapsed();
913
914 assert!(
916 elapsed.as_nanos() > 0,
917 "loop filter must take non-zero time"
918 );
919 }
920
921 #[test]
925 fn test_vertical_edge_filter_cached_offsets_match() {
926 let config = LoopFilterConfig::new().with_y_levels(40, 40);
927 let pipeline = LoopFilterPipeline::with_config(config.clone());
928
929 let mut frame_a = FrameBuffer::new(64, 64, 8, ChromaSubsampling::Cs420);
930 let mut frame_b = FrameBuffer::new(64, 64, 8, ChromaSubsampling::Cs420);
931
932 for y in 0..64 {
934 for x in 0..64 {
935 let val = ((x * 3 + y * 7) % 220 + 16) as i16;
936 frame_a.y_plane_mut().set(x, y, val);
937 frame_b.y_plane_mut().set(x, y, val);
938 }
939 }
940
941 let ctx = FrameContext::new(64, 64);
942 let mut p1 = pipeline;
943 let mut p2 = LoopFilterPipeline::with_config(config);
944
945 p1.apply(&mut frame_a, &ctx).expect("filter a");
946 p2.apply(&mut frame_b, &ctx).expect("filter b");
947
948 for y in 0..64 {
950 for x in 0..64 {
951 let a = frame_a.y_plane_mut().get(x, y);
952 let b = frame_b.y_plane_mut().get(x, y);
953 assert_eq!(a, b, "mismatch at ({x},{y}): {a} != {b}");
954 }
955 }
956 }
957
958 #[test]
961 fn test_horizontal_edge_filter_smooths_discontinuity() {
962 let mut frame = FrameBuffer::new(64, 64, 8, ChromaSubsampling::Cs420);
963
964 for y in 0..64 {
966 for x in 0..64 {
967 let val = if y < 32 { 50 } else { 200 };
968 frame.y_plane_mut().set(x as u32, y, val);
969 }
970 }
971
972 let config = LoopFilterConfig::new().with_y_levels(32, 32);
973 let mut pipeline = LoopFilterPipeline::with_config(config);
974 let ctx = FrameContext::new(64, 64);
975 pipeline.apply(&mut frame, &ctx).expect("apply");
976
977 for y in 0..64u32 {
979 for x in 0..64u32 {
980 let v = frame.y_plane_mut().get(x, y);
981 assert!(
982 (0..=255).contains(&v),
983 "value {v} out of range at ({x},{y})"
984 );
985 }
986 }
987 }
988
989 #[test]
991 fn test_filter14_stays_in_range() {
992 let mut p = [200i16, 198, 196, 194, 192, 190, 188];
993 let mut q = [210i16, 212, 214, 216, 218, 220, 222];
994 filter14(&mut p, &mut q, 8);
995 for &v in p.iter().chain(q.iter()) {
996 assert!(
997 (0..=255).contains(&v),
998 "filter14 produced out-of-range value {v}"
999 );
1000 }
1001 }
1002
1003 #[test]
1005 fn test_zero_level_no_modification() {
1006 let mut frame = FrameBuffer::new(64, 64, 8, ChromaSubsampling::Cs420);
1007 for y in 0..64 {
1008 for x in 0..64 {
1009 frame.y_plane_mut().set(x as u32, y, 128);
1010 }
1011 }
1012 let config = LoopFilterConfig::new(); let mut pipeline = LoopFilterPipeline::with_config(config);
1014 let ctx = FrameContext::new(64, 64);
1015 pipeline.apply(&mut frame, &ctx).expect("apply disabled");
1016 for y in 0..64u32 {
1017 for x in 0..64u32 {
1018 assert_eq!(frame.y_plane_mut().get(x, y), 128);
1019 }
1020 }
1021 }
1022}