Skip to main content

rav1d_safe/src/
managed.rs

1//! 100% Safe Rust API for rav1d-safe decoder
2//!
3//! This module provides a fully safe, zero-copy API for decoding AV1 video.
4//! It wraps the internal `rav1d` decoder with type-safe, lifetime-safe abstractions.
5//!
6//! # Features
7//!
8//! - **100% Safe Rust** - No `unsafe` code in this module
9//! - **Zero-Copy** - Direct access to decoded pixel data without copying
10//! - **Type Safety** - Enums and strong types instead of raw integers
11//! - **HDR Support** - Full access to HDR10, HLG metadata
12//! - **Multi-threaded** - Configurable thread pool for parallel decoding
13//!
14//! # Example
15//!
16//! ```no_run
17//! use rav1d_safe::src::managed::{Decoder, Settings, Planes};
18//!
19//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
20//! let mut decoder = Decoder::new()?;
21//! let obu_data = b"..."; // AV1 OBU bitstream data
22//!
23//! if let Some(frame) = decoder.decode(obu_data)? {
24//!     println!("Decoded {}x{} frame at {}-bit",
25//!              frame.width(), frame.height(), frame.bit_depth());
26//!
27//!     // Zero-copy access to pixel data
28//!     match frame.planes() {
29//!         Planes::Depth8(planes) => {
30//!             let y_plane = planes.y();
31//!             for row in y_plane.rows() {
32//!                 // Process 8-bit row data
33//!             }
34//!         }
35//!         Planes::Depth16(planes) => {
36//!             let y_plane = planes.y();
37//!             let pixel = y_plane.pixel(0, 0);
38//!             println!("Top-left pixel: {}", pixel);
39//!         }
40//!     }
41//! }
42//! # Ok(())
43//! # }
44//! ```
45
46#![forbid(unsafe_code)]
47
48use crate::include::common::bitdepth::{BitDepth8, BitDepth16};
49use crate::include::dav1d::data::Rav1dData;
50use crate::include::dav1d::dav1d::{Rav1dDecodeFrameType, Rav1dInloopFilterType, Rav1dSettings};
51use crate::include::dav1d::headers::{
52    Rav1dColorPrimaries, Rav1dMatrixCoefficients, Rav1dPixelLayout, Rav1dTransferCharacteristics,
53};
54use crate::include::dav1d::picture::{Rav1dPicture, Rav1dPictureDataComponentInner};
55use crate::src::c_arc::CArc;
56use crate::src::disjoint_mut::DisjointImmutGuard;
57use crate::src::error::Rav1dError;
58use crate::src::internal::Rav1dContext;
59use std::ops::Deref;
60use std::sync::Arc;
61
62/// Decoder errors
63#[derive(Debug, Clone, PartialEq, Eq)]
64pub enum Error {
65    InvalidSettings(&'static str),
66    InitFailed,
67    OutOfMemory,
68    InvalidData,
69    NeedMoreData,
70    Other(String),
71}
72
73impl std::fmt::Display for Error {
74    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75        match self {
76            Self::InvalidSettings(msg) => write!(f, "invalid settings: {}", msg),
77            Self::InitFailed => write!(f, "decoder initialization failed"),
78            Self::OutOfMemory => write!(f, "out of memory"),
79            Self::InvalidData => write!(f, "invalid data"),
80            Self::NeedMoreData => write!(f, "need more data"),
81            Self::Other(msg) => write!(f, "decode error: {}", msg),
82        }
83    }
84}
85
86impl std::error::Error for Error {}
87
88impl From<Rav1dError> for Error {
89    fn from(err: Rav1dError) -> Self {
90        match err {
91            Rav1dError::EAGAIN => Self::NeedMoreData,
92            Rav1dError::ENOMEM => Self::OutOfMemory,
93            Rav1dError::EINVAL => Self::InvalidData,
94            Rav1dError::EGeneric => Self::Other("generic error".to_string()),
95            _ => Self::Other(format!("{:?}", err)),
96        }
97    }
98}
99
100pub type Result<T, E = Error> = std::result::Result<T, E>;
101
102/// Decoder configuration settings
103///
104/// Use `Settings::default()` or struct update syntax (`Settings { threads: 4, ..Default::default() }`)
105/// to construct. New fields may be added in minor releases.
106#[derive(Clone, Debug)]
107#[non_exhaustive]
108pub struct Settings {
109    /// Number of threads for decoding
110    ///
111    /// * `0` = auto-detect (enables frame threading for better performance but asynchronous behavior)
112    /// * `1` = single-threaded (default, simpler synchronous behavior)
113    /// * `2+` = multi-threaded with frame threading
114    ///
115    /// With frame threading enabled (threads >= 2 or threads == 0), `decode()` may return `None`
116    /// even when complete frame data is provided, as frames are processed asynchronously.
117    /// Call `decode()` or `flush()` multiple times to drain buffered frames.
118    ///
119    /// **Note:** Multithreading requires the `unchecked` feature. Without it,
120    /// the decoder silently falls back to single-threaded to prevent runtime
121    /// panics from DisjointMut overlap detection on stride gap bytes.
122    pub threads: u32,
123
124    /// Apply film grain synthesis during decoding
125    pub apply_grain: bool,
126
127    /// Maximum frame size in total pixels, i.e. width * height (0 = unlimited)
128    ///
129    /// Default: 35,389,440 (8K UHD: 8192 × 4320). Set to 0 to disable the limit.
130    /// Frames exceeding this limit are rejected during OBU parsing with `Err(InvalidData)`.
131    pub frame_size_limit: u32,
132
133    /// Decode all layers or just the selected operating point
134    pub all_layers: bool,
135
136    /// Operating point to decode (0-31)
137    pub operating_point: u8,
138
139    /// Output invisible frames (frames not meant for display)
140    pub output_invisible_frames: bool,
141
142    /// Inloop filters to apply during decoding
143    pub inloop_filters: InloopFilters,
144
145    /// Which frame types to decode
146    pub decode_frame_type: DecodeFrameType,
147
148    /// Maximum number of frames in flight for frame threading.
149    ///
150    /// * `0` = auto (default, derived from thread count: `min(sqrt(threads), 8)`)
151    /// * `1` = no frame threading (tile parallelism only — ideal for still images)
152    /// * `2+` = up to N frames decoded in parallel
153    ///
154    /// For still image formats (AVIF, HEIC), set this to `1` to get tile-level
155    /// parallelism without frame threading overhead or async decode behavior.
156    pub max_frame_delay: u32,
157
158    /// Enforce strict standard compliance
159    pub strict_std_compliance: bool,
160
161    /// CPU feature level for SIMD dispatch.
162    ///
163    /// Controls which instruction sets the decoder is allowed to use.
164    /// Default is `CpuLevel::Native` (use all detected features).
165    ///
166    /// Set to a lower level to force the decoder through a specific code path,
167    /// e.g. `CpuLevel::Scalar` to test the pure-Rust fallback.
168    pub cpu_level: CpuLevel,
169}
170
171impl Default for Settings {
172    fn default() -> Self {
173        Self {
174            // Use single-threaded decoding by default for simpler, deterministic behavior.
175            // With threads=0 (auto), frame threading is enabled which causes decode() to
176            // return None even when a complete frame has been provided, because frame threads
177            // need time to process. Users can set threads=0 for better performance but must
178            // handle the asynchronous behavior by calling decode() or flush() multiple times.
179            threads: 1,
180            apply_grain: true,
181            frame_size_limit: 8192 * 4320, // 8K UHD (~35MP)
182            all_layers: true,
183            operating_point: 0,
184            max_frame_delay: 0,
185            output_invisible_frames: false,
186            inloop_filters: InloopFilters::all(),
187            decode_frame_type: DecodeFrameType::All,
188            strict_std_compliance: false,
189            cpu_level: CpuLevel::Native,
190        }
191    }
192}
193
194impl From<Settings> for Rav1dSettings {
195    fn from(settings: Settings) -> Self {
196        Self {
197            n_threads: settings.threads as i32,
198            max_frame_delay: settings.max_frame_delay as i32,
199            apply_grain: settings.apply_grain,
200            operating_point: settings.operating_point,
201            all_layers: settings.all_layers,
202            frame_size_limit: settings.frame_size_limit,
203            allocator: Default::default(),
204            logger: None,
205            strict_std_compliance: settings.strict_std_compliance,
206            output_invisible_frames: settings.output_invisible_frames,
207            inloop_filters: settings.inloop_filters.into(),
208            decode_frame_type: settings.decode_frame_type.into(),
209        }
210    }
211}
212
213/// Inloop filter flags
214#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
215pub struct InloopFilters {
216    bits: u8,
217}
218
219impl InloopFilters {
220    pub const DEBLOCK: Self = Self {
221        bits: Rav1dInloopFilterType::DEBLOCK.bits(),
222    };
223    pub const CDEF: Self = Self {
224        bits: Rav1dInloopFilterType::CDEF.bits(),
225    };
226    pub const RESTORATION: Self = Self {
227        bits: Rav1dInloopFilterType::RESTORATION.bits(),
228    };
229
230    /// Enable all inloop filters
231    pub const fn all() -> Self {
232        Self {
233            bits: Rav1dInloopFilterType::DEBLOCK.bits()
234                | Rav1dInloopFilterType::CDEF.bits()
235                | Rav1dInloopFilterType::RESTORATION.bits(),
236        }
237    }
238
239    /// Disable all inloop filters
240    pub const fn none() -> Self {
241        Self { bits: 0 }
242    }
243
244    pub const fn contains(&self, other: Self) -> bool {
245        (self.bits & other.bits) == other.bits
246    }
247
248    pub const fn union(self, other: Self) -> Self {
249        Self {
250            bits: self.bits | other.bits,
251        }
252    }
253}
254
255impl From<InloopFilters> for Rav1dInloopFilterType {
256    fn from(filters: InloopFilters) -> Self {
257        Self::from_bits_retain(filters.bits)
258    }
259}
260
261/// Which frame types to decode
262#[derive(Clone, Copy, Debug, PartialEq, Eq)]
263pub enum DecodeFrameType {
264    /// Decode all frame types
265    All,
266    /// Decode only reference frames
267    Reference,
268    /// Decode only intra frames
269    Intra,
270    /// Decode only key frames
271    Key,
272}
273
274impl From<DecodeFrameType> for Rav1dDecodeFrameType {
275    fn from(ft: DecodeFrameType) -> Self {
276        match ft {
277            DecodeFrameType::All => Self::All,
278            DecodeFrameType::Reference => Self::Reference,
279            DecodeFrameType::Intra => Self::Intra,
280            DecodeFrameType::Key => Self::Key,
281        }
282    }
283}
284
285/// CPU feature level for SIMD dispatch control.
286///
287/// Controls which instruction sets the decoder is allowed to use at runtime.
288/// Higher levels include all instructions from lower levels. Setting a level
289/// that isn't available on the current hardware is safe — it falls back to
290/// the highest available level below it.
291///
292/// Use [`CpuLevel::platform_levels()`] to discover which levels are testable
293/// on the current hardware.
294///
295/// # Example
296///
297/// ```no_run
298/// use rav1d_safe::src::managed::{Decoder, Settings, CpuLevel};
299///
300/// // Force scalar-only decode (no SIMD)
301/// let mut settings = Settings::default();
302/// settings.cpu_level = CpuLevel::Scalar;
303/// let mut decoder = Decoder::with_settings(settings).unwrap();
304/// ```
305#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
306#[non_exhaustive]
307#[derive(Default)]
308pub enum CpuLevel {
309    /// No SIMD — pure scalar Rust. Works on all platforms. Slowest.
310    Scalar,
311
312    /// x86-64-v2: SSE2 + SSSE3 + SSE4.1.
313    /// Baseline for most x86-64 CPUs since ~2008.
314    X86V2,
315
316    /// x86-64-v3: V2 + AVX2 + FMA.
317    /// Haswell (2013) and newer. This is the primary SIMD path for rav1d-safe.
318    X86V3,
319
320    /// x86-64-v4: V3 + AVX-512 (Ice Lake subset).
321    /// Ice Lake (2019) and newer. Only used for a few functions in rav1d.
322    X86V4,
323
324    /// ARM NEON baseline (mandatory on AArch64).
325    Neon,
326
327    /// ARM NEON + dot product instructions (ARMv8.2+).
328    NeonDotprod,
329
330    /// ARM NEON + i8mm instructions (ARMv8.6+).
331    NeonI8mm,
332
333    /// Use all features detected at runtime. Default.
334    #[default]
335    Native,
336}
337
338impl CpuLevel {
339    /// Convert to the raw bitmask for `rav1d_set_cpu_flags_mask`.
340    ///
341    /// On a platform where the level doesn't apply (e.g. `X86V3` on ARM),
342    /// returns `0` (scalar).
343    pub const fn to_mask(self) -> u32 {
344        match self {
345            Self::Scalar => 0,
346
347            // x86_64 flags: SSE2=0, SSSE3=1, SSE41=2, AVX2=3, AVX512ICL=4
348            Self::X86V2 => {
349                (1 << 0) | (1 << 1) | (1 << 2) // SSE2 + SSSE3 + SSE4.1
350            }
351            Self::X86V3 => {
352                (1 << 0) | (1 << 1) | (1 << 2) | (1 << 3) // + AVX2
353            }
354            Self::X86V4 => {
355                (1 << 0) | (1 << 1) | (1 << 2) | (1 << 3) | (1 << 4) // + AVX-512 ICL
356            }
357
358            // aarch64 flags: NEON=0, DOTPROD=1, I8MM=2, SVE=3, SVE2=4
359            Self::Neon => 1 << 0,
360            Self::NeonDotprod => (1 << 0) | (1 << 1),
361            Self::NeonI8mm => (1 << 0) | (1 << 1) | (1 << 2),
362
363            Self::Native => u32::MAX,
364        }
365    }
366
367    /// List all CPU levels relevant to the current platform, from most
368    /// restrictive (Scalar) to least restrictive (Native).
369    ///
370    /// Only includes levels that differ in behavior on this platform.
371    /// For example, on x86_64 this returns `[Scalar, X86V2, X86V3, X86V4, Native]`.
372    pub fn platform_levels() -> &'static [CpuLevel] {
373        #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
374        {
375            &[
376                CpuLevel::Scalar,
377                CpuLevel::X86V2,
378                CpuLevel::X86V3,
379                CpuLevel::X86V4,
380                CpuLevel::Native,
381            ]
382        }
383        #[cfg(any(target_arch = "arm", target_arch = "aarch64"))]
384        {
385            &[
386                CpuLevel::Scalar,
387                CpuLevel::Neon,
388                CpuLevel::NeonDotprod,
389                CpuLevel::NeonI8mm,
390                CpuLevel::Native,
391            ]
392        }
393        #[cfg(not(any(
394            target_arch = "x86",
395            target_arch = "x86_64",
396            target_arch = "arm",
397            target_arch = "aarch64",
398        )))]
399        {
400            &[CpuLevel::Scalar, CpuLevel::Native]
401        }
402    }
403
404    /// Short human-readable name for this level.
405    pub const fn name(self) -> &'static str {
406        match self {
407            Self::Scalar => "scalar",
408            Self::X86V2 => "x86-64-v2",
409            Self::X86V3 => "x86-64-v3",
410            Self::X86V4 => "x86-64-v4",
411            Self::Neon => "neon",
412            Self::NeonDotprod => "neon-dotprod",
413            Self::NeonI8mm => "neon-i8mm",
414            Self::Native => "native",
415        }
416    }
417}
418
419impl std::fmt::Display for CpuLevel {
420    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
421        f.write_str(self.name())
422    }
423}
424
425/// Safe AV1 decoder instance
426///
427/// This is the main entry point for decoding AV1 video. It wraps the internal
428/// `rav1d` decoder with a safe, type-safe interface.
429///
430/// # Example
431///
432/// ```no_run
433/// use rav1d_safe::src::managed::Decoder;
434///
435/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
436/// let mut decoder = Decoder::new()?;
437/// let obu_data = b"...";
438///
439/// if let Some(frame) = decoder.decode(obu_data)? {
440///     println!("Decoded {}x{}", frame.width(), frame.height());
441/// }
442/// # Ok(())
443/// # }
444/// ```
445pub struct Decoder {
446    ctx: Arc<Rav1dContext>,
447    worker_handles: Vec<std::thread::JoinHandle<()>>,
448}
449
450/// Returns `true` if the `unchecked` feature is enabled.
451///
452/// When unchecked:
453/// - DisjointMut borrow tracking is disabled (enables multithreading)
454/// - SIMD hot-path bounds checks use `get_unchecked` with debug_assert
455/// - msac entropy coding uses inlined SSE2 intrinsics on x86_64
456pub const fn is_unchecked() -> bool {
457    cfg!(feature = "unchecked")
458}
459
460impl Decoder {
461    /// Create a new decoder with default settings
462    pub fn new() -> Result<Self> {
463        Self::with_settings(Settings::default())
464    }
465
466    /// Create a decoder with custom settings
467    pub fn with_settings(settings: Settings) -> Result<Self> {
468        // Apply CPU feature level mask before decoder init (affects SIMD dispatch)
469        crate::src::cpu::rav1d_set_cpu_flags_mask(settings.cpu_level.to_mask());
470
471        let rav1d_settings: Rav1dSettings = settings.into();
472        let (ctx, worker_handles) =
473            crate::src::lib::rav1d_open(&rav1d_settings).map_err(|_| Error::InitFailed)?;
474        Ok(Self {
475            ctx,
476            worker_handles,
477        })
478    }
479
480    /// Decode AV1 OBU data from a byte slice
481    ///
482    /// Returns `Ok(None)` if more data is needed (the decoder is waiting for more input).
483    /// Returns `Ok(Some(frame))` when a frame is successfully decoded.
484    ///
485    /// # Example
486    ///
487    /// ```no_run
488    /// # use rav1d_safe::src::managed::Decoder;
489    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
490    /// let mut decoder = Decoder::new()?;
491    /// let data = b"..."; // AV1 OBU data
492    ///
493    /// match decoder.decode(data)? {
494    ///     Some(frame) => {
495    ///         println!("Got frame: {}x{}", frame.width(), frame.height());
496    ///     }
497    ///     None => {
498    ///         println!("Need more data");
499    ///     }
500    /// }
501    /// # Ok(())
502    /// # }
503    /// ```
504    pub fn decode(&mut self, data: &[u8]) -> Result<Option<Frame>> {
505        // Create Rav1dData from slice by copying to a CArc-owned buffer
506        let mut rav1d_data = if !data.is_empty() {
507            // Allocate and copy in one go: convert to Vec, then Box, then CBox, then CArc
508            let owned = data.to_vec().into_boxed_slice();
509            let cbox = crate::src::c_box::CBox::from_box(owned);
510            let carc = CArc::wrap(cbox).map_err(|_| Error::OutOfMemory)?;
511            Rav1dData {
512                data: Some(carc),
513                m: Default::default(),
514            }
515        } else {
516            Rav1dData {
517                data: None,
518                m: Default::default(),
519            }
520        };
521
522        // Send data to decoder
523        crate::src::lib::rav1d_send_data(&self.ctx, &mut rav1d_data)?;
524
525        // Try to get a picture
526        let mut pic = Rav1dPicture::default();
527        match crate::src::lib::rav1d_get_picture(&self.ctx, &mut pic) {
528            Ok(()) => Ok(Some(Frame { inner: pic })),
529            Err(Rav1dError::EAGAIN) => Ok(None),
530            Err(e) => Err(e.into()),
531        }
532    }
533
534    /// Try to get a decoded frame without sending new data.
535    ///
536    /// After calling [`decode()`](Self::decode), the decoder may have buffered
537    /// additional frames (e.g. from a raw OBU stream containing multiple temporal
538    /// units). Call this in a loop until it returns `Ok(None)` to drain them.
539    pub fn get_frame(&mut self) -> Result<Option<Frame>> {
540        let mut pic = Rav1dPicture::default();
541        match crate::src::lib::rav1d_get_picture(&self.ctx, &mut pic) {
542            Ok(()) => Ok(Some(Frame { inner: pic })),
543            Err(Rav1dError::EAGAIN) => Ok(None),
544            Err(e) => Err(e.into()),
545        }
546    }
547
548    /// Flush the decoder and return all remaining frames
549    ///
550    /// This should be called after all input data has been fed to the decoder
551    /// to retrieve any buffered frames.
552    pub fn flush(&mut self) -> Result<Vec<Frame>> {
553        crate::src::lib::rav1d_flush(&self.ctx);
554
555        let mut frames = Vec::new();
556        loop {
557            let mut pic = Rav1dPicture::default();
558            match crate::src::lib::rav1d_get_picture(&self.ctx, &mut pic) {
559                Ok(()) => frames.push(Frame { inner: pic }),
560                Err(Rav1dError::EAGAIN) => break,
561                Err(e) => return Err(e.into()),
562            }
563        }
564        Ok(frames)
565    }
566}
567
568impl Drop for Decoder {
569    fn drop(&mut self) {
570        // Signal worker threads to exit
571        self.ctx.tell_worker_threads_to_die();
572
573        // Join all worker threads synchronously
574        // This is safe because:
575        // 1. We're on the main thread (Decoder is not Send)
576        // 2. Workers have been signaled to exit via tell_worker_threads_to_die
577        // 3. We own the JoinHandles, not the workers themselves
578        for handle in self.worker_handles.drain(..) {
579            match handle.join() {
580                Ok(()) => {}
581                Err(e) => {
582                    eprintln!("rav1d worker thread panicked during shutdown: {:?}", e);
583                }
584            }
585        }
586
587        // Now drop the Arc<Rav1dContext>
588        // Workers have exited, so we're likely the last Arc holder
589    }
590}
591
592/// A decoded AV1 frame with zero-copy access to pixel data
593///
594/// Frames are cheap to clone (they use `Arc` internally).
595/// Multiple `Frame` instances can safely reference the same decoded data.
596#[derive(Clone)]
597pub struct Frame {
598    inner: Rav1dPicture,
599}
600
601impl Frame {
602    /// Frame width in pixels
603    pub fn width(&self) -> u32 {
604        self.inner.p.w as u32
605    }
606
607    /// Frame height in pixels
608    pub fn height(&self) -> u32 {
609        self.inner.p.h as u32
610    }
611
612    /// Bit depth (8, 10, or 12)
613    pub fn bit_depth(&self) -> u8 {
614        self.inner.p.bpc
615    }
616
617    /// Pixel layout (chroma subsampling)
618    pub fn pixel_layout(&self) -> PixelLayout {
619        self.inner.p.layout.into()
620    }
621
622    /// Access pixel data according to bit depth
623    ///
624    /// Returns an enum that dispatches to either 8-bit or 16-bit plane access.
625    pub fn planes(&self) -> Planes<'_> {
626        match self.bit_depth() {
627            8 => Planes::Depth8(Planes8 { frame: self }),
628            10 | 12 => Planes::Depth16(Planes16 { frame: self }),
629            _ => unreachable!("invalid bit depth: {}", self.bit_depth()),
630        }
631    }
632
633    /// Color metadata
634    pub fn color_info(&self) -> ColorInfo {
635        let seq_hdr = self.inner.seq_hdr.as_ref().expect("missing seq_hdr");
636        ColorInfo {
637            primaries: seq_hdr.rav1d.pri.into(),
638            transfer_characteristics: seq_hdr.rav1d.trc.into(),
639            matrix_coefficients: seq_hdr.rav1d.mtrx.into(),
640            color_range: if seq_hdr.rav1d.color_range != 0 {
641                ColorRange::Full
642            } else {
643                ColorRange::Limited
644            },
645        }
646    }
647
648    /// HDR content light level metadata, if present
649    pub fn content_light(&self) -> Option<ContentLightLevel> {
650        self.inner
651            .content_light
652            .as_ref()
653            .map(|arc| ContentLightLevel {
654                max_content_light_level: arc.max_content_light_level,
655                max_frame_average_light_level: arc.max_frame_average_light_level,
656            })
657    }
658
659    /// HDR mastering display metadata, if present
660    pub fn mastering_display(&self) -> Option<MasteringDisplay> {
661        self.inner
662            .mastering_display
663            .as_ref()
664            .map(|arc| MasteringDisplay {
665                primaries: arc.primaries,
666                white_point: arc.white_point,
667                max_luminance: arc.max_luminance,
668                min_luminance: arc.min_luminance,
669            })
670    }
671
672    /// Timestamp from input data (arbitrary units)
673    pub fn timestamp(&self) -> i64 {
674        self.inner.m.timestamp
675    }
676
677    /// Duration from input data (arbitrary units)
678    pub fn duration(&self) -> i64 {
679        self.inner.m.duration
680    }
681}
682
683/// Pixel layout (chroma subsampling)
684#[derive(Clone, Copy, Debug, PartialEq, Eq)]
685pub enum PixelLayout {
686    /// 4:0:0 (grayscale, no chroma)
687    I400,
688    /// 4:2:0 (most common, half-resolution chroma horizontally and vertically)
689    I420,
690    /// 4:2:2 (half horizontal chroma resolution)
691    I422,
692    /// 4:4:4 (full resolution chroma)
693    I444,
694}
695
696impl From<Rav1dPixelLayout> for PixelLayout {
697    fn from(layout: Rav1dPixelLayout) -> Self {
698        match layout {
699            Rav1dPixelLayout::I400 => Self::I400,
700            Rav1dPixelLayout::I420 => Self::I420,
701            Rav1dPixelLayout::I422 => Self::I422,
702            Rav1dPixelLayout::I444 => Self::I444,
703        }
704    }
705}
706
707/// Zero-copy access to pixel planes
708///
709/// Dispatches on bit depth for type safety.
710pub enum Planes<'a> {
711    Depth8(Planes8<'a>),
712    Depth16(Planes16<'a>),
713}
714
715/// 8-bit pixel plane accessor
716pub struct Planes8<'a> {
717    frame: &'a Frame,
718}
719
720impl<'a> Planes8<'a> {
721    /// Y (luma) plane as a 2D strided view
722    pub fn y(&self) -> PlaneView8<'a> {
723        let data = self
724            .frame
725            .inner
726            .data
727            .as_ref()
728            .expect("missing picture data");
729        let guard = data.data[0].slice::<BitDepth8, _>(..);
730
731        let stride = self.frame.inner.stride[0] as usize;
732        let buffer_height = guard.len().checked_div(stride).unwrap_or(0);
733        // Use frame's reported height, capped at buffer capacity
734        let height = (self.frame.height() as usize).min(buffer_height);
735
736        PlaneView8 {
737            guard,
738            stride,
739            width: self.frame.width() as usize,
740            height,
741        }
742    }
743
744    /// U (chroma) plane, if present (None for grayscale)
745    pub fn u(&self) -> Option<PlaneView8<'a>> {
746        if self.frame.pixel_layout() == PixelLayout::I400 {
747            return None;
748        }
749
750        let (w, h) = self.chroma_dimensions();
751        let data = self
752            .frame
753            .inner
754            .data
755            .as_ref()
756            .expect("missing picture data");
757        let guard = data.data[1].slice::<BitDepth8, _>(..);
758
759        let stride = self.frame.inner.stride[1] as usize;
760        let buffer_height = guard.len().checked_div(stride).unwrap_or(0);
761        let height = h.min(buffer_height);
762
763        Some(PlaneView8 {
764            guard,
765            stride,
766            width: w,
767            height,
768        })
769    }
770
771    /// V (chroma) plane, if present (None for grayscale)
772    pub fn v(&self) -> Option<PlaneView8<'a>> {
773        if self.frame.pixel_layout() == PixelLayout::I400 {
774            return None;
775        }
776
777        let (w, h) = self.chroma_dimensions();
778        let data = self
779            .frame
780            .inner
781            .data
782            .as_ref()
783            .expect("missing picture data");
784        let guard = data.data[2].slice::<BitDepth8, _>(..);
785
786        let stride = self.frame.inner.stride[1] as usize;
787        let buffer_height = guard.len().checked_div(stride).unwrap_or(0);
788        let height = h.min(buffer_height);
789
790        Some(PlaneView8 {
791            guard,
792            stride,
793            width: w,
794            height,
795        })
796    }
797
798    fn chroma_dimensions(&self) -> (usize, usize) {
799        let w = self.frame.width() as usize;
800        let h = self.frame.height() as usize;
801        match self.frame.pixel_layout() {
802            PixelLayout::I420 => (w.div_ceil(2), h.div_ceil(2)),
803            PixelLayout::I422 => (w.div_ceil(2), h),
804            PixelLayout::I444 => (w, h),
805            PixelLayout::I400 => (0, 0),
806        }
807    }
808}
809
810/// 10/12-bit pixel plane accessor
811pub struct Planes16<'a> {
812    frame: &'a Frame,
813}
814
815impl<'a> Planes16<'a> {
816    /// Y (luma) plane as a 2D strided view
817    pub fn y(&self) -> PlaneView16<'a> {
818        let data = self
819            .frame
820            .inner
821            .data
822            .as_ref()
823            .expect("missing picture data");
824        let guard = data.data[0].slice::<BitDepth16, _>(..);
825
826        // stride[0] is in bytes; divide by 2 for u16 element stride
827        let stride = self.frame.inner.stride[0] as usize / 2;
828        let buffer_height = guard.len().checked_div(stride).unwrap_or(0);
829        let height = (self.frame.height() as usize).min(buffer_height);
830
831        PlaneView16 {
832            guard,
833            stride,
834            width: self.frame.width() as usize,
835            height,
836        }
837    }
838
839    /// U (chroma) plane, if present
840    pub fn u(&self) -> Option<PlaneView16<'a>> {
841        if self.frame.pixel_layout() == PixelLayout::I400 {
842            return None;
843        }
844
845        let (w, h) = self.chroma_dimensions();
846        let data = self
847            .frame
848            .inner
849            .data
850            .as_ref()
851            .expect("missing picture data");
852        let guard = data.data[1].slice::<BitDepth16, _>(..);
853
854        // stride[1] is in bytes; divide by 2 for u16 element stride
855        let stride = self.frame.inner.stride[1] as usize / 2;
856        let buffer_height = guard.len().checked_div(stride).unwrap_or(0);
857        let height = h.min(buffer_height);
858
859        Some(PlaneView16 {
860            guard,
861            stride,
862            width: w,
863            height,
864        })
865    }
866
867    /// V (chroma) plane, if present
868    pub fn v(&self) -> Option<PlaneView16<'a>> {
869        if self.frame.pixel_layout() == PixelLayout::I400 {
870            return None;
871        }
872
873        let (w, h) = self.chroma_dimensions();
874        let data = self
875            .frame
876            .inner
877            .data
878            .as_ref()
879            .expect("missing picture data");
880        let guard = data.data[2].slice::<BitDepth16, _>(..);
881
882        // stride[1] is in bytes; divide by 2 for u16 element stride
883        let stride = self.frame.inner.stride[1] as usize / 2;
884        let buffer_height = guard.len().checked_div(stride).unwrap_or(0);
885        let height = h.min(buffer_height);
886
887        Some(PlaneView16 {
888            guard,
889            stride,
890            width: w,
891            height,
892        })
893    }
894
895    fn chroma_dimensions(&self) -> (usize, usize) {
896        let w = self.frame.width() as usize;
897        let h = self.frame.height() as usize;
898        match self.frame.pixel_layout() {
899            PixelLayout::I420 => (w.div_ceil(2), h.div_ceil(2)),
900            PixelLayout::I422 => (w.div_ceil(2), h),
901            PixelLayout::I444 => (w, h),
902            PixelLayout::I400 => (0, 0),
903        }
904    }
905}
906
907/// Zero-copy view of an 8-bit plane
908///
909/// Provides row-based and pixel-based access to decoded data.
910pub struct PlaneView8<'a> {
911    guard: DisjointImmutGuard<'a, Rav1dPictureDataComponentInner, [u8]>,
912    stride: usize,
913    width: usize,
914    height: usize,
915}
916
917impl<'a> PlaneView8<'a> {
918    /// Get a row by index (0-based)
919    ///
920    /// # Panics
921    ///
922    /// Panics if `y >= height`.
923    pub fn row(&self, y: usize) -> &[u8] {
924        assert!(
925            y < self.height,
926            "row index {} out of bounds (height: {})",
927            y,
928            self.height
929        );
930        let start = y * self.stride;
931        &self.guard[start..start + self.width]
932    }
933
934    /// Get a single pixel value
935    ///
936    /// # Panics
937    ///
938    /// Panics if coordinates are out of bounds.
939    pub fn pixel(&self, x: usize, y: usize) -> u8 {
940        assert!(
941            x < self.width && y < self.height,
942            "pixel coordinates ({}, {}) out of bounds ({}x{})",
943            x,
944            y,
945            self.width,
946            self.height
947        );
948        self.guard[y * self.stride + x]
949    }
950
951    /// Iterate over rows
952    pub fn rows(&'a self) -> impl Iterator<Item = &'a [u8]> + 'a {
953        (0..self.height).map(move |y| self.row(y))
954    }
955
956    /// Raw slice (includes padding, use stride for 2D indexing)
957    pub fn as_slice(&self) -> &[u8] {
958        self.guard.deref()
959    }
960
961    pub fn width(&self) -> usize {
962        self.width
963    }
964
965    pub fn height(&self) -> usize {
966        self.height
967    }
968
969    pub fn stride(&self) -> usize {
970        self.stride
971    }
972}
973
974/// Zero-copy view of a 10/12-bit plane
975pub struct PlaneView16<'a> {
976    guard: DisjointImmutGuard<'a, Rav1dPictureDataComponentInner, [u16]>,
977    stride: usize,
978    width: usize,
979    height: usize,
980}
981
982impl<'a> PlaneView16<'a> {
983    /// Get a row by index (0-based)
984    ///
985    /// # Panics
986    ///
987    /// Panics if `y >= height`.
988    pub fn row(&self, y: usize) -> &[u16] {
989        assert!(
990            y < self.height,
991            "row index {} out of bounds (height: {})",
992            y,
993            self.height
994        );
995        let start = y * self.stride;
996        &self.guard[start..start + self.width]
997    }
998
999    /// Get a single pixel value
1000    ///
1001    /// # Panics
1002    ///
1003    /// Panics if coordinates are out of bounds.
1004    pub fn pixel(&self, x: usize, y: usize) -> u16 {
1005        assert!(
1006            x < self.width && y < self.height,
1007            "pixel coordinates ({}, {}) out of bounds ({}x{})",
1008            x,
1009            y,
1010            self.width,
1011            self.height
1012        );
1013        self.guard[y * self.stride + x]
1014    }
1015
1016    /// Iterate over rows
1017    pub fn rows(&'a self) -> impl Iterator<Item = &'a [u16]> + 'a {
1018        (0..self.height).map(move |y| self.row(y))
1019    }
1020
1021    /// Raw slice (includes padding, use stride for 2D indexing)
1022    pub fn as_slice(&self) -> &[u16] {
1023        self.guard.deref()
1024    }
1025
1026    pub fn width(&self) -> usize {
1027        self.width
1028    }
1029
1030    pub fn height(&self) -> usize {
1031        self.height
1032    }
1033
1034    pub fn stride(&self) -> usize {
1035        self.stride
1036    }
1037}
1038
1039/// Color information
1040#[derive(Clone, Copy, Debug)]
1041pub struct ColorInfo {
1042    pub primaries: ColorPrimaries,
1043    pub transfer_characteristics: TransferCharacteristics,
1044    pub matrix_coefficients: MatrixCoefficients,
1045    pub color_range: ColorRange,
1046}
1047
1048/// Color primaries (CIE 1931 xy chromaticity coordinates)
1049#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1050#[repr(u8)]
1051pub enum ColorPrimaries {
1052    Unknown = 0,
1053    BT709 = 1,
1054    Unspecified = 2,
1055    BT470M = 4,
1056    BT470BG = 5,
1057    BT601 = 6,
1058    SMPTE240 = 7,
1059    Film = 8,
1060    BT2020 = 9,
1061    XYZ = 10,
1062    SMPTE431 = 11,
1063    SMPTE432 = 12,
1064    EBU3213 = 22,
1065}
1066
1067impl From<Rav1dColorPrimaries> for ColorPrimaries {
1068    fn from(pri: Rav1dColorPrimaries) -> Self {
1069        match pri.0 {
1070            1 => Self::BT709,
1071            4 => Self::BT470M,
1072            5 => Self::BT470BG,
1073            6 => Self::BT601,
1074            7 => Self::SMPTE240,
1075            8 => Self::Film,
1076            9 => Self::BT2020,
1077            10 => Self::XYZ,
1078            11 => Self::SMPTE431,
1079            12 => Self::SMPTE432,
1080            22 => Self::EBU3213,
1081            _ => Self::Unspecified,
1082        }
1083    }
1084}
1085
1086/// Transfer characteristics (EOTF / gamma curve)
1087#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1088#[repr(u8)]
1089pub enum TransferCharacteristics {
1090    Reserved = 0,
1091    BT709 = 1,
1092    Unspecified = 2,
1093    BT470M = 4,
1094    BT470BG = 5,
1095    BT601 = 6,
1096    SMPTE240 = 7,
1097    Linear = 8,
1098    Log100 = 9,
1099    Log100Sqrt10 = 10,
1100    IEC61966 = 11,
1101    BT1361 = 12,
1102    SRGB = 13,
1103    BT2020_10bit = 14,
1104    BT2020_12bit = 15,
1105    /// SMPTE 2084 - Perceptual Quantizer for HDR10
1106    SMPTE2084 = 16,
1107    SMPTE428 = 17,
1108    /// Hybrid Log-Gamma for HLG HDR
1109    HLG = 18,
1110}
1111
1112impl From<Rav1dTransferCharacteristics> for TransferCharacteristics {
1113    fn from(trc: Rav1dTransferCharacteristics) -> Self {
1114        match trc.0 {
1115            1 => Self::BT709,
1116            4 => Self::BT470M,
1117            5 => Self::BT470BG,
1118            6 => Self::BT601,
1119            7 => Self::SMPTE240,
1120            8 => Self::Linear,
1121            9 => Self::Log100,
1122            10 => Self::Log100Sqrt10,
1123            11 => Self::IEC61966,
1124            12 => Self::BT1361,
1125            13 => Self::SRGB,
1126            14 => Self::BT2020_10bit,
1127            15 => Self::BT2020_12bit,
1128            16 => Self::SMPTE2084,
1129            17 => Self::SMPTE428,
1130            18 => Self::HLG,
1131            _ => Self::Unspecified,
1132        }
1133    }
1134}
1135
1136/// Matrix coefficients (YUV to RGB conversion)
1137#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1138#[repr(u8)]
1139pub enum MatrixCoefficients {
1140    Identity = 0,
1141    BT709 = 1,
1142    Unspecified = 2,
1143    Reserved = 3,
1144    FCC = 4,
1145    BT470BG = 5,
1146    BT601 = 6,
1147    SMPTE240 = 7,
1148    YCgCo = 8,
1149    BT2020NCL = 9,
1150    BT2020CL = 10,
1151    SMPTE2085 = 11,
1152    ChromaDerivedNCL = 12,
1153    ChromaDerivedCL = 13,
1154    ICtCp = 14,
1155}
1156
1157impl From<Rav1dMatrixCoefficients> for MatrixCoefficients {
1158    fn from(mtrx: Rav1dMatrixCoefficients) -> Self {
1159        match mtrx.0 {
1160            0 => Self::Identity,
1161            1 => Self::BT709,
1162            4 => Self::FCC,
1163            5 => Self::BT470BG,
1164            6 => Self::BT601,
1165            7 => Self::SMPTE240,
1166            8 => Self::YCgCo,
1167            9 => Self::BT2020NCL,
1168            10 => Self::BT2020CL,
1169            11 => Self::SMPTE2085,
1170            12 => Self::ChromaDerivedNCL,
1171            13 => Self::ChromaDerivedCL,
1172            14 => Self::ICtCp,
1173            _ => Self::Unspecified,
1174        }
1175    }
1176}
1177
1178/// Color range
1179#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1180pub enum ColorRange {
1181    /// Limited/studio range (Y: 16-235, UV: 16-240 for 8-bit)
1182    Limited,
1183    /// Full range (0-255 for 8-bit, 0-1023 for 10-bit, 0-4095 for 12-bit)
1184    Full,
1185}
1186
1187/// HDR content light level (SMPTE 2086 / CTA-861.3)
1188#[derive(Clone, Copy, Debug)]
1189pub struct ContentLightLevel {
1190    /// Maximum content light level in cd/m² (nits)
1191    pub max_content_light_level: u16,
1192    /// Maximum frame-average light level in cd/m² (nits)
1193    pub max_frame_average_light_level: u16,
1194}
1195
1196/// HDR mastering display color volume (SMPTE 2086)
1197#[derive(Clone, Copy, Debug)]
1198pub struct MasteringDisplay {
1199    /// RGB primaries in 0.00002 increments \[R\], \[G\], \[B\]
1200    /// Each is [x, y] chromaticity coordinate
1201    pub primaries: [[u16; 2]; 3],
1202    /// White point [x, y] in 0.00002 increments
1203    pub white_point: [u16; 2],
1204    /// Maximum luminance in 0.0001 cd/m² increments
1205    pub max_luminance: u32,
1206    /// Minimum luminance in 0.0001 cd/m² increments
1207    pub min_luminance: u32,
1208}
1209
1210impl MasteringDisplay {
1211    /// Get max luminance in nits (cd/m²)
1212    pub fn max_luminance_nits(&self) -> f64 {
1213        self.max_luminance as f64 / 10000.0
1214    }
1215
1216    /// Get min luminance in nits (cd/m²)
1217    pub fn min_luminance_nits(&self) -> f64 {
1218        self.min_luminance as f64 / 10000.0
1219    }
1220
1221    /// Get primary chromaticity as normalized floats [0.0, 1.0]
1222    ///
1223    /// `index` should be 0 (red), 1 (green), or 2 (blue).
1224    pub fn primary_chromaticity(&self, index: usize) -> [f64; 2] {
1225        assert!(index < 3, "primary index must be 0-2");
1226        [
1227            self.primaries[index][0] as f64 / 50000.0,
1228            self.primaries[index][1] as f64 / 50000.0,
1229        ]
1230    }
1231
1232    /// Get white point as normalized floats [0.0, 1.0]
1233    pub fn white_point_chromaticity(&self) -> [f64; 2] {
1234        [
1235            self.white_point[0] as f64 / 50000.0,
1236            self.white_point[1] as f64 / 50000.0,
1237        ]
1238    }
1239}
1240
1241/// Returns a comma-delimited string of enabled compile-time feature flags.
1242///
1243/// Useful for runtime verification of which safety level and capabilities
1244/// were compiled in.
1245///
1246/// ```
1247/// let features = rav1d_safe::enabled_features();
1248/// assert!(features.contains("bitdepth_8"));
1249/// ```
1250pub fn enabled_features() -> String {
1251    let mut features = Vec::new();
1252
1253    if cfg!(feature = "asm") {
1254        features.push("asm");
1255    }
1256    if cfg!(feature = "partial_asm") {
1257        features.push("partial_asm");
1258    }
1259    if cfg!(feature = "c-ffi") {
1260        features.push("c-ffi");
1261    }
1262    if cfg!(feature = "unchecked") {
1263        features.push("unchecked");
1264    }
1265    if cfg!(feature = "bitdepth_8") {
1266        features.push("bitdepth_8");
1267    }
1268    if cfg!(feature = "bitdepth_16") {
1269        features.push("bitdepth_16");
1270    }
1271
1272    // Safety level summary
1273    if cfg!(feature = "asm") {
1274        features.push("safety:asm");
1275    } else if cfg!(feature = "partial_asm") {
1276        features.push("safety:partial-asm");
1277    } else if cfg!(feature = "c-ffi") {
1278        features.push("safety:c-ffi");
1279    } else if cfg!(feature = "unchecked") {
1280        features.push("safety:unchecked");
1281    } else {
1282        features.push("safety:forbid-unsafe");
1283    }
1284
1285    features.join(", ")
1286}