Skip to main content

oximedia_codec/reconstruct/
loop_filter.rs

1//! Loop filter pipeline for edge filtering.
2//!
3//! The loop filter is applied after transform reconstruction to reduce
4//! blocking artifacts at block boundaries. This module implements both
5//! horizontal and vertical edge filtering.
6
7#![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
38// =============================================================================
39// Constants
40// =============================================================================
41
42/// Maximum loop filter level.
43pub const MAX_LOOP_FILTER_LEVEL: u8 = 63;
44
45/// Maximum sharpness level.
46pub const MAX_SHARPNESS_LEVEL: u8 = 7;
47
48/// Number of filter taps for narrow filter.
49pub const NARROW_FILTER_TAPS: usize = 4;
50
51/// Number of filter taps for wide filter.
52pub const WIDE_FILTER_TAPS: usize = 8;
53
54/// Number of filter taps for extra-wide filter.
55pub const EXTRA_WIDE_FILTER_TAPS: usize = 14;
56
57// =============================================================================
58// Filter Direction
59// =============================================================================
60
61/// Direction of edge filtering.
62#[derive(Clone, Copy, Debug, PartialEq, Eq)]
63pub enum FilterDirection {
64    /// Vertical edges (filter horizontally).
65    Vertical,
66    /// Horizontal edges (filter vertically).
67    Horizontal,
68}
69
70impl FilterDirection {
71    /// Get the perpendicular direction.
72    #[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// =============================================================================
82// Edge Filter
83// =============================================================================
84
85/// Configuration for a single edge filter.
86#[derive(Clone, Copy, Debug, Default)]
87pub struct EdgeFilter {
88    /// Filter level (0-63).
89    pub level: u8,
90    /// Limit value.
91    pub limit: u8,
92    /// Threshold for flat detection.
93    pub threshold: u8,
94    /// High edge variance threshold.
95    pub hev_threshold: u8,
96    /// Filter size (4, 8, or 14 taps).
97    pub filter_size: u8,
98}
99
100impl EdgeFilter {
101    /// Create a new edge filter.
102    #[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    /// Compute limit based on level and sharpness.
118    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    /// Compute high edge variance threshold.
130    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    /// Check if filtering should be applied.
141    #[must_use]
142    pub const fn should_filter(&self) -> bool {
143        self.level > 0
144    }
145
146    /// Check if this is a flat region (use wide filter).
147    #[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        // Check p side
156        for i in 1..4 {
157            if (p[i] - p[0]).abs() > flat_threshold {
158                return false;
159            }
160        }
161
162        // Check q side
163        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    /// Check if this is an extra-flat region (use widest filter).
173    #[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        // Check extended p side
182        for i in 4..7 {
183            if (p[i] - p[0]).abs() > flat_threshold {
184                return false;
185            }
186        }
187
188        // Check extended q side
189        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    /// Check for high edge variance.
199    #[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// =============================================================================
207// Filter Parameters
208// =============================================================================
209
210/// Parameters for loop filtering.
211#[derive(Clone, Debug, Default)]
212pub struct LoopFilterConfig {
213    /// Filter levels for Y plane [vertical, horizontal].
214    pub y_levels: [u8; 2],
215    /// Filter levels for U plane [vertical, horizontal].
216    pub u_levels: [u8; 2],
217    /// Filter levels for V plane [vertical, horizontal].
218    pub v_levels: [u8; 2],
219    /// Sharpness level.
220    pub sharpness: u8,
221    /// Delta enabled.
222    pub delta_enabled: bool,
223    /// Reference frame deltas.
224    pub ref_deltas: [i8; 8],
225    /// Mode deltas.
226    pub mode_deltas: [i8; 2],
227}
228
229impl LoopFilterConfig {
230    /// Create a new loop filter configuration.
231    #[must_use]
232    pub fn new() -> Self {
233        Self::default()
234    }
235
236    /// Set Y plane levels.
237    #[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    /// Set sharpness.
244    #[must_use]
245    pub const fn with_sharpness(mut self, sharpness: u8) -> Self {
246        self.sharpness = sharpness;
247        self
248    }
249
250    /// Get the filter level for a plane and direction.
251    #[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    /// Check if any filtering is enabled.
266    #[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    /// Create edge filter for given parameters.
274    #[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// =============================================================================
282// Filter Kernels
283// =============================================================================
284
285/// Apply 4-tap narrow filter. Returns (new_p1, new_p0, new_q0, new_q1).
286fn filter4(p1: i16, p0: i16, q0: i16, q1: i16, hev: bool, bd: u8) -> (i16, i16, i16, i16) {
287    let max_val = (1i16 << bd) - 1;
288
289    let filter = if hev {
290        // High edge variance: stronger filtering
291        let f = (p1 - q1).clamp(-128, 127) + 3 * (q0 - p0);
292        f.clamp(-128, 127)
293    } else {
294        // Low variance: mild filtering
295        3 * (q0 - p0)
296    };
297
298    let filter1 = (filter + 4).clamp(-128, 127) >> 3;
299    let filter2 = (filter + 3).clamp(-128, 127) >> 3;
300
301    let new_q0 = (q0 - filter1).clamp(0, max_val);
302    let new_p0 = (p0 + filter2).clamp(0, max_val);
303
304    let (new_p1, new_q1) = if !hev {
305        // Additional filtering for p1 and q1
306        let filter3 = (filter1 + 1) >> 1;
307        (
308            (p1 + filter3).clamp(0, max_val),
309            (q1 - filter3).clamp(0, max_val),
310        )
311    } else {
312        (p1, q1)
313    };
314
315    (new_p1, new_p0, new_q0, new_q1)
316}
317
318/// Apply 8-tap wide filter.
319fn filter8(p: &mut [i16], q: &mut [i16], bd: u8) {
320    if p.len() < 4 || q.len() < 4 {
321        return;
322    }
323
324    let max_val = (1i16 << bd) - 1;
325
326    // Wide filter uses weighted average
327    let p0 = i32::from(p[0]);
328    let p1 = i32::from(p[1]);
329    let p2 = i32::from(p[2]);
330    let p3 = i32::from(p[3]);
331    let q0 = i32::from(q[0]);
332    let q1 = i32::from(q[1]);
333    let q2 = i32::from(q[2]);
334    let q3 = i32::from(q[3]);
335
336    // Compute filtered values
337    p[0] = ((p0 * 6 + p1 * 2 + q0 * 2 + q1 + p2 + 6) >> 3).clamp(0, i32::from(max_val)) as i16;
338    p[1] = ((p1 * 6 + p0 * 2 + p2 * 2 + q0 + p3 + 6) >> 3).clamp(0, i32::from(max_val)) as i16;
339    p[2] = ((p2 * 6 + p1 * 2 + p3 * 2 + p0 + q0 + 6) >> 3).clamp(0, i32::from(max_val)) as i16;
340
341    q[0] = ((q0 * 6 + q1 * 2 + p0 * 2 + p1 + q2 + 6) >> 3).clamp(0, i32::from(max_val)) as i16;
342    q[1] = ((q1 * 6 + q0 * 2 + q2 * 2 + p0 + q3 + 6) >> 3).clamp(0, i32::from(max_val)) as i16;
343    q[2] = ((q2 * 6 + q1 * 2 + q3 * 2 + q0 + p0 + 6) >> 3).clamp(0, i32::from(max_val)) as i16;
344}
345
346/// Apply 14-tap extra-wide filter.
347fn filter14(p: &mut [i16], q: &mut [i16], bd: u8) {
348    if p.len() < 7 || q.len() < 7 {
349        return;
350    }
351
352    let max_val = (1i32 << bd) - 1;
353
354    // Store original values
355    let orig_p: Vec<i32> = p.iter().map(|&x| i32::from(x)).collect();
356    let orig_q: Vec<i32> = q.iter().map(|&x| i32::from(x)).collect();
357
358    // 14-tap filter with Gaussian-like weights
359    let weights = [1, 1, 2, 2, 4, 2, 2, 1, 1];
360    let sum_weights = 16;
361
362    // Filter p side
363    for i in 0..6 {
364        let mut sum = 0i32;
365        for (j, &w) in weights.iter().enumerate() {
366            let idx = i as i32 - 4 + j as i32;
367            let val = if idx < 0 {
368                orig_p[(-idx) as usize].min(orig_p[6])
369            } else if idx < 7 {
370                orig_p[idx as usize]
371            } else {
372                orig_q[(idx - 7) as usize].min(orig_q[6])
373            };
374            sum += val * i32::from(w);
375        }
376        p[i] = ((sum + sum_weights / 2) / sum_weights).clamp(0, max_val) as i16;
377    }
378
379    // Filter q side
380    for i in 0..6 {
381        let mut sum = 0i32;
382        for (j, &w) in weights.iter().enumerate() {
383            let idx = i as i32 - 4 + j as i32;
384            let val = if idx < 0 {
385                orig_p[(6 + idx) as usize].min(orig_p[6])
386            } else if idx < 7 {
387                orig_q[idx as usize]
388            } else {
389                orig_q[6]
390            };
391            sum += val * i32::from(w);
392        }
393        q[i] = ((sum + sum_weights / 2) / sum_weights).clamp(0, max_val) as i16;
394    }
395}
396
397// =============================================================================
398// Loop Filter Pipeline
399// =============================================================================
400
401/// Loop filter pipeline for applying edge filtering.
402#[derive(Debug)]
403pub struct LoopFilterPipeline {
404    /// Filter configuration.
405    config: LoopFilterConfig,
406    /// Block size for processing.
407    block_size: usize,
408}
409
410impl Default for LoopFilterPipeline {
411    fn default() -> Self {
412        Self::new()
413    }
414}
415
416impl LoopFilterPipeline {
417    /// Create a new loop filter pipeline.
418    #[must_use]
419    pub fn new() -> Self {
420        Self {
421            config: LoopFilterConfig::new(),
422            block_size: 8,
423        }
424    }
425
426    /// Create with specific configuration.
427    #[must_use]
428    pub fn with_config(config: LoopFilterConfig) -> Self {
429        Self {
430            config,
431            block_size: 8,
432        }
433    }
434
435    /// Set the filter configuration.
436    pub fn set_config(&mut self, config: LoopFilterConfig) {
437        self.config = config;
438    }
439
440    /// Get the current configuration.
441    #[must_use]
442    pub fn config(&self) -> &LoopFilterConfig {
443        &self.config
444    }
445
446    /// Apply loop filter to a frame.
447    ///
448    /// # Errors
449    ///
450    /// Returns error if filtering fails.
451    pub fn apply(
452        &mut self,
453        frame: &mut FrameBuffer,
454        _context: &FrameContext,
455    ) -> ReconstructResult<()> {
456        if !self.config.is_enabled() {
457            return Ok(());
458        }
459
460        // Filter Y plane
461        self.filter_plane(frame.y_plane_mut(), PlaneType::Y)?;
462
463        // Filter U plane
464        if let Some(u_plane) = frame.u_plane_mut() {
465            self.filter_plane(u_plane, PlaneType::U)?;
466        }
467
468        // Filter V plane
469        if let Some(v_plane) = frame.v_plane_mut() {
470            self.filter_plane(v_plane, PlaneType::V)?;
471        }
472
473        Ok(())
474    }
475
476    /// Filter a single plane.
477    fn filter_plane(
478        &self,
479        plane: &mut PlaneBuffer,
480        plane_type: PlaneType,
481    ) -> ReconstructResult<()> {
482        let bit_depth = plane.bit_depth();
483        let width = plane.width() as usize;
484        let height = plane.height() as usize;
485
486        // Filter vertical edges (left boundaries)
487        let v_filter = self
488            .config
489            .create_edge_filter(plane_type, FilterDirection::Vertical);
490        if v_filter.should_filter() {
491            for by in 0..(height / self.block_size) {
492                for bx in 1..(width / self.block_size) {
493                    self.filter_vertical_edge(
494                        plane,
495                        (bx * self.block_size) as u32,
496                        (by * self.block_size) as u32,
497                        &v_filter,
498                        bit_depth,
499                    );
500                }
501            }
502        }
503
504        // Filter horizontal edges (top boundaries)
505        let h_filter = self
506            .config
507            .create_edge_filter(plane_type, FilterDirection::Horizontal);
508        if h_filter.should_filter() {
509            for by in 1..(height / self.block_size) {
510                for bx in 0..(width / self.block_size) {
511                    self.filter_horizontal_edge(
512                        plane,
513                        (bx * self.block_size) as u32,
514                        (by * self.block_size) as u32,
515                        &h_filter,
516                        bit_depth,
517                    );
518                }
519            }
520        }
521
522        Ok(())
523    }
524
525    /// Filter a vertical edge at the given block position.
526    fn filter_vertical_edge(
527        &self,
528        plane: &mut PlaneBuffer,
529        x: u32,
530        y: u32,
531        filter: &EdgeFilter,
532        bd: u8,
533    ) {
534        for row in 0..self.block_size {
535            let py = y + row as u32;
536
537            // Get p and q samples
538            let mut p = [
539                plane.get(x.saturating_sub(1), py),
540                plane.get(x.saturating_sub(2), py),
541                plane.get(x.saturating_sub(3), py),
542                plane.get(x.saturating_sub(4), py),
543            ];
544            let mut q = [
545                plane.get(x, py),
546                plane.get(x + 1, py),
547                plane.get(x + 2, py),
548                plane.get(x + 3, py),
549            ];
550
551            // Check filter mask
552            if !self.filter_mask(&p, &q, filter) {
553                continue;
554            }
555
556            let hev = filter.has_hev(p[1], p[0], q[0], q[1]);
557
558            if filter.is_flat(&p, &q) {
559                filter8(&mut p, &mut q, bd);
560            } else {
561                let (new_p1, new_p0, new_q0, new_q1) = filter4(p[1], p[0], q[0], q[1], hev, bd);
562                p[1] = new_p1;
563                p[0] = new_p0;
564                q[0] = new_q0;
565                q[1] = new_q1;
566            }
567
568            // Write back
569            plane.set(x.saturating_sub(1), py, p[0]);
570            plane.set(x.saturating_sub(2), py, p[1]);
571            plane.set(x.saturating_sub(3), py, p[2]);
572            plane.set(x.saturating_sub(4), py, p[3]);
573            plane.set(x, py, q[0]);
574            plane.set(x + 1, py, q[1]);
575            plane.set(x + 2, py, q[2]);
576            plane.set(x + 3, py, q[3]);
577        }
578    }
579
580    /// Filter a horizontal edge at the given block position.
581    fn filter_horizontal_edge(
582        &self,
583        plane: &mut PlaneBuffer,
584        x: u32,
585        y: u32,
586        filter: &EdgeFilter,
587        bd: u8,
588    ) {
589        for col in 0..self.block_size {
590            let px = x + col as u32;
591
592            // Get p and q samples
593            let mut p = [
594                plane.get(px, y.saturating_sub(1)),
595                plane.get(px, y.saturating_sub(2)),
596                plane.get(px, y.saturating_sub(3)),
597                plane.get(px, y.saturating_sub(4)),
598            ];
599            let mut q = [
600                plane.get(px, y),
601                plane.get(px, y + 1),
602                plane.get(px, y + 2),
603                plane.get(px, y + 3),
604            ];
605
606            // Check filter mask
607            if !self.filter_mask(&p, &q, filter) {
608                continue;
609            }
610
611            let hev = filter.has_hev(p[1], p[0], q[0], q[1]);
612
613            if filter.is_flat(&p, &q) {
614                filter8(&mut p, &mut q, bd);
615            } else {
616                let (new_p1, new_p0, new_q0, new_q1) = filter4(p[1], p[0], q[0], q[1], hev, bd);
617                p[1] = new_p1;
618                p[0] = new_p0;
619                q[0] = new_q0;
620                q[1] = new_q1;
621            }
622
623            // Write back
624            plane.set(px, y.saturating_sub(1), p[0]);
625            plane.set(px, y.saturating_sub(2), p[1]);
626            plane.set(px, y.saturating_sub(3), p[2]);
627            plane.set(px, y.saturating_sub(4), p[3]);
628            plane.set(px, y, q[0]);
629            plane.set(px, y + 1, q[1]);
630            plane.set(px, y + 2, q[2]);
631            plane.set(px, y + 3, q[3]);
632        }
633    }
634
635    /// Check if the filter mask passes.
636    fn filter_mask(&self, p: &[i16], q: &[i16], filter: &EdgeFilter) -> bool {
637        if p.len() < 2 || q.len() < 2 {
638            return false;
639        }
640
641        let limit = i16::from(filter.limit);
642        let threshold = i16::from(filter.threshold);
643
644        // Check p0-q0 difference
645        if (p[0] - q[0]).abs() > (limit * 2 + threshold) {
646            return false;
647        }
648
649        // Check p1-p0 and q1-q0 differences
650        if (p[1] - p[0]).abs() > limit {
651            return false;
652        }
653
654        if (q[1] - q[0]).abs() > limit {
655            return false;
656        }
657
658        true
659    }
660}
661
662// =============================================================================
663// Tests
664// =============================================================================
665
666#[cfg(test)]
667mod tests {
668    use super::*;
669    use crate::reconstruct::ChromaSubsampling;
670
671    #[test]
672    fn test_filter_direction() {
673        assert_eq!(
674            FilterDirection::Vertical.perpendicular(),
675            FilterDirection::Horizontal
676        );
677        assert_eq!(
678            FilterDirection::Horizontal.perpendicular(),
679            FilterDirection::Vertical
680        );
681    }
682
683    #[test]
684    fn test_edge_filter_new() {
685        let filter = EdgeFilter::new(32, 0);
686        assert_eq!(filter.level, 32);
687        assert!(filter.limit > 0);
688        assert!(filter.should_filter());
689
690        let filter_zero = EdgeFilter::new(0, 0);
691        assert!(!filter_zero.should_filter());
692    }
693
694    #[test]
695    fn test_edge_filter_hev_threshold() {
696        let filter_low = EdgeFilter::new(10, 0);
697        assert_eq!(filter_low.hev_threshold, 0);
698
699        let filter_mid = EdgeFilter::new(30, 0);
700        assert_eq!(filter_mid.hev_threshold, 1);
701
702        let filter_high = EdgeFilter::new(50, 0);
703        assert_eq!(filter_high.hev_threshold, 2);
704    }
705
706    #[test]
707    fn test_edge_filter_flat_detection() {
708        let filter = EdgeFilter::new(32, 0);
709
710        // Flat region
711        let p = [128i16, 128, 128, 128];
712        let q = [128i16, 128, 128, 128];
713        assert!(filter.is_flat(&p, &q));
714
715        // Non-flat region
716        let p_nonflat = [128i16, 100, 128, 128];
717        assert!(!filter.is_flat(&p_nonflat, &q));
718    }
719
720    #[test]
721    fn test_loop_filter_config() {
722        let config = LoopFilterConfig::new()
723            .with_y_levels(32, 32)
724            .with_sharpness(2);
725
726        assert_eq!(
727            config.get_level(PlaneType::Y, FilterDirection::Vertical),
728            32
729        );
730        assert_eq!(
731            config.get_level(PlaneType::Y, FilterDirection::Horizontal),
732            32
733        );
734        assert!(config.is_enabled());
735    }
736
737    #[test]
738    fn test_loop_filter_config_disabled() {
739        let config = LoopFilterConfig::new();
740        assert!(!config.is_enabled());
741    }
742
743    #[test]
744    fn test_loop_filter_pipeline_creation() {
745        let pipeline = LoopFilterPipeline::new();
746        assert!(!pipeline.config().is_enabled());
747    }
748
749    #[test]
750    fn test_loop_filter_pipeline_with_config() {
751        let config = LoopFilterConfig::new().with_y_levels(20, 20);
752        let pipeline = LoopFilterPipeline::with_config(config);
753        assert!(pipeline.config().is_enabled());
754    }
755
756    #[test]
757    fn test_filter4() {
758        let p1 = 100i16;
759        let p0 = 120i16;
760        let q0 = 140i16;
761        let q1 = 130i16;
762
763        let (_new_p1, new_p0, new_q0, _new_q1) = filter4(p1, p0, q0, q1, false, 8);
764
765        // After filtering, the edge should be smoother
766        assert!((new_p0 - new_q0).abs() < (120 - 140i16).abs());
767    }
768
769    #[test]
770    fn test_filter8() {
771        let mut p = [130i16, 128, 126, 124];
772        let mut q = [140i16, 142, 144, 146];
773
774        filter8(&mut p, &mut q, 8);
775
776        // After wide filtering, values should be averaged
777        // The edge between p[0] and q[0] should be less pronounced
778        let edge_diff_after = (p[0] - q[0]).abs();
779        assert!(edge_diff_after < 15);
780    }
781
782    #[test]
783    fn test_loop_filter_apply_disabled() {
784        let mut frame = FrameBuffer::new(64, 64, 8, ChromaSubsampling::Cs420);
785        let context = FrameContext::new(64, 64);
786
787        let mut pipeline = LoopFilterPipeline::new();
788        let result = pipeline.apply(&mut frame, &context);
789
790        assert!(result.is_ok());
791    }
792
793    #[test]
794    fn test_loop_filter_apply_enabled() {
795        let mut frame = FrameBuffer::new(64, 64, 8, ChromaSubsampling::Cs420);
796
797        // Set some edge values
798        for y in 0..64 {
799            for x in 0..8 {
800                frame.y_plane_mut().set(x, y as u32, 100);
801            }
802            for x in 8..64 {
803                frame.y_plane_mut().set(x as u32, y as u32, 150);
804            }
805        }
806
807        let context = FrameContext::new(64, 64);
808        let config = LoopFilterConfig::new().with_y_levels(32, 32);
809        let mut pipeline = LoopFilterPipeline::with_config(config);
810
811        let result = pipeline.apply(&mut frame, &context);
812        assert!(result.is_ok());
813    }
814
815    #[test]
816    fn test_constants() {
817        assert_eq!(MAX_LOOP_FILTER_LEVEL, 63);
818        assert_eq!(MAX_SHARPNESS_LEVEL, 7);
819        assert_eq!(NARROW_FILTER_TAPS, 4);
820        assert_eq!(WIDE_FILTER_TAPS, 8);
821        assert_eq!(EXTRA_WIDE_FILTER_TAPS, 14);
822    }
823}