mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 05:25:40 +09:00
1057 lines
36 KiB
Rust
1057 lines
36 KiB
Rust
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
#![allow(dead_code)]
|
|
|
|
use napi_derive::napi;
|
|
|
|
const NVENC_NATIVE_INPUTS: &[&str] = &["dmabuf"];
|
|
const VIDEOTOOLBOX_CODECS: &[&str] = &["h264", "h265"];
|
|
|
|
#[napi(object)]
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct HardwareEncoderCapability {
|
|
pub available: bool,
|
|
pub backend: String,
|
|
pub compiled: bool,
|
|
pub runtime: bool,
|
|
pub codecs: Vec<String>,
|
|
pub zero_copy: bool,
|
|
pub native_inputs: Vec<String>,
|
|
pub reason: Option<String>,
|
|
pub detail: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
struct NvencRuntimeProbe {
|
|
libcuda: bool,
|
|
libnvidia_encode: bool,
|
|
get_max_supported_version: bool,
|
|
driver_version_compatible: bool,
|
|
create_instance: bool,
|
|
cuda_init: bool,
|
|
cuda_device_count: Option<i32>,
|
|
cuda_device: bool,
|
|
cuda_context: bool,
|
|
checked_cuda_devices: u32,
|
|
supported_cuda_devices: u32,
|
|
open_encode_session: bool,
|
|
query_encode_guids: bool,
|
|
supports_h264: bool,
|
|
supports_h265: bool,
|
|
last_open_encode_session_status: Option<i32>,
|
|
}
|
|
|
|
impl NvencRuntimeProbe {
|
|
fn missing() -> Self {
|
|
Self {
|
|
libcuda: false,
|
|
libnvidia_encode: false,
|
|
get_max_supported_version: false,
|
|
driver_version_compatible: false,
|
|
create_instance: false,
|
|
cuda_init: false,
|
|
cuda_device_count: None,
|
|
cuda_device: false,
|
|
cuda_context: false,
|
|
checked_cuda_devices: 0,
|
|
supported_cuda_devices: 0,
|
|
open_encode_session: false,
|
|
query_encode_guids: false,
|
|
supports_h264: false,
|
|
supports_h265: false,
|
|
last_open_encode_session_status: None,
|
|
}
|
|
}
|
|
|
|
fn ready(self) -> bool {
|
|
self.libcuda
|
|
&& self.libnvidia_encode
|
|
&& self.get_max_supported_version
|
|
&& self.driver_version_compatible
|
|
&& self.create_instance
|
|
&& self.cuda_init
|
|
&& self.cuda_device
|
|
&& self.cuda_context
|
|
&& self.open_encode_session
|
|
&& self.has_supported_codec()
|
|
}
|
|
|
|
fn has_supported_codec(self) -> bool {
|
|
self.supports_h264 || self.supports_h265
|
|
}
|
|
|
|
fn supported_codecs(self) -> Vec<&'static str> {
|
|
let mut codecs = Vec::new();
|
|
if self.supports_h264 {
|
|
codecs.push("h264");
|
|
}
|
|
if self.supports_h265 {
|
|
codecs.push("h265");
|
|
}
|
|
codecs
|
|
}
|
|
|
|
fn missing_detail(self, platform: CapabilityPlatform) -> String {
|
|
let (cuda_library, encode_library) = nvenc_library_names(platform);
|
|
let mut missing = Vec::new();
|
|
if !self.libcuda {
|
|
missing.push(cuda_library.to_string());
|
|
}
|
|
if !self.libnvidia_encode {
|
|
missing.push(encode_library.to_string());
|
|
}
|
|
if self.libnvidia_encode && !self.get_max_supported_version {
|
|
missing.push("NvEncodeAPIGetMaxSupportedVersion".to_string());
|
|
}
|
|
if self.get_max_supported_version && !self.driver_version_compatible {
|
|
missing.push("NVENC driver support for the compiled SDK version".to_string());
|
|
}
|
|
if self.libnvidia_encode && !self.create_instance {
|
|
missing.push("NvEncodeAPICreateInstance".to_string());
|
|
}
|
|
if self.libcuda && !self.cuda_init {
|
|
missing.push("cuInit".to_string());
|
|
}
|
|
if self.cuda_init && self.cuda_device_count == Some(0) {
|
|
missing.push("CUDA devices".to_string());
|
|
}
|
|
if self.cuda_init && !self.cuda_device {
|
|
missing.push("CUDA device".to_string());
|
|
}
|
|
if self.cuda_device && !self.cuda_context {
|
|
missing.push("CUDA context".to_string());
|
|
}
|
|
if self.cuda_context && !self.open_encode_session {
|
|
missing.push(format!(
|
|
"NVENC encode session ({})",
|
|
self.open_encode_session_detail()
|
|
));
|
|
}
|
|
if self.open_encode_session && !self.query_encode_guids {
|
|
missing.push("NVENC encode GUID query".to_string());
|
|
}
|
|
if self.open_encode_session && self.query_encode_guids && !self.has_supported_codec() {
|
|
missing.push("NVENC H264/H265 encode GUID".to_string());
|
|
}
|
|
format!("missing {}", missing.join(", "))
|
|
}
|
|
|
|
fn unavailable_reason(self) -> &'static str {
|
|
if self.get_max_supported_version && !self.driver_version_compatible {
|
|
return "outdated_driver";
|
|
}
|
|
if self.cuda_init && self.cuda_device_count == Some(0) {
|
|
return "no_devices";
|
|
}
|
|
if self.cuda_context && !self.open_encode_session && self.checked_cuda_devices > 0 {
|
|
return "no_supported_devices";
|
|
}
|
|
if self.open_encode_session && !self.has_supported_codec() {
|
|
return "no_supported_codecs";
|
|
}
|
|
"runtime_prerequisite_missing"
|
|
}
|
|
|
|
fn open_encode_session_detail(self) -> String {
|
|
let mut parts = vec![format!(
|
|
"checked {} CUDA device{}",
|
|
self.checked_cuda_devices,
|
|
if self.checked_cuda_devices == 1 {
|
|
""
|
|
} else {
|
|
"s"
|
|
}
|
|
)];
|
|
if let Some(status) = self.last_open_encode_session_status {
|
|
parts.push(format!(
|
|
"last status {} ({status})",
|
|
nvenc_status_name(status)
|
|
));
|
|
}
|
|
parts.join("; ")
|
|
}
|
|
}
|
|
|
|
fn nvenc_status_name(status: i32) -> &'static str {
|
|
match status {
|
|
0 => "NV_ENC_SUCCESS",
|
|
1 => "NV_ENC_ERR_NO_ENCODE_DEVICE",
|
|
2 => "NV_ENC_ERR_UNSUPPORTED_DEVICE",
|
|
3 => "NV_ENC_ERR_INVALID_ENCODERDEVICE",
|
|
4 => "NV_ENC_ERR_INVALID_DEVICE",
|
|
5 => "NV_ENC_ERR_DEVICE_NOT_EXIST",
|
|
6 => "NV_ENC_ERR_INVALID_PTR",
|
|
7 => "NV_ENC_ERR_INVALID_EVENT",
|
|
8 => "NV_ENC_ERR_INVALID_PARAM",
|
|
9 => "NV_ENC_ERR_INVALID_CALL",
|
|
10 => "NV_ENC_ERR_OUT_OF_MEMORY",
|
|
11 => "NV_ENC_ERR_ENCODER_NOT_INITIALIZED",
|
|
12 => "NV_ENC_ERR_UNSUPPORTED_PARAM",
|
|
13 => "NV_ENC_ERR_LOCK_BUSY",
|
|
14 => "NV_ENC_ERR_NOT_ENOUGH_BUFFER",
|
|
15 => "NV_ENC_ERR_INVALID_VERSION",
|
|
16 => "NV_ENC_ERR_MAP_FAILED",
|
|
17 => "NV_ENC_ERR_NEED_MORE_INPUT",
|
|
18 => "NV_ENC_ERR_ENCODER_BUSY",
|
|
19 => "NV_ENC_ERR_EVENT_NOT_REGISTERD",
|
|
20 => "NV_ENC_ERR_GENERIC",
|
|
21 => "NV_ENC_ERR_INCOMPATIBLE_CLIENT_KEY",
|
|
22 => "NV_ENC_ERR_UNIMPLEMENTED",
|
|
23 => "NV_ENC_ERR_RESOURCE_REGISTER_FAILED",
|
|
24 => "NV_ENC_ERR_RESOURCE_NOT_REGISTERED",
|
|
25 => "NV_ENC_ERR_RESOURCE_NOT_MAPPED",
|
|
26 => "NV_ENC_ERR_NEED_MORE_OUTPUT",
|
|
_ => "NV_ENC_ERR_UNKNOWN",
|
|
}
|
|
}
|
|
|
|
fn videotoolbox_codec_detail(create_status: Option<i32>, prepare_status: Option<i32>) -> String {
|
|
match (create_status, prepare_status) {
|
|
(Some(0), Some(0)) => "hardware session ready".to_string(),
|
|
(Some(0), Some(status)) => format!(
|
|
"created, prepare status {} ({status})",
|
|
videotoolbox_status_name(status)
|
|
),
|
|
(Some(status), _) => format!(
|
|
"create status {} ({status})",
|
|
videotoolbox_status_name(status)
|
|
),
|
|
(None, _) => "probe did not run".to_string(),
|
|
}
|
|
}
|
|
|
|
fn videotoolbox_status_name(status: i32) -> &'static str {
|
|
match status {
|
|
0 => "noErr",
|
|
-12901 => "kVTPropertyNotSupportedErr",
|
|
-12902 => "kVTPropertyReadOnlyErr",
|
|
-12903 => "kVTParameterErr",
|
|
-12904 => "kVTInvalidSessionErr",
|
|
-12905 => "kVTAllocationFailedErr",
|
|
-12906 => "kVTPixelTransferNotSupportedErr",
|
|
-12907 => "kVTCouldNotFindVideoDecoderErr",
|
|
-12908 => "kVTCouldNotCreateInstanceErr",
|
|
-12909 => "kVTCouldNotFindVideoEncoderErr",
|
|
-12910 => "kVTVideoDecoderBadDataErr",
|
|
-12911 => "kVTVideoDecoderUnsupportedDataFormatErr",
|
|
-12912 => "kVTVideoDecoderMalfunctionErr",
|
|
-12913 => "kVTVideoEncoderMalfunctionErr",
|
|
-12914 => "kVTVideoDecoderNotAvailableNowErr",
|
|
-12915 => "kVTVideoEncoderNotAvailableNowErr",
|
|
-12916 => "kVTFormatDescriptionChangeNotSupportedErr",
|
|
_ => "OSStatus",
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
enum CapabilityPlatform {
|
|
Linux,
|
|
MacOS,
|
|
Windows,
|
|
Unsupported(&'static str),
|
|
}
|
|
|
|
fn nvenc_library_names(platform: CapabilityPlatform) -> (&'static str, &'static str) {
|
|
match platform {
|
|
CapabilityPlatform::Windows => {
|
|
if cfg!(target_pointer_width = "32") {
|
|
("nvcuda.dll", "nvEncodeAPI.dll")
|
|
} else {
|
|
("nvcuda.dll", "nvEncodeAPI64.dll")
|
|
}
|
|
}
|
|
_ => ("libcuda.so.1", "libnvidia-encode.so.1"),
|
|
}
|
|
}
|
|
|
|
fn nvenc_zero_copy_inputs(platform: CapabilityPlatform) -> &'static [&'static str] {
|
|
match platform {
|
|
CapabilityPlatform::Linux => NVENC_NATIVE_INPUTS,
|
|
_ => &[],
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
struct VideoToolboxRuntimeProbe {
|
|
h264_create_status: Option<i32>,
|
|
h264_prepare_status: Option<i32>,
|
|
h265_create_status: Option<i32>,
|
|
h265_prepare_status: Option<i32>,
|
|
}
|
|
|
|
impl VideoToolboxRuntimeProbe {
|
|
fn missing() -> Self {
|
|
Self {
|
|
h264_create_status: None,
|
|
h264_prepare_status: None,
|
|
h265_create_status: None,
|
|
h265_prepare_status: None,
|
|
}
|
|
}
|
|
|
|
fn supports_h264(self) -> bool {
|
|
self.h264_create_status == Some(0) && self.h264_prepare_status == Some(0)
|
|
}
|
|
|
|
fn supports_h265(self) -> bool {
|
|
self.h265_create_status == Some(0) && self.h265_prepare_status == Some(0)
|
|
}
|
|
|
|
fn ready(self) -> bool {
|
|
self.supports_h264() || self.supports_h265()
|
|
}
|
|
|
|
fn supported_codecs(self) -> Vec<&'static str> {
|
|
let mut codecs = Vec::new();
|
|
if self.supports_h264() {
|
|
codecs.push("h264");
|
|
}
|
|
if self.supports_h265() {
|
|
codecs.push("h265");
|
|
}
|
|
codecs
|
|
}
|
|
|
|
fn missing_detail(self) -> String {
|
|
format!(
|
|
"VideoToolbox hardware encoder sessions unavailable: h264 {}; h265 {}",
|
|
videotoolbox_codec_detail(self.h264_create_status, self.h264_prepare_status),
|
|
videotoolbox_codec_detail(self.h265_create_status, self.h265_prepare_status)
|
|
)
|
|
}
|
|
}
|
|
|
|
#[napi]
|
|
pub fn get_hardware_encoder_capability() -> HardwareEncoderCapability {
|
|
hardware_encoder_capability()
|
|
}
|
|
|
|
pub fn hardware_encoder_capability() -> HardwareEncoderCapability {
|
|
if cfg!(target_os = "macos") {
|
|
return describe_videotoolbox_capability(probe_macos_videotoolbox_runtime());
|
|
}
|
|
|
|
let platform = current_platform();
|
|
let compiled = match platform {
|
|
CapabilityPlatform::Linux => cfg!(fluxer_linux_nvenc),
|
|
CapabilityPlatform::Windows => {
|
|
cfg!(fluxer_windows_nvenc) && cfg!(fluxer_windows_nvenc_encoder)
|
|
}
|
|
_ => false,
|
|
};
|
|
describe_capability(
|
|
platform,
|
|
compiled,
|
|
if compiled {
|
|
probe_nvenc_runtime()
|
|
} else {
|
|
None
|
|
},
|
|
)
|
|
}
|
|
|
|
fn current_platform() -> CapabilityPlatform {
|
|
if cfg!(target_os = "linux") {
|
|
CapabilityPlatform::Linux
|
|
} else if cfg!(target_os = "macos") {
|
|
CapabilityPlatform::MacOS
|
|
} else if cfg!(target_os = "windows") {
|
|
CapabilityPlatform::Windows
|
|
} else {
|
|
CapabilityPlatform::Unsupported(std::env::consts::OS)
|
|
}
|
|
}
|
|
|
|
fn describe_videotoolbox_capability(
|
|
runtime_probe: Option<VideoToolboxRuntimeProbe>,
|
|
) -> HardwareEncoderCapability {
|
|
let runtime_probe = runtime_probe.unwrap_or_else(VideoToolboxRuntimeProbe::missing);
|
|
let runtime = runtime_probe.ready();
|
|
if runtime {
|
|
let codecs = runtime_probe.supported_codecs();
|
|
return HardwareEncoderCapability {
|
|
available: true,
|
|
backend: "videotoolbox".to_string(),
|
|
compiled: true,
|
|
runtime: true,
|
|
codecs: string_vec(&codecs),
|
|
zero_copy: false,
|
|
native_inputs: Vec::new(),
|
|
reason: None,
|
|
detail: Some(format!(
|
|
"VideoToolbox can create hardware-accelerated compression sessions for {}; supported hardware-codec probe set: {}",
|
|
codecs.join(", "),
|
|
VIDEOTOOLBOX_CODECS.join(", ")
|
|
)),
|
|
};
|
|
}
|
|
|
|
HardwareEncoderCapability {
|
|
available: false,
|
|
backend: "none".to_string(),
|
|
compiled: cfg!(fluxer_macos_videotoolbox),
|
|
runtime: false,
|
|
codecs: Vec::new(),
|
|
zero_copy: false,
|
|
native_inputs: Vec::new(),
|
|
reason: Some("no_supported_codecs".to_string()),
|
|
detail: Some(runtime_probe.missing_detail()),
|
|
}
|
|
}
|
|
|
|
fn describe_capability(
|
|
platform: CapabilityPlatform,
|
|
compiled: bool,
|
|
runtime_probe: Option<NvencRuntimeProbe>,
|
|
) -> HardwareEncoderCapability {
|
|
match platform {
|
|
CapabilityPlatform::Unsupported(os) => HardwareEncoderCapability {
|
|
available: false,
|
|
backend: "none".to_string(),
|
|
compiled: false,
|
|
runtime: false,
|
|
codecs: Vec::new(),
|
|
zero_copy: false,
|
|
native_inputs: Vec::new(),
|
|
reason: Some("unsupported_platform".to_string()),
|
|
detail: Some(format!(
|
|
"NVENC hardware encoder capability probing is supported on Linux and Windows only; current platform is {os}"
|
|
)),
|
|
},
|
|
CapabilityPlatform::MacOS => describe_videotoolbox_capability(None),
|
|
CapabilityPlatform::Linux | CapabilityPlatform::Windows => {
|
|
let runtime_probe = runtime_probe.unwrap_or_else(NvencRuntimeProbe::missing);
|
|
let runtime = runtime_probe.ready();
|
|
if compiled && runtime {
|
|
let codecs = runtime_probe.supported_codecs();
|
|
let native_inputs = nvenc_zero_copy_inputs(platform);
|
|
let native_input_detail = if native_inputs.is_empty() {
|
|
"none".to_string()
|
|
} else {
|
|
native_inputs.join(", ")
|
|
};
|
|
let (cuda_library, encode_library) = nvenc_library_names(platform);
|
|
return HardwareEncoderCapability {
|
|
available: true,
|
|
backend: "nvenc".to_string(),
|
|
compiled: true,
|
|
runtime: true,
|
|
codecs: string_vec(&codecs),
|
|
zero_copy: !native_inputs.is_empty(),
|
|
native_inputs: string_vec(native_inputs),
|
|
reason: None,
|
|
detail: Some(format!(
|
|
"{cuda_library} and {encode_library} can open an NVENC encode session for {}; native zero-copy inputs: {}",
|
|
codecs.join(", "),
|
|
native_input_detail
|
|
)),
|
|
};
|
|
}
|
|
|
|
let (reason, detail) = if !compiled {
|
|
match platform {
|
|
CapabilityPlatform::Linux => (
|
|
"nvenc_not_compiled",
|
|
"NVENC support was not compiled for this Linux target; verify the architecture is supported and the vendored CUDA Driver API header is present".to_string(),
|
|
),
|
|
CapabilityPlatform::Windows => (
|
|
"nvenc_not_compiled",
|
|
"Windows NVENC publishing is not compiled into this addon yet; CUDA/NVENC runtime probing alone is not enough to safely publish H.265 without codec fallback".to_string(),
|
|
),
|
|
_ => unreachable!(),
|
|
}
|
|
} else {
|
|
(
|
|
runtime_probe.unavailable_reason(),
|
|
runtime_probe.missing_detail(platform),
|
|
)
|
|
};
|
|
|
|
HardwareEncoderCapability {
|
|
available: false,
|
|
backend: "none".to_string(),
|
|
compiled,
|
|
runtime,
|
|
codecs: Vec::new(),
|
|
zero_copy: false,
|
|
native_inputs: Vec::new(),
|
|
reason: Some(reason.to_string()),
|
|
detail: Some(detail),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn string_vec(values: &[&str]) -> Vec<String> {
|
|
values.iter().map(|value| (*value).to_string()).collect()
|
|
}
|
|
|
|
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
|
fn probe_nvenc_runtime() -> Option<NvencRuntimeProbe> {
|
|
nvenc_probe::probe_runtime()
|
|
}
|
|
|
|
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
|
mod nvenc_probe {
|
|
use super::{NvencRuntimeProbe, current_platform, nvenc_library_names};
|
|
use libloading::Library;
|
|
use std::ffi::c_void;
|
|
use std::mem::transmute;
|
|
|
|
const CUDA_SUCCESS: i32 = 0;
|
|
const NV_ENC_SUCCESS: i32 = 0;
|
|
const NV_ENC_DEVICE_TYPE_CUDA: u32 = 1;
|
|
const NVENCAPI_MAJOR_VERSION: u32 = 12;
|
|
const NVENCAPI_MINOR_VERSION: u32 = 0;
|
|
const NVENCAPI_VERSION: u32 = NVENCAPI_MAJOR_VERSION | (NVENCAPI_MINOR_VERSION << 24);
|
|
const NVENCAPI_COMPARABLE_VERSION: u32 = (NVENCAPI_MAJOR_VERSION << 4) | NVENCAPI_MINOR_VERSION;
|
|
const NV_ENCODE_API_FUNCTION_LIST_VER: u32 = nvencapi_struct_version(2);
|
|
const NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS_VER: u32 = nvencapi_struct_version(1);
|
|
|
|
type CuInit = unsafe extern "system" fn(u32) -> i32;
|
|
type CuDeviceGetCount = unsafe extern "system" fn(*mut i32) -> i32;
|
|
type CuDeviceGet = unsafe extern "system" fn(*mut i32, i32) -> i32;
|
|
type CuCtxCreateLegacy = unsafe extern "system" fn(*mut *mut c_void, u32, i32) -> i32;
|
|
type CuCtxCreateCuda13 =
|
|
unsafe extern "system" fn(*mut *mut c_void, *mut c_void, u32, i32) -> i32;
|
|
type CuCtxDestroy = unsafe extern "system" fn(*mut c_void) -> i32;
|
|
type NvEncodeApiGetMaxSupportedVersion = unsafe extern "system" fn(*mut u32) -> i32;
|
|
type NvEncodeApiCreateInstance = unsafe extern "system" fn(*mut NvEncodeApiFunctionList) -> i32;
|
|
type NvEncGetEncodeGuidCount = unsafe extern "system" fn(*mut c_void, *mut u32) -> i32;
|
|
type NvEncGetEncodeGuids =
|
|
unsafe extern "system" fn(*mut c_void, *mut Guid, u32, *mut u32) -> i32;
|
|
type NvEncOpenEncodeSessionEx =
|
|
unsafe extern "system" fn(*mut NvEncOpenEncodeSessionExParams, *mut *mut c_void) -> i32;
|
|
type NvEncDestroyEncoder = unsafe extern "system" fn(*mut c_void) -> i32;
|
|
|
|
#[repr(C)]
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
struct Guid {
|
|
data1: u32,
|
|
data2: u16,
|
|
data3: u16,
|
|
data4: [u8; 8],
|
|
}
|
|
|
|
const NV_ENC_CODEC_H264_GUID: Guid = Guid {
|
|
data1: 0x6bc82762,
|
|
data2: 0x4e63,
|
|
data3: 0x4ca4,
|
|
data4: [0xaa, 0x85, 0x1e, 0x50, 0xf3, 0x21, 0xf6, 0xbf],
|
|
};
|
|
const NV_ENC_CODEC_HEVC_GUID: Guid = Guid {
|
|
data1: 0x790cdc88,
|
|
data2: 0x4522,
|
|
data3: 0x4d7b,
|
|
data4: [0x94, 0x25, 0xbd, 0xa9, 0x97, 0x5f, 0x76, 0x03],
|
|
};
|
|
|
|
#[repr(C)]
|
|
struct NvEncodeApiFunctionList {
|
|
version: u32,
|
|
reserved: u32,
|
|
functions: [*mut c_void; 41],
|
|
reserved2: [*mut c_void; 277],
|
|
}
|
|
|
|
impl NvEncodeApiFunctionList {
|
|
fn new() -> Self {
|
|
Self {
|
|
version: NV_ENCODE_API_FUNCTION_LIST_VER,
|
|
reserved: 0,
|
|
functions: [std::ptr::null_mut(); 41],
|
|
reserved2: [std::ptr::null_mut(); 277],
|
|
}
|
|
}
|
|
|
|
fn destroy_encoder(&self) -> Option<NvEncDestroyEncoder> {
|
|
let pointer = self.functions[27];
|
|
(!pointer.is_null()).then(|| unsafe { transmute(pointer) })
|
|
}
|
|
|
|
fn get_encode_guid_count(&self) -> Option<NvEncGetEncodeGuidCount> {
|
|
let pointer = self.functions[1];
|
|
(!pointer.is_null()).then(|| unsafe { transmute(pointer) })
|
|
}
|
|
|
|
fn get_encode_guids(&self) -> Option<NvEncGetEncodeGuids> {
|
|
let pointer = self.functions[4];
|
|
(!pointer.is_null()).then(|| unsafe { transmute(pointer) })
|
|
}
|
|
|
|
fn open_encode_session_ex(&self) -> Option<NvEncOpenEncodeSessionEx> {
|
|
let pointer = self.functions[29];
|
|
(!pointer.is_null()).then(|| unsafe { transmute(pointer) })
|
|
}
|
|
}
|
|
|
|
#[repr(C)]
|
|
struct NvEncOpenEncodeSessionExParams {
|
|
version: u32,
|
|
device_type: u32,
|
|
device: *mut c_void,
|
|
reserved: *mut c_void,
|
|
api_version: u32,
|
|
reserved1: [u32; 253],
|
|
reserved2: [*mut c_void; 64],
|
|
}
|
|
|
|
impl NvEncOpenEncodeSessionExParams {
|
|
fn cuda(device: *mut c_void) -> Self {
|
|
Self {
|
|
version: NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS_VER,
|
|
device_type: NV_ENC_DEVICE_TYPE_CUDA,
|
|
device,
|
|
reserved: std::ptr::null_mut(),
|
|
api_version: NVENCAPI_VERSION,
|
|
reserved1: [0; 253],
|
|
reserved2: [std::ptr::null_mut(); 64],
|
|
}
|
|
}
|
|
}
|
|
|
|
enum CuCtxCreate {
|
|
Legacy(CuCtxCreateLegacy),
|
|
Cuda13(CuCtxCreateCuda13),
|
|
}
|
|
|
|
impl CuCtxCreate {
|
|
unsafe fn call(&self, context: *mut *mut c_void, device: i32) -> i32 {
|
|
match self {
|
|
Self::Legacy(create) => unsafe { create(context, 0, device) },
|
|
Self::Cuda13(create) => unsafe { create(context, std::ptr::null_mut(), 0, device) },
|
|
}
|
|
}
|
|
}
|
|
|
|
const fn nvencapi_struct_version(version: u32) -> u32 {
|
|
NVENCAPI_VERSION | (version << 16) | (0x7 << 28)
|
|
}
|
|
|
|
fn load_cuda_context_create(cuda: &Library) -> Option<CuCtxCreate> {
|
|
let cuda_version = option_env!("FLUXER_CUDA_VERSION")
|
|
.and_then(|value| value.parse::<u32>().ok())
|
|
.unwrap_or(0);
|
|
if cuda_version >= 13000 {
|
|
unsafe { cuda.get::<CuCtxCreateCuda13>(b"cuCtxCreate\0") }
|
|
.map(|symbol| CuCtxCreate::Cuda13(*symbol))
|
|
.ok()
|
|
.or_else(|| {
|
|
unsafe { cuda.get::<CuCtxCreateLegacy>(b"cuCtxCreate_v2\0") }
|
|
.map(|symbol| CuCtxCreate::Legacy(*symbol))
|
|
.ok()
|
|
})
|
|
} else {
|
|
unsafe { cuda.get::<CuCtxCreateLegacy>(b"cuCtxCreate_v2\0") }
|
|
.or_else(|_| unsafe { cuda.get::<CuCtxCreateLegacy>(b"cuCtxCreate\0") })
|
|
.map(|symbol| CuCtxCreate::Legacy(*symbol))
|
|
.ok()
|
|
}
|
|
}
|
|
|
|
fn load_cuda_context_destroy(cuda: &Library) -> Option<CuCtxDestroy> {
|
|
unsafe { cuda.get::<CuCtxDestroy>(b"cuCtxDestroy_v2\0") }
|
|
.or_else(|_| unsafe { cuda.get::<CuCtxDestroy>(b"cuCtxDestroy\0") })
|
|
.map(|symbol| *symbol)
|
|
.ok()
|
|
}
|
|
|
|
fn query_nvenc_codecs(
|
|
api: &NvEncodeApiFunctionList,
|
|
encoder: *mut c_void,
|
|
probe: &mut NvencRuntimeProbe,
|
|
) {
|
|
let (Some(get_count), Some(get_guids)) =
|
|
(api.get_encode_guid_count(), api.get_encode_guids())
|
|
else {
|
|
return;
|
|
};
|
|
let mut count = 0;
|
|
if unsafe { get_count(encoder, &mut count) } != NV_ENC_SUCCESS || count == 0 {
|
|
return;
|
|
}
|
|
let mut guids = vec![
|
|
Guid {
|
|
data1: 0,
|
|
data2: 0,
|
|
data3: 0,
|
|
data4: [0; 8],
|
|
};
|
|
count as usize
|
|
];
|
|
let mut written = 0;
|
|
if unsafe { get_guids(encoder, guids.as_mut_ptr(), count, &mut written) } != NV_ENC_SUCCESS
|
|
{
|
|
return;
|
|
}
|
|
probe.query_encode_guids = true;
|
|
for guid in guids.into_iter().take(written as usize) {
|
|
if guid == NV_ENC_CODEC_H264_GUID {
|
|
probe.supports_h264 = true;
|
|
} else if guid == NV_ENC_CODEC_HEVC_GUID {
|
|
probe.supports_h265 = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(super) fn probe_runtime() -> Option<NvencRuntimeProbe> {
|
|
let mut probe = NvencRuntimeProbe::missing();
|
|
let Some((cuda, encode)) = load_nvenc_libraries(&mut probe) else {
|
|
return Some(probe);
|
|
};
|
|
let Some(api) = create_encode_api(&encode, &mut probe) else {
|
|
return Some(probe);
|
|
};
|
|
let Some((cu_device_get, cuda_device_count)) = init_cuda(&cuda, &mut probe) else {
|
|
return Some(probe);
|
|
};
|
|
probe_encode_sessions(&cuda, &api, cu_device_get, cuda_device_count, &mut probe);
|
|
Some(probe)
|
|
}
|
|
|
|
fn load_nvenc_libraries(probe: &mut NvencRuntimeProbe) -> Option<(Library, Library)> {
|
|
let (cuda_library, encode_library) = nvenc_library_names(current_platform());
|
|
let Ok(cuda) = (unsafe { Library::new(cuda_library) }) else {
|
|
return None;
|
|
};
|
|
probe.libcuda = true;
|
|
|
|
let Ok(encode) = (unsafe { Library::new(encode_library) }) else {
|
|
return None;
|
|
};
|
|
probe.libnvidia_encode = true;
|
|
Some((cuda, encode))
|
|
}
|
|
|
|
fn create_encode_api(
|
|
encode: &Library,
|
|
probe: &mut NvencRuntimeProbe,
|
|
) -> Option<NvEncodeApiFunctionList> {
|
|
let Ok(get_max_supported_version) = (unsafe {
|
|
encode.get::<NvEncodeApiGetMaxSupportedVersion>(b"NvEncodeAPIGetMaxSupportedVersion\0")
|
|
}) else {
|
|
return None;
|
|
};
|
|
probe.get_max_supported_version = true;
|
|
|
|
let mut max_supported_version = 0;
|
|
if unsafe { get_max_supported_version(&mut max_supported_version) } != NV_ENC_SUCCESS
|
|
|| NVENCAPI_COMPARABLE_VERSION > max_supported_version
|
|
{
|
|
return None;
|
|
}
|
|
probe.driver_version_compatible = true;
|
|
|
|
let Ok(create_instance) =
|
|
(unsafe { encode.get::<NvEncodeApiCreateInstance>(b"NvEncodeAPICreateInstance\0") })
|
|
else {
|
|
return None;
|
|
};
|
|
let mut api = NvEncodeApiFunctionList::new();
|
|
if unsafe { create_instance(&mut api) } != NV_ENC_SUCCESS {
|
|
return None;
|
|
}
|
|
probe.create_instance = true;
|
|
Some(api)
|
|
}
|
|
|
|
fn init_cuda(cuda: &Library, probe: &mut NvencRuntimeProbe) -> Option<(CuDeviceGet, i32)> {
|
|
let Ok(cu_init) = (unsafe { cuda.get::<CuInit>(b"cuInit\0") }) else {
|
|
return None;
|
|
};
|
|
if unsafe { cu_init(0) } != CUDA_SUCCESS {
|
|
return None;
|
|
}
|
|
probe.cuda_init = true;
|
|
|
|
let Ok(cu_device_get) = (unsafe { cuda.get::<CuDeviceGet>(b"cuDeviceGet\0") }) else {
|
|
return None;
|
|
};
|
|
let cu_device_get = *cu_device_get;
|
|
|
|
let Ok(cu_device_get_count) =
|
|
(unsafe { cuda.get::<CuDeviceGetCount>(b"cuDeviceGetCount\0") })
|
|
else {
|
|
return None;
|
|
};
|
|
let mut cuda_device_count = 0;
|
|
if unsafe { cu_device_get_count(&mut cuda_device_count) } != CUDA_SUCCESS {
|
|
return None;
|
|
}
|
|
probe.cuda_device_count = Some(cuda_device_count);
|
|
if cuda_device_count <= 0 {
|
|
return None;
|
|
}
|
|
Some((cu_device_get, cuda_device_count))
|
|
}
|
|
|
|
fn probe_encode_sessions(
|
|
cuda: &Library,
|
|
api: &NvEncodeApiFunctionList,
|
|
cu_device_get: CuDeviceGet,
|
|
cuda_device_count: i32,
|
|
probe: &mut NvencRuntimeProbe,
|
|
) {
|
|
let Some(cu_ctx_create) = load_cuda_context_create(cuda) else {
|
|
return;
|
|
};
|
|
let Some(cu_ctx_destroy) = load_cuda_context_destroy(cuda) else {
|
|
return;
|
|
};
|
|
let Some(open_encode_session_ex) = api.open_encode_session_ex() else {
|
|
return;
|
|
};
|
|
let Some(destroy_encoder) = api.destroy_encoder() else {
|
|
return;
|
|
};
|
|
|
|
for ordinal in 0..cuda_device_count {
|
|
let mut cuda_device = 0;
|
|
if unsafe { cu_device_get(&mut cuda_device, ordinal) } != CUDA_SUCCESS {
|
|
continue;
|
|
}
|
|
probe.cuda_device = true;
|
|
|
|
let mut cuda_context = std::ptr::null_mut();
|
|
if unsafe { cu_ctx_create.call(&mut cuda_context, cuda_device) } != CUDA_SUCCESS
|
|
|| cuda_context.is_null()
|
|
{
|
|
continue;
|
|
}
|
|
probe.cuda_context = true;
|
|
probe.checked_cuda_devices += 1;
|
|
|
|
let mut params = NvEncOpenEncodeSessionExParams::cuda(cuda_context);
|
|
let mut encoder = std::ptr::null_mut();
|
|
let status = unsafe { open_encode_session_ex(&mut params, &mut encoder) };
|
|
probe.last_open_encode_session_status = Some(status);
|
|
if status == NV_ENC_SUCCESS && !encoder.is_null() {
|
|
query_nvenc_codecs(api, encoder, probe);
|
|
let _ = unsafe { destroy_encoder(encoder) };
|
|
probe.supported_cuda_devices += 1;
|
|
probe.open_encode_session = true;
|
|
}
|
|
|
|
let _ = unsafe { cu_ctx_destroy(cuda_context) };
|
|
|
|
if probe.open_encode_session && probe.has_supported_codec() {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(not(any(target_os = "linux", target_os = "windows")))]
|
|
fn probe_nvenc_runtime() -> Option<NvencRuntimeProbe> {
|
|
None
|
|
}
|
|
|
|
#[cfg(target_os = "macos")]
|
|
fn probe_macos_videotoolbox_runtime() -> Option<VideoToolboxRuntimeProbe> {
|
|
use std::ffi::c_void;
|
|
|
|
type CFAllocatorRef = *const c_void;
|
|
type CFDictionaryRef = *const c_void;
|
|
type CFIndex = isize;
|
|
type CFStringRef = *const c_void;
|
|
type CFTypeRef = *const c_void;
|
|
type CMVideoCodecType = u32;
|
|
type OSStatus = i32;
|
|
type VTCompressionSessionRef = *mut c_void;
|
|
type VTCompressionOutputCallback = Option<
|
|
unsafe extern "C" fn(
|
|
output_callback_refcon: *mut c_void,
|
|
source_frame_refcon: *mut c_void,
|
|
status: OSStatus,
|
|
info_flags: u32,
|
|
sample_buffer: *mut c_void,
|
|
),
|
|
>;
|
|
|
|
const K_CM_VIDEO_CODEC_TYPE_H264: CMVideoCodecType = 0x6176_6331;
|
|
const K_CM_VIDEO_CODEC_TYPE_HEVC: CMVideoCodecType = 0x6876_6331;
|
|
|
|
unsafe extern "C" {
|
|
static kCFBooleanTrue: CFTypeRef;
|
|
static kVTVideoEncoderSpecification_RequireHardwareAcceleratedVideoEncoder: CFStringRef;
|
|
|
|
fn CFDictionaryCreate(
|
|
allocator: CFAllocatorRef,
|
|
keys: *const *const c_void,
|
|
values: *const *const c_void,
|
|
num_values: CFIndex,
|
|
key_callbacks: *const c_void,
|
|
value_callbacks: *const c_void,
|
|
) -> CFDictionaryRef;
|
|
fn CFRelease(value: CFTypeRef);
|
|
fn VTCompressionSessionCreate(
|
|
allocator: CFAllocatorRef,
|
|
width: i32,
|
|
height: i32,
|
|
codec_type: CMVideoCodecType,
|
|
encoder_specification: CFDictionaryRef,
|
|
image_buffer_attributes: CFDictionaryRef,
|
|
compressed_data_allocator: CFAllocatorRef,
|
|
output_callback: VTCompressionOutputCallback,
|
|
output_callback_refcon: *mut c_void,
|
|
compression_session_out: *mut VTCompressionSessionRef,
|
|
) -> OSStatus;
|
|
fn VTCompressionSessionInvalidate(session: VTCompressionSessionRef);
|
|
fn VTCompressionSessionPrepareToEncodeFrames(session: VTCompressionSessionRef) -> OSStatus;
|
|
}
|
|
|
|
unsafe fn hardware_required_dictionary() -> CFDictionaryRef {
|
|
let key = unsafe { kVTVideoEncoderSpecification_RequireHardwareAcceleratedVideoEncoder };
|
|
let value = unsafe { kCFBooleanTrue };
|
|
let keys = [key];
|
|
let values = [value];
|
|
unsafe {
|
|
CFDictionaryCreate(
|
|
std::ptr::null(),
|
|
keys.as_ptr(),
|
|
values.as_ptr(),
|
|
1,
|
|
std::ptr::null(),
|
|
std::ptr::null(),
|
|
)
|
|
}
|
|
}
|
|
|
|
unsafe fn probe_codec(codec_type: CMVideoCodecType) -> (Option<i32>, Option<i32>) {
|
|
let encoder_specification = unsafe { hardware_required_dictionary() };
|
|
let mut session = std::ptr::null_mut();
|
|
let create_status = unsafe {
|
|
VTCompressionSessionCreate(
|
|
std::ptr::null(),
|
|
1280,
|
|
720,
|
|
codec_type,
|
|
encoder_specification,
|
|
std::ptr::null(),
|
|
std::ptr::null(),
|
|
None,
|
|
std::ptr::null_mut(),
|
|
&mut session,
|
|
)
|
|
};
|
|
if !encoder_specification.is_null() {
|
|
unsafe { CFRelease(encoder_specification as CFTypeRef) };
|
|
}
|
|
if create_status != 0 || session.is_null() {
|
|
return (Some(create_status), None);
|
|
}
|
|
|
|
let prepare_status = unsafe { VTCompressionSessionPrepareToEncodeFrames(session) };
|
|
unsafe {
|
|
VTCompressionSessionInvalidate(session);
|
|
CFRelease(session as CFTypeRef);
|
|
}
|
|
(Some(create_status), Some(prepare_status))
|
|
}
|
|
|
|
let (h264_create_status, h264_prepare_status) =
|
|
unsafe { probe_codec(K_CM_VIDEO_CODEC_TYPE_H264) };
|
|
let (h265_create_status, h265_prepare_status) =
|
|
unsafe { probe_codec(K_CM_VIDEO_CODEC_TYPE_HEVC) };
|
|
|
|
Some(VideoToolboxRuntimeProbe {
|
|
h264_create_status,
|
|
h264_prepare_status,
|
|
h265_create_status,
|
|
h265_prepare_status,
|
|
})
|
|
}
|
|
|
|
#[cfg(not(target_os = "macos"))]
|
|
fn probe_macos_videotoolbox_runtime() -> Option<VideoToolboxRuntimeProbe> {
|
|
None
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn runtime_ready() -> NvencRuntimeProbe {
|
|
NvencRuntimeProbe {
|
|
libcuda: true,
|
|
libnvidia_encode: true,
|
|
get_max_supported_version: true,
|
|
driver_version_compatible: true,
|
|
create_instance: true,
|
|
cuda_init: true,
|
|
cuda_device_count: Some(1),
|
|
cuda_device: true,
|
|
cuda_context: true,
|
|
checked_cuda_devices: 1,
|
|
supported_cuda_devices: 1,
|
|
open_encode_session: true,
|
|
query_encode_guids: true,
|
|
supports_h264: true,
|
|
supports_h265: true,
|
|
last_open_encode_session_status: Some(0),
|
|
}
|
|
}
|
|
|
|
fn videotoolbox_ready() -> VideoToolboxRuntimeProbe {
|
|
VideoToolboxRuntimeProbe {
|
|
h264_create_status: Some(0),
|
|
h264_prepare_status: Some(0),
|
|
h265_create_status: Some(0),
|
|
h265_prepare_status: Some(0),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn reports_h264_only_when_hevc_guid_is_absent() {
|
|
let capability = describe_capability(
|
|
CapabilityPlatform::Linux,
|
|
true,
|
|
Some(NvencRuntimeProbe {
|
|
supports_h265: false,
|
|
..runtime_ready()
|
|
}),
|
|
);
|
|
|
|
assert!(capability.available);
|
|
assert_eq!(capability.codecs, vec!["h264"]);
|
|
assert!(capability.zero_copy);
|
|
}
|
|
|
|
#[test]
|
|
fn reports_no_supported_codecs_when_guid_query_finds_no_fluxer_codecs() {
|
|
let capability = describe_capability(
|
|
CapabilityPlatform::Linux,
|
|
true,
|
|
Some(NvencRuntimeProbe {
|
|
supports_h264: false,
|
|
supports_h265: false,
|
|
..runtime_ready()
|
|
}),
|
|
);
|
|
|
|
assert!(!capability.available);
|
|
assert_eq!(capability.reason.as_deref(), Some("no_supported_codecs"));
|
|
assert_eq!(capability.codecs, Vec::<String>::new());
|
|
assert!(
|
|
capability
|
|
.detail
|
|
.as_deref()
|
|
.unwrap_or_default()
|
|
.contains("NVENC H264/H265 encode GUID")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn reports_videotoolbox_h264_only_when_hevc_session_is_unavailable() {
|
|
let capability = describe_videotoolbox_capability(Some(VideoToolboxRuntimeProbe {
|
|
h265_create_status: Some(-12909),
|
|
h265_prepare_status: None,
|
|
..videotoolbox_ready()
|
|
}));
|
|
|
|
assert!(capability.available);
|
|
assert_eq!(capability.backend, "videotoolbox");
|
|
assert_eq!(capability.codecs, vec!["h264"]);
|
|
assert_eq!(capability.reason, None);
|
|
}
|
|
}
|