Skip to main content

oximedia_codec/intra/
directional.rs

1//! Directional intra prediction implementations.
2//!
3//! Directional prediction projects samples from neighboring blocks along
4//! specified angles. Supported angles range from 0 to 270 degrees with
5//! various resolutions depending on the codec.
6//!
7//! # Prediction Modes
8//!
9//! - **Vertical (90 degrees)**: Copy top samples down
10//! - **Horizontal (180 degrees)**: Copy left samples across
11//! - **D45 (45 degrees)**: Diagonal down-right
12//! - **D135 (135 degrees)**: Diagonal up-left
13//! - **D113/D117 (113/117 degrees)**: Near-vertical
14//! - **D157/D153 (157/153 degrees)**: Near-horizontal
15//! - **D203/D207 (203/207 degrees)**: Bottom-left diagonal
16//! - **D67/D63 (67/63 degrees)**: Top-right diagonal
17//!
18//! # Sub-pixel Interpolation
19//!
20//! For angles that don't align with sample positions, linear interpolation
21//! is used to generate fractional samples.
22
23#![forbid(unsafe_code)]
24#![allow(dead_code)]
25#![allow(clippy::too_many_arguments)]
26#![allow(clippy::similar_names)]
27#![allow(clippy::cast_possible_truncation)]
28#![allow(clippy::cast_sign_loss)]
29#![allow(clippy::cast_possible_wrap)]
30#![allow(clippy::needless_pass_by_value)]
31#![allow(clippy::unused_self)]
32#![allow(clippy::trivially_copy_pass_by_ref)]
33#![allow(clippy::needless_range_loop)]
34#![allow(clippy::manual_memcpy)]
35#![allow(clippy::if_same_then_else)]
36#![allow(clippy::comparison_chain)]
37#![allow(clippy::manual_rem_euclid)]
38#![allow(clippy::cast_lossless)]
39
40use super::{
41    AngleDelta, BitDepth, BlockDimensions, DirectionalMode, IntraPredContext, IntraPredictor,
42};
43
44/// Directional predictor that handles all angular modes.
45#[derive(Clone, Copy, Debug)]
46pub struct DirectionalPredictor {
47    /// Base angle in degrees.
48    angle: u16,
49    /// Angle delta adjustment.
50    delta: AngleDelta,
51    /// Bit depth.
52    bit_depth: BitDepth,
53}
54
55impl DirectionalPredictor {
56    /// Create a new directional predictor.
57    #[must_use]
58    pub const fn new(angle: u16, bit_depth: BitDepth) -> Self {
59        Self {
60            angle,
61            delta: AngleDelta::Zero,
62            bit_depth,
63        }
64    }
65
66    /// Create with angle delta (AV1).
67    #[must_use]
68    pub const fn with_delta(angle: u16, delta: AngleDelta, bit_depth: BitDepth) -> Self {
69        Self {
70            angle,
71            delta,
72            bit_depth,
73        }
74    }
75
76    /// Get effective angle including delta.
77    #[must_use]
78    pub const fn effective_angle(&self) -> i16 {
79        self.angle as i16 + self.delta.degrees()
80    }
81
82    /// Predict using the configured angle.
83    pub fn predict_angle(
84        &self,
85        ctx: &IntraPredContext,
86        output: &mut [u16],
87        stride: usize,
88        dims: BlockDimensions,
89    ) {
90        let angle = self.effective_angle();
91
92        // Route to specialized implementations
93        match angle {
94            90 => self.predict_vertical(ctx, output, stride, dims),
95            180 => self.predict_horizontal(ctx, output, stride, dims),
96            45 => self.predict_d45(ctx, output, stride, dims),
97            135 => self.predict_d135(ctx, output, stride, dims),
98            _ => self.predict_generic(ctx, output, stride, dims, angle),
99        }
100    }
101
102    /// Vertical prediction (90 degrees).
103    fn predict_vertical(
104        &self,
105        ctx: &IntraPredContext,
106        output: &mut [u16],
107        stride: usize,
108        dims: BlockDimensions,
109    ) {
110        let top = ctx.top_samples();
111
112        for y in 0..dims.height {
113            let row_start = y * stride;
114            for x in 0..dims.width {
115                output[row_start + x] = top[x];
116            }
117        }
118    }
119
120    /// Horizontal prediction (180 degrees).
121    fn predict_horizontal(
122        &self,
123        ctx: &IntraPredContext,
124        output: &mut [u16],
125        stride: usize,
126        dims: BlockDimensions,
127    ) {
128        let left = ctx.left_samples();
129
130        for y in 0..dims.height {
131            let row_start = y * stride;
132            let left_val = left[y];
133            for x in 0..dims.width {
134                output[row_start + x] = left_val;
135            }
136        }
137    }
138
139    /// D45 prediction (45 degrees, down-right diagonal).
140    fn predict_d45(
141        &self,
142        ctx: &IntraPredContext,
143        output: &mut [u16],
144        stride: usize,
145        dims: BlockDimensions,
146    ) {
147        let top = ctx.top_samples();
148
149        for y in 0..dims.height {
150            let row_start = y * stride;
151            for x in 0..dims.width {
152                // Sample from top-right diagonal
153                let idx = x + y + 1;
154                let sample = if idx < ctx.top_samples().len() {
155                    top[idx]
156                } else {
157                    // Use last available sample
158                    top[ctx.top_samples().len().saturating_sub(1)]
159                };
160                output[row_start + x] = sample;
161            }
162        }
163    }
164
165    /// D135 prediction (135 degrees, up-left diagonal).
166    fn predict_d135(
167        &self,
168        ctx: &IntraPredContext,
169        output: &mut [u16],
170        stride: usize,
171        dims: BlockDimensions,
172    ) {
173        let top = ctx.top_samples();
174        let left = ctx.left_samples();
175        let top_left = ctx.top_left_sample();
176
177        for y in 0..dims.height {
178            let row_start = y * stride;
179            for x in 0..dims.width {
180                let sample = if y > x {
181                    // Use left neighbor
182                    left[y - x - 1]
183                } else if y < x {
184                    // Use top neighbor
185                    top[x - y - 1]
186                } else {
187                    // Use top-left
188                    top_left
189                };
190                output[row_start + x] = sample;
191            }
192        }
193    }
194
195    /// Generic angular prediction with interpolation.
196    fn predict_generic(
197        &self,
198        ctx: &IntraPredContext,
199        output: &mut [u16],
200        stride: usize,
201        dims: BlockDimensions,
202        angle: i16,
203    ) {
204        // Calculate dx and dy for the angle (in 1/256ths of a pixel)
205        let (dx, dy) = get_direction_deltas(angle);
206
207        if angle < 90 {
208            // Angles 0-89: primarily vertical, from top
209            self.predict_from_top(ctx, output, stride, dims, dx, dy);
210        } else if angle < 180 {
211            // Angles 90-179: blend of top and left
212            self.predict_from_top_left(ctx, output, stride, dims, dx, dy, angle);
213        } else if angle < 270 {
214            // Angles 180-269: primarily horizontal, from left
215            self.predict_from_left(ctx, output, stride, dims, dx, dy);
216        } else {
217            // Angles 270+: from left going up
218            self.predict_from_left_up(ctx, output, stride, dims, dx, dy);
219        }
220    }
221
222    /// Predict from top samples (angles 0-89).
223    fn predict_from_top(
224        &self,
225        ctx: &IntraPredContext,
226        output: &mut [u16],
227        stride: usize,
228        dims: BlockDimensions,
229        dx: i32,
230        _dy: i32,
231    ) {
232        let top = ctx.top_samples();
233        let max_idx = top.len().saturating_sub(1);
234
235        for y in 0..dims.height {
236            let row_start = y * stride;
237            for x in 0..dims.width {
238                // Calculate source position
239                let src_x = ((x as i32) * 256 + (y as i32 + 1) * dx) / 256;
240                let frac = (((x as i32) * 256 + (y as i32 + 1) * dx) % 256) as u16;
241
242                let idx = src_x.clamp(0, max_idx as i32) as usize;
243                let idx_next = (idx + 1).min(max_idx);
244
245                // Linear interpolation
246                let sample = interpolate(top[idx], top[idx_next], frac);
247                output[row_start + x] = sample;
248            }
249        }
250    }
251
252    /// Predict from top-left (angles around 90-179).
253    fn predict_from_top_left(
254        &self,
255        ctx: &IntraPredContext,
256        output: &mut [u16],
257        stride: usize,
258        dims: BlockDimensions,
259        dx: i32,
260        dy: i32,
261        angle: i16,
262    ) {
263        let top = ctx.top_samples();
264        let left = ctx.left_samples();
265        let top_left = ctx.top_left_sample();
266
267        for y in 0..dims.height {
268            let row_start = y * stride;
269            for x in 0..dims.width {
270                let sample = if angle <= 90 {
271                    // Use top samples
272                    let src_x = (x as i32) + ((y as i32 + 1) * dx) / 256;
273                    let frac = ((y as i32 + 1) * dx) % 256;
274                    get_sample_from_neighbors(
275                        top,
276                        left,
277                        top_left,
278                        src_x,
279                        -1, // indicating top row
280                        frac.unsigned_abs() as u16,
281                        true,
282                    )
283                } else {
284                    // Use left samples
285                    let src_y = (y as i32) + ((x as i32 + 1) * dy) / 256;
286                    let frac = ((x as i32 + 1) * dy) % 256;
287                    get_sample_from_neighbors(
288                        top,
289                        left,
290                        top_left,
291                        -1, // indicating left column
292                        src_y,
293                        frac.unsigned_abs() as u16,
294                        false,
295                    )
296                };
297                output[row_start + x] = sample;
298            }
299        }
300    }
301
302    /// Predict from left samples (angles 180-269).
303    fn predict_from_left(
304        &self,
305        ctx: &IntraPredContext,
306        output: &mut [u16],
307        stride: usize,
308        dims: BlockDimensions,
309        _dx: i32,
310        dy: i32,
311    ) {
312        let left = ctx.left_samples();
313        let max_idx = left.len().saturating_sub(1);
314
315        for y in 0..dims.height {
316            let row_start = y * stride;
317            for x in 0..dims.width {
318                // Calculate source position
319                let src_y = ((y as i32) * 256 + (x as i32 + 1) * dy) / 256;
320                let frac = (((y as i32) * 256 + (x as i32 + 1) * dy) % 256) as u16;
321
322                let idx = src_y.clamp(0, max_idx as i32) as usize;
323                let idx_next = (idx + 1).min(max_idx);
324
325                // Linear interpolation
326                let sample = interpolate(left[idx], left[idx_next], frac);
327                output[row_start + x] = sample;
328            }
329        }
330    }
331
332    /// Predict from left going upward (angles 270+).
333    fn predict_from_left_up(
334        &self,
335        ctx: &IntraPredContext,
336        output: &mut [u16],
337        stride: usize,
338        dims: BlockDimensions,
339        dx: i32,
340        _dy: i32,
341    ) {
342        let left = ctx.left_samples();
343        let top = ctx.top_samples();
344        let top_left = ctx.top_left_sample();
345
346        for y in 0..dims.height {
347            let row_start = y * stride;
348            for x in 0..dims.width {
349                let src_idx = (y as i32) - ((x as i32 + 1) * dx.abs()) / 256;
350                let frac = (((x as i32 + 1) * dx.abs()) % 256) as u16;
351
352                let sample = if src_idx >= 0 {
353                    let idx = src_idx as usize;
354                    let idx_next = (idx + 1).min(left.len().saturating_sub(1));
355                    interpolate(left[idx], left[idx_next], frac)
356                } else {
357                    // Sample from top
358                    let top_idx = (-(src_idx + 1)) as usize;
359                    if top_idx == 0 {
360                        top_left
361                    } else if top_idx <= top.len() {
362                        top[top_idx - 1]
363                    } else {
364                        top[top.len().saturating_sub(1)]
365                    }
366                };
367                output[row_start + x] = sample;
368            }
369        }
370    }
371}
372
373impl IntraPredictor for DirectionalPredictor {
374    fn predict(
375        &self,
376        ctx: &IntraPredContext,
377        output: &mut [u16],
378        stride: usize,
379        dims: BlockDimensions,
380    ) {
381        self.predict_angle(ctx, output, stride, dims);
382    }
383}
384
385/// Vertical predictor (90 degrees).
386#[derive(Clone, Copy, Debug, Default)]
387pub struct VerticalPredictor;
388
389impl VerticalPredictor {
390    /// Create a new vertical predictor.
391    #[must_use]
392    pub const fn new() -> Self {
393        Self
394    }
395}
396
397impl IntraPredictor for VerticalPredictor {
398    fn predict(
399        &self,
400        ctx: &IntraPredContext,
401        output: &mut [u16],
402        stride: usize,
403        dims: BlockDimensions,
404    ) {
405        let top = ctx.top_samples();
406
407        for y in 0..dims.height {
408            let row_start = y * stride;
409            for x in 0..dims.width {
410                output[row_start + x] = top[x];
411            }
412        }
413    }
414}
415
416/// Horizontal predictor (180 degrees).
417#[derive(Clone, Copy, Debug, Default)]
418pub struct HorizontalPredictor;
419
420impl HorizontalPredictor {
421    /// Create a new horizontal predictor.
422    #[must_use]
423    pub const fn new() -> Self {
424        Self
425    }
426}
427
428impl IntraPredictor for HorizontalPredictor {
429    fn predict(
430        &self,
431        ctx: &IntraPredContext,
432        output: &mut [u16],
433        stride: usize,
434        dims: BlockDimensions,
435    ) {
436        let left = ctx.left_samples();
437
438        for y in 0..dims.height {
439            let row_start = y * stride;
440            let left_val = left[y];
441            for x in 0..dims.width {
442                output[row_start + x] = left_val;
443            }
444        }
445    }
446}
447
448/// D45 predictor (45 degrees).
449#[derive(Clone, Copy, Debug, Default)]
450pub struct D45Predictor;
451
452impl D45Predictor {
453    /// Create a new D45 predictor.
454    #[must_use]
455    pub const fn new() -> Self {
456        Self
457    }
458}
459
460impl IntraPredictor for D45Predictor {
461    fn predict(
462        &self,
463        ctx: &IntraPredContext,
464        output: &mut [u16],
465        stride: usize,
466        dims: BlockDimensions,
467    ) {
468        let predictor = DirectionalPredictor::new(45, BitDepth::Bits8);
469        predictor.predict_d45(ctx, output, stride, dims);
470    }
471}
472
473/// D63 predictor (63 degrees, VP9 naming).
474#[derive(Clone, Copy, Debug, Default)]
475pub struct D63Predictor;
476
477impl D63Predictor {
478    /// Create a new D63 predictor.
479    #[must_use]
480    pub const fn new() -> Self {
481        Self
482    }
483}
484
485impl IntraPredictor for D63Predictor {
486    fn predict(
487        &self,
488        ctx: &IntraPredContext,
489        output: &mut [u16],
490        stride: usize,
491        dims: BlockDimensions,
492    ) {
493        let predictor = DirectionalPredictor::new(63, BitDepth::Bits8);
494        predictor.predict_angle(ctx, output, stride, dims);
495    }
496}
497
498/// D67 predictor (67 degrees, AV1 naming).
499#[derive(Clone, Copy, Debug, Default)]
500pub struct D67Predictor;
501
502impl D67Predictor {
503    /// Create a new D67 predictor.
504    #[must_use]
505    pub const fn new() -> Self {
506        Self
507    }
508}
509
510impl IntraPredictor for D67Predictor {
511    fn predict(
512        &self,
513        ctx: &IntraPredContext,
514        output: &mut [u16],
515        stride: usize,
516        dims: BlockDimensions,
517    ) {
518        let predictor = DirectionalPredictor::new(67, BitDepth::Bits8);
519        predictor.predict_angle(ctx, output, stride, dims);
520    }
521}
522
523/// D113 predictor (113 degrees, AV1 naming).
524#[derive(Clone, Copy, Debug, Default)]
525pub struct D113Predictor;
526
527impl D113Predictor {
528    /// Create a new D113 predictor.
529    #[must_use]
530    pub const fn new() -> Self {
531        Self
532    }
533}
534
535impl IntraPredictor for D113Predictor {
536    fn predict(
537        &self,
538        ctx: &IntraPredContext,
539        output: &mut [u16],
540        stride: usize,
541        dims: BlockDimensions,
542    ) {
543        let predictor = DirectionalPredictor::new(113, BitDepth::Bits8);
544        predictor.predict_angle(ctx, output, stride, dims);
545    }
546}
547
548/// D117 predictor (117 degrees, VP9 naming).
549#[derive(Clone, Copy, Debug, Default)]
550pub struct D117Predictor;
551
552impl D117Predictor {
553    /// Create a new D117 predictor.
554    #[must_use]
555    pub const fn new() -> Self {
556        Self
557    }
558}
559
560impl IntraPredictor for D117Predictor {
561    fn predict(
562        &self,
563        ctx: &IntraPredContext,
564        output: &mut [u16],
565        stride: usize,
566        dims: BlockDimensions,
567    ) {
568        let predictor = DirectionalPredictor::new(117, BitDepth::Bits8);
569        predictor.predict_angle(ctx, output, stride, dims);
570    }
571}
572
573/// D135 predictor (135 degrees).
574#[derive(Clone, Copy, Debug, Default)]
575pub struct D135Predictor;
576
577impl D135Predictor {
578    /// Create a new D135 predictor.
579    #[must_use]
580    pub const fn new() -> Self {
581        Self
582    }
583}
584
585impl IntraPredictor for D135Predictor {
586    fn predict(
587        &self,
588        ctx: &IntraPredContext,
589        output: &mut [u16],
590        stride: usize,
591        dims: BlockDimensions,
592    ) {
593        let predictor = DirectionalPredictor::new(135, BitDepth::Bits8);
594        predictor.predict_d135(ctx, output, stride, dims);
595    }
596}
597
598/// D153 predictor (153 degrees, VP9 naming).
599#[derive(Clone, Copy, Debug, Default)]
600pub struct D153Predictor;
601
602impl D153Predictor {
603    /// Create a new D153 predictor.
604    #[must_use]
605    pub const fn new() -> Self {
606        Self
607    }
608}
609
610impl IntraPredictor for D153Predictor {
611    fn predict(
612        &self,
613        ctx: &IntraPredContext,
614        output: &mut [u16],
615        stride: usize,
616        dims: BlockDimensions,
617    ) {
618        let predictor = DirectionalPredictor::new(153, BitDepth::Bits8);
619        predictor.predict_angle(ctx, output, stride, dims);
620    }
621}
622
623/// D157 predictor (157 degrees, AV1 naming).
624#[derive(Clone, Copy, Debug, Default)]
625pub struct D157Predictor;
626
627impl D157Predictor {
628    /// Create a new D157 predictor.
629    #[must_use]
630    pub const fn new() -> Self {
631        Self
632    }
633}
634
635impl IntraPredictor for D157Predictor {
636    fn predict(
637        &self,
638        ctx: &IntraPredContext,
639        output: &mut [u16],
640        stride: usize,
641        dims: BlockDimensions,
642    ) {
643        let predictor = DirectionalPredictor::new(157, BitDepth::Bits8);
644        predictor.predict_angle(ctx, output, stride, dims);
645    }
646}
647
648/// D203 predictor (203 degrees, AV1 naming).
649#[derive(Clone, Copy, Debug, Default)]
650pub struct D203Predictor;
651
652impl D203Predictor {
653    /// Create a new D203 predictor.
654    #[must_use]
655    pub const fn new() -> Self {
656        Self
657    }
658}
659
660impl IntraPredictor for D203Predictor {
661    fn predict(
662        &self,
663        ctx: &IntraPredContext,
664        output: &mut [u16],
665        stride: usize,
666        dims: BlockDimensions,
667    ) {
668        let predictor = DirectionalPredictor::new(203, BitDepth::Bits8);
669        predictor.predict_angle(ctx, output, stride, dims);
670    }
671}
672
673/// D207 predictor (207 degrees, VP9 naming).
674#[derive(Clone, Copy, Debug, Default)]
675pub struct D207Predictor;
676
677impl D207Predictor {
678    /// Create a new D207 predictor.
679    #[must_use]
680    pub const fn new() -> Self {
681        Self
682    }
683}
684
685impl IntraPredictor for D207Predictor {
686    fn predict(
687        &self,
688        ctx: &IntraPredContext,
689        output: &mut [u16],
690        stride: usize,
691        dims: BlockDimensions,
692    ) {
693        let predictor = DirectionalPredictor::new(207, BitDepth::Bits8);
694        predictor.predict_angle(ctx, output, stride, dims);
695    }
696}
697
698/// Get direction deltas (dx, dy) for an angle.
699/// Returns values in 1/256ths of a pixel.
700#[must_use]
701fn get_direction_deltas(angle: i16) -> (i32, i32) {
702    // Normalize angle to 0-360
703    let angle = ((angle % 360) + 360) % 360;
704
705    // Calculate dx and dy based on angle
706    // Using lookup for common angles, interpolation for others
707    match angle {
708        0 => (0, 256),
709        45 => (181, 181),
710        90 => (256, 0),
711        135 => (181, -181),
712        180 => (0, -256),
713        225 => (-181, -181),
714        270 => (-256, 0),
715        315 => (-181, 181),
716        _ => {
717            // Approximate using trigonometry
718            let radians = (angle as f64) * std::f64::consts::PI / 180.0;
719            let dx = (radians.sin() * 256.0).round() as i32;
720            let dy = (radians.cos() * 256.0).round() as i32;
721            (dx, dy)
722        }
723    }
724}
725
726/// Linear interpolation between two samples.
727#[inline]
728fn interpolate(a: u16, b: u16, frac: u16) -> u16 {
729    // frac is in 1/256ths (0-255)
730    let a32 = u32::from(a);
731    let b32 = u32::from(b);
732    let frac32 = u32::from(frac);
733
734    let result = (a32 * (256 - frac32) + b32 * frac32 + 128) / 256;
735    result as u16
736}
737
738/// Get sample from combined top/left neighbors.
739fn get_sample_from_neighbors(
740    top: &[u16],
741    left: &[u16],
742    top_left: u16,
743    x: i32,
744    y: i32,
745    frac: u16,
746    use_top: bool,
747) -> u16 {
748    if use_top {
749        // Sample from top row
750        let idx = x.clamp(0, (top.len() - 1) as i32) as usize;
751        let idx_next = (idx + 1).min(top.len() - 1);
752        interpolate(top[idx], top[idx_next], frac)
753    } else {
754        // Sample from left column
755        if y < 0 {
756            top_left
757        } else {
758            let idx = y.clamp(0, (left.len() - 1) as i32) as usize;
759            let idx_next = (idx + 1).min(left.len() - 1);
760            interpolate(left[idx], left[idx_next], frac)
761        }
762    }
763}
764
765/// Get direction samples from neighbors based on angle.
766pub fn get_direction_samples(
767    ctx: &IntraPredContext,
768    angle: i16,
769    width: usize,
770    height: usize,
771) -> Vec<u16> {
772    let mode = DirectionalMode::new(angle as u16);
773    let mut samples = Vec::with_capacity(width * height);
774
775    let top = ctx.top_samples();
776    let left = ctx.left_samples();
777
778    if mode.is_vertical_ish() {
779        // Primarily from top
780        for y in 0..height {
781            for x in 0..width {
782                let idx = (x + y).min(top.len() - 1);
783                samples.push(top[idx]);
784            }
785        }
786    } else {
787        // Primarily from left
788        for y in 0..height {
789            for x in 0..width {
790                let idx = (x + y).min(left.len() - 1);
791                samples.push(left[idx]);
792            }
793        }
794    }
795
796    samples
797}
798
799#[cfg(test)]
800mod tests {
801    use super::*;
802    use crate::intra::context::IntraPredContext;
803
804    fn create_test_context() -> IntraPredContext {
805        let mut ctx = IntraPredContext::new(8, 8, BitDepth::Bits8);
806
807        // Set top samples: [10, 20, 30, 40, 50, 60, 70, 80, ...]
808        for i in 0..16 {
809            ctx.set_top_sample(i, ((i + 1) * 10) as u16);
810        }
811
812        // Set left samples: [15, 25, 35, 45, 55, 65, 75, 85, ...]
813        for i in 0..16 {
814            ctx.set_left_sample(i, (15 + i * 10) as u16);
815        }
816
817        ctx.set_top_left_sample(5);
818        ctx.set_availability(true, true);
819
820        ctx
821    }
822
823    #[test]
824    fn test_vertical_prediction() {
825        let ctx = create_test_context();
826        let predictor = VerticalPredictor::new();
827        let dims = BlockDimensions::new(4, 4);
828        let mut output = vec![0u16; 16];
829
830        predictor.predict(&ctx, &mut output, 4, dims);
831
832        // Each column should have the same value (from top)
833        assert_eq!(output[0], 10); // x=0
834        assert_eq!(output[1], 20); // x=1
835        assert_eq!(output[2], 30); // x=2
836        assert_eq!(output[3], 40); // x=3
837
838        // Row 1 should be the same
839        assert_eq!(output[4], 10);
840        assert_eq!(output[5], 20);
841    }
842
843    #[test]
844    fn test_horizontal_prediction() {
845        let ctx = create_test_context();
846        let predictor = HorizontalPredictor::new();
847        let dims = BlockDimensions::new(4, 4);
848        let mut output = vec![0u16; 16];
849
850        predictor.predict(&ctx, &mut output, 4, dims);
851
852        // Each row should have the same value (from left)
853        // Row 0: all 15
854        assert_eq!(output[0], 15);
855        assert_eq!(output[1], 15);
856        assert_eq!(output[2], 15);
857        assert_eq!(output[3], 15);
858
859        // Row 1: all 25
860        assert_eq!(output[4], 25);
861        assert_eq!(output[5], 25);
862    }
863
864    #[test]
865    fn test_d45_prediction() {
866        let ctx = create_test_context();
867        let predictor = D45Predictor::new();
868        let dims = BlockDimensions::new(4, 4);
869        let mut output = vec![0u16; 16];
870
871        predictor.predict(&ctx, &mut output, 4, dims);
872
873        // D45 samples from top diagonal
874        // (0,0) -> top[1] = 20
875        // (1,0) -> top[2] = 30
876        // (0,1) -> top[2] = 30
877        assert_eq!(output[0], 20);
878        assert_eq!(output[1], 30);
879        assert_eq!(output[4], 30); // row 1, col 0
880    }
881
882    #[test]
883    fn test_d135_prediction() {
884        let ctx = create_test_context();
885        let predictor = D135Predictor::new();
886        let dims = BlockDimensions::new(4, 4);
887        let mut output = vec![0u16; 16];
888
889        predictor.predict(&ctx, &mut output, 4, dims);
890
891        // D135 samples along up-left diagonal
892        // (0,0) -> top_left = 5
893        // (1,0) -> top[0] = 10
894        // (0,1) -> left[0] = 15
895        assert_eq!(output[0], 5);
896        assert_eq!(output[1], 10);
897        assert_eq!(output[4], 15); // row 1, col 0
898    }
899
900    #[test]
901    fn test_interpolation() {
902        // No interpolation (frac = 0)
903        assert_eq!(interpolate(100, 200, 0), 100);
904
905        // Full interpolation (frac = 255)
906        let result = interpolate(100, 200, 255);
907        assert!(result >= 199 && result <= 200);
908
909        // Half interpolation (frac = 128)
910        let result = interpolate(100, 200, 128);
911        assert!(result >= 149 && result <= 151);
912    }
913
914    #[test]
915    fn test_direction_deltas() {
916        let (dx, dy) = get_direction_deltas(0);
917        assert_eq!((dx, dy), (0, 256));
918
919        let (dx, dy) = get_direction_deltas(90);
920        assert_eq!((dx, dy), (256, 0));
921
922        let (dx, dy) = get_direction_deltas(180);
923        assert_eq!((dx, dy), (0, -256));
924
925        let (dx, dy) = get_direction_deltas(45);
926        assert_eq!((dx, dy), (181, 181));
927    }
928
929    #[test]
930    fn test_directional_predictor() {
931        let ctx = create_test_context();
932        let predictor = DirectionalPredictor::new(90, BitDepth::Bits8);
933        let dims = BlockDimensions::new(4, 4);
934        let mut output = vec![0u16; 16];
935
936        predictor.predict(&ctx, &mut output, 4, dims);
937
938        // Should be same as vertical
939        assert_eq!(output[0], 10);
940        assert_eq!(output[1], 20);
941    }
942
943    #[test]
944    fn test_directional_with_delta() {
945        let predictor = DirectionalPredictor::with_delta(90, AngleDelta::Plus3, BitDepth::Bits8);
946        assert_eq!(predictor.effective_angle(), 99);
947    }
948}