Skip to main content

vpl_sys/
dispatcher.rs

1//! MVP dispatcher: load a driver-shipped oneVPL implementation at runtime.
2//!
3//! Resolves the ~10 entry points this crate's Stage 1 CPU-upload H.264
4//! encode path needs, via `libloading`'s `GetProcAddress`/`dlsym`.
5//!
6//! This is a **deliberately reduced reimplementation** of Intel's own
7//! dispatcher (`MFXLoad`/`MFXCreateConfig`/`MFXEnumImplementations`/
8//! `MFXCreateSession`, exported by `libvpl.dll` on this machine) — see
9//! `mediaway-encoder-quicksync/adr/0001-onevpl-quicksync-encode-surface.md`
10//! for why: no vendored/built Intel C dispatcher, no import-lib linking.
11//! **First working Intel GPU implementation wins**; multi-adapter selection,
12//! full capability filtering, and CPU-implementation fallback are out of
13//! scope. Do not assume official-dispatcher parity (env-var config files,
14//! versioned implementation ranking, …).
15//!
16//! Verified on this workspace's Windows dev box (Intel UHD 770,
17//! `iigd_dch.inf` driver package): the real implementation library
18//! (`libmfxhw64.dll`) is present directly under `%SystemRoot%\System32` and
19//! exports `MFXInit`/`MFXInitEx`/`MFXClose`/`MFXQueryIMPL`/`MFXQueryVersion`/
20//! `MFXVideoCORE_*`/`MFXVideoENCODE_*` directly (confirmed via
21//! `llvm-readobj --coff-exports`, not assumed) — exactly the "runtime library
22//! itself directly exports the MFX* entry points" shape the ADR's license
23//! research documented from Intel's own docs.
24
25#![allow(unsafe_code)]
26
27use std::env;
28use std::ffi::c_void;
29use std::path::PathBuf;
30
31use libloading::Library;
32use thiserror::Error;
33
34use crate::consts::{MFX_ERR_NONE, mfx_succeeded};
35use crate::raw::{
36    mfxBitstream, mfxEncodeCtrl, mfxFrameSurface1, mfxHandleType, mfxIMPL, mfxInitParam,
37    mfxSession, mfxStatus, mfxSyncPoint, mfxVersion, mfxVideoParam,
38};
39
40/// Errors opening the MVP dispatcher or a oneVPL session on it.
41#[derive(Debug, Error)]
42#[non_exhaustive]
43pub enum VplError {
44    /// No candidate oneVPL implementation library loaded (see
45    /// [`Loader::open`]'s search order).
46    #[error("no oneVPL implementation library found (tried: {tried})")]
47    NotFound {
48        /// Candidate paths/names tried, joined with `", "`, for diagnostics.
49        tried: String,
50    },
51    /// A required entry point was missing from an otherwise-loaded library —
52    /// loaded the wrong DLL, or a genuinely incompatible/older runtime.
53    #[error("required oneVPL entry point {symbol} missing from the loaded library")]
54    MissingSymbol {
55        /// The C symbol name that failed to resolve.
56        symbol: &'static str,
57    },
58    /// A oneVPL call returned a negative (error) `mfxStatus`.
59    #[error("oneVPL call {call} failed: mfxStatus {status}")]
60    Status {
61        /// Name of the failing `MFX*` call, for diagnostics.
62        call: &'static str,
63        /// The raw `mfxStatus` value returned.
64        status: mfxStatus,
65    },
66}
67
68type PfnMfxInitEx = unsafe extern "C" fn(par: mfxInitParam, session: *mut mfxSession) -> mfxStatus;
69type PfnMfxClose = unsafe extern "C" fn(session: mfxSession) -> mfxStatus;
70type PfnMfxQueryVersion =
71    unsafe extern "C" fn(session: mfxSession, version: *mut mfxVersion) -> mfxStatus;
72type PfnMfxQueryImpl = unsafe extern "C" fn(session: mfxSession, r#impl: *mut mfxIMPL) -> mfxStatus;
73type PfnMfxVideoEncodeQuery = unsafe extern "C" fn(
74    session: mfxSession,
75    r#in: *mut mfxVideoParam,
76    out: *mut mfxVideoParam,
77) -> mfxStatus;
78type PfnMfxVideoEncodeInit =
79    unsafe extern "C" fn(session: mfxSession, par: *mut mfxVideoParam) -> mfxStatus;
80type PfnMfxVideoEncodeClose = unsafe extern "C" fn(session: mfxSession) -> mfxStatus;
81type PfnMfxVideoEncodeEncodeFrameAsync = unsafe extern "C" fn(
82    session: mfxSession,
83    ctrl: *mut mfxEncodeCtrl,
84    surface: *mut mfxFrameSurface1,
85    bs: *mut mfxBitstream,
86    syncp: *mut mfxSyncPoint,
87) -> mfxStatus;
88type PfnMfxVideoCoreSyncOperation =
89    unsafe extern "C" fn(session: mfxSession, syncp: mfxSyncPoint, wait: u32) -> mfxStatus;
90type PfnMfxVideoCoreSetHandle = unsafe extern "C" fn(
91    session: mfxSession,
92    handle_type: mfxHandleType,
93    hdl: *mut c_void,
94) -> mfxStatus;
95
96/// A loaded oneVPL implementation library plus its resolved Stage 1 entry points.
97///
98/// Owns the `libloading::Library` (kept mapped for as long as any [`Session`]
99/// built from it is alive — every fn pointer field is only ever called
100/// through `&self`/`&Session` methods, never after both are dropped).
101pub struct Loader {
102    _lib: Library,
103    fn_init_ex: PfnMfxInitEx,
104    fn_close: PfnMfxClose,
105    fn_query_version: PfnMfxQueryVersion,
106    fn_query_impl: PfnMfxQueryImpl,
107    fn_encode_query: PfnMfxVideoEncodeQuery,
108    fn_encode_init: PfnMfxVideoEncodeInit,
109    fn_encode_close: PfnMfxVideoEncodeClose,
110    fn_encode_frame_async: PfnMfxVideoEncodeEncodeFrameAsync,
111    fn_core_sync_operation: PfnMfxVideoCoreSyncOperation,
112    #[allow(
113        dead_code,
114        reason = "Stage 1 is CPU-upload only; wired for the D3D11 ZC follow-up"
115    )]
116    fn_core_set_handle: PfnMfxVideoCoreSetHandle,
117}
118
119/// Default search list: on Windows, `libmfxhw64.dll` is the real Intel GPU
120/// implementation library shipped inside the graphics driver package
121/// (confirmed present directly under `%SystemRoot%\System32` on this
122/// workspace's Intel UHD 770 dev box). Bare names are resolved by the OS
123/// loader's standard search order (which includes `System32`), so no full
124/// path is required in the common case.
125#[cfg(windows)]
126const DEFAULT_CANDIDATES: &[&str] = &["libmfxhw64.dll"];
127
128#[cfg(not(windows))]
129const DEFAULT_CANDIDATES: &[&str] = &[];
130
131impl Loader {
132    /// Open the first working implementation library: `ONEVPL_SEARCH_PATH`
133    /// (if set, a single directory) is tried first for each candidate name,
134    /// then each candidate's bare name (OS default search order). Returns
135    /// [`VplError::NotFound`] if nothing loads, or
136    /// [`VplError::MissingSymbol`] if a candidate loads but is missing one of
137    /// this crate's required entry points (wrong/incompatible DLL).
138    ///
139    /// # Errors
140    ///
141    /// See variants above.
142    pub fn open() -> Result<Self, VplError> {
143        let mut tried = Vec::new();
144        let search_dir = env::var_os("ONEVPL_SEARCH_PATH").map(PathBuf::from);
145
146        for candidate in DEFAULT_CANDIDATES {
147            if let Some(dir) = &search_dir {
148                let full = dir.join(candidate);
149                tried.push(full.display().to_string());
150                // SAFETY: `Library::new` maps a PE image at a caller-supplied
151                // path; oneVPL implementation DLLs run arbitrary module-init
152                // code like any other native library, an accepted risk for
153                // this crate's whole reason to exist. No symbols are used
154                // before `resolve` validates every one this crate needs.
155                if let Ok(lib) = unsafe { Library::new(&full) } {
156                    return Self::resolve(lib);
157                }
158            }
159            tried.push((*candidate).to_string());
160            // SAFETY: same as above, bare name resolved via the OS loader's
161            // standard search order (includes `System32` on Windows, where
162            // this crate's default candidate is confirmed present).
163            if let Ok(lib) = unsafe { Library::new(candidate) } {
164                return Self::resolve(lib);
165            }
166        }
167
168        Err(VplError::NotFound {
169            tried: tried.join(", "),
170        })
171    }
172
173    fn resolve(lib: Library) -> Result<Self, VplError> {
174        macro_rules! sym {
175            ($name:literal) => {{
176                // SAFETY: `$name` is a NUL-terminated C symbol name; the
177                // resulting function pointer is only ever called with
178                // arguments matching the real oneVPL C signature transcribed
179                // in this module's `Pfn*` typedefs (verified against the
180                // vendored headers — see `vendor/README.md`), and only for as
181                // long as `self._lib` (this same `Library`) stays alive.
182                match unsafe { lib.get(concat!($name, "\0").as_bytes()) } {
183                    Ok(sym) => *sym,
184                    Err(_) => {
185                        return Err(VplError::MissingSymbol { symbol: $name });
186                    }
187                }
188            }};
189        }
190
191        let fn_init_ex = sym!("MFXInitEx");
192        let fn_close = sym!("MFXClose");
193        let fn_query_version = sym!("MFXQueryVersion");
194        let fn_query_impl = sym!("MFXQueryIMPL");
195        let fn_encode_query = sym!("MFXVideoENCODE_Query");
196        let fn_encode_init = sym!("MFXVideoENCODE_Init");
197        let fn_encode_close = sym!("MFXVideoENCODE_Close");
198        let fn_encode_frame_async = sym!("MFXVideoENCODE_EncodeFrameAsync");
199        let fn_core_sync_operation = sym!("MFXVideoCORE_SyncOperation");
200        let fn_core_set_handle = sym!("MFXVideoCORE_SetHandle");
201
202        Ok(Self {
203            _lib: lib,
204            fn_init_ex,
205            fn_close,
206            fn_query_version,
207            fn_query_impl,
208            fn_encode_query,
209            fn_encode_init,
210            fn_encode_close,
211            fn_encode_frame_async,
212            fn_core_sync_operation,
213            fn_core_set_handle,
214        })
215    }
216
217    /// `MFXInitEx` — create a session against `impl_hint` (typically
218    /// [`crate::consts::MFX_IMPL_HARDWARE`]). Consumes `self`: a [`Session`]
219    /// owns its `Loader` for the rest of its lifetime (every later call
220    /// resolves through the same loaded library).
221    ///
222    /// # Errors
223    ///
224    /// [`VplError::Status`] when `MFXInitEx` returns a negative `mfxStatus`.
225    pub fn create_session(self, impl_hint: mfxIMPL) -> Result<Session, VplError> {
226        let par = mfxInitParam {
227            Implementation: impl_hint,
228            ..Default::default()
229        };
230        let mut session: mfxSession = std::ptr::null_mut();
231        // SAFETY: `fn_init_ex` was resolved from `MFXInitEx`, called with a
232        // `mfxInitParam` built from the real header layout (`bindgen`
233        // generated) and a valid out-pointer for the session handle.
234        let status = unsafe { (self.fn_init_ex)(par, &raw mut session) };
235        if !mfx_succeeded(status) {
236            return Err(VplError::Status {
237                call: "MFXInitEx",
238                status,
239            });
240        }
241        Ok(Session {
242            loader: self,
243            session,
244        })
245    }
246}
247
248/// An open oneVPL session (`MFXInitEx` succeeded) plus the `Loader` it was created from.
249///
250/// `Drop` calls `MFXClose` — callers that already closed the encoder should
251/// still let this run (idempotent-safe: a session is only ever closed once,
252/// `Session` has no `close`-without-drop path this stage).
253pub struct Session {
254    loader: Loader,
255    session: mfxSession,
256}
257
258// SAFETY: `mfxSession` (a raw pointer) does not implement `Send`/`Sync` by
259// default; oneVPL sessions are documented as safe to move across threads as
260// long as calls into the same session are not made concurrently from
261// multiple threads — this crate never does (every `Session` method takes
262// `&mut self`), so `Send` is sound. Not `Sync` (no shared-reference calls).
263unsafe impl Send for Session {}
264
265impl Session {
266    /// `MFXQueryVersion`.
267    ///
268    /// # Errors
269    ///
270    /// [`VplError::Status`] on a negative `mfxStatus`.
271    pub fn query_version(&mut self) -> Result<mfxVersion, VplError> {
272        let mut version = mfxVersion::default();
273        // SAFETY: `fn_query_version` was resolved from `MFXQueryVersion`;
274        // `self.session` came from a successful `MFXInitEx` in `Loader::create_session`.
275        let status = unsafe { (self.loader.fn_query_version)(self.session, &raw mut version) };
276        Self::check("MFXQueryVersion", status)?;
277        Ok(version)
278    }
279
280    /// `MFXQueryIMPL` — which implementation the runtime actually selected
281    /// (hardware vs. a software fallback the caller did not ask for).
282    ///
283    /// # Errors
284    ///
285    /// [`VplError::Status`] on a negative `mfxStatus`.
286    pub fn query_impl(&mut self) -> Result<mfxIMPL, VplError> {
287        let mut out: mfxIMPL = 0;
288        // SAFETY: see `query_version`.
289        let status = unsafe { (self.loader.fn_query_impl)(self.session, &raw mut out) };
290        Self::check("MFXQueryIMPL", status)?;
291        Ok(out)
292    }
293
294    /// `MFXVideoENCODE_Query` — validate/adjust `par` before `Init`.
295    ///
296    /// # Errors
297    ///
298    /// [`VplError::Status`] on a negative `mfxStatus`.
299    pub fn encode_query(&mut self, par: &mut mfxVideoParam) -> Result<mfxStatus, VplError> {
300        let mut out = *par;
301        // SAFETY: `par`/`out` are real `mfxVideoParam` values (bindgen-generated
302        // layout); `fn_encode_query` was resolved from `MFXVideoENCODE_Query`.
303        let status =
304            unsafe { (self.loader.fn_encode_query)(self.session, &raw mut *par, &raw mut out) };
305        if status < MFX_ERR_NONE {
306            return Err(VplError::Status {
307                call: "MFXVideoENCODE_Query",
308                status,
309            });
310        }
311        *par = out;
312        Ok(status)
313    }
314
315    /// `MFXVideoENCODE_Init`.
316    ///
317    /// # Errors
318    ///
319    /// [`VplError::Status`] on a negative `mfxStatus`.
320    pub fn encode_init(&mut self, par: &mut mfxVideoParam) -> Result<(), VplError> {
321        // SAFETY: see `encode_query`.
322        let status = unsafe { (self.loader.fn_encode_init)(self.session, &raw mut *par) };
323        Self::check("MFXVideoENCODE_Init", status)
324    }
325
326    /// `MFXVideoENCODE_Close`.
327    ///
328    /// # Errors
329    ///
330    /// [`VplError::Status`] on a negative `mfxStatus`.
331    pub fn encode_close(&mut self) -> Result<(), VplError> {
332        // SAFETY: `fn_encode_close` resolved from `MFXVideoENCODE_Close`.
333        let status = unsafe { (self.loader.fn_encode_close)(self.session) };
334        Self::check("MFXVideoENCODE_Close", status)
335    }
336
337    /// `MFXVideoENCODE_EncodeFrameAsync`. Pass `surface = None` to signal
338    /// end-of-stream (drain buffered frames during [flush][crate]).
339    ///
340    /// Returns the raw `mfxStatus` (not just success/error) so the caller can
341    /// distinguish `MFX_ERR_NONE` (packet ready, `syncp` populated) from
342    /// `MFX_ERR_MORE_DATA` (no packet yet — not a failure) and
343    /// `MFX_WRN_DEVICE_BUSY` (retry).
344    ///
345    /// # Errors
346    ///
347    /// [`VplError::Status`] only for status values this crate does not know
348    /// how to interpret as non-fatal (i.e. a genuine negative status other
349    /// than `MFX_ERR_MORE_DATA`).
350    pub fn encode_frame_async(
351        &mut self,
352        surface: Option<&mut mfxFrameSurface1>,
353        bs: &mut mfxBitstream,
354    ) -> Result<(mfxStatus, mfxSyncPoint), VplError> {
355        let surface_ptr = surface.map_or(std::ptr::null_mut(), std::ptr::from_mut);
356        let mut syncp: mfxSyncPoint = std::ptr::null_mut();
357        // SAFETY: `bs`/`surface` (when present) are real, live oneVPL structs;
358        // `fn_encode_frame_async` resolved from `MFXVideoENCODE_EncodeFrameAsync`.
359        let status = unsafe {
360            (self.loader.fn_encode_frame_async)(
361                self.session,
362                std::ptr::null_mut(),
363                surface_ptr,
364                std::ptr::from_mut(bs),
365                &raw mut syncp,
366            )
367        };
368        Ok((status, syncp))
369    }
370
371    /// `MFXVideoCORE_SyncOperation` — block (up to `wait_ms`) for `syncp` to
372    /// complete, after a `MFX_ERR_NONE` [`Self::encode_frame_async`].
373    ///
374    /// # Errors
375    ///
376    /// [`VplError::Status`] on a negative `mfxStatus`.
377    #[allow(
378        clippy::not_unsafe_ptr_arg_deref,
379        reason = "syncp (mfxSyncPoint) is an opaque handle this crate never dereferences itself \
380                  — it is only ever forwarded verbatim to MFXVideoCORE_SyncOperation, which owns \
381                  it; the raw pointer type just mirrors oneVPL's own opaque-handle C API"
382    )]
383    pub fn sync_operation(&mut self, syncp: mfxSyncPoint, wait_ms: u32) -> Result<(), VplError> {
384        // SAFETY: `syncp` came from a `MFX_ERR_NONE` `encode_frame_async` on
385        // this same session; `fn_core_sync_operation` resolved from
386        // `MFXVideoCORE_SyncOperation`.
387        let status = unsafe { (self.loader.fn_core_sync_operation)(self.session, syncp, wait_ms) };
388        Self::check("MFXVideoCORE_SyncOperation", status)
389    }
390
391    const fn check(call: &'static str, status: mfxStatus) -> Result<(), VplError> {
392        if mfx_succeeded(status) {
393            Ok(())
394        } else {
395            Err(VplError::Status { call, status })
396        }
397    }
398}
399
400impl Drop for Session {
401    fn drop(&mut self) {
402        // SAFETY: `self.session` is a valid handle from a successful
403        // `MFXInitEx` (or already-nulled by nothing else — `Session` never
404        // exposes a way to null it before `Drop`); `self.loader` is still
405        // alive (it is a field of `self`, dropped only after this fn
406        // returns). Ignoring the status here matches every other Windows/
407        // Linux backend's teardown-`Drop` convention in this workspace (no
408        // `unwrap`/`panic!` in a destructor).
409        let _status: mfxStatus = unsafe { (self.loader.fn_close)(self.session) };
410    }
411}
412
413#[cfg(test)]
414#[path = "dispatcher_tests.rs"]
415mod tests;