Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- *(hwdec)* VAAPI hardware decode backend (linux-gnu): HEVC Main/Main10 and
AV1 Profile 0 still pictures to NV12/P010, libva dlopen'd at runtime —
absence of libva or a render node degrades to `decoder() == None`, never a
link failure. NVIDIA is covered via the `nvidia-vaapi-driver` translation
layer (see `docs/SUPPORT.md`). HEIC pixel decode in `rawshift-image` now
works end-to-end on hardware through this backend.

## [0.1.1](https://github.com/justin13888/rawshift/compare/v0.1.0...v0.1.1) - 2026-05-29

### Added
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 11 additions & 4 deletions crates/rawshift-hwdec/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ rustdoc-args = ["--cfg", "docsrs"]
gamut-color = { workspace = true }
thiserror = { workspace = true }

# VAAPI is dlopen'd at runtime (never linked): libloading is the mature safe
# dlopen wrapper. Target-gated so non-Linux builds never see it; the backend
# module that uses it only compiles under cfg(hwdec_backend = "vaapi").
[target.'cfg(target_os = "linux")'.dependencies]
libloading = "0.8"

[features]
# Verified backend feature flags — see docs/SUPPORT.md for the permanent
# target/API matrix. Each explicit backend flag hard-fails the compile
Expand All @@ -33,10 +39,11 @@ thiserror = { workspace = true }
# (windows-msvc, linux-musl, wasm) emits a build-script warning and compiles
# the no-backend stub.
#
# This crate currently ships only the stub + the compile boundaries: the
# platform backends land as separate issues (VideoToolbox / VAAPI #29 /
# MediaCodec). Until then `decoder()` returns `None`, `backend()` returns
# `None`, and `available_codecs()` is empty on every target.
# Backends implemented: VAAPI (linux-gnu; dlopen'd libva, HEVC Main/Main10 +
# AV1 Profile 0 still pictures). VideoToolbox / MediaCodec land as separate
# issues; builds without a selected backend compile the no-backend stub —
# `decoder()` returns `None`, `backend()` returns `None`, and
# `available_codecs()` is empty.
videotoolbox = []
vaapi = []
mediacodec = []
Expand Down
37 changes: 32 additions & 5 deletions crates/rawshift-hwdec/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,43 @@ MediaCodec (Android).

This is the **only** crate in the workspace where platform FFI may live
(`#![deny(unsafe_op_in_unsafe_fn)]`, safe public items, documented invariants
on every unsafe block). The platform backends land as separate issues; until
one does, the crate compiles a no-backend stub: `decoder()` returns `None`,
`backend()` returns `None`, `available_codecs()` is empty, and dependants
surface `HwDecoderUnavailable`.
on every unsafe block). The **VAAPI backend is implemented**; VideoToolbox
and MediaCodec land as separate issues. On builds/targets with no backend the
crate compiles a no-backend stub: `decoder()` returns `None`, `backend()`
returns `None`, `available_codecs()` is empty, and dependants surface
`HwDecoderUnavailable`.

## VAAPI backend (linux-gnu)

libva is **dlopen'd at runtime** (`libva.so.2` + `libva-drm.so.2` via
`libloading`) — nothing links against it, so a machine without libva, a
`/dev/dri/renderD*` node, or driver support simply reports "no decoder"
instead of failing to start (headless/CI safe). The probe answers from the
driver's real `vaQueryConfigProfiles`/`vaQueryConfigEntrypoints` lists.

Still-picture scope:

| Codec | Profiles | Output |
| --- | --- | --- |
| HEVC (HEIC) | Main, Main 10 — IRAP intra, 4:2:0/monochrome, tiles/WPP OK; scaling lists and range/SCC extensions are rejected with a clear error | NV12 (8-bit), P010 (10-bit) |
| AV1 (AVIF) | Profile 0 (Main) — one intra frame, any tiling, film grain OK; profiles 1/2 and large-scale tile rejected | NV12 (8-bit), P010 (10-bit) |

### GPU vendors — including NVIDIA

