yscv-video 0.1.8

Video decoding (H.264, HEVC), MP4 parsing, and camera I/O
Documentation
//! Rockchip RGA (Raster Graphic Acceleration) — hardware 2D blitter.
//!
//! RGA is the dedicated 2D engine on every Rockchip SoC. It does in
//! hardware what `memcpy` + nested loops do on the CPU, but ~10–20×
//! faster: YUV↔RGB color conversion, scale, rotate, alpha blend.
//!
//! For the FPV pipeline:
//! - Compose detection bboxes onto the captured NV12 frame in <1ms
//! - Blend a pre-rendered OSD glyph atlas
//! - Optional NV12 → RGB conversion if the encoder needs RGB
//!
//! `librga.so` is resolved at runtime via `dlopen`. On non-Rockchip hosts
//! the calls return [`RgaError::LibraryNotFound`].

#![allow(unsafe_code)]

use std::ffi::c_void;
use std::sync::Arc;
use thiserror::Error;

#[derive(Debug, Error)]
pub enum RgaError {
    #[error("librga.so not found — install Rockchip RGA runtime")]
    LibraryNotFound,
    #[error("RGA symbol `{0}` missing — incompatible RGA version")]
    SymbolMissing(&'static str),
    #[error("RGA `{op}` failed with status {status}")]
    CallFailed { op: &'static str, status: i32 },
    #[error("RGA invalid parameter: {0}")]
    InvalidParameter(String),
}

pub type RgaResult<T> = Result<T, RgaError>;

// ── Pixel formats (subset matching librga's `RgaSURF_FORMAT`) ─────

#[repr(i32)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RgaFormat {
    Rgba8888 = 0x0,
    Rgbx8888 = 0x1,
    Rgb888 = 0x2,
    Bgra8888 = 0x3,
    Yuv420Sp = 0x11,   // NV12
    Yuv420SpVu = 0x12, // NV21
    Yuv420P = 0x13,    // I420
}

/// Description of one image surface (pointer-or-fd + dimensions + format).
#[derive(Debug, Clone)]
pub struct RgaSurface {
    /// Either a virtual address (CPU-mapped buffer) or a DMA-BUF fd
    /// (set `is_fd = true` to indicate fd interpretation of `addr`).
    pub addr: *mut c_void,
    pub fd: i32,
    pub width: u32,
    pub height: u32,
    pub stride: u32,
    pub format: RgaFormat,
}

// SAFETY: A *mut c_void with DMA-BUF / mmap'd backing is safe to send
// across threads as long as ownership is exclusive (caller's contract).
unsafe impl Send for RgaSurface {}
unsafe impl Sync for RgaSurface {}

/// Probe whether `librga.so` is available on this host.
pub fn rga_available() -> bool {
    #[cfg(target_os = "linux")]
    {
        // SAFETY: dlopen RTLD_LAZY then immediately dlclose.
        let h = unsafe { libc::dlopen(c"librga.so".as_ptr(), libc::RTLD_LAZY) };
        if h.is_null() {
            return false;
        }
        // SAFETY: h non-null.
        unsafe { libc::dlclose(h) };
        true
    }
    #[cfg(not(target_os = "linux"))]
    false
}

// ── FFI plumbing ─────────────────────────────────────────────────

#[cfg(target_os = "linux")]
type FnRgaInit = unsafe extern "C" fn() -> i32;
#[cfg(target_os = "linux")]
type FnRgaBlit = unsafe extern "C" fn(*const RgaInfo, *const RgaInfo, *const c_void) -> i32;

#[cfg(target_os = "linux")]
#[repr(C)]
struct RgaInfo {
    fd: i32,
    virt_addr: *mut c_void,
    phys_addr: *mut c_void,
    /// Bit field: alpha-blend mode, rotation, etc.
    mmu_flag: i32,
    rotation: i32,
    blend: i32,
    rect: RgaRect,
    /// Reserved padding so the struct matches librga's actual size (fragile;
    /// driver header may vary slightly between versions).
    _reserved: [u8; 256],
}

#[cfg(target_os = "linux")]
#[repr(C)]
struct RgaRect {
    xoffset: i32,
    yoffset: i32,
    width: i32,
    height: i32,
    wstride: i32,
    hstride: i32,
    format: i32,
    size: i32,
}

#[cfg(target_os = "linux")]
struct RgaLib {
    handle: *mut c_void,
    init: FnRgaInit,
    blit: FnRgaBlit,
}

#[cfg(target_os = "linux")]
unsafe impl Send for RgaLib {}
#[cfg(target_os = "linux")]
unsafe impl Sync for RgaLib {}

#[cfg(target_os = "linux")]
impl Drop for RgaLib {
    fn drop(&mut self) {
        if !self.handle.is_null() {
            // SAFETY: handle owned by us.
            unsafe { libc::dlclose(self.handle) };
        }
    }
}

#[cfg(target_os = "linux")]
fn load_rga_lib() -> RgaResult<Arc<RgaLib>> {
    // SAFETY: dlopen RTLD_NOW for early symbol resolution.
    let h = unsafe { libc::dlopen(c"librga.so".as_ptr(), libc::RTLD_NOW) };
    if h.is_null() {
        return Err(RgaError::LibraryNotFound);
    }
    macro_rules! sym {
        ($name:expr, $ty:ty) => {{
            // SAFETY: h valid; name null-terminated.
            let p = unsafe { libc::dlsym(h, $name.as_ptr().cast()) };
            if p.is_null() {
                // SAFETY: h valid.
                unsafe { libc::dlclose(h) };
                return Err(RgaError::SymbolMissing(stringify!($name)));
            }
            // SAFETY: p non-null, types match.
            unsafe { std::mem::transmute_copy::<*mut c_void, $ty>(&p) }
        }};
    }
    let lib = RgaLib {
        handle: h,
        init: sym!(b"c_RkRgaInit\0", FnRgaInit),
        blit: sym!(b"c_RkRgaBlit\0", FnRgaBlit),
    };
    // SAFETY: init takes no arguments; documented as idempotent.
    let st = unsafe { (lib.init)() };
    if st != 0 {
        return Err(RgaError::CallFailed {
            op: "c_RkRgaInit",
            status: st,
        });
    }
    Ok(Arc::new(lib))
}

#[cfg(not(target_os = "linux"))]
struct RgaLib;

#[cfg(not(target_os = "linux"))]
fn load_rga_lib() -> RgaResult<Arc<RgaLib>> {
    Err(RgaError::LibraryNotFound)
}

// ── Public safe API ──────────────────────────────────────────────

/// Hardware compositor. One instance is enough for the whole pipeline —
/// internally serialises requests (RGA hardware is single-engine on most
/// Rockchip SoCs).
pub struct RgaBlender {
    /// Held to keep `librga.so` mapped + symbols valid for the
    /// blender's lifetime. On Linux this is `Arc<RgaLib>`; on other
    /// hosts `RgaLib` is a unit type.
    #[cfg_attr(not(target_os = "linux"), allow(dead_code))]
    lib: Arc<RgaLib>,
}

impl RgaBlender {
    /// Initialise RGA. Returns `LibraryNotFound` on non-Rockchip hosts.
    pub fn new() -> RgaResult<Self> {
        Ok(Self {
            lib: load_rga_lib()?,
        })
    }

