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;