VAAPI covers **Intel** (media-driver / i965) and **AMD** (Mesa radeonsi)
natively. **NVIDIA GPUs are supported through the maintained
[`nvidia-vaapi-driver`](https://github.com/elFarto/nvidia-vaapi-driver)
translation layer over NVDEC** — install it (and set
`NVD_BACKEND`/`LIBVA_DRIVER_NAME` per its README if needed) and this backend
picks it up through the same dlopen path with no rawshift changes. This is
why rawshift has no separate NVDEC backend; see the permanent matrix and
justification in [`docs/SUPPORT.md`](../../docs/SUPPORT.md).

## Feature flags (verified)

| Feature | Meaning |
| --- | --- |
| `hw` | Portable: select the native backend for the compile target (build-script warning + stub on targets with no hardware decode API). |
| `hw` | Portable: select the native backend for the compile target (VAAPI on linux-gnu; build-script warning + stub on targets with no hardware decode API). |
| `videotoolbox` | Pin VideoToolbox; `compile_error!` on non-Apple targets. |
| `vaapi` | Pin VAAPI; `compile_error!` off linux-gnu. |
| `mediacodec` | Pin MediaCodec; `compile_error!` off Android. |
93 changes: 73 additions & 20 deletions crates/rawshift-hwdec/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,16 @@
//!
//! Per `PRINCIPLES.md`, **all** platform FFI for hardware decode lives in this
//! crate and nowhere else: `#![deny(unsafe_op_in_unsafe_fn)]`, every public
//! item is safe, and every future `unsafe` block must document its invariants
//! inside the platform backend module that owns it. This revision ships **no
//! platform code at all** (and therefore no `unsafe`): the backends land as
//! separate issues (VAAPI is next), and until one lands every entry point
//! reports "no decoder" — [`decoder`] returns `None`, [`backend`] returns
//! `None`, and [`available_codecs`] is empty.
//! item is safe, and every `unsafe` block documents its invariants inside the
//! platform backend module that owns it. The **VAAPI backend** (linux-gnu,
//! dlopen'd libva — see the `vaapi` module) is implemented; VideoToolbox and
//! MediaCodec land as separate issues. On targets/builds with no backend
//! every entry point reports "no decoder" — [`decoder`] returns `None`,
//! [`backend`] returns `None`, and [`available_codecs`] is empty.
//!
//! On NVIDIA GPUs the VAAPI backend works through the maintained
//! [`nvidia-vaapi-driver`](https://github.com/elFarto/nvidia-vaapi-driver)
//! translation layer over NVDEC (see `docs/SUPPORT.md`).
//!
//! ## Verified feature flags
//!
Expand Down Expand Up @@ -44,6 +48,11 @@

#![deny(unsafe_op_in_unsafe_fn)]

// The VAAPI platform backend: compiled only when build.rs selected it
// (`vaapi` explicit flag, or `hw` on a linux-gnu target).
#[cfg(hwdec_backend = "vaapi")]
mod vaapi;

// ── Verified feature boundaries ─────────────────────────────────────────────
// Explicit backend flags are hard errors on targets whose platform API does
// not exist — a mis-pinned build must fail at compile time, not degrade
Expand Down Expand Up @@ -461,57 +470,101 @@ pub enum HwDecodeError {
},
}

// ── Backend discovery (stub until the platform backends land) ───────────────
// ── Backend discovery ───────────────────────────────────────────────────────

/// Returns a decoder for `codec`, or `None` when no compiled-in backend can
/// decode it at runtime.
///
/// No platform backend is implemented yet (they land as separate issues;
/// VAAPI is next), so this currently returns `None` on every target and
/// feature combination.
/// With the VAAPI backend compiled in (`vaapi`, or `hw` on linux-gnu), this
/// dlopens libva on first use and answers from the driver's actual
/// profile/entrypoint list; missing libraries, render nodes, or driver
/// support all degrade to `None` (never a link or startup failure).
/// VideoToolbox and MediaCodec land as separate issues; without a backend
/// this returns `None` everywhere.
#[must_use]
pub fn decoder(codec: HwCodec) -> Option<Box<dyn HwStillDecoder>> {
let _ = codec;
None
#[cfg(hwdec_backend = "vaapi")]
{
vaapi::decoder(codec)
}
#[cfg(not(hwdec_backend = "vaapi"))]
{
let _ = codec;
None
}
}

/// The platform backend compiled into this build and usable at runtime, or
/// `None`.
///
/// No platform backend is implemented yet, so this currently returns `None`
/// everywhere; once a backend lands it reports `Some` only when the runtime
/// probe succeeds (e.g. VAAPI's dlopen finding a usable driver).
/// Reports `Some` only when the runtime probe succeeds (e.g. VAAPI's dlopen
/// finding a driver with at least one supported codec at the VLD entry
/// point).
#[must_use]
pub fn backend() -> Option<HwBackend> {
None
#[cfg(hwdec_backend = "vaapi")]
{
vaapi::backend()
}
#[cfg(not(hwdec_backend = "vaapi"))]
{
None
}
}

/// The codecs [`decoder`] can currently return a decoder for.
/// The codecs [`decoder`] can currently return a decoder for, per the
/// runtime probe (e.g. `vaQueryConfigProfiles` for VAAPI).
///
/// Empty until a platform backend lands.
/// Empty when no backend is compiled in or usable.
#[must_use]
pub fn available_codecs() -> &'static [HwCodec] {
&[]
#[cfg(hwdec_backend = "vaapi")]
{
vaapi::available_codecs()
}
#[cfg(not(hwdec_backend = "vaapi"))]
{
&[]
}
}

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

// ── stub behaviour ──────────────────────────────────────────────────────
// ── stub behaviour (builds with no selected backend) ────────────────────

#[cfg(not(hwdec_backend = "vaapi"))]
#[test]
fn stub_has_no_decoder_for_any_codec() {
assert!(decoder(HwCodec::Hevc).is_none());
assert!(decoder(HwCodec::Av1).is_none());
}

#[cfg(not(hwdec_backend = "vaapi"))]
#[test]
fn stub_reports_no_backend_and_no_codecs() {
assert_eq!(backend(), None);
assert!(available_codecs().is_empty());
}

// ── backend discovery consistency (any build) ───────────────────────────

/// `decoder()`, `backend()`, and `available_codecs()` must agree with
/// each other whether or not a platform backend/driver is present.
#[test]
fn discovery_entry_points_are_consistent() {
let codecs = available_codecs();
assert_eq!(backend().is_some(), !codecs.is_empty());
for codec in [HwCodec::Hevc, HwCodec::Av1] {
assert_eq!(
decoder(codec).is_some(),
codecs.contains(&codec),
"decoder({codec}) must match available_codecs()"
);
}
}

// ── request construction ────────────────────────────────────────────────

#[test]
Expand Down
Loading