Skip to main content

oximedia_codec/av1/
conformance.rs

1//! AV1 Bitstream Conformance Validation.
2//!
3//! This module provides validators for AV1 bitstream syntax and semantic
4//! conformance as specified in the AV1 specification.
5//!
6//! # Validators
7//!
8//! - [`SequenceHeaderValidator`] — validates a parsed `SequenceHeader` against spec constraints
9//! - [`ObuValidator`] — validates the syntactic structure of a complete AV1 bitstream
10//!
11//! # Example
12//!
13//! ```ignore
14//! use oximedia_codec::av1::conformance::{ObuValidator, SequenceHeaderValidator};
15//!
16//! let result = SequenceHeaderValidator::validate(&header);
17//! if !result.is_valid {
18//!     for error in &result.errors {
19//!         eprintln!("Conformance error: {error}");
20//!     }
21//! }
22//! ```
23
24use super::obu::{parse_obu, ObuType};
25use super::sequence::SequenceHeader;
26
27// =============================================================================
28// ValidationResult
29// =============================================================================
30
31/// Result of an AV1 conformance validation pass.
32#[derive(Clone, Debug, Default)]
33pub struct ValidationResult {
34    /// `true` if no errors were found (warnings are allowed).
35    pub is_valid: bool,
36    /// Hard constraint violations from the AV1 spec.
37    pub errors: Vec<String>,
38    /// Advisory issues that do not strictly violate the spec but may indicate
39    /// non-conformant or unusual configurations.
40    pub warnings: Vec<String>,
41}
42
43impl ValidationResult {
44    /// Create a new, initially-valid result.
45    #[must_use]
46    fn new() -> Self {
47        Self {
48            is_valid: true,
49            errors: Vec::new(),
50            warnings: Vec::new(),
51        }
52    }
53
54    /// Record a hard error and mark the result as invalid.
55    fn error(&mut self, msg: impl Into<String>) {
56        self.errors.push(msg.into());
57        self.is_valid = false;
58    }
59
60    /// Record a warning (does not affect `is_valid`).
61    fn warn(&mut self, msg: impl Into<String>) {
62        self.warnings.push(msg.into());
63    }
64}
65
66// =============================================================================
67// SequenceHeaderValidator
68// =============================================================================
69
70/// Validates a parsed [`SequenceHeader`] against AV1 specification constraints.
71///
72/// Checks cover:
73/// - `seq_profile` range (0–2)
74/// - Bit depth validity (8, 10, or 12)
75/// - Profile-specific chroma subsampling requirements
76/// - `order_hint_bits` range and consistency with `enable_order_hint`
77/// - `film_grain_params_present` against profile constraints
78pub struct SequenceHeaderValidator;
79
80impl SequenceHeaderValidator {
81    /// Validate a [`SequenceHeader`] and return a [`ValidationResult`].
82    ///
83    /// This is a pure, side-effect-free function.
84    #[must_use]
85    pub fn validate(header: &SequenceHeader) -> ValidationResult {
86        let mut result = ValidationResult::new();
87
88        // ── Profile range ────────────────────────────────────────────────────
89        if header.profile > 2 {
90            result.error(format!(
91                "seq_profile {} is out of range; must be 0, 1, or 2",
92                header.profile
93            ));
94        }
95
96        // ── Bit depth ────────────────────────────────────────────────────────
97        let bd = header.color_config.bit_depth;
98        if bd != 8 && bd != 10 && bd != 12 {
99            result.error(format!("bit_depth {bd} is invalid; must be 8, 10, or 12"));
100        }
101
102        // ── Profile-specific color constraints ───────────────────────────────
103        match header.profile {
104            0 => {
105                // Profile 0 (Main): must use 4:2:0 subsampling, no 12-bit
106                if !header.color_config.subsampling_x || !header.color_config.subsampling_y {
107                    result.error(
108                        "profile 0 (Main) requires 4:2:0 subsampling \
109                         (subsampling_x and subsampling_y must both be true)"
110                            .to_string(),
111                    );
112                }
113                if bd == 12 {
114                    result.error("profile 0 (Main) does not allow 12-bit depth".to_string());
115                }
116            }
117            1 => {
118                // Profile 1 (High): must use 4:4:4, mono_chrome forbidden
119                if header.color_config.mono_chrome {
120                    result.error("profile 1 (High) forbids mono_chrome".to_string());
121                }
122                if header.color_config.subsampling_x || header.color_config.subsampling_y {
123                    result.error(
124                        "profile 1 (High) requires 4:4:4 \
125                         (subsampling_x and subsampling_y must both be false)"
126                            .to_string(),
127                    );
128                }
129                if bd == 12 {
130                    result.error("profile 1 (High) does not allow 12-bit depth".to_string());
131                }
132            }
133            2 => {
134                // Profile 2 (Professional): bit_depth must be 12 if twelve_bit flag is set;
135                // we simply verify the stored bit_depth is one of the allowed values
136                // (already checked above).  No additional subsampling restriction.
137            }
138            _ => {
139                // Already caught above; no further checks needed.
140            }
141        }
142
143        // ── order_hint_bits range & consistency ──────────────────────────────
144        if header.order_hint_bits > 8 {
145            result.error(format!(
146                "order_hint_bits {} exceeds maximum of 8",
147                header.order_hint_bits
148            ));
149        }
150        if !header.enable_order_hint && header.order_hint_bits != 0 {
151            result.warn(format!(
152                "enable_order_hint is false but order_hint_bits is {}; \
153                 expected 0 when order hints are disabled",
154                header.order_hint_bits
155            ));
156        }
157
158        // ── film_grain_params_present constraints ────────────────────────────
159        // Film grain is only meaningful with luma + chroma planes (num_planes == 3).
160        // Profile 1 mandates num_planes == 3 (YUV444); profile 0 and 2 may be mono.
161        if header.film_grain_params_present && header.color_config.num_planes != 3 {
162            result.warn(
163                "film_grain_params_present is set but num_planes != 3; \
164                 film grain synthesis requires luma and chroma planes"
165                    .to_string(),
166            );
167        }
168
169        // Profile 1 + mono_chrome is already an error above; guard here is
170        // redundant but adds clarity for edge-case combinations.
171        if header.film_grain_params_present
172            && header.profile == 1
173            && header.color_config.mono_chrome
174        {
175            result.warn(
176                "film_grain_params_present with profile 1 and mono_chrome \
177                 is contradictory (profile 1 forbids mono_chrome)"
178                    .to_string(),
179            );
180        }
181
182        result
183    }
184}
185
186// =============================================================================
187// ObuValidator
188// =============================================================================
189
190/// Validates the syntactic structure of an AV1 bitstream at the OBU level.
191///
192/// Checks include:
193/// - Correct first OBU type
194/// - `FrameHeader`/`Frame` OBU appearing before any `SequenceHeader`
195/// - OBU size fields remaining within data bounds (surfaced from parse errors)
196pub struct ObuValidator;
197
198impl ObuValidator {
199    /// Validate the OBU-level structure of `data` and return a [`ValidationResult`].
200    ///
201    /// The validator iterates all OBUs, accumulating errors and warnings
202    /// without aborting on the first issue (except for a parse error that
203    /// makes forward progress impossible).
204    #[must_use]
205    pub fn validate_bitstream(data: &[u8]) -> ValidationResult {
206        let mut result = ValidationResult::new();
207
208        if data.is_empty() {
209            result.warn("bitstream is empty".to_string());
210            return result;
211        }
212
213        let mut offset = 0usize;
214        let mut first_obu = true;
215        let mut seen_sequence_header = false;
216
217        while offset < data.len() {
218            let remaining = &data[offset..];
219
220            match parse_obu(remaining) {
221                Err(e) => {
222                    result.error(format!("OBU parse error at byte offset {offset}: {e}"));
223                    // Cannot continue safely; stop iteration.
224                    break;
225                }
226                Ok((header, _payload, total_size)) => {
227                    // ── First OBU check ──────────────────────────────────
228                    if first_obu {
229                        match header.obu_type {
230                            ObuType::TemporalDelimiter | ObuType::SequenceHeader => {
231                                // Conformant start
232                            }
233                            _ => {
234                                result.warn(format!(
235                                    "first OBU at offset 0 is {:?}; \
236                                     expected TemporalDelimiter or SequenceHeader",
237                                    header.obu_type
238                                ));
239                            }
240                        }
241                        first_obu = false;
242                    }
243
244                    // ── Track SequenceHeader presence ────────────────────
245                    if matches!(header.obu_type, ObuType::SequenceHeader) {
246                        seen_sequence_header = true;
247                    }
248
249                    // ── FrameHeader / Frame before SequenceHeader ─────────
250                    if !seen_sequence_header {
251                        match header.obu_type {
252                            ObuType::FrameHeader
253                            | ObuType::Frame
254                            | ObuType::RedundantFrameHeader => {
255                                result.error(format!(
256                                    "{:?} OBU at byte offset {offset} appears \
257                                     before any SequenceHeader",
258                                    header.obu_type
259                                ));
260                            }
261                            _ => {}
262                        }
263                    }
264
265                    offset += total_size;
266                }
267            }
268        }
269
270        result
271    }
272
273    /// Count the number of successfully parseable OBUs in `data`.
274    ///
275    /// Stops counting at the first parse error.
276    #[must_use]
277    pub fn count_obus(data: &[u8]) -> usize {
278        let mut count = 0usize;
279        let mut offset = 0usize;
280
281        while offset < data.len() {
282            match parse_obu(&data[offset..]) {
283                Err(_) => break,
284                Ok((_header, _payload, total_size)) => {
285                    count += 1;
286                    offset += total_size;
287                }
288            }
289        }
290
291        count
292    }
293
294    /// Return the byte offset of the first `SequenceHeader` OBU in `data`,
295    /// or `None` if no such OBU is found.
296    #[must_use]
297    pub fn find_sequence_header(data: &[u8]) -> Option<usize> {
298        let mut offset = 0usize;
299
300        while offset < data.len() {
301            match parse_obu(&data[offset..]) {
302                Err(_) => break,
303                Ok((header, _payload, total_size)) => {
304                    if matches!(header.obu_type, ObuType::SequenceHeader) {
305                        return Some(offset);
306                    }
307                    offset += total_size;
308                }
309            }
310        }
311
312        None
313    }
314}
315
316// =============================================================================
317// Tests
318// =============================================================================
319
320#[cfg(test)]
321mod tests {
322    use super::super::film_grain::{FilmGrainParams, ScalingPoint};
323    use super::super::sequence::{ColorConfig, SequenceHeader};
324    use super::{ObuValidator, SequenceHeaderValidator};
325
326    fn valid_profile0_header() -> SequenceHeader {
327        SequenceHeader {
328            profile: 0,
329            still_picture: false,
330            reduced_still_picture_header: false,
331            max_frame_width_minus_1: 1919,
332            max_frame_height_minus_1: 1079,
333            enable_order_hint: true,
334            order_hint_bits: 7,
335            enable_superres: false,
336            enable_cdef: true,
337            enable_restoration: true,
338            color_config: ColorConfig {
339                bit_depth: 8,
340                mono_chrome: false,
341                num_planes: 3,
342                color_primaries: 1,
343                transfer_characteristics: 1,
344                matrix_coefficients: 1,
345                color_range: false,
346                subsampling_x: true,
347                subsampling_y: true,
348                separate_uv_delta_q: false,
349            },
350            film_grain_params_present: false,
351        }
352    }
353
354    #[test]
355    fn test_sequence_header_validator_valid_profile0() {
356        let header = valid_profile0_header();
357        let result = SequenceHeaderValidator::validate(&header);
358        assert!(
359            result.is_valid,
360            "expected valid profile-0 header; errors: {:?}",
361            result.errors
362        );
363        assert!(result.errors.is_empty());
364    }
365
366    #[test]
367    fn test_sequence_header_validator_profile1_requires_444() {
368        // Profile 1 requires subsampling_x == false && subsampling_y == false.
369        // Using subsampling_x: true violates the 4:4:4 requirement.
370        let header = SequenceHeader {
371            profile: 1,
372            color_config: ColorConfig {
373                bit_depth: 8,
374                mono_chrome: false,
375                num_planes: 3,
376                color_primaries: 1,
377                transfer_characteristics: 1,
378                matrix_coefficients: 1,
379                color_range: false,
380                subsampling_x: true, // wrong for profile 1
381                subsampling_y: false,
382                separate_uv_delta_q: false,
383            },
384            ..valid_profile0_header()
385        };
386        let result = SequenceHeaderValidator::validate(&header);
387        assert!(
388            !result.is_valid,
389            "expected validation failure for profile 1 with 4:2:2 subsampling"
390        );
391        assert!(
392            !result.errors.is_empty(),
393            "expected at least one error for profile 1 subsampling violation"
394        );
395    }
396
397    #[test]
398    fn test_sequence_header_validator_invalid_bit_depth() {
399        let header = SequenceHeader {
400            profile: 0,
401            color_config: ColorConfig {
402                bit_depth: 7, // invalid
403                ..valid_profile0_header().color_config
404            },
405            ..valid_profile0_header()
406        };
407        let result = SequenceHeaderValidator::validate(&header);
408        assert!(!result.is_valid, "bit_depth=7 should be invalid");
409        assert!(result.errors.iter().any(|e| e.contains("bit_depth")));
410    }
411
412    #[test]
413    fn test_sequence_header_validator_order_hint_bits_too_large() {
414        let header = SequenceHeader {
415            enable_order_hint: true,
416            order_hint_bits: 9, // exceeds max of 8
417            ..valid_profile0_header()
418        };
419        let result = SequenceHeaderValidator::validate(&header);
420        assert!(!result.is_valid, "order_hint_bits=9 should be invalid");
421        assert!(result.errors.iter().any(|e| e.contains("order_hint_bits")));
422    }
423
424    #[test]
425    fn test_sequence_header_validator_order_hint_disabled_nonzero_bits_warns() {
426        let header = SequenceHeader {
427            enable_order_hint: false,
428            order_hint_bits: 4, // non-zero but order hint disabled
429            ..valid_profile0_header()
430        };
431        let result = SequenceHeaderValidator::validate(&header);
432        // Should still be valid (warning only)
433        assert!(
434            result.is_valid,
435            "non-zero order_hint_bits with enable_order_hint=false should only warn"
436        );
437        assert!(
438            !result.warnings.is_empty(),
439            "expected a warning for inconsistent order_hint_bits"
440        );
441    }
442
443    #[test]
444    fn test_sequence_header_validator_profile0_rejects_12bit() {
445        let header = SequenceHeader {
446            profile: 0,
447            color_config: ColorConfig {
448                bit_depth: 12,
449                ..valid_profile0_header().color_config
450            },
451            ..valid_profile0_header()
452        };
453        let result = SequenceHeaderValidator::validate(&header);
454        assert!(!result.is_valid, "profile 0 should reject 12-bit depth");
455    }
456
457    #[test]
458    fn test_obu_validator_count_obus_empty() {
459        assert_eq!(ObuValidator::count_obus(&[]), 0);
460    }
461
462    #[test]
463    fn test_obu_validator_find_sequence_header_none() {
464        assert_eq!(ObuValidator::find_sequence_header(&[]), None);
465    }
466
467    #[test]
468    fn test_obu_validator_minimal_valid_bitstream() {
469        // TemporalDelimiter: type=2, no extension, has_size=true → byte = (2<<3)|(1<<1) = 0x12
470        //                    followed by LEB128 size=0 → 0x00
471        // SequenceHeader:    type=1, no extension, has_size=true → byte = (1<<3)|(1<<1) = 0x0A
472        //                    followed by LEB128 size=0 → 0x00
473        let bitstream: &[u8] = &[0x12, 0x00, 0x0A, 0x00];
474
475        let count = ObuValidator::count_obus(bitstream);
476        assert_eq!(count, 2, "expected 2 OBUs in minimal bitstream");
477
478        let seq_offset = ObuValidator::find_sequence_header(bitstream);
479        assert_eq!(
480            seq_offset,
481            Some(2),
482            "SequenceHeader should be at byte offset 2"
483        );
484    }
485
486    #[test]
487    fn test_obu_validator_validate_bitstream_empty_warns() {
488        let result = ObuValidator::validate_bitstream(&[]);
489        // Empty bitstream → warning only, not an error
490        assert!(
491            result.is_valid,
492            "empty bitstream should yield is_valid=true with warnings"
493        );
494        assert!(
495            !result.warnings.is_empty(),
496            "empty bitstream should produce at least one warning"
497        );
498    }
499
500    #[test]
501    fn test_obu_validator_validate_bitstream_minimal() {
502        let bitstream: &[u8] = &[0x12, 0x00, 0x0A, 0x00];
503        let result = ObuValidator::validate_bitstream(bitstream);
504        assert!(
505            result.is_valid,
506            "minimal TD+SH bitstream should be valid; errors: {:?}",
507            result.errors
508        );
509    }
510
511    #[test]
512    fn test_obu_validator_validate_bitstream_frame_before_sequence_header() {
513        // Frame OBU (type=6) with has_size=true and payload size 0, no prior SequenceHeader.
514        // header byte = (6<<3)|(1<<1) = 0x32
515        let bitstream: &[u8] = &[0x32, 0x00];
516        let result = ObuValidator::validate_bitstream(bitstream);
517        assert!(
518            !result.is_valid,
519            "Frame OBU before SequenceHeader should produce an error"
520        );
521        assert!(
522            result
523                .errors
524                .iter()
525                .any(|e| e.contains("before any SequenceHeader")),
526            "error should mention SequenceHeader ordering; got: {:?}",
527            result.errors
528        );
529    }
530
531    #[test]
532    fn test_av1_film_grain_params_serialize_deserialize() {
533        let mut params = FilmGrainParams::new();
534        params.apply_grain = true;
535        params.grain_seed = 0xDEAD;
536        params.update_grain = true;
537        params.film_grain_params_present = true;
538        params.num_y_points = 2;
539        params.y_points[0] = ScalingPoint::new(0, 64);
540        params.y_points[1] = ScalingPoint::new(128, 128);
541
542        // Clone acts as serialise→deserialise round-trip for in-memory representation.
543        let cloned = params.clone();
544
545        assert_eq!(params.grain_seed, cloned.grain_seed);
546        assert_eq!(params.num_y_points, cloned.num_y_points);
547        assert_eq!(params.y_points[0], cloned.y_points[0]);
548        assert_eq!(params.y_points[1], cloned.y_points[1]);
549        assert!(params.apply_grain);
550        assert!(params.update_grain);
551        assert!(params.film_grain_params_present);
552
553        // Validate the params are internally consistent.
554        assert!(
555            params.validate(),
556            "constructed FilmGrainParams should pass validate()"
557        );
558    }
559
560    #[test]
561    fn test_av1_film_grain_params_default_valid() {
562        let params = FilmGrainParams::default();
563        assert!(params.validate(), "default FilmGrainParams should be valid");
564    }
565
566    #[test]
567    fn test_av1_film_grain_params_invalid_ar_coeff_lag() {
568        let mut params = FilmGrainParams::new();
569        params.ar_coeff_lag = 4; // max is MAX_AR_LAG=3
570        assert!(!params.validate(), "ar_coeff_lag=4 should fail validate()");
571    }
572}