Skip to main content

oximedia_codec/reconstruct/
pipeline.rs

1//! Decoder pipeline for coordinating reconstruction stages.
2//!
3//! The `DecoderPipeline` coordinates all stages of video frame reconstruction,
4//! from parsing through final output formatting.
5
6#![forbid(unsafe_code)]
7#![allow(clippy::unreadable_literal)]
8#![allow(clippy::items_after_statements)]
9#![allow(clippy::unnecessary_wraps)]
10#![allow(clippy::struct_excessive_bools)]
11#![allow(clippy::identity_op)]
12#![allow(clippy::range_plus_one)]
13#![allow(clippy::needless_range_loop)]
14#![allow(clippy::useless_conversion)]
15#![allow(clippy::redundant_closure_for_method_calls)]
16#![allow(clippy::single_match_else)]
17#![allow(dead_code)]
18#![allow(clippy::doc_markdown)]
19#![allow(clippy::unused_self)]
20#![allow(clippy::trivially_copy_pass_by_ref)]
21#![allow(clippy::missing_errors_doc)]
22#![allow(clippy::similar_names)]
23#![allow(clippy::cast_possible_truncation)]
24#![allow(clippy::cast_precision_loss)]
25#![allow(clippy::cast_lossless)]
26#![allow(clippy::cast_sign_loss)]
27#![allow(clippy::needless_pass_by_value)]
28
29use super::{
30    BufferPool, CdefApplicator, ChromaSubsampling, DeblockFilter, FilmGrainSynthesizer,
31    FrameBuffer, LoopFilterPipeline, OutputFormatter, ReconstructResult, ReconstructionError,
32    ResidualBuffer, SuperResUpscaler, MAX_BIT_DEPTH, MAX_FRAME_HEIGHT, MAX_FRAME_WIDTH,
33    MIN_BIT_DEPTH, NUM_REF_FRAMES,
34};
35
36// =============================================================================
37// Pipeline Stage
38// =============================================================================
39
40/// Pipeline processing stage.
41#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
42pub enum PipelineStage {
43    /// Bitstream parsing.
44    Parse,
45    /// Entropy decoding.
46    Entropy,
47    /// Intra/inter prediction.
48    Predict,
49    /// Inverse transform.
50    Transform,
51    /// Deblocking filter.
52    Deblock,
53    /// Loop filter (edge filtering).
54    LoopFilter,
55    /// CDEF (Constrained Directional Enhancement Filter).
56    Cdef,
57    /// Super-resolution upscaling.
58    SuperRes,
59    /// Film grain synthesis.
60    FilmGrain,
61    /// Output formatting.
62    Output,
63}
64
65impl PipelineStage {
66    /// Get the stage name.
67    #[must_use]
68    pub const fn name(self) -> &'static str {
69        match self {
70            Self::Parse => "Parse",
71            Self::Entropy => "Entropy",
72            Self::Predict => "Predict",
73            Self::Transform => "Transform",
74            Self::Deblock => "Deblock",
75            Self::LoopFilter => "LoopFilter",
76            Self::Cdef => "CDEF",
77            Self::SuperRes => "SuperRes",
78            Self::FilmGrain => "FilmGrain",
79            Self::Output => "Output",
80        }
81    }
82
83    /// Get all stages in processing order.
84    #[must_use]
85    pub const fn all_ordered() -> &'static [Self] {
86        &[
87            Self::Parse,
88            Self::Entropy,
89            Self::Predict,
90            Self::Transform,
91            Self::Deblock,
92            Self::LoopFilter,
93            Self::Cdef,
94            Self::SuperRes,
95            Self::FilmGrain,
96            Self::Output,
97        ]
98    }
99
100    /// Check if this stage is a filter stage.
101    #[must_use]
102    pub const fn is_filter(self) -> bool {
103        matches!(
104            self,
105            Self::Deblock | Self::LoopFilter | Self::Cdef | Self::FilmGrain
106        )
107    }
108
109    /// Check if this stage can be skipped.
110    #[must_use]
111    pub const fn is_optional(self) -> bool {
112        matches!(
113            self,
114            Self::Deblock | Self::LoopFilter | Self::Cdef | Self::SuperRes | Self::FilmGrain
115        )
116    }
117}
118
119// =============================================================================
120// Stage Result
121// =============================================================================
122
123/// Result from a pipeline stage.
124#[derive(Clone, Debug)]
125pub struct StageResult {
126    /// The stage that produced this result.
127    pub stage: PipelineStage,
128    /// Processing time in microseconds.
129    pub processing_time_us: u64,
130    /// Whether the stage was skipped.
131    pub skipped: bool,
132    /// Additional metrics.
133    pub metrics: StageMetrics,
134}
135
136impl StageResult {
137    /// Create a new stage result.
138    #[must_use]
139    pub fn new(stage: PipelineStage) -> Self {
140        Self {
141            stage,
142            processing_time_us: 0,
143            skipped: false,
144            metrics: StageMetrics::default(),
145        }
146    }
147
148    /// Create a skipped stage result.
149    #[must_use]
150    pub fn skipped(stage: PipelineStage) -> Self {
151        Self {
152            stage,
153            processing_time_us: 0,
154            skipped: true,
155            metrics: StageMetrics::default(),
156        }
157    }
158
159    /// Set processing time.
160    #[must_use]
161    pub const fn with_time(mut self, time_us: u64) -> Self {
162        self.processing_time_us = time_us;
163        self
164    }
165}
166
167/// Metrics from a pipeline stage.
168#[derive(Clone, Debug, Default)]
169pub struct StageMetrics {
170    /// Number of blocks processed.
171    pub blocks_processed: u64,
172    /// Number of pixels processed.
173    pub pixels_processed: u64,
174    /// Number of operations performed.
175    pub operations: u64,
176}
177
178// =============================================================================
179// Pipeline Configuration
180// =============================================================================
181
182/// Configuration for the decoder pipeline.
183#[derive(Clone, Debug)]
184pub struct PipelineConfig {
185    /// Enable deblocking filter.
186    pub enable_deblock: bool,
187    /// Enable loop filter.
188    pub enable_loop_filter: bool,
189    /// Enable CDEF.
190    pub enable_cdef: bool,
191    /// Enable super-resolution.
192    pub enable_super_res: bool,
193    /// Enable film grain synthesis.
194    pub enable_film_grain: bool,
195    /// Frame dimensions.
196    pub width: u32,
197    /// Frame height.
198    pub height: u32,
199    /// Bit depth.
200    pub bit_depth: u8,
201    /// Chroma subsampling.
202    pub subsampling: ChromaSubsampling,
203    /// Number of threads for parallel processing.
204    pub threads: usize,
205    /// Buffer pool size.
206    pub buffer_pool_size: usize,
207    /// Enable performance metrics collection.
208    pub collect_metrics: bool,
209}
210
211impl Default for PipelineConfig {
212    fn default() -> Self {
213        Self {
214            enable_deblock: true,
215            enable_loop_filter: true,
216            enable_cdef: true,
217            enable_super_res: false,
218            enable_film_grain: false,
219            width: 1920,
220            height: 1080,
221            bit_depth: 8,
222            subsampling: ChromaSubsampling::Cs420,
223            threads: 1,
224            buffer_pool_size: 4,
225            collect_metrics: false,
226        }
227    }
228}
229
230impl PipelineConfig {
231    /// Create a new pipeline configuration.
232    #[must_use]
233    pub fn new(width: u32, height: u32) -> Self {
234        Self {
235            width,
236            height,
237            ..Default::default()
238        }
239    }
240
241    /// Set bit depth.
242    #[must_use]
243    pub const fn with_bit_depth(mut self, bit_depth: u8) -> Self {
244        self.bit_depth = bit_depth;
245        self
246    }
247
248    /// Set chroma subsampling.
249    #[must_use]
250    pub const fn with_subsampling(mut self, subsampling: ChromaSubsampling) -> Self {
251        self.subsampling = subsampling;
252        self
253    }
254
255    /// Enable all filters.
256    #[must_use]
257    pub const fn with_all_filters(mut self) -> Self {
258        self.enable_deblock = true;
259        self.enable_loop_filter = true;
260        self.enable_cdef = true;
261        self
262    }
263
264    /// Disable all filters.
265    #[must_use]
266    pub const fn without_filters(mut self) -> Self {
267        self.enable_deblock = false;
268        self.enable_loop_filter = false;
269        self.enable_cdef = false;
270        self.enable_film_grain = false;
271        self
272    }
273
274    /// Set number of threads.
275    #[must_use]
276    pub const fn with_threads(mut self, threads: usize) -> Self {
277        self.threads = threads;
278        self
279    }
280
281    /// Validate the configuration.
282    pub fn validate(&self) -> ReconstructResult<()> {
283        if self.width == 0 || self.width > MAX_FRAME_WIDTH {
284            return Err(ReconstructionError::InvalidDimensions {
285                width: self.width,
286                height: self.height,
287            });
288        }
289
290        if self.height == 0 || self.height > MAX_FRAME_HEIGHT {
291            return Err(ReconstructionError::InvalidDimensions {
292                width: self.width,
293                height: self.height,
294            });
295        }
296
297        if self.bit_depth < MIN_BIT_DEPTH || self.bit_depth > MAX_BIT_DEPTH {
298            return Err(ReconstructionError::UnsupportedBitDepth(self.bit_depth));
299        }
300
301        Ok(())
302    }
303}
304
305// =============================================================================
306// Frame Context
307// =============================================================================
308
309/// Context for processing a single frame.
310#[derive(Clone, Debug)]
311pub struct FrameContext {
312    /// Frame number in decode order.
313    pub decode_order: u64,
314    /// Frame number in display order.
315    pub display_order: u64,
316    /// Frame width (may differ from config for super-res).
317    pub width: u32,
318    /// Frame height.
319    pub height: u32,
320    /// Bit depth for this frame.
321    pub bit_depth: u8,
322    /// Is this a keyframe?
323    pub is_keyframe: bool,
324    /// Is this a show frame?
325    pub show_frame: bool,
326    /// Reference frame indices.
327    pub ref_frame_indices: [Option<usize>; NUM_REF_FRAMES],
328    /// Super-resolution scale factor (1.0 = no scaling).
329    pub super_res_scale: f32,
330    /// Film grain parameters present.
331    pub has_film_grain: bool,
332}
333
334impl Default for FrameContext {
335    fn default() -> Self {
336        Self {
337            decode_order: 0,
338            display_order: 0,
339            width: 0,
340            height: 0,
341            bit_depth: 8,
342            is_keyframe: true,
343            show_frame: true,
344            ref_frame_indices: [None; NUM_REF_FRAMES],
345            super_res_scale: 1.0,
346            has_film_grain: false,
347        }
348    }
349}
350
351impl FrameContext {
352    /// Create a new frame context.
353    #[must_use]
354    pub fn new(width: u32, height: u32) -> Self {
355        Self {
356            width,
357            height,
358            ..Default::default()
359        }
360    }
361
362    /// Check if super-resolution is needed.
363    #[must_use]
364    pub fn needs_super_res(&self) -> bool {
365        (self.super_res_scale - 1.0).abs() > f32::EPSILON
366    }
367}
368
369// =============================================================================
370// Decoder Pipeline
371// =============================================================================
372
373/// Main decoder pipeline coordinating all reconstruction stages.
374#[derive(Debug)]
375pub struct DecoderPipeline {
376    /// Pipeline configuration.
377    config: PipelineConfig,
378    /// Buffer pool for frame allocation.
379    buffer_pool: BufferPool,
380    /// Reference frame manager.
381    reference_frames: Vec<Option<FrameBuffer>>,
382    /// Deblocking filter.
383    deblock_filter: DeblockFilter,
384    /// Loop filter pipeline.
385    loop_filter: LoopFilterPipeline,
386    /// CDEF applicator.
387    cdef: CdefApplicator,
388    /// Super-resolution upscaler.
389    super_res: SuperResUpscaler,
390    /// Film grain synthesizer.
391    film_grain: FilmGrainSynthesizer,
392    /// Output formatter.
393    output: OutputFormatter,
394    /// Current working buffer.
395    work_buffer: Option<FrameBuffer>,
396    /// Residual buffer.
397    residual_buffer: ResidualBuffer,
398    /// Frame counter.
399    frame_count: u64,
400    /// Stage results for last frame.
401    last_stage_results: Vec<StageResult>,
402}
403
404impl DecoderPipeline {
405    /// Create a new decoder pipeline.
406    ///
407    /// # Errors
408    ///
409    /// Returns error if configuration is invalid.
410    pub fn new(config: PipelineConfig) -> ReconstructResult<Self> {
411        config.validate()?;
412
413        let buffer_pool = BufferPool::new(
414            config.width,
415            config.height,
416            config.bit_depth,
417            config.subsampling,
418            config.buffer_pool_size,
419        );
420
421        let reference_frames = vec![None; NUM_REF_FRAMES];
422        let residual_buffer = ResidualBuffer::new(config.width, config.height, config.subsampling);
423
424        Ok(Self {
425            config: config.clone(),
426            buffer_pool,
427            reference_frames,
428            deblock_filter: DeblockFilter::new(),
429            loop_filter: LoopFilterPipeline::new(),
430            cdef: CdefApplicator::new(config.width, config.height, config.bit_depth),
431            super_res: SuperResUpscaler::new(),
432            film_grain: FilmGrainSynthesizer::new(config.bit_depth),
433            output: OutputFormatter::new(),
434            work_buffer: None,
435            residual_buffer,
436            frame_count: 0,
437            last_stage_results: Vec::new(),
438        })
439    }
440
441    /// Process a complete frame through the pipeline.
442    ///
443    /// # Errors
444    ///
445    /// Returns error if any pipeline stage fails.
446    pub fn process_frame(
447        &mut self,
448        data: &[u8],
449        context: &FrameContext,
450    ) -> ReconstructResult<FrameBuffer> {
451        self.last_stage_results.clear();
452
453        // Allocate work buffer
454        self.work_buffer = Some(self.buffer_pool.acquire()?);
455
456        // Process each stage
457        self.stage_parse(data, context)?;
458        self.stage_entropy(context)?;
459        self.stage_predict(context)?;
460        self.stage_transform(context)?;
461        self.stage_deblock(context)?;
462        self.stage_loop_filter(context)?;
463        self.stage_cdef(context)?;
464        self.stage_super_res(context)?;
465        self.stage_film_grain(context)?;
466
467        // Get the final buffer
468        let output = self.work_buffer.take().ok_or_else(|| {
469            ReconstructionError::Internal("Work buffer not available".to_string())
470        })?;
471
472        // Update reference frames if this is a reference frame
473        if context.show_frame || context.is_keyframe {
474            self.update_references(&output, context);
475        }
476
477        self.frame_count += 1;
478
479        Ok(output)
480    }
481
482    /// Parse stage - parse bitstream data.
483    fn stage_parse(&mut self, _data: &[u8], _context: &FrameContext) -> ReconstructResult<()> {
484        let result = StageResult::new(PipelineStage::Parse);
485        self.last_stage_results.push(result);
486        Ok(())
487    }
488
489    /// Entropy decoding stage.
490    fn stage_entropy(&mut self, _context: &FrameContext) -> ReconstructResult<()> {
491        let result = StageResult::new(PipelineStage::Entropy);
492        self.last_stage_results.push(result);
493        Ok(())
494    }
495
496    /// Prediction stage - intra and inter prediction.
497    fn stage_predict(&mut self, _context: &FrameContext) -> ReconstructResult<()> {
498        let result = StageResult::new(PipelineStage::Predict);
499        self.last_stage_results.push(result);
500        Ok(())
501    }
502
503    /// Transform stage - inverse transform and residual addition.
504    fn stage_transform(&mut self, _context: &FrameContext) -> ReconstructResult<()> {
505        let result = StageResult::new(PipelineStage::Transform);
506        self.last_stage_results.push(result);
507        Ok(())
508    }
509
510    /// Deblocking filter stage.
511    fn stage_deblock(&mut self, context: &FrameContext) -> ReconstructResult<()> {
512        if !self.config.enable_deblock {
513            self.last_stage_results
514                .push(StageResult::skipped(PipelineStage::Deblock));
515            return Ok(());
516        }
517
518        if let Some(ref mut buffer) = self.work_buffer {
519            self.deblock_filter.apply(buffer, context)?;
520        }
521
522        let result = StageResult::new(PipelineStage::Deblock);
523        self.last_stage_results.push(result);
524        Ok(())
525    }
526
527    /// Loop filter stage.
528    fn stage_loop_filter(&mut self, context: &FrameContext) -> ReconstructResult<()> {
529        if !self.config.enable_loop_filter {
530            self.last_stage_results
531                .push(StageResult::skipped(PipelineStage::LoopFilter));
532            return Ok(());
533        }
534
535        if let Some(ref mut buffer) = self.work_buffer {
536            self.loop_filter.apply(buffer, context)?;
537        }
538
539        let result = StageResult::new(PipelineStage::LoopFilter);
540        self.last_stage_results.push(result);
541        Ok(())
542    }
543
544    /// CDEF stage.
545    fn stage_cdef(&mut self, context: &FrameContext) -> ReconstructResult<()> {
546        if !self.config.enable_cdef {
547            self.last_stage_results
548                .push(StageResult::skipped(PipelineStage::Cdef));
549            return Ok(());
550        }
551
552        if let Some(ref mut buffer) = self.work_buffer {
553            self.cdef.apply(buffer, context)?;
554        }
555
556        let result = StageResult::new(PipelineStage::Cdef);
557        self.last_stage_results.push(result);
558        Ok(())
559    }
560
561    /// Super-resolution stage.
562    fn stage_super_res(&mut self, context: &FrameContext) -> ReconstructResult<()> {
563        if !self.config.enable_super_res || !context.needs_super_res() {
564            self.last_stage_results
565                .push(StageResult::skipped(PipelineStage::SuperRes));
566            return Ok(());
567        }
568
569        if let Some(ref mut buffer) = self.work_buffer {
570            self.super_res.apply(buffer, context)?;
571        }
572
573        let result = StageResult::new(PipelineStage::SuperRes);
574        self.last_stage_results.push(result);
575        Ok(())
576    }
577
578    /// Film grain synthesis stage.
579    fn stage_film_grain(&mut self, context: &FrameContext) -> ReconstructResult<()> {
580        if !self.config.enable_film_grain || !context.has_film_grain {
581            self.last_stage_results
582                .push(StageResult::skipped(PipelineStage::FilmGrain));
583            return Ok(());
584        }
585
586        if let Some(ref mut buffer) = self.work_buffer {
587            self.film_grain.apply(buffer, context)?;
588        }
589
590        let result = StageResult::new(PipelineStage::FilmGrain);
591        self.last_stage_results.push(result);
592        Ok(())
593    }
594
595    /// Update reference frame storage.
596    fn update_references(&mut self, frame: &FrameBuffer, context: &FrameContext) {
597        // For keyframes, clear all references
598        if context.is_keyframe {
599            for ref_frame in &mut self.reference_frames {
600                *ref_frame = None;
601            }
602        }
603
604        // Store in the first available slot or overwrite oldest
605        // In a full implementation, this would use ref_frame_indices from context
606        let slot = (self.frame_count as usize) % NUM_REF_FRAMES;
607        self.reference_frames[slot] = Some(frame.clone());
608    }
609
610    /// Get a reference frame by index.
611    pub fn get_reference(&self, index: usize) -> ReconstructResult<&FrameBuffer> {
612        self.reference_frames
613            .get(index)
614            .and_then(|r| r.as_ref())
615            .ok_or(ReconstructionError::ReferenceNotAvailable(index))
616    }
617
618    /// Get the pipeline configuration.
619    #[must_use]
620    pub fn config(&self) -> &PipelineConfig {
621        &self.config
622    }
623
624    /// Get the number of frames processed.
625    #[must_use]
626    pub const fn frame_count(&self) -> u64 {
627        self.frame_count
628    }
629
630    /// Get results from the last frame's stages.
631    #[must_use]
632    pub fn last_stage_results(&self) -> &[StageResult] {
633        &self.last_stage_results
634    }
635
636    /// Reset the pipeline state.
637    pub fn reset(&mut self) {
638        self.frame_count = 0;
639        self.work_buffer = None;
640        self.last_stage_results.clear();
641        for ref_frame in &mut self.reference_frames {
642            *ref_frame = None;
643        }
644        self.buffer_pool.reset();
645        self.residual_buffer.clear();
646    }
647
648    /// Reconfigure the pipeline with new settings.
649    ///
650    /// # Errors
651    ///
652    /// Returns error if the new configuration is invalid.
653    pub fn reconfigure(&mut self, config: PipelineConfig) -> ReconstructResult<()> {
654        config.validate()?;
655
656        // Check if dimensions changed
657        let dimensions_changed =
658            self.config.width != config.width || self.config.height != config.height;
659
660        self.config = config.clone();
661
662        if dimensions_changed {
663            self.buffer_pool = BufferPool::new(
664                config.width,
665                config.height,
666                config.bit_depth,
667                config.subsampling,
668                config.buffer_pool_size,
669            );
670            self.residual_buffer =
671                ResidualBuffer::new(config.width, config.height, config.subsampling);
672            self.cdef = CdefApplicator::new(config.width, config.height, config.bit_depth);
673        }
674
675        Ok(())
676    }
677}
678
679// =============================================================================
680// Tests
681// =============================================================================
682
683#[cfg(test)]
684mod tests {
685    use super::*;
686
687    #[test]
688    fn test_pipeline_stage_name() {
689        assert_eq!(PipelineStage::Parse.name(), "Parse");
690        assert_eq!(PipelineStage::Cdef.name(), "CDEF");
691        assert_eq!(PipelineStage::SuperRes.name(), "SuperRes");
692    }
693
694    #[test]
695    fn test_pipeline_stage_is_filter() {
696        assert!(!PipelineStage::Parse.is_filter());
697        assert!(PipelineStage::Deblock.is_filter());
698        assert!(PipelineStage::LoopFilter.is_filter());
699        assert!(PipelineStage::Cdef.is_filter());
700    }
701
702    #[test]
703    fn test_pipeline_stage_is_optional() {
704        assert!(!PipelineStage::Parse.is_optional());
705        assert!(!PipelineStage::Entropy.is_optional());
706        assert!(PipelineStage::Deblock.is_optional());
707        assert!(PipelineStage::SuperRes.is_optional());
708        assert!(PipelineStage::FilmGrain.is_optional());
709    }
710
711    #[test]
712    fn test_stage_result() {
713        let result = StageResult::new(PipelineStage::Parse);
714        assert_eq!(result.stage, PipelineStage::Parse);
715        assert!(!result.skipped);
716
717        let skipped = StageResult::skipped(PipelineStage::Cdef);
718        assert!(skipped.skipped);
719    }
720
721    #[test]
722    fn test_pipeline_config_default() {
723        let config = PipelineConfig::default();
724        assert_eq!(config.width, 1920);
725        assert_eq!(config.height, 1080);
726        assert_eq!(config.bit_depth, 8);
727        assert!(config.enable_deblock);
728        assert!(config.enable_loop_filter);
729        assert!(config.enable_cdef);
730    }
731
732    #[test]
733    fn test_pipeline_config_builder() {
734        let config = PipelineConfig::new(1280, 720)
735            .with_bit_depth(10)
736            .with_subsampling(ChromaSubsampling::Cs422)
737            .with_threads(4);
738
739        assert_eq!(config.width, 1280);
740        assert_eq!(config.height, 720);
741        assert_eq!(config.bit_depth, 10);
742        assert_eq!(config.subsampling, ChromaSubsampling::Cs422);
743        assert_eq!(config.threads, 4);
744    }
745
746    #[test]
747    fn test_pipeline_config_validation() {
748        let valid = PipelineConfig::new(1920, 1080);
749        assert!(valid.validate().is_ok());
750
751        let invalid_width = PipelineConfig::new(0, 1080);
752        assert!(invalid_width.validate().is_err());
753
754        let invalid_height = PipelineConfig::new(1920, 0);
755        assert!(invalid_height.validate().is_err());
756
757        let invalid_bit_depth = PipelineConfig::new(1920, 1080).with_bit_depth(7);
758        assert!(invalid_bit_depth.validate().is_err());
759    }
760
761    #[test]
762    fn test_frame_context_default() {
763        let ctx = FrameContext::default();
764        assert_eq!(ctx.width, 0);
765        assert_eq!(ctx.height, 0);
766        assert!(ctx.is_keyframe);
767        assert!(ctx.show_frame);
768    }
769
770    #[test]
771    fn test_frame_context_super_res() {
772        let mut ctx = FrameContext::new(1920, 1080);
773        assert!(!ctx.needs_super_res());
774
775        ctx.super_res_scale = 1.5;
776        assert!(ctx.needs_super_res());
777    }
778
779    #[test]
780    fn test_decoder_pipeline_creation() {
781        let config = PipelineConfig::new(1920, 1080);
782        let pipeline = DecoderPipeline::new(config);
783        assert!(pipeline.is_ok());
784    }
785
786    #[test]
787    fn test_decoder_pipeline_process_frame() {
788        let config = PipelineConfig::new(64, 64).without_filters();
789        let mut pipeline = DecoderPipeline::new(config).expect("should succeed");
790
791        let context = FrameContext::new(64, 64);
792        let result = pipeline.process_frame(&[], &context);
793        assert!(result.is_ok());
794
795        assert_eq!(pipeline.frame_count(), 1);
796    }
797
798    #[test]
799    fn test_decoder_pipeline_reset() {
800        let config = PipelineConfig::new(64, 64).without_filters();
801        let mut pipeline = DecoderPipeline::new(config).expect("should succeed");
802
803        let context = FrameContext::new(64, 64);
804        let _ = pipeline.process_frame(&[], &context);
805
806        pipeline.reset();
807        assert_eq!(pipeline.frame_count(), 0);
808    }
809
810    #[test]
811    fn test_decoder_pipeline_reconfigure() {
812        let config = PipelineConfig::new(64, 64);
813        let mut pipeline = DecoderPipeline::new(config).expect("should succeed");
814
815        let new_config = PipelineConfig::new(128, 128);
816        assert!(pipeline.reconfigure(new_config).is_ok());
817        assert_eq!(pipeline.config().width, 128);
818        assert_eq!(pipeline.config().height, 128);
819    }
820
821    #[test]
822    fn test_stage_results() {
823        let config = PipelineConfig::new(64, 64).without_filters();
824        let mut pipeline = DecoderPipeline::new(config).expect("should succeed");
825
826        let context = FrameContext::new(64, 64);
827        let _ = pipeline.process_frame(&[], &context);
828
829        let results = pipeline.last_stage_results();
830        assert!(!results.is_empty());
831
832        // Check that filter stages are skipped
833        for result in results {
834            if result.stage.is_filter() {
835                assert!(result.skipped);
836            }
837        }
838    }
839}