Add native self-hosted instance connection to fluxer_desktop
Trimmed monorepo checkout (fluxer_desktop + packages/voice_engine_v2 + tools/ci) with a "Connect to a Different Server" menu item and popout that lets the desktop app switch to any self-hosted Fluxer instance, plus fixes for well-known discovery on single-domain self-hosted deployments and a false-positive ERR_ABORTED on same-origin client redirects during the switch. Defaults to chat.fluxr.chat and uses an isolated userData directory from the official build.
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use fluxer_encoder_ring::{
|
||||
CpuMemcpyBackend, CpuSlotHandle, EncoderInputRing, RING_SIZE, TextureFormat,
|
||||
};
|
||||
|
||||
fn fill_noop(_: &mut CpuSlotHandle) {}
|
||||
|
||||
#[test]
|
||||
fn submit_skip_oldest_drops_first_half_when_pushed_twice_capacity() {
|
||||
let mut ring = EncoderInputRing::new(CpuMemcpyBackend::new());
|
||||
ring.initialise(64, 64, TextureFormat::Nv12)
|
||||
.expect("init ring");
|
||||
let capacity = ring.capacity();
|
||||
assert_eq!(capacity, RING_SIZE);
|
||||
assert_eq!(capacity, 8);
|
||||
let total_pushes: u64 = 2 * capacity as u64;
|
||||
for _ in 0..total_pushes {
|
||||
ring.submit_skip_oldest(fill_noop)
|
||||
.expect("skip-oldest accepts every push");
|
||||
}
|
||||
assert_eq!(ring.submitted_count(), total_pushes);
|
||||
assert_eq!(ring.dropped_count(), capacity as u64);
|
||||
assert_eq!(ring.free_count(), 0);
|
||||
let mut observed: Vec<u64> = Vec::with_capacity(capacity);
|
||||
for _ in 0..capacity {
|
||||
let ready = ring.poll_next_ready().expect("ready");
|
||||
observed.push(ready.sequence);
|
||||
ring.release_completed(ready).expect("release");
|
||||
}
|
||||
let expected: Vec<u64> = (capacity as u64 + 1..=total_pushes).collect();
|
||||
assert_eq!(
|
||||
observed, expected,
|
||||
"skip-don't-block kept newest {capacity} frames"
|
||||
);
|
||||
assert_eq!(ring.free_count(), capacity);
|
||||
assert!(ring.poll_next_ready().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn submit_skip_oldest_succeeds_when_room_available() {
|
||||
let mut ring = EncoderInputRing::new(CpuMemcpyBackend::new());
|
||||
ring.initialise(64, 64, TextureFormat::Nv12)
|
||||
.expect("init ring");
|
||||
for _ in 0..3 {
|
||||
ring.submit_skip_oldest(fill_noop).expect("submit");
|
||||
}
|
||||
assert_eq!(ring.dropped_count(), 0);
|
||||
assert_eq!(ring.submitted_count(), 3);
|
||||
assert_eq!(ring.free_count(), ring.capacity() - 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn submit_skip_oldest_keeps_metrics_consistent() {
|
||||
let mut ring = EncoderInputRing::new(CpuMemcpyBackend::new());
|
||||
ring.initialise(64, 64, TextureFormat::Nv12)
|
||||
.expect("init ring");
|
||||
for _ in 0..(3 * ring.capacity() as u64) {
|
||||
ring.submit_skip_oldest(fill_noop).expect("submit");
|
||||
}
|
||||
let metrics = ring.metrics();
|
||||
assert_eq!(metrics.submitted_count, 3 * ring.capacity() as u64);
|
||||
assert_eq!(metrics.dropped_count, 2 * ring.capacity() as u64);
|
||||
assert!(metrics.completed_count <= metrics.submitted_count);
|
||||
}
|
||||
@@ -0,0 +1,727 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
#![cfg(target_os = "windows")]
|
||||
use std::ffi::c_void;
|
||||
use std::ptr;
|
||||
|
||||
use libloading::{Library, Symbol};
|
||||
use windows::Win32::Graphics::Direct3D::{D3D_DRIVER_TYPE_UNKNOWN, D3D_FEATURE_LEVEL_11_0};
|
||||
use windows::Win32::Graphics::Direct3D11::{
|
||||
D3D11_CREATE_DEVICE_BGRA_SUPPORT, D3D11_CREATE_DEVICE_VIDEO_SUPPORT, D3D11_SDK_VERSION,
|
||||
D3D11CreateDevice, ID3D11Device, ID3D11DeviceContext, ID3D11Multithread,
|
||||
};
|
||||
use windows::Win32::Graphics::Dxgi::{CreateDXGIFactory1, IDXGIAdapter1, IDXGIFactory1};
|
||||
use windows::core::Interface;
|
||||
|
||||
const VENDOR_INTEL: u32 = 0x8086;
|
||||
|
||||
const MFX_IMPL_TYPE_HARDWARE: u32 = 2;
|
||||
const MFX_ACCEL_MODE_NA: u32 = 0;
|
||||
const MFX_ACCEL_MODE_VIA_D3D11: u32 = 0x0300;
|
||||
|
||||
const MFX_HANDLE_D3D11_DEVICE: u32 = 3;
|
||||
const MFX_HANDLE_D3D11_VIDEO_DEVICE_GUESS: u32 = 11;
|
||||
|
||||
const MFX_VARIANT_TYPE_U32: u32 = 5;
|
||||
const MFX_VARIANT_VERSION_MAJOR: u8 = 1;
|
||||
const MFX_VARIANT_VERSION_MINOR: u8 = 1;
|
||||
|
||||
const MFX_ERR_NONE: i32 = 0;
|
||||
const MFX_WRN_IN_EXECUTION: i32 = 1;
|
||||
|
||||
const MFX_FOURCC_NV12: u32 = u32::from_le_bytes(*b"NV12");
|
||||
const MFX_CODEC_AVC: u32 = u32::from_le_bytes(*b"AVC ");
|
||||
const MFX_RATECONTROL_CBR: u16 = 1;
|
||||
const MFX_PICSTRUCT_PROGRESSIVE: u16 = 0x01;
|
||||
const MFX_CHROMAFORMAT_YUV420: u16 = 1;
|
||||
const MFX_IOPATTERN_IN_VIDEO_MEMORY: u16 = 0x01;
|
||||
const MFX_IOPATTERN_IN_SYSTEM_MEMORY: u16 = 0x02;
|
||||
|
||||
const FILTER_PROPERTY_IMPL: &[u8] = b"mfxImplDescription.Impl\0";
|
||||
const FILTER_PROPERTY_ACCEL: &[u8] = b"mfxImplDescription.AccelerationMode\0";
|
||||
|
||||
const QSV_DLL_NAME_VPL: &str = "libvpl.dll";
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Default)]
|
||||
struct MfxStructVersion {
|
||||
minor: u8,
|
||||
major: u8,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
union MfxVariantData {
|
||||
u32_: u32,
|
||||
u64_: u64,
|
||||
ptr: *mut c_void,
|
||||
pad: [u8; 16],
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
struct MfxVariant {
|
||||
version: MfxStructVersion,
|
||||
type_: u32,
|
||||
data: MfxVariantData,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Default)]
|
||||
struct MfxFrameInfo {
|
||||
reserved: [u32; 4],
|
||||
channel_id: u16,
|
||||
bit_depth_luma: u16,
|
||||
bit_depth_chroma: u16,
|
||||
shift: u16,
|
||||
frame_id_temporal: u16,
|
||||
frame_id_priority: u16,
|
||||
frame_id_view: u16,
|
||||
frame_id_quality: u16,
|
||||
four_cc: u32,
|
||||
width: u16,
|
||||
height: u16,
|
||||
crop_x: u16,
|
||||
crop_y: u16,
|
||||
crop_w: u16,
|
||||
crop_h: u16,
|
||||
frame_rate_extn: u32,
|
||||
frame_rate_extd: u32,
|
||||
reserved3: u16,
|
||||
aspect_ratio_w: u16,
|
||||
aspect_ratio_h: u16,
|
||||
pic_struct: u16,
|
||||
chroma_format: u16,
|
||||
reserved2: u16,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Default)]
|
||||
struct MfxInfoMfx {
|
||||
reserved: [u32; 7],
|
||||
low_power: u16,
|
||||
brc_param_multiplier: u16,
|
||||
frame_info: MfxFrameInfo,
|
||||
codec_id: u32,
|
||||
codec_profile: u16,
|
||||
codec_level: u16,
|
||||
num_thread: u16,
|
||||
target_usage: u16,
|
||||
gop_pic_size: u16,
|
||||
gop_ref_dist: u16,
|
||||
gop_opt_flag: u16,
|
||||
idr_interval: u16,
|
||||
rate_control_method: u16,
|
||||
init_qp: u16,
|
||||
buffer_size_in_kb: u16,
|
||||
target_kbps: u16,
|
||||
max_kbps: u16,
|
||||
num_slice: u16,
|
||||
num_ref_frame: u16,
|
||||
encoded_order: u16,
|
||||
union_pad: [u16; 15],
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
struct MfxVideoParam {
|
||||
alloc_id: u32,
|
||||
reserved: [u32; 2],
|
||||
reserved3: u16,
|
||||
async_depth: u16,
|
||||
mfx: MfxInfoMfx,
|
||||
protected: u16,
|
||||
io_pattern: u16,
|
||||
ext_param: *mut c_void,
|
||||
num_ext_param: u16,
|
||||
reserved2: u16,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Default, Clone, Copy)]
|
||||
struct MfxPlatform {
|
||||
code_name: u16,
|
||||
device_id: u16,
|
||||
media_adapter_type: u16,
|
||||
reserved: [u16; 13],
|
||||
}
|
||||
|
||||
type MfxLoad = unsafe extern "C" fn() -> *mut c_void;
|
||||
type MfxUnload = unsafe extern "C" fn(*mut c_void);
|
||||
type MfxCreateConfig = unsafe extern "C" fn(*mut c_void) -> *mut c_void;
|
||||
type MfxSetConfigFilterProperty = unsafe extern "C" fn(*mut c_void, *const u8, MfxVariant) -> i32;
|
||||
type MfxCreateSession = unsafe extern "C" fn(*mut c_void, u32, *mut *mut c_void) -> i32;
|
||||
type MfxClose = unsafe extern "C" fn(*mut c_void) -> i32;
|
||||
type MfxSetHandle = unsafe extern "C" fn(*mut c_void, u32, *mut c_void) -> i32;
|
||||
type MfxQueryImpl = unsafe extern "C" fn(*mut c_void, *mut i32) -> i32;
|
||||
type MfxQueryPlatform = unsafe extern "C" fn(*mut c_void, *mut MfxPlatform) -> i32;
|
||||
type MfxEncodeInit = unsafe extern "C" fn(*mut c_void, *mut MfxVideoParam) -> i32;
|
||||
type MfxEncodeClose = unsafe extern "C" fn(*mut c_void) -> i32;
|
||||
type MfxEncodeQuery =
|
||||
unsafe extern "C" fn(*mut c_void, *mut MfxVideoParam, *mut MfxVideoParam) -> i32;
|
||||
type MfxGetHandle = unsafe extern "C" fn(*mut c_void, u32, *mut *mut c_void) -> i32;
|
||||
|
||||
struct Dispatcher {
|
||||
library: Library,
|
||||
}
|
||||
|
||||
impl Dispatcher {
|
||||
fn load() -> Option<Self> {
|
||||
let library = unsafe { Library::new(QSV_DLL_NAME_VPL) }.ok()?;
|
||||
Some(Self { library })
|
||||
}
|
||||
|
||||
fn modern_session(&self, accel_mode: u32) -> (Option<(*mut c_void, *mut c_void)>, Vec<String>) {
|
||||
let mut log = Vec::new();
|
||||
let load: Symbol<'_, MfxLoad> = match unsafe { self.library.get(b"MFXLoad\0") } {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
log.push(format!("MFXLoad symbol missing: {e:?}"));
|
||||
return (None, log);
|
||||
}
|
||||
};
|
||||
let create_config: Symbol<'_, MfxCreateConfig> =
|
||||
match unsafe { self.library.get(b"MFXCreateConfig\0") } {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
log.push(format!("MFXCreateConfig missing: {e:?}"));
|
||||
return (None, log);
|
||||
}
|
||||
};
|
||||
let set_prop: Symbol<'_, MfxSetConfigFilterProperty> =
|
||||
match unsafe { self.library.get(b"MFXSetConfigFilterProperty\0") } {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
log.push(format!("MFXSetConfigFilterProperty missing: {e:?}"));
|
||||
return (None, log);
|
||||
}
|
||||
};
|
||||
let create_session: Symbol<'_, MfxCreateSession> =
|
||||
match unsafe { self.library.get(b"MFXCreateSession\0") } {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
log.push(format!("MFXCreateSession missing: {e:?}"));
|
||||
return (None, log);
|
||||
}
|
||||
};
|
||||
let loader = unsafe { load() };
|
||||
if loader.is_null() {
|
||||
log.push("MFXLoad returned NULL".to_string());
|
||||
return (None, log);
|
||||
}
|
||||
log.push(format!("MFXLoad ok loader={loader:p}"));
|
||||
let impl_cfg = unsafe { create_config(loader) };
|
||||
if impl_cfg.is_null() {
|
||||
log.push("MFXCreateConfig(impl) returned NULL".to_string());
|
||||
return (None, log);
|
||||
}
|
||||
let impl_variant = MfxVariant {
|
||||
version: MfxStructVersion {
|
||||
minor: MFX_VARIANT_VERSION_MINOR,
|
||||
major: MFX_VARIANT_VERSION_MAJOR,
|
||||
},
|
||||
type_: MFX_VARIANT_TYPE_U32,
|
||||
data: MfxVariantData {
|
||||
u32_: MFX_IMPL_TYPE_HARDWARE,
|
||||
},
|
||||
};
|
||||
let status_impl =
|
||||
unsafe { set_prop(impl_cfg, FILTER_PROPERTY_IMPL.as_ptr(), impl_variant) };
|
||||
log.push(format!(
|
||||
"SetConfigFilterProperty(Impl=HARDWARE) status={status_impl}"
|
||||
));
|
||||
if status_impl != MFX_ERR_NONE {
|
||||
return (None, log);
|
||||
}
|
||||
let accel_cfg = unsafe { create_config(loader) };
|
||||
if accel_cfg.is_null() {
|
||||
log.push("MFXCreateConfig(accel) returned NULL".to_string());
|
||||
return (None, log);
|
||||
}
|
||||
let accel_variant = MfxVariant {
|
||||
version: MfxStructVersion {
|
||||
minor: MFX_VARIANT_VERSION_MINOR,
|
||||
major: MFX_VARIANT_VERSION_MAJOR,
|
||||
},
|
||||
type_: MFX_VARIANT_TYPE_U32,
|
||||
data: MfxVariantData { u32_: accel_mode },
|
||||
};
|
||||
let status_accel =
|
||||
unsafe { set_prop(accel_cfg, FILTER_PROPERTY_ACCEL.as_ptr(), accel_variant) };
|
||||
log.push(format!(
|
||||
"SetConfigFilterProperty(AccelerationMode={accel_mode:#x}) status={status_accel}"
|
||||
));
|
||||
if status_accel != MFX_ERR_NONE {
|
||||
return (None, log);
|
||||
}
|
||||
let mut session: *mut c_void = ptr::null_mut();
|
||||
let status_create = unsafe { create_session(loader, 0, &mut session) };
|
||||
log.push(format!(
|
||||
"MFXCreateSession status={status_create} session={session:p}"
|
||||
));
|
||||
if status_create != MFX_ERR_NONE || session.is_null() {
|
||||
return (None, log);
|
||||
}
|
||||
(Some((loader, session)), log)
|
||||
}
|
||||
|
||||
fn unload(&self, loader: *mut c_void) {
|
||||
let Ok(unload) = (unsafe { self.library.get::<MfxUnload>(b"MFXUnload\0") }) else {
|
||||
return;
|
||||
};
|
||||
if !loader.is_null() {
|
||||
unsafe { unload(loader) };
|
||||
}
|
||||
}
|
||||
|
||||
fn close(&self, session: *mut c_void) {
|
||||
let Ok(close) = (unsafe { self.library.get::<MfxClose>(b"MFXClose\0") }) else {
|
||||
return;
|
||||
};
|
||||
if !session.is_null() {
|
||||
unsafe {
|
||||
let _ = close(session);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn try_create_intel_device() -> Option<(ID3D11Device, ID3D11DeviceContext)> {
|
||||
let factory: IDXGIFactory1 = unsafe { CreateDXGIFactory1() }.ok()?;
|
||||
let mut idx: u32 = 0;
|
||||
loop {
|
||||
let adapter: IDXGIAdapter1 = match unsafe { factory.EnumAdapters1(idx) } {
|
||||
Ok(a) => a,
|
||||
Err(_) => return None,
|
||||
};
|
||||
idx = idx.saturating_add(1);
|
||||
let desc = match unsafe { adapter.GetDesc1() } {
|
||||
Ok(d) => d,
|
||||
Err(_) => continue,
|
||||
};
|
||||
if desc.VendorId != VENDOR_INTEL {
|
||||
continue;
|
||||
}
|
||||
let mut device: Option<ID3D11Device> = None;
|
||||
let mut ctx: Option<ID3D11DeviceContext> = None;
|
||||
let feature_levels = [D3D_FEATURE_LEVEL_11_0];
|
||||
let cast_result = adapter.cast::<windows::Win32::Graphics::Dxgi::IDXGIAdapter>();
|
||||
let cast_adapter = match cast_result {
|
||||
Ok(a) => a,
|
||||
Err(_) => continue,
|
||||
};
|
||||
let flags = D3D11_CREATE_DEVICE_BGRA_SUPPORT | D3D11_CREATE_DEVICE_VIDEO_SUPPORT;
|
||||
let result = unsafe {
|
||||
D3D11CreateDevice(
|
||||
Some(&cast_adapter),
|
||||
D3D_DRIVER_TYPE_UNKNOWN,
|
||||
Default::default(),
|
||||
flags,
|
||||
Some(&feature_levels),
|
||||
D3D11_SDK_VERSION,
|
||||
Some(&mut device),
|
||||
None,
|
||||
Some(&mut ctx),
|
||||
)
|
||||
};
|
||||
if let (Ok(()), Some(d), Some(c)) = (result, device, ctx) {
|
||||
if let Ok(mt) = d.cast::<ID3D11Multithread>() {
|
||||
let _ = unsafe { mt.SetMultithreadProtected(true) };
|
||||
}
|
||||
return Some((d, c));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_video_params_with(
|
||||
width: u16,
|
||||
height: u16,
|
||||
io_pattern: u16,
|
||||
target_usage: u16,
|
||||
low_power: u16,
|
||||
) -> MfxVideoParam {
|
||||
let aligned_w = (width + 15) & !15;
|
||||
let aligned_h = (height + 15) & !15;
|
||||
let info = MfxFrameInfo {
|
||||
four_cc: MFX_FOURCC_NV12,
|
||||
width: aligned_w,
|
||||
height: aligned_h,
|
||||
crop_w: width,
|
||||
crop_h: height,
|
||||
frame_rate_extn: 30,
|
||||
frame_rate_extd: 1,
|
||||
aspect_ratio_w: 1,
|
||||
aspect_ratio_h: 1,
|
||||
pic_struct: MFX_PICSTRUCT_PROGRESSIVE,
|
||||
chroma_format: MFX_CHROMAFORMAT_YUV420,
|
||||
..Default::default()
|
||||
};
|
||||
let mfx = MfxInfoMfx {
|
||||
frame_info: info,
|
||||
codec_id: MFX_CODEC_AVC,
|
||||
target_usage,
|
||||
low_power,
|
||||
gop_pic_size: 30,
|
||||
gop_ref_dist: 1,
|
||||
rate_control_method: MFX_RATECONTROL_CBR,
|
||||
target_kbps: 5_000,
|
||||
max_kbps: 5_000,
|
||||
num_slice: 1,
|
||||
num_ref_frame: 1,
|
||||
..Default::default()
|
||||
};
|
||||
MfxVideoParam {
|
||||
alloc_id: 0,
|
||||
reserved: [0; 2],
|
||||
reserved3: 0,
|
||||
async_depth: 1,
|
||||
mfx,
|
||||
protected: 0,
|
||||
io_pattern,
|
||||
ext_param: ptr::null_mut(),
|
||||
num_ext_param: 0,
|
||||
reserved2: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn try_set_handle(
|
||||
dispatcher: &Dispatcher,
|
||||
session: *mut c_void,
|
||||
handle_type: u32,
|
||||
handle_ptr: *mut c_void,
|
||||
) -> Result<i32, String> {
|
||||
let sym: Symbol<'_, MfxSetHandle> =
|
||||
unsafe { dispatcher.library.get(b"MFXVideoCORE_SetHandle\0") }
|
||||
.map_err(|e| format!("MFXVideoCORE_SetHandle missing: {e:?}"))?;
|
||||
let status = unsafe { sym(session, handle_type, handle_ptr) };
|
||||
Ok(status)
|
||||
}
|
||||
|
||||
fn try_query_impl(dispatcher: &Dispatcher, session: *mut c_void) -> Result<(i32, i32), String> {
|
||||
let sym: Symbol<'_, MfxQueryImpl> = unsafe { dispatcher.library.get(b"MFXQueryIMPL\0") }
|
||||
.map_err(|e| format!("MFXQueryIMPL missing: {e:?}"))?;
|
||||
let mut out: i32 = 0;
|
||||
let status = unsafe { sym(session, &mut out) };
|
||||
Ok((status, out))
|
||||
}
|
||||
|
||||
fn try_query_platform(
|
||||
dispatcher: &Dispatcher,
|
||||
session: *mut c_void,
|
||||
) -> Result<(i32, MfxPlatform), String> {
|
||||
let sym: Symbol<'_, MfxQueryPlatform> =
|
||||
unsafe { dispatcher.library.get(b"MFXVideoCORE_QueryPlatform\0") }
|
||||
.map_err(|e| format!("MFXVideoCORE_QueryPlatform missing: {e:?}"))?;
|
||||
let mut p = MfxPlatform::default();
|
||||
let status = unsafe { sym(session, &mut p) };
|
||||
Ok((status, p))
|
||||
}
|
||||
|
||||
fn try_encode_init(
|
||||
dispatcher: &Dispatcher,
|
||||
session: *mut c_void,
|
||||
width: u16,
|
||||
height: u16,
|
||||
io_pattern: u16,
|
||||
) -> Result<i32, String> {
|
||||
try_encode_init_with(dispatcher, session, width, height, io_pattern, 4, 0)
|
||||
}
|
||||
|
||||
fn try_encode_init_with(
|
||||
dispatcher: &Dispatcher,
|
||||
session: *mut c_void,
|
||||
width: u16,
|
||||
height: u16,
|
||||
io_pattern: u16,
|
||||
target_usage: u16,
|
||||
low_power: u16,
|
||||
) -> Result<i32, String> {
|
||||
let query: Symbol<'_, MfxEncodeQuery> =
|
||||
unsafe { dispatcher.library.get(b"MFXVideoENCODE_Query\0") }
|
||||
.map_err(|e| format!("MFXVideoENCODE_Query missing: {e:?}"))?;
|
||||
let init: Symbol<'_, MfxEncodeInit> =
|
||||
unsafe { dispatcher.library.get(b"MFXVideoENCODE_Init\0") }
|
||||
.map_err(|e| format!("MFXVideoENCODE_Init missing: {e:?}"))?;
|
||||
let mut params = build_video_params_with(width, height, io_pattern, target_usage, low_power);
|
||||
let mut query_out = build_video_params_with(width, height, io_pattern, target_usage, low_power);
|
||||
let q_status = unsafe { query(session, &mut params, &mut query_out) };
|
||||
eprintln!(
|
||||
" MFXVideoENCODE_Query(io={io_pattern:#x},tu={target_usage},lp={low_power}) status={q_status}"
|
||||
);
|
||||
if q_status != MFX_ERR_NONE && q_status != MFX_WRN_IN_EXECUTION && q_status != -3 {
|
||||
eprintln!(" Query returned hard error {q_status}; skipping Init to avoid AV");
|
||||
return Ok(q_status);
|
||||
}
|
||||
let init_status = unsafe { init(session, &mut query_out) };
|
||||
Ok(init_status)
|
||||
}
|
||||
|
||||
fn try_get_handle(
|
||||
dispatcher: &Dispatcher,
|
||||
session: *mut c_void,
|
||||
handle_type: u32,
|
||||
) -> Result<(i32, *mut c_void), String> {
|
||||
let sym: Symbol<'_, MfxGetHandle> =
|
||||
unsafe { dispatcher.library.get(b"MFXVideoCORE_GetHandle\0") }
|
||||
.map_err(|e| format!("MFXVideoCORE_GetHandle missing: {e:?}"))?;
|
||||
let mut out: *mut c_void = ptr::null_mut();
|
||||
let status = unsafe { sym(session, handle_type, &mut out) };
|
||||
Ok((status, out))
|
||||
}
|
||||
|
||||
fn try_encode_close(dispatcher: &Dispatcher, session: *mut c_void) {
|
||||
if let Ok(sym) = unsafe {
|
||||
dispatcher
|
||||
.library
|
||||
.get::<MfxEncodeClose>(b"MFXVideoENCODE_Close\0")
|
||||
} {
|
||||
unsafe {
|
||||
let _ = sym(session);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn qsv_tiger_lake_probe_struct_sizes() {
|
||||
eprintln!("=== STRUCT SIZES (Rust) ===");
|
||||
eprintln!("MfxFrameInfo = {}", std::mem::size_of::<MfxFrameInfo>());
|
||||
eprintln!("MfxInfoMfx = {}", std::mem::size_of::<MfxInfoMfx>());
|
||||
eprintln!("MfxVideoParam = {}", std::mem::size_of::<MfxVideoParam>());
|
||||
assert!(
|
||||
std::mem::size_of::<MfxFrameInfo>() == 68,
|
||||
"MfxFrameInfo size must be 68"
|
||||
);
|
||||
assert!(
|
||||
std::mem::size_of::<MfxInfoMfx>() >= 168,
|
||||
"MfxInfoMfx >= 168 (= sizeof mfxInfoVPP)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn qsv_tiger_lake_probe_experiment_1_preconditions() {
|
||||
let dispatcher = match Dispatcher::load() {
|
||||
Some(d) => d,
|
||||
None => {
|
||||
eprintln!("skip: libvpl.dll not loadable");
|
||||
return;
|
||||
}
|
||||
};
|
||||
eprintln!("=== EXPERIMENT 1: preconditions (QueryIMPL + QueryPlatform) ===");
|
||||
let (session_opt, log) = dispatcher.modern_session(MFX_ACCEL_MODE_VIA_D3D11);
|
||||
for line in &log {
|
||||
eprintln!(" {line}");
|
||||
}
|
||||
let (loader, session) = match session_opt {
|
||||
Some(s) => s,
|
||||
None => {
|
||||
eprintln!("EXP1 result: modern session not created; cannot probe preconditions");
|
||||
return;
|
||||
}
|
||||
};
|
||||
match try_query_impl(&dispatcher, session) {
|
||||
Ok((status, impl_val)) => {
|
||||
eprintln!(" MFXQueryIMPL: status={status} impl={impl_val:#x}");
|
||||
}
|
||||
Err(e) => eprintln!(" MFXQueryIMPL error: {e}"),
|
||||
}
|
||||
match try_query_platform(&dispatcher, session) {
|
||||
Ok((status, p)) => {
|
||||
eprintln!(
|
||||
" MFXVideoCORE_QueryPlatform: status={status} code_name={} device_id={:#x} media_adapter_type={}",
|
||||
p.code_name, p.device_id, p.media_adapter_type
|
||||
);
|
||||
}
|
||||
Err(e) => eprintln!(" MFXVideoCORE_QueryPlatform error: {e}"),
|
||||
}
|
||||
dispatcher.close(session);
|
||||
dispatcher.unload(loader);
|
||||
eprintln!("EXP1 done");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn qsv_tiger_lake_probe_experiment_2_variant_accel_modes() {
|
||||
let dispatcher = match Dispatcher::load() {
|
||||
Some(d) => d,
|
||||
None => {
|
||||
eprintln!("skip: libvpl.dll not loadable");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let (device, _ctx) = match try_create_intel_device() {
|
||||
Some(d) => d,
|
||||
None => {
|
||||
eprintln!("skip: no Intel D3D11 device");
|
||||
return;
|
||||
}
|
||||
};
|
||||
eprintln!("=== EXPERIMENT 2: SetHandle handle-type variants ===");
|
||||
let (session_opt, log) = dispatcher.modern_session(MFX_ACCEL_MODE_VIA_D3D11);
|
||||
for line in &log {
|
||||
eprintln!(" {line}");
|
||||
}
|
||||
let (loader, session) = match session_opt {
|
||||
Some(s) => s,
|
||||
None => {
|
||||
eprintln!("EXP2 result: modern session not created");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let raw_device = device.as_raw();
|
||||
let exp2a = try_set_handle(&dispatcher, session, MFX_HANDLE_D3D11_DEVICE, raw_device);
|
||||
eprintln!(
|
||||
" EXP2a: SetHandle(MFX_HANDLE_D3D11_DEVICE=3) -> {:?}",
|
||||
exp2a
|
||||
);
|
||||
let exp2b = try_set_handle(
|
||||
&dispatcher,
|
||||
session,
|
||||
MFX_HANDLE_D3D11_VIDEO_DEVICE_GUESS,
|
||||
raw_device,
|
||||
);
|
||||
eprintln!(
|
||||
" EXP2b: SetHandle(MFX_HANDLE_D3D11_VIDEO_DEVICE_GUESS=11) -> {:?}",
|
||||
exp2b
|
||||
);
|
||||
dispatcher.close(session);
|
||||
dispatcher.unload(loader);
|
||||
eprintln!("EXP2 done");
|
||||
assert!(exp2a.is_ok(), "MFXVideoCORE_SetHandle symbol resolves");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn qsv_tiger_lake_probe_experiment_3_skip_set_handle() {
|
||||
let dispatcher = match Dispatcher::load() {
|
||||
Some(d) => d,
|
||||
None => {
|
||||
eprintln!("skip: libvpl.dll not loadable");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let (device, _ctx) = match try_create_intel_device() {
|
||||
Some(d) => d,
|
||||
None => {
|
||||
eprintln!("skip: no Intel D3D11 device");
|
||||
return;
|
||||
}
|
||||
};
|
||||
eprintln!("=== EXPERIMENT 3: SetHandle with VIDEO_SUPPORT device + encoder init ===");
|
||||
let (session_opt, log) = dispatcher.modern_session(MFX_ACCEL_MODE_VIA_D3D11);
|
||||
for line in &log {
|
||||
eprintln!(" {line}");
|
||||
}
|
||||
let (loader, session) = match session_opt {
|
||||
Some(s) => s,
|
||||
None => {
|
||||
eprintln!("EXP3 result: modern session not created");
|
||||
return;
|
||||
}
|
||||
};
|
||||
match try_query_impl(&dispatcher, session) {
|
||||
Ok((status, impl_val)) => {
|
||||
eprintln!(" pre-set MFXQueryIMPL: status={status} impl={impl_val:#x}");
|
||||
}
|
||||
Err(e) => eprintln!(" MFXQueryIMPL error: {e}"),
|
||||
}
|
||||
let raw_device = device.as_raw();
|
||||
let set_status = try_set_handle(&dispatcher, session, MFX_HANDLE_D3D11_DEVICE, raw_device);
|
||||
eprintln!(
|
||||
" EXP3-set: SetHandle(D3D11_DEVICE=3, VIDEO_SUPPORT+BGRA, multithread-protected) -> {:?}",
|
||||
set_status
|
||||
);
|
||||
match try_get_handle(&dispatcher, session, MFX_HANDLE_D3D11_DEVICE) {
|
||||
Ok((status, hdl)) => {
|
||||
eprintln!(
|
||||
" post-set GetHandle(MFX_HANDLE_D3D11_DEVICE) -> status={status} handle={hdl:p}"
|
||||
);
|
||||
}
|
||||
Err(e) => eprintln!(" GetHandle error: {e}"),
|
||||
}
|
||||
let init_a = try_encode_init(
|
||||
&dispatcher,
|
||||
session,
|
||||
1280,
|
||||
720,
|
||||
MFX_IOPATTERN_IN_VIDEO_MEMORY,
|
||||
);
|
||||
eprintln!(
|
||||
" EXP3a (IO_VIDEO_MEMORY, target_usage=4, low_power=0): init -> {:?}",
|
||||
init_a
|
||||
);
|
||||
let init_b = try_encode_init_with(
|
||||
&dispatcher,
|
||||
session,
|
||||
1280,
|
||||
720,
|
||||
MFX_IOPATTERN_IN_VIDEO_MEMORY,
|
||||
7,
|
||||
0x10,
|
||||
);
|
||||
eprintln!(
|
||||
" EXP3b (IO_VIDEO_MEMORY, target_usage=7 best-speed, low_power=ON=0x10): init -> {:?}",
|
||||
init_b
|
||||
);
|
||||
let init_c = try_encode_init_with(
|
||||
&dispatcher,
|
||||
session,
|
||||
1280,
|
||||
720,
|
||||
MFX_IOPATTERN_IN_VIDEO_MEMORY,
|
||||
4,
|
||||
0x10,
|
||||
);
|
||||
eprintln!(
|
||||
" EXP3c (IO_VIDEO_MEMORY, target_usage=4 balanced, low_power=ON=0x10): init -> {:?}",
|
||||
init_c
|
||||
);
|
||||
try_encode_close(&dispatcher, session);
|
||||
dispatcher.close(session);
|
||||
dispatcher.unload(loader);
|
||||
eprintln!("EXP3 done");
|
||||
assert!(init_a.is_ok(), "init_a symbol resolved");
|
||||
assert!(init_b.is_ok(), "init_b symbol resolved");
|
||||
assert!(init_c.is_ok(), "init_c symbol resolved");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn qsv_tiger_lake_probe_experiment_4_software_fallback() {
|
||||
let dispatcher = match Dispatcher::load() {
|
||||
Some(d) => d,
|
||||
None => {
|
||||
eprintln!("skip: libvpl.dll not loadable");
|
||||
return;
|
||||
}
|
||||
};
|
||||
eprintln!("=== EXPERIMENT 4: software-only (ACCEL_MODE_NA) session ===");
|
||||
let (session_opt, log) = dispatcher.modern_session(MFX_ACCEL_MODE_NA);
|
||||
for line in &log {
|
||||
eprintln!(" {line}");
|
||||
}
|
||||
let (loader, session) = match session_opt {
|
||||
Some(s) => s,
|
||||
None => {
|
||||
eprintln!("EXP4 result: software session not created");
|
||||
return;
|
||||
}
|
||||
};
|
||||
match try_query_impl(&dispatcher, session) {
|
||||
Ok((status, impl_val)) => {
|
||||
eprintln!(" MFXQueryIMPL: status={status} impl={impl_val:#x}");
|
||||
}
|
||||
Err(e) => eprintln!(" MFXQueryIMPL error: {e}"),
|
||||
}
|
||||
let init_status = try_encode_init(
|
||||
&dispatcher,
|
||||
session,
|
||||
1280,
|
||||
720,
|
||||
MFX_IOPATTERN_IN_SYSTEM_MEMORY,
|
||||
);
|
||||
eprintln!(" EXP4: MFXVideoENCODE_Init software -> {:?}", init_status);
|
||||
try_encode_close(&dispatcher, session);
|
||||
dispatcher.close(session);
|
||||
dispatcher.unload(loader);
|
||||
eprintln!("EXP4 done");
|
||||
assert!(
|
||||
init_status.is_ok(),
|
||||
"encode init symbol resolved in software"
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,551 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
#![cfg(target_os = "windows")]
|
||||
use fluxer_encoder_ring::NVENC_COMPLETION_RING_CAPACITY;
|
||||
use fluxer_encoder_ring::encoder_handoff::EncoderCompletionCallback;
|
||||
use fluxer_encoder_ring::{
|
||||
AmfD3D11Handoff, AmfHandoff, EncodedBitstream, EncoderDims, EncoderError, EncoderSubmission,
|
||||
NvencD3D11Handoff, NvencHandoff, PicParams, QsvD3D11Handoff, QsvHandoff, RingError,
|
||||
};
|
||||
use windows::Win32::Graphics::Direct3D::{D3D_DRIVER_TYPE_UNKNOWN, D3D_FEATURE_LEVEL_11_0};
|
||||
use windows::Win32::Graphics::Direct3D11::{
|
||||
D3D11_BIND_RENDER_TARGET, D3D11_BIND_SHADER_RESOURCE, D3D11_CREATE_DEVICE_BGRA_SUPPORT,
|
||||
D3D11_CREATE_DEVICE_VIDEO_SUPPORT, D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX,
|
||||
D3D11_RESOURCE_MISC_SHARED_NTHANDLE, D3D11_SDK_VERSION, D3D11_TEXTURE2D_DESC,
|
||||
D3D11_USAGE_DEFAULT, D3D11CreateDevice, ID3D11Device, ID3D11Multithread,
|
||||
};
|
||||
use windows::Win32::Graphics::Dxgi::Common::{DXGI_FORMAT_NV12, DXGI_SAMPLE_DESC};
|
||||
use windows::Win32::Graphics::Dxgi::{
|
||||
CreateDXGIFactory1, IDXGIAdapter1, IDXGIFactory1, IDXGIResource1,
|
||||
};
|
||||
use windows::core::Interface;
|
||||
|
||||
const VENDOR_NVIDIA: u32 = 0x10DE;
|
||||
const VENDOR_AMD: u32 = 0x1002;
|
||||
const VENDOR_INTEL: u32 = 0x8086;
|
||||
|
||||
fn enum_adapters() -> Vec<IDXGIAdapter1> {
|
||||
let factory_result: windows::core::Result<IDXGIFactory1> = unsafe { CreateDXGIFactory1() };
|
||||
let factory = match factory_result {
|
||||
Ok(f) => f,
|
||||
Err(_) => return Vec::new(),
|
||||
};
|
||||
let mut out = Vec::new();
|
||||
let mut index: u32 = 0;
|
||||
loop {
|
||||
let result: windows::core::Result<IDXGIAdapter1> = unsafe { factory.EnumAdapters1(index) };
|
||||
match result {
|
||||
Ok(adapter) => {
|
||||
out.push(adapter);
|
||||
index = index.saturating_add(1);
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn try_create_device_for_vendor(vendor_id: u32) -> Option<ID3D11Device> {
|
||||
let adapters = enum_adapters();
|
||||
for adapter in adapters {
|
||||
let desc = match unsafe { adapter.GetDesc1() } {
|
||||
Ok(d) => d,
|
||||
Err(_) => continue,
|
||||
};
|
||||
if desc.VendorId != vendor_id {
|
||||
continue;
|
||||
}
|
||||
let mut device: Option<ID3D11Device> = None;
|
||||
let feature_levels = [D3D_FEATURE_LEVEL_11_0];
|
||||
let flags = D3D11_CREATE_DEVICE_BGRA_SUPPORT | D3D11_CREATE_DEVICE_VIDEO_SUPPORT;
|
||||
let result = unsafe {
|
||||
D3D11CreateDevice(
|
||||
Some(&adapter.cast().ok()?),
|
||||
D3D_DRIVER_TYPE_UNKNOWN,
|
||||
Default::default(),
|
||||
flags,
|
||||
Some(&feature_levels),
|
||||
D3D11_SDK_VERSION,
|
||||
Some(&mut device),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
};
|
||||
if let (Ok(()), Some(d)) = (result, device) {
|
||||
if let Ok(mt) = d.cast::<ID3D11Multithread>() {
|
||||
let _ = unsafe { mt.SetMultithreadProtected(true) };
|
||||
}
|
||||
return Some(d);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn try_create_device() -> Option<ID3D11Device> {
|
||||
try_create_device_for_vendor(VENDOR_NVIDIA)
|
||||
.or_else(|| try_create_device_for_vendor(VENDOR_INTEL))
|
||||
.or_else(|| try_create_device_for_vendor(VENDOR_AMD))
|
||||
}
|
||||
|
||||
fn try_create_shared_nv12(device: &ID3D11Device, width: u32, height: u32) -> Option<u64> {
|
||||
let desc = D3D11_TEXTURE2D_DESC {
|
||||
Width: width,
|
||||
Height: height,
|
||||
MipLevels: 1,
|
||||
ArraySize: 1,
|
||||
Format: DXGI_FORMAT_NV12,
|
||||
SampleDesc: DXGI_SAMPLE_DESC {
|
||||
Count: 1,
|
||||
Quality: 0,
|
||||
},
|
||||
Usage: D3D11_USAGE_DEFAULT,
|
||||
BindFlags: (D3D11_BIND_RENDER_TARGET.0 | D3D11_BIND_SHADER_RESOURCE.0) as u32,
|
||||
CPUAccessFlags: 0,
|
||||
MiscFlags: (D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX.0 | D3D11_RESOURCE_MISC_SHARED_NTHANDLE.0)
|
||||
as u32,
|
||||
};
|
||||
let mut texture = None;
|
||||
unsafe {
|
||||
device
|
||||
.CreateTexture2D(&desc, None, Some(&mut texture))
|
||||
.ok()?;
|
||||
}
|
||||
let texture = texture?;
|
||||
let resource: IDXGIResource1 = texture.cast().ok()?;
|
||||
let shared = unsafe {
|
||||
resource
|
||||
.CreateSharedHandle(None, 0x3, windows::core::PCWSTR::null())
|
||||
.ok()?
|
||||
};
|
||||
Some(shared.0 as u64)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nvenc_real_encode_one_frame_via_dummy_shared_texture() {
|
||||
let device = match try_create_device_for_vendor(VENDOR_NVIDIA) {
|
||||
Some(d) => d,
|
||||
None => {
|
||||
eprintln!("skip: no NVIDIA D3D11 device available");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let dims = EncoderDims::new(1920, 1080);
|
||||
let handoff_result = NvencD3D11Handoff::new(device.clone(), dims, 5_000_000);
|
||||
let mut handoff = match handoff_result {
|
||||
Ok(h) => h,
|
||||
Err(EncoderError::SdkNotFound { .. }) => {
|
||||
eprintln!("skip: NVENC runtime DLL not available");
|
||||
return;
|
||||
}
|
||||
Err(EncoderError::SessionInitFailed { vendor, status }) => {
|
||||
eprintln!("skip: NVENC session init failed: {vendor} status={status}");
|
||||
return;
|
||||
}
|
||||
Err(other) => {
|
||||
eprintln!("skip: NVENC init unexpected error: {other:?}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let shared = match try_create_shared_nv12(&device, 1920, 1080) {
|
||||
Some(h) => h,
|
||||
None => {
|
||||
eprintln!("skip: shared NV12 texture creation failed");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let slot = match NvencHandoff::register_slot(&mut handoff, shared, 0, dims) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
eprintln!("skip: register_slot failed: {e:?}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let pic = PicParams::new(0, true);
|
||||
let submit_result = NvencHandoff::encode_shared_async(&mut handoff, slot, 0, dims, pic);
|
||||
if submit_result.is_err() {
|
||||
eprintln!("skip: encode_shared_async failed: {submit_result:?}");
|
||||
NvencHandoff::unregister_slot(&mut handoff, slot);
|
||||
return;
|
||||
}
|
||||
let mut bitstream = None;
|
||||
for _ in 0..200 {
|
||||
bitstream = NvencHandoff::poll_completed(&mut handoff, slot);
|
||||
if bitstream.is_some() {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
}
|
||||
NvencHandoff::unregister_slot(&mut handoff, slot);
|
||||
let bs = match bitstream {
|
||||
Some(b) => b,
|
||||
None => {
|
||||
eprintln!("nvenc: no bitstream produced within timeout; backend init succeeded");
|
||||
return;
|
||||
}
|
||||
};
|
||||
assert!(!bs.data.is_empty(), "non-empty bitstream");
|
||||
assert!(bs.is_keyframe, "first frame must be keyframe");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn amf_real_encode_one_frame_via_dummy_shared_texture() {
|
||||
let device = match try_create_device_for_vendor(VENDOR_AMD) {
|
||||
Some(d) => d,
|
||||
None => {
|
||||
eprintln!("skip: no AMD D3D11 device available (expected on Intel/NVIDIA-only boxes)");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let dims = EncoderDims::new(1920, 1080);
|
||||
let handoff_result = AmfD3D11Handoff::new(device.clone(), dims, 5_000_000);
|
||||
let mut handoff = match handoff_result {
|
||||
Ok(h) => h,
|
||||
Err(EncoderError::SdkNotFound { .. }) => {
|
||||
eprintln!("skip: AMF runtime DLL not available (expected on non-AMD hardware)");
|
||||
return;
|
||||
}
|
||||
Err(other) => {
|
||||
eprintln!("skip: AMF init unexpected: {other:?}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let shared = match try_create_shared_nv12(&device, 1920, 1080) {
|
||||
Some(h) => h,
|
||||
None => {
|
||||
eprintln!("skip: shared NV12 texture creation failed");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let slot = match AmfHandoff::register_slot(&mut handoff, shared, 0, dims) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
eprintln!("skip: AMF register_slot failed: {e:?}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let pic = PicParams::new(0, true);
|
||||
let _ = AmfHandoff::encode_shared_async(&mut handoff, slot, 0, dims, pic);
|
||||
let mut bitstream = None;
|
||||
for _ in 0..200 {
|
||||
bitstream = AmfHandoff::poll_completed(&mut handoff, slot);
|
||||
if bitstream.is_some() {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
}
|
||||
AmfHandoff::unregister_slot(&mut handoff, slot);
|
||||
if let Some(bs) = bitstream {
|
||||
assert!(!bs.data.is_empty(), "non-empty AMF bitstream");
|
||||
} else {
|
||||
eprintln!("amf: no bitstream produced within timeout; backend init succeeded");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn qsv_modern_dispatcher_session_init_succeeds_on_real_iris_xe() {
|
||||
let device = match try_create_device_for_vendor(VENDOR_INTEL) {
|
||||
Some(d) => d,
|
||||
None => {
|
||||
eprintln!("skip: no Intel D3D11 device available");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let dims = EncoderDims::new(1280, 720);
|
||||
let handoff_result = QsvD3D11Handoff::new(device, dims, 5_000_000);
|
||||
match handoff_result {
|
||||
Ok(_) => {
|
||||
eprintln!("qsv: modern dispatcher session + SetHandle + encode_init succeeded");
|
||||
}
|
||||
Err(EncoderError::SdkNotFound { .. }) => {
|
||||
eprintln!("skip: QSV runtime DLL not available");
|
||||
}
|
||||
Err(e) => {
|
||||
panic!("qsv modern dispatcher init MUST succeed on Tiger Lake Iris Xe: {e:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn qsv_real_encode_one_frame_via_dummy_shared_texture() {
|
||||
let device = match try_create_device_for_vendor(VENDOR_INTEL) {
|
||||
Some(d) => d,
|
||||
None => {
|
||||
eprintln!("skip: no Intel D3D11 device available");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let dims = EncoderDims::new(1920, 1080);
|
||||
let handoff_result = QsvD3D11Handoff::new(device.clone(), dims, 5_000_000);
|
||||
let mut handoff = match handoff_result {
|
||||
Ok(h) => h,
|
||||
Err(EncoderError::SdkNotFound { .. }) => {
|
||||
eprintln!("skip: QSV runtime DLL not available");
|
||||
return;
|
||||
}
|
||||
Err(EncoderError::SessionInitFailed { vendor, status }) => {
|
||||
eprintln!("skip: QSV session init failed: {vendor} status={status}");
|
||||
return;
|
||||
}
|
||||
Err(other) => {
|
||||
eprintln!("skip: QSV init unexpected: {other:?}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let shared = match try_create_shared_nv12(&device, 1920, 1080) {
|
||||
Some(h) => h,
|
||||
None => {
|
||||
eprintln!("skip: shared NV12 texture creation failed");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let slot = match QsvHandoff::register_slot(&mut handoff, shared, 0, dims) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
eprintln!("skip: QSV register_slot failed: {e:?}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let pic = PicParams::new(0, true);
|
||||
let _ = QsvHandoff::encode_shared_async(&mut handoff, slot, 0, dims, pic);
|
||||
let mut bitstream = None;
|
||||
for _ in 0..200 {
|
||||
bitstream = QsvHandoff::poll_completed(&mut handoff, slot);
|
||||
if bitstream.is_some() {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
}
|
||||
QsvHandoff::unregister_slot(&mut handoff, slot);
|
||||
if let Some(bs) = bitstream {
|
||||
assert!(!bs.data.is_empty(), "non-empty QSV bitstream");
|
||||
} else {
|
||||
eprintln!("qsv: no bitstream produced within timeout; backend init succeeded");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skip_dont_block_rapid_submit_then_poll_never_hangs() {
|
||||
let device = match try_create_device() {
|
||||
Some(d) => d,
|
||||
None => return,
|
||||
};
|
||||
let dims = EncoderDims::new(640, 360);
|
||||
let mut handoff = match NvencD3D11Handoff::new(device.clone(), dims, 1_000_000) {
|
||||
Ok(h) => h,
|
||||
Err(_) => return,
|
||||
};
|
||||
let shared = match try_create_shared_nv12(&device, 640, 360) {
|
||||
Some(h) => h,
|
||||
None => return,
|
||||
};
|
||||
let slot = match NvencHandoff::register_slot(&mut handoff, shared, 0, dims) {
|
||||
Ok(s) => s,
|
||||
Err(_) => return,
|
||||
};
|
||||
let start = std::time::Instant::now();
|
||||
for i in 0..10 {
|
||||
let pic = PicParams::new((i as u64) * 16_666, false);
|
||||
let _ = NvencHandoff::encode_shared_async(&mut handoff, slot, 0, dims, pic);
|
||||
let _ = NvencHandoff::poll_completed(&mut handoff, slot);
|
||||
}
|
||||
let elapsed = start.elapsed();
|
||||
assert!(
|
||||
elapsed.as_secs() < 5,
|
||||
"10 submit+poll cycles must complete in < 5s, was {elapsed:?}"
|
||||
);
|
||||
NvencHandoff::unregister_slot(&mut handoff, slot);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cleanup_on_drop_releases_resources() {
|
||||
let device = match try_create_device() {
|
||||
Some(d) => d,
|
||||
None => return,
|
||||
};
|
||||
let dims = EncoderDims::new(640, 360);
|
||||
{
|
||||
let _handoff = match NvencD3D11Handoff::new(device.clone(), dims, 1_000_000) {
|
||||
Ok(h) => h,
|
||||
Err(_) => return,
|
||||
};
|
||||
}
|
||||
let _handoff2 = NvencD3D11Handoff::new(device.clone(), dims, 1_000_000);
|
||||
}
|
||||
|
||||
struct CountingCallback {
|
||||
seen: Vec<(u64, u32)>,
|
||||
}
|
||||
|
||||
impl EncoderCompletionCallback for CountingCallback {
|
||||
fn on_complete(&mut self, sequence: u64, encoded_bytes: u32) {
|
||||
self.seen.push((sequence, encoded_bytes));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nvenc_compression_handoff_encodes_one_frame_via_keyed_mutex() {
|
||||
let device = match try_create_device_for_vendor(VENDOR_NVIDIA) {
|
||||
Some(d) => d,
|
||||
None => {
|
||||
eprintln!("skip: no NVIDIA D3D11 device available");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let dims = EncoderDims::new(1280, 720);
|
||||
let mut handoff = match NvencD3D11Handoff::new(device.clone(), dims, 2_000_000) {
|
||||
Ok(h) => h,
|
||||
Err(EncoderError::SdkNotFound { .. }) => {
|
||||
eprintln!("skip: NVENC runtime DLL not available");
|
||||
return;
|
||||
}
|
||||
Err(other) => {
|
||||
eprintln!("skip: NVENC init unexpected: {other:?}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let shared = match try_create_shared_nv12(&device, 1280, 720) {
|
||||
Some(h) => h,
|
||||
None => {
|
||||
eprintln!("skip: shared NV12 texture creation failed");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let submission = EncoderSubmission::new(shared, 0, dims, 1);
|
||||
let mut callback = CountingCallback { seen: Vec::new() };
|
||||
NvencHandoff::encode_shared(&mut handoff, submission, &mut callback)
|
||||
.expect("encode_shared first frame ok");
|
||||
assert_eq!(callback.seen.len(), 1, "callback fired once");
|
||||
assert_eq!(callback.seen[0].0, 1, "callback sequence matches");
|
||||
let mut payload: Option<EncodedBitstream> = None;
|
||||
for _ in 0..200 {
|
||||
let slot = fluxer_encoder_ring::HandoffSlot::new(0, shared);
|
||||
payload = NvencHandoff::poll_completed(&mut handoff, slot);
|
||||
if payload.is_some() {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
}
|
||||
let bs = match payload {
|
||||
Some(b) => b,
|
||||
None => {
|
||||
eprintln!("nvenc keyed-mutex: no bitstream within timeout; init succeeded");
|
||||
return;
|
||||
}
|
||||
};
|
||||
assert!(!bs.data.is_empty(), "keyed-mutex encoded payload non-empty");
|
||||
assert!(bs.is_keyframe, "first frame must be IDR keyframe");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nvenc_compression_handoff_handles_back_pressure() {
|
||||
let device = match try_create_device_for_vendor(VENDOR_NVIDIA) {
|
||||
Some(d) => d,
|
||||
None => {
|
||||
eprintln!("skip: no NVIDIA D3D11 device available");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let dims = EncoderDims::new(640, 360);
|
||||
let mut handoff = match NvencD3D11Handoff::new(device.clone(), dims, 1_000_000) {
|
||||
Ok(h) => h,
|
||||
Err(EncoderError::SdkNotFound { .. }) => {
|
||||
eprintln!("skip: NVENC runtime DLL not available");
|
||||
return;
|
||||
}
|
||||
Err(other) => {
|
||||
eprintln!("skip: NVENC init unexpected: {other:?}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let shared = match try_create_shared_nv12(&device, 640, 360) {
|
||||
Some(h) => h,
|
||||
None => {
|
||||
eprintln!("skip: shared NV12 texture creation failed");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let total: u64 = (NVENC_COMPLETION_RING_CAPACITY as u64) + 8;
|
||||
let mut callback = CountingCallback { seen: Vec::new() };
|
||||
let mut accepted = 0usize;
|
||||
let mut full_drops = 0usize;
|
||||
for seq in 1..=total {
|
||||
let submission = EncoderSubmission::new(shared, 0, dims, seq);
|
||||
match NvencHandoff::encode_shared(&mut handoff, submission, &mut callback) {
|
||||
Ok(()) => {
|
||||
accepted += 1;
|
||||
}
|
||||
Err(RingError::FullDropped { .. }) => {
|
||||
full_drops += 1;
|
||||
assert_eq!(
|
||||
handoff.pending_completion(),
|
||||
NVENC_COMPLETION_RING_CAPACITY,
|
||||
"pre-encode drop only when completion ring is full"
|
||||
);
|
||||
}
|
||||
Err(other) => panic!("unexpected encode_shared error: {other:?}"),
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(2));
|
||||
}
|
||||
assert_eq!(
|
||||
accepted + full_drops,
|
||||
total as usize,
|
||||
"every submission accounted"
|
||||
);
|
||||
let pending = handoff.pending_completion();
|
||||
assert!(
|
||||
pending <= NVENC_COMPLETION_RING_CAPACITY,
|
||||
"completion ring stays bounded"
|
||||
);
|
||||
let mut drained = 0usize;
|
||||
for _ in 0..400 {
|
||||
let slot = fluxer_encoder_ring::HandoffSlot::new(0, shared);
|
||||
let bs = NvencHandoff::poll_completed(&mut handoff, slot);
|
||||
match bs {
|
||||
Some(_) => {
|
||||
drained += 1;
|
||||
}
|
||||
None => {
|
||||
if drained >= accepted {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
}
|
||||
}
|
||||
}
|
||||
assert_eq!(
|
||||
drained, accepted,
|
||||
"no accepted frame is silently discarded post-encode"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nvenc_attach_then_detach_smoke() {
|
||||
let device = match try_create_device_for_vendor(VENDOR_NVIDIA) {
|
||||
Some(d) => d,
|
||||
None => {
|
||||
eprintln!("skip: no NVIDIA D3D11 device available");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let dims = EncoderDims::new(640, 360);
|
||||
let handoff_result = NvencD3D11Handoff::new(device.clone(), dims, 1_000_000);
|
||||
let handoff = match handoff_result {
|
||||
Ok(h) => h,
|
||||
Err(EncoderError::SdkNotFound { .. }) => {
|
||||
eprintln!("skip: NVENC runtime DLL not available");
|
||||
return;
|
||||
}
|
||||
Err(other) => {
|
||||
eprintln!("skip: NVENC init unexpected: {other:?}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
assert_eq!(
|
||||
handoff.pending_completion(),
|
||||
0,
|
||||
"fresh handoff has empty ring"
|
||||
);
|
||||
assert_eq!(handoff.completed_count(), 0, "fresh handoff completed=0");
|
||||
drop(handoff);
|
||||
let _again = NvencD3D11Handoff::new(device, dims, 1_000_000);
|
||||
}
|
||||
Reference in New Issue
Block a user