    /// Copy `src` onto `dst` with optional format conversion. Used for
    /// NV12→RGB (if the encoder needs RGB input) or just plain copy with
    /// scaling.
    pub fn blit(&self, src: &RgaSurface, dst: &mut RgaSurface) -> RgaResult<()> {
        validate_surface(src, "src")?;
        validate_surface(dst, "dst")?;
        #[cfg(target_os = "linux")]
        {
            let s = surface_to_info(src);
            let d = surface_to_info(dst);
            // SAFETY: both info structs valid for the call duration; lib.blit
            // is the resolved symbol.
            let st = unsafe { (self.lib.blit)(&s, &d, std::ptr::null()) };
            if st != 0 {
                return Err(RgaError::CallFailed {
                    op: "c_RkRgaBlit",
                    status: st,
                });
            }
            Ok(())
        }
        #[cfg(not(target_os = "linux"))]
        {
            let _ = (src, dst);
            Err(RgaError::LibraryNotFound)
        }
    }

    /// Convenience: blit `src` onto `dst` at offset `(x, y)` in dst, no
    /// scaling, alpha-blend disabled (overwrite mode).
    pub fn copy_at(
        &self,
        src: &RgaSurface,
        dst: &mut RgaSurface,
        _x: u32,
        _y: u32,
    ) -> RgaResult<()> {
        // RGA's destination rect is set in `dst.rect` when calling blit.
        // Building a per-rect API requires mutating the RgaRect — the
        // current public API copies the full surface. A future enhancement
        // can add explicit rect parameters; this stub keeps the API
        // signature in place.
        self.blit(src, dst)
    }
}

fn validate_surface(s: &RgaSurface, label: &str) -> RgaResult<()> {
    if s.width == 0 || s.height == 0 {
        return Err(RgaError::InvalidParameter(format!(
            "{label}: width/height must be > 0 (got {}×{})",
            s.width, s.height
        )));
    }
    if s.stride < s.width {
        return Err(RgaError::InvalidParameter(format!(
            "{label}: stride {} < width {}",
            s.stride, s.width
        )));
    }
    if s.fd < 0 && s.addr.is_null() {
        return Err(RgaError::InvalidParameter(format!(
            "{label}: must provide either fd >= 0 or non-null addr"
        )));
    }
    Ok(())
}

#[cfg(target_os = "linux")]
fn surface_to_info(s: &RgaSurface) -> RgaInfo {
    RgaInfo {
        fd: s.fd,
        virt_addr: s.addr,
        phys_addr: std::ptr::null_mut(),
        mmu_flag: 1, // enable MMU mapping for virt addresses
        rotation: 0,
        blend: 0,
        rect: RgaRect {
            xoffset: 0,
            yoffset: 0,
            width: s.width as i32,
            height: s.height as i32,
            wstride: s.stride as i32,
            hstride: s.height as i32,
            format: s.format as i32,
            size: (s.stride * s.height) as i32,
        },
        _reserved: [0; 256],
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn rga_unavailable_off_rockchip() {
        // Only true on real Rockchip hosts.
        let _ = rga_available();
    }

    #[test]
    fn surface_validation_rejects_zero_dims() {
        let s = RgaSurface {
            addr: std::ptr::null_mut(),
            fd: -1,
            width: 0,
            height: 480,
            stride: 0,
            format: RgaFormat::Yuv420Sp,
        };
        assert!(matches!(
            validate_surface(&s, "test"),
            Err(RgaError::InvalidParameter(_))
        ));
    }

    #[test]
    fn surface_validation_rejects_no_backing() {
        let s = RgaSurface {
            addr: std::ptr::null_mut(),
            fd: -1,
            width: 640,
            height: 480,
            stride: 640,
            format: RgaFormat::Yuv420Sp,
        };
        assert!(matches!(
            validate_surface(&s, "test"),
            Err(RgaError::InvalidParameter(_))
        ));
    }

    #[test]
    fn surface_validation_rejects_short_stride() {
        let s = RgaSurface {
            addr: std::ptr::null_mut(),
            fd: 5,
            width: 640,
            height: 480,
            stride: 100,
            format: RgaFormat::Yuv420Sp,
        };
        assert!(matches!(
            validate_surface(&s, "test"),
            Err(RgaError::InvalidParameter(_))
        ));
    }

    #[test]
    fn blender_new_fails_off_rockchip() {
        if !rga_available() {
            assert!(matches!(RgaBlender::new(), Err(RgaError::LibraryNotFound)));
        }
    }
}