#![cfg_attr(
not(any(target_os = "linux", target_os = "macos", target_os = "windows")),
allow(dead_code, unused_variables, clippy::needless_pass_by_value)
)]
#[cfg(target_os = "windows")]
mod d3d12va;
#[cfg(target_os = "macos")]
mod videotoolbox;
#[cfg(target_os = "linux")]
mod vulkan;
use std::path::Path;
use std::sync::atomic::{AtomicU64, Ordering};
use alpha2_media::{ColorMetadata, EncodeError, VideoRateControl};
use alpha2_time::{FrameRate, MediaTime};
#[cfg(all(test, target_os = "windows"))]
pub(crate) use d3d12va::encode_no_rate_control_frames;
#[cfg(target_os = "windows")]
pub(crate) use d3d12va::probe_d3d12va_rate_control;
#[cfg(target_os = "windows")]
pub use d3d12va::{
D3D12EncodeProbe, D3D12RateControlOpen, D3D12RateControlRunner, D3D12TestEncodeRunner,
D3D12VA_TEST_ENCODE_SIZE, d3d12va_encoder, open_d3d12va_rate_control, probe_d3d12va_encode,
set_d3d12va_rate_control_runner, set_d3d12va_test_encode_runner,
};
#[cfg(target_os = "macos")]
pub use videotoolbox::{VideoToolboxEncodeProbe, probe_videotoolbox_encode};
#[cfg(target_os = "linux")]
pub(crate) use vulkan::probe_vulkan_rate_control;
#[cfg(target_os = "linux")]
pub use vulkan::{VulkanEncodeProbe, probe_vulkan_encode};
use crate::{EncodeStageTimings, NativeVideoCodec, NativeVideoEncoderSelection};
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum HardwareEncodeBackend {
Vulkan,
VideoToolbox,
D3D12Va,
}
impl HardwareEncodeBackend {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Vulkan => "vulkan",
Self::VideoToolbox => "videotoolbox",
Self::D3D12Va => "d3d12va",
}
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum EncoderInputLayout {
Nv12,
}
impl EncoderInputLayout {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Nv12 => "nv12",
}
}
#[must_use]
pub const fn frame_bytes(self, width: u32, height: u32) -> u64 {
match self {
Self::Nv12 => {
let luma = width as u64 * height as u64;
luma + luma / 2
}
}
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum EncoderInputHandoff {
GpuCopy,
DirectTarget,
}
impl EncoderInputHandoff {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::GpuCopy => "gpu-copy",
Self::DirectTarget => "direct-target",
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct HardwareEncodeCapability {
pub backend: HardwareEncodeBackend,
pub encoder: &'static str,
pub codec: NativeVideoCodec,
pub gpu_input_only: bool,
pub input_layout: EncoderInputLayout,
pub handoff: EncoderInputHandoff,
pub shares_renderer_device: bool,
pub minimum: (u32, u32),
pub maximum: (u32, u32),
pub granularity: (u32, u32),
pub max_p_picture_l0_references: Option<u32>,
pub max_b_picture_l0_references: Option<u32>,
pub max_l1_references: Option<u32>,
pub rate_control_modes: Vec<&'static str>,
pub plane_ring_slots: usize,
pub handoff_retained_surfaces: usize,
pub codec_retained_surfaces: usize,
}
impl HardwareEncodeCapability {
#[must_use]
pub const fn retained_surfaces(&self) -> usize {
self.handoff_retained_surfaces
.saturating_add(self.codec_retained_surfaces)
}
}
impl HardwareEncodeCapability {
#[must_use]
pub const fn coded(&self, width: u32, height: u32) -> (u32, u32) {
(
align_up(width, self.granularity.0),
align_up(height, self.granularity.1),
)
}
pub fn accepts(&self, width: u32, height: u32) -> Result<(), String> {
if !width.is_multiple_of(2) || !height.is_multiple_of(2) {
return Err(format!(
"a {width}x{height} output has no 4:2:0 chroma plane"
));
}
let coded = self.coded(width, height);
if coded.0 < self.minimum.0
|| coded.1 < self.minimum.1
|| coded.0 > self.maximum.0
|| coded.1 > self.maximum.1
{
return Err(format!(
"a {width}x{height} output codes as {}x{}, outside {}'s {}x{}..{}x{}",
coded.0,
coded.1,
self.encoder,
self.minimum.0,
self.minimum.1,
self.maximum.0,
self.maximum.1
));
}
Ok(())
}
}
const fn align_up(value: u32, granularity: u32) -> u32 {
if granularity <= 1 {
return value;
}
value.div_ceil(granularity).saturating_mul(granularity)
}
#[derive(Clone)]
pub struct HardwareEncodeDevice {
#[cfg(target_os = "linux")]
vulkan: alpha2_native_gpu::SharedVulkanContext,
#[cfg(target_os = "macos")]
metal: alpha2_native_gpu::SharedMetalContext,
#[cfg(target_os = "windows")]
d3d12: alpha2_native_gpu::SharedD3D12Context,
}
impl std::fmt::Debug for HardwareEncodeDevice {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("HardwareEncodeDevice")
.finish_non_exhaustive()
}
}
#[derive(Clone, Debug)]
pub struct HardwareEncodeRequest<'a> {
pub width: u32,
pub height: u32,
pub codec: NativeVideoCodec,
pub frame_rate: FrameRate,
pub rate_control: VideoRateControl,
pub color: ColorMetadata,
pub output: &'a Path,
pub container: alpha2_media::Container,
pub audio: Option<crate::AudioStreamConfig>,
}
pub(crate) fn session_frame_rate(rate: FrameRate) -> Result<(i32, i32), EncodeError> {
match (
i32::try_from(rate.numerator()),
i32::try_from(rate.denominator()),
) {
(Ok(numerator), Ok(denominator)) => Ok((numerator, denominator)),
_ => Err(EncodeError::Adapter(format!(
"frame rate {}/{} is out of range",
rate.numerator(),
rate.denominator()
))),
}
}
#[derive(Debug)]
pub enum HardwareEncodeOpenError {
Refused(String),
Failed(EncodeError),
}
impl std::fmt::Display for HardwareEncodeOpenError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Refused(reason) => formatter.write_str(reason),
Self::Failed(error) => write!(formatter, "{error}"),
}
}
}
impl HardwareEncodeDevice {
#[cfg(target_os = "linux")]
#[must_use]
pub const fn vulkan(context: alpha2_native_gpu::SharedVulkanContext) -> Self {
Self { vulkan: context }
}
#[cfg(target_os = "macos")]
#[must_use]
pub const fn metal(context: alpha2_native_gpu::SharedMetalContext) -> Self {
Self { metal: context }
}
#[cfg(target_os = "windows")]
#[must_use]
pub const fn d3d12(context: alpha2_native_gpu::SharedD3D12Context) -> Self {
Self { d3d12: context }
}
#[cfg_attr(
not(any(target_os = "linux", target_os = "macos", target_os = "windows")),
allow(clippy::unused_self)
)]
pub fn capability(&self, codec: NativeVideoCodec) -> Result<HardwareEncodeCapability, String> {
#[cfg(target_os = "windows")]
return d3d12va::capability(&self.d3d12, codec);
#[cfg(any(target_os = "linux", target_os = "macos"))]
{
let capability = self.preferred_capability()?;
if capability.codec == codec {
Ok(capability)
} else {
Err(format!(
"{} encodes {}, not {}",
capability.encoder,
capability.codec.as_str(),
codec.as_str()
))
}
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
Err("this platform has no hardware encode backend yet".to_owned())
}
#[cfg_attr(
not(any(target_os = "linux", target_os = "macos", target_os = "windows")),
allow(clippy::unused_self)
)]
pub fn preferred_capability(&self) -> Result<HardwareEncodeCapability, String> {
#[cfg(target_os = "linux")]
return vulkan::capability(&self.vulkan);
#[cfg(target_os = "macos")]
return videotoolbox::capability(&self.metal);
#[cfg(target_os = "windows")]
return d3d12va::preferred_capability(&self.d3d12);
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
Err("this platform has no hardware encode backend yet".to_owned())
}
#[cfg_attr(
not(any(target_os = "linux", target_os = "macos", target_os = "windows")),
allow(clippy::unused_self)
)]
pub fn open(
&self,
request: &HardwareEncodeRequest<'_>,
) -> Result<(HardwareEncodeSession, HardwareEncodeInput), HardwareEncodeOpenError> {
let generation = NEXT_POOL_GENERATION.fetch_add(1, Ordering::Relaxed);
#[cfg(any(target_os = "linux", target_os = "macos"))]
self.capability(request.codec)
.map_err(HardwareEncodeOpenError::Refused)?;
#[cfg(target_os = "linux")]
{
let (session, input) = vulkan::open(&self.vulkan, request, generation)?;
Ok((
HardwareEncodeSession(SessionBackend::Vulkan(session)),
HardwareEncodeInput {
backend: InputBackend::Vulkan(input),
generation,
},
))
}
#[cfg(target_os = "macos")]
{
let (session, input) = videotoolbox::open(&self.metal, request, generation)?;
Ok((
HardwareEncodeSession(SessionBackend::VideoToolbox(session)),
HardwareEncodeInput {
backend: InputBackend::VideoToolbox(input),
generation,
},
))
}
#[cfg(target_os = "windows")]
{
let (session, input) = d3d12va::open(&self.d3d12, request, generation)?;
Ok((
HardwareEncodeSession(SessionBackend::D3D12(session)),
HardwareEncodeInput {
backend: InputBackend::D3D12(input),
generation,
},
))
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
{
let _ = (request, generation);
Err(HardwareEncodeOpenError::Refused(
"this platform has no hardware encode backend yet".to_owned(),
))
}
}
}
static NEXT_POOL_GENERATION: AtomicU64 = AtomicU64::new(1);
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct EncoderSurfaceDescriptor {
pub backend: HardwareEncodeBackend,
pub pool_generation: u64,
pub layout: EncoderInputLayout,
pub visible: (u32, u32),
pub color: ColorMetadata,
}
pub struct EncoderSurface {
descriptor: EncoderSurfaceDescriptor,
backing: SurfaceBacking,
}
enum SurfaceBacking {
#[cfg(target_os = "linux")]
Vulkan(vulkan::VulkanEncoderFrame),
#[cfg(target_os = "macos")]
VideoToolbox(videotoolbox::VideoToolboxEncoderFrame),
#[cfg(target_os = "windows")]
D3D12(d3d12va::D3D12EncoderFrame),
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
Unsupported(std::convert::Infallible),
}
impl EncoderSurface {
#[must_use]
pub const fn descriptor(&self) -> &EncoderSurfaceDescriptor {
&self.descriptor
}
}
#[must_use = "a begun frame must be submitted, or its planes stay claimed"]
pub struct InputFrame {
#[cfg(any(target_os = "linux", target_os = "windows"))]
slot: usize,
#[cfg(target_os = "macos")]
pixel_buffer: usize,
}
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
pub struct EncoderPoolStats {
pub acquired: u64,
pub allocated: u64,
pub copy_operations: u64,
pub copy_bytes: u64,
}
pub struct HardwareEncodeInput {
backend: InputBackend,
generation: u64,
}
enum InputBackend {
#[cfg(target_os = "linux")]
Vulkan(vulkan::VulkanEncodeInput),
#[cfg(target_os = "macos")]
VideoToolbox(videotoolbox::VideoToolboxEncodeInput),
#[cfg(target_os = "windows")]
D3D12(d3d12va::D3D12EncodeInput),
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
Unsupported(std::convert::Infallible),
}
impl HardwareEncodeInput {
pub fn acquire_surface(&mut self) -> Result<EncoderSurface, EncodeError> {
match &mut self.backend {
#[cfg(target_os = "linux")]
InputBackend::Vulkan(input) => {
let frame = input.acquire_frame()?;
Ok(EncoderSurface {
descriptor: input.descriptor(self.generation),
backing: SurfaceBacking::Vulkan(frame),
})
}
#[cfg(target_os = "macos")]
InputBackend::VideoToolbox(input) => {
let frame = input.acquire_frame()?;
Ok(EncoderSurface {
descriptor: input.descriptor(self.generation),
backing: SurfaceBacking::VideoToolbox(frame),
})
}
#[cfg(target_os = "windows")]
InputBackend::D3D12(input) => {
let frame = input.acquire_frame()?;
Ok(EncoderSurface {
descriptor: input.descriptor(self.generation),
backing: SurfaceBacking::D3D12(frame),
})
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
InputBackend::Unsupported(never) => match *never {},
}
}
pub fn begin_frame(&mut self, surface: &EncoderSurface) -> Result<InputFrame, EncodeError> {
self.check(surface)?;
match &mut self.backend {
#[cfg(target_os = "linux")]
InputBackend::Vulkan(input) => input.begin_frame(),
#[cfg(target_os = "macos")]
InputBackend::VideoToolbox(input) => match &surface.backing {
SurfaceBacking::VideoToolbox(backing) => input.begin_frame(backing),
},
#[cfg(target_os = "windows")]
InputBackend::D3D12(input) => input.begin_frame(),
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
InputBackend::Unsupported(never) => match *never {},
}
}
#[must_use]
pub fn planes(&self, frame: &InputFrame) -> (&wgpu::Texture, &wgpu::Texture) {
match &self.backend {
#[cfg(target_os = "linux")]
InputBackend::Vulkan(input) => input.planes(frame),
#[cfg(target_os = "macos")]
InputBackend::VideoToolbox(input) => input.planes(frame),
#[cfg(target_os = "windows")]
InputBackend::D3D12(input) => input.planes(frame),
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
InputBackend::Unsupported(never) => match *never {},
}
}
pub fn submit(
&mut self,
frame: InputFrame,
surface: &mut EncoderSurface,
) -> Result<(), EncodeError> {
self.check(surface)?;
match (&mut self.backend, &mut surface.backing) {
#[cfg(target_os = "linux")]
(InputBackend::Vulkan(input), SurfaceBacking::Vulkan(backing)) => {
input.submit(frame, backing)
}
#[cfg(target_os = "macos")]
(InputBackend::VideoToolbox(input), SurfaceBacking::VideoToolbox(backing)) => {
input.submit(frame, backing)
}
#[cfg(target_os = "windows")]
(InputBackend::D3D12(input), SurfaceBacking::D3D12(backing)) => {
input.submit(frame, backing)
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
(InputBackend::Unsupported(never), _) => match *never {},
}
}
pub fn set_time(&self, surface: &mut EncoderSurface, time: MediaTime) {
match (&self.backend, &mut surface.backing) {
#[cfg(target_os = "linux")]
(InputBackend::Vulkan(input), SurfaceBacking::Vulkan(backing)) => {
backing.set_pts(input.timestamp(time));
}
#[cfg(target_os = "macos")]
(InputBackend::VideoToolbox(input), SurfaceBacking::VideoToolbox(backing)) => {
backing.set_pts(input.timestamp(time));
}
#[cfg(target_os = "windows")]
(InputBackend::D3D12(input), SurfaceBacking::D3D12(backing)) => {
backing.set_pts(input.timestamp(time));
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
(InputBackend::Unsupported(never), _) => match *never {},
}
}
#[must_use]
pub fn stats(&self) -> EncoderPoolStats {
match &self.backend {
#[cfg(target_os = "linux")]
InputBackend::Vulkan(input) => input.stats(),
#[cfg(target_os = "macos")]
InputBackend::VideoToolbox(input) => input.stats(),
#[cfg(target_os = "windows")]
InputBackend::D3D12(input) => input.stats(),
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
InputBackend::Unsupported(never) => match *never {},
}
}
pub fn finish(self) -> Result<(), EncodeError> {
match self.backend {
#[cfg(target_os = "linux")]
InputBackend::Vulkan(input) => input.finish(),
#[cfg(target_os = "macos")]
InputBackend::VideoToolbox(input) => input.finish(),
#[cfg(target_os = "windows")]
InputBackend::D3D12(input) => input.finish(),
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
InputBackend::Unsupported(never) => match never {},
}
}
fn check(&self, surface: &EncoderSurface) -> Result<(), EncodeError> {
if surface.descriptor.pool_generation == self.generation {
Ok(())
} else {
Err(EncodeError::Adapter(format!(
"encoder surface from pool {} reached pool {}",
surface.descriptor.pool_generation, self.generation
)))
}
}
}
pub struct HardwareEncodeSession(SessionBackend);
enum SessionBackend {
#[cfg(target_os = "linux")]
Vulkan(vulkan::VulkanEncodeSession),
#[cfg(target_os = "macos")]
VideoToolbox(videotoolbox::VideoToolboxEncodeSession),
#[cfg(target_os = "windows")]
D3D12(d3d12va::D3D12EncodeSession),
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
Unsupported(std::convert::Infallible),
}
impl HardwareEncodeSession {
#[must_use]
pub fn selection(&self) -> &NativeVideoEncoderSelection {
match &self.0 {
#[cfg(target_os = "linux")]
SessionBackend::Vulkan(session) => session.selection(),
#[cfg(target_os = "macos")]
SessionBackend::VideoToolbox(session) => session.selection(),
#[cfg(target_os = "windows")]
SessionBackend::D3D12(session) => session.selection(),
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
SessionBackend::Unsupported(never) => match *never {},
}
}
#[must_use]
pub fn stage_timings(&self) -> EncodeStageTimings {
match &self.0 {
#[cfg(target_os = "linux")]
SessionBackend::Vulkan(session) => session.stage_timings(),
#[cfg(target_os = "macos")]
SessionBackend::VideoToolbox(session) => session.stage_timings(),
#[cfg(target_os = "windows")]
SessionBackend::D3D12(session) => session.stage_timings(),
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
SessionBackend::Unsupported(never) => match *never {},
}
}
pub fn encode(&mut self, surface: &EncoderSurface) -> Result<(), EncodeError> {
match (&mut self.0, &surface.backing) {
#[cfg(target_os = "linux")]
(SessionBackend::Vulkan(session), SurfaceBacking::Vulkan(frame)) => {
session.encode(surface.descriptor.pool_generation, frame)
}
#[cfg(target_os = "macos")]
(SessionBackend::VideoToolbox(session), SurfaceBacking::VideoToolbox(frame)) => {
session.encode(surface.descriptor.pool_generation, frame)
}
#[cfg(target_os = "windows")]
(SessionBackend::D3D12(session), SurfaceBacking::D3D12(frame)) => {
session.encode(surface.descriptor.pool_generation, frame)
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
(SessionBackend::Unsupported(never), _) => match *never {},
}
}
pub fn write_audio(&mut self, block: &alpha2_media::AudioBlock) -> Result<(), EncodeError> {
match &mut self.0 {
#[cfg(target_os = "linux")]
SessionBackend::Vulkan(session) => session.write_audio(block),
#[cfg(target_os = "macos")]
SessionBackend::VideoToolbox(session) => session.write_audio(block),
#[cfg(target_os = "windows")]
SessionBackend::D3D12(session) => session.write_audio(block),
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
SessionBackend::Unsupported(never) => match *never {},
}
}
pub fn finish(&mut self) -> Result<(), EncodeError> {
match &mut self.0 {
#[cfg(target_os = "linux")]
SessionBackend::Vulkan(session) => session.finish(),
#[cfg(target_os = "macos")]
SessionBackend::VideoToolbox(session) => session.finish(),
#[cfg(target_os = "windows")]
SessionBackend::D3D12(session) => session.finish(),
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
SessionBackend::Unsupported(never) => match *never {},
}
}
pub fn abort(&mut self) {
match &mut self.0 {
#[cfg(target_os = "linux")]
SessionBackend::Vulkan(session) => session.abort(),
#[cfg(target_os = "macos")]
SessionBackend::VideoToolbox(session) => session.abort(),
#[cfg(target_os = "windows")]
SessionBackend::D3D12(session) => session.abort(),
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
SessionBackend::Unsupported(never) => match *never {},
}
}
}