use std::collections::HashSet;
use std::ffi::CStr;
use std::sync::Mutex;
use alpha2_media::{BitrateMode, ColorMetadata, EncodeError, VideoRateControl};
use alpha2_native_gpu::{D3D12EncodeCodec, D3D12EncodeExtent, SharedD3D12Context};
use alpha2_time::MediaTime;
use rsmpeg::avcodec::{AVCodec, AVCodecContext, AVPacket};
use rsmpeg::avutil::{AVFrame, AVHWDeviceContext, AVHWFramesContext, AVRational, av_rescale_q, ra};
use rsmpeg::error::RsmpegError;
use rsmpeg::ffi;
use super::{
EncoderInputHandoff, EncoderInputLayout, EncoderPoolStats, EncoderSurfaceDescriptor,
HardwareEncodeBackend, HardwareEncodeCapability, HardwareEncodeOpenError,
HardwareEncodeRequest, InputFrame,
};
use crate::NativeVideoCodec;
use crate::bitstream::EncodedVideoFilter;
use crate::convert::{encode_adapter_error, tick_base};
use crate::encode::{EncodeStageTimings, timed};
use crate::mux::Muxer;
use crate::rate_control::RateControlMechanism;
#[path = "d3d12va_rate_control.rs"]
mod rate_control_probe;
#[cfg(test)]
pub(crate) use rate_control_probe::encode_no_rate_control_frames;
pub(crate) use rate_control_probe::probe_d3d12va_rate_control;
pub use rate_control_probe::{
D3D12RateControlOpen, D3D12RateControlRunner, open_d3d12va_rate_control,
set_d3d12va_rate_control_runner,
};
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
struct Encoder {
codec: NativeVideoCodec,
d3d12: D3D12EncodeCodec,
name: &'static str,
c_name: &'static CStr,
profile: &'static CStr,
}
impl Encoder {
fn for_codec(codec: NativeVideoCodec) -> Result<Self, String> {
let (d3d12, name, c_name) = match codec {
NativeVideoCodec::H264 => (D3D12EncodeCodec::H264, "h264_d3d12va", c"h264_d3d12va"),
NativeVideoCodec::Hevc => (D3D12EncodeCodec::Hevc, "hevc_d3d12va", c"hevc_d3d12va"),
NativeVideoCodec::Av1 => (D3D12EncodeCodec::Av1, "av1_d3d12va", c"av1_d3d12va"),
other => {
return Err(format!(
"D3D12 Video Encode encodes h264, hevc or av1, not {}",
other.as_str()
));
}
};
let profile = c"main";
Ok(Self {
codec,
d3d12,
name,
c_name,
profile,
})
}
fn find(self) -> Result<rsmpeg::avcodec::AVCodecRef<'static>, String> {
AVCodec::find_encoder_by_name(self.c_name)
.ok_or_else(|| format!("this FFmpeg runtime has no {} encoder", self.name))
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct D3D12EncodeProbe {
pub encoder: String,
pub codec: NativeVideoCodec,
pub rate_control: &'static str,
pub coded_size: (u32, u32),
pub opened_profile: i32,
pub extradata_size: usize,
pub profile: Option<u8>,
pub first_packet_units: Vec<u8>,
pub packets: u32,
pub packet_bytes: u32,
pub decode_failure: Option<String>,
}
impl D3D12EncodeProbe {
pub fn first_packet_decodes(&self) -> Result<(), String> {
let units = &self.first_packet_units;
let has = |kinds: &[u8]| kinds.iter().all(|kind| units.contains(kind));
let (parameter_sets, picture, unit) = match self.codec {
NativeVideoCodec::H264 => (
has(&[7, 8]),
units.iter().any(|kind| (1..=5).contains(kind)),
"NAL unit",
),
NativeVideoCodec::Hevc => (
has(&[32, 33, 34]),
units.iter().any(|kind| *kind <= 21),
"NAL unit",
),
_ => (has(&[1]), has(&[6]) || has(&[3, 4]), "OBU"),
};
let lacks = match (parameter_sets, picture) {
(true, true) => {
return match &self.decode_failure {
None => Ok(()),
Some(failure) => Err(format!(
"{}'s packets do not decode: {failure}",
self.encoder
)),
};
}
(false, true) => "its parameter sets",
(true, false) => "a coded picture",
(false, false) => "both its parameter sets and a coded picture",
};
Err(format!(
"{}'s first packet lacks {lacks} ({unit} types {units:?}), so no decoder can start \
from it",
self.encoder
))
}
}
pub fn probe_d3d12va_encode(
shared: &SharedD3D12Context,
codec: NativeVideoCodec,
width: u32,
height: u32,
) -> Result<D3D12EncodeProbe, EncodeError> {
let encoder = Encoder::for_codec(codec).map_err(EncodeError::Adapter)?;
encoder.find().map_err(EncodeError::Adapter)?;
let device = DeviceModes::of(shared, encoder).map_err(EncodeError::Adapter)?;
let rate_control = probe_rate_control(&device).map_err(EncodeError::Adapter)?;
let (mut frames, mut context, selection) = open_encoder(
&shared.ffmpeg_device(),
encoder,
width,
height,
rate_control,
(30, 1),
ColorMetadata::EXPORT,
&device,
)?;
let extradata_size = usize::try_from(context.extradata_size).unwrap_or(0);
let opened_profile = context.profile;
let mut frame = AVFrame::new();
frames
.get_buffer(&mut frame)
.map_err(encode_adapter_error)?;
let info = alpha2_ffmpeg_bridge::d3d12_frame_info(&frame).map_err(encode_adapter_error)?;
frame.set_pts(0);
context
.send_frame(Some(&frame))
.map_err(encode_adapter_error)?;
context.send_frame(None).map_err(encode_adapter_error)?;
let mut packets = Vec::new();
loop {
match context.receive_packet() {
Ok(packet) => packets.push(packet),
Err(RsmpegError::EncoderDrainError | RsmpegError::EncoderFlushedError) => break,
Err(error) => return Err(encode_adapter_error(error)),
}
}
let Some(first) = packets.first() else {
return Err(EncodeError::Adapter(format!(
"{} opened but produced no packet",
encoder.name
)));
};
let (first_packet_units, profile) =
stream_units(codec, &alpha2_ffmpeg_bridge::packet_bytes(first));
let packet_bytes = packets.iter().fold(0_u32, |sum, packet| {
sum.saturating_add(packet.size.unsigned_abs())
});
let decode_failure = software_decode_failure(codec, &packets);
Ok(D3D12EncodeProbe {
encoder: encoder.name.to_owned(),
codec,
rate_control: match selection.rate_control {
crate::AppliedRateControl::Quantizer => "disabled",
crate::AppliedRateControl::Bitrate if rate_control.mode == BitrateMode::Constant => {
"cbr"
}
crate::AppliedRateControl::Bitrate => "vbr",
},
coded_size: (info.surface_width, info.surface_height),
opened_profile,
extradata_size,
profile,
first_packet_units,
packets: u32::try_from(packets.len()).unwrap_or(u32::MAX),
packet_bytes,
decode_failure,
})
}
fn software_decode_failure(codec: NativeVideoCodec, packets: &[AVPacket]) -> Option<String> {
let decoder = match crate::decode::software_video_decoder(codec.codec_id()) {
Ok(decoder) => decoder,
Err(error) => {
return Some(format!(
"there is no software decoder to check them: {error}"
));
}
};
let name = decoder.name().to_string_lossy().into_owned();
let mut context = AVCodecContext::new(&decoder);
if let Err(error) = context.open(None) {
return Some(format!("the software {name} decoder did not open: {error}"));
}
let corrupt = i32::try_from(ffi::AV_FRAME_FLAG_CORRUPT).unwrap_or(0);
let mut pictures = 0_usize;
for (at, packet) in packets.iter().map(Some).chain([None]).enumerate() {
if let Err(error) = context.send_packet(packet) {
return Some(match packet {
Some(_) => format!("the software {name} decoder refused packet {at}: {error}"),
None => format!("the software {name} decoder did not flush: {error}"),
});
}
loop {
match context.receive_frame() {
Ok(frame) if frame.flags & corrupt != 0 || frame.decode_error_flags != 0 => {
return Some(format!(
"the software {name} decoder decoded picture {pictures} of {} corrupt",
packets.len()
));
}
Ok(_) => pictures += 1,
Err(RsmpegError::DecoderDrainError | RsmpegError::DecoderFlushedError) => break,
Err(error) => return Some(format!("the software {name} decoder failed: {error}")),
}
}
}
(pictures != packets.len()).then(|| {
format!(
"the software {name} decoder decoded {pictures} pictures from {} packets",
packets.len()
)
})
}
fn probe_rate_control(device: &DeviceModes) -> Result<VideoRateControl, String> {
let rate = |mode| VideoRateControl {
bitrate: 4_000_000,
mode,
quantizer: None,
};
if device.lists("disabled") {
Ok(VideoRateControl {
quantizer: Some(26),
..rate(BitrateMode::Variable)
})
} else if device.lists("vbr") {
Ok(rate(BitrateMode::Variable))
} else if device.lists("cbr") {
Ok(rate(BitrateMode::Constant))
} else {
Err(format!(
"{}: {} lists no rate-control mode an export asks for (it lists {})",
device.adapter,
device.encoder.name,
device.listed()
))
}
}
fn stream_units(codec: NativeVideoCodec, bytes: &[u8]) -> (Vec<u8>, Option<u8>) {
let mut types = Vec::new();
let mut profile = None;
if codec == NativeVideoCodec::Av1 {
let mut at = 0;
while at < bytes.len() {
let header = bytes[at];
let kind = (header >> 3) & 0x0f;
types.push(kind);
at += 1 + usize::from(header & 0x04 != 0);
if header & 0x02 == 0 {
break;
}
let (mut size, mut shift) = (0_usize, 0);
while at < bytes.len() {
let byte = bytes[at];
at += 1;
size |= usize::from(byte & 0x7f) << shift;
shift += 7;
if byte & 0x80 == 0 || shift >= 56 {
break;
}
}
if kind == 1 && profile.is_none() {
profile = bytes.get(at).map(|byte| byte >> 5);
}
at = at.saturating_add(size);
}
return (types, profile);
}
let mut at = 0;
while at + 3 <= bytes.len() {
if bytes[at..at + 3] != [0, 0, 1] {
at += 1;
continue;
}
at += 3;
let Some(&header) = bytes.get(at) else {
break;
};
if codec == NativeVideoCodec::Hevc {
let kind = (header >> 1) & 0x3f;
types.push(kind);
if kind == 33 && profile.is_none() {
profile = bytes.get(at + 3).map(|byte| byte & 0x1f);
}
} else {
let kind = header & 0x1f;
types.push(kind);
if kind == 7 && profile.is_none() {
profile = bytes.get(at + 1).copied();
}
}
}
(types, profile)
}
pub const D3D12VA_TEST_ENCODE_SIZE: (u32, u32) = (256, 256);
pub fn d3d12va_encoder(codec: NativeVideoCodec) -> Result<&'static str, String> {
let encoder = Encoder::for_codec(codec)?;
encoder.find()?;
Ok(encoder.name)
}
pub type D3D12TestEncodeRunner =
fn(&SharedD3D12Context, NativeVideoCodec) -> Result<D3D12EncodeProbe, String>;
static RUNNER: std::sync::OnceLock<D3D12TestEncodeRunner> = std::sync::OnceLock::new();
pub fn set_d3d12va_test_encode_runner(
runner: D3D12TestEncodeRunner,
) -> Result<(), D3D12TestEncodeRunner> {
RUNNER.set(runner)
}
type TestEncode = ((u64, D3D12EncodeCodec), Result<D3D12EncodeProbe, String>);
static PROBED: Mutex<Vec<TestEncode>> = Mutex::new(Vec::new());
static TIMED_OUT: std::sync::OnceLock<String> = std::sync::OnceLock::new();
pub(super) fn capability(
shared: &SharedD3D12Context,
codec: NativeVideoCodec,
) -> Result<HardwareEncodeCapability, String> {
let encoder = Encoder::for_codec(codec)?;
encoder.find()?;
let device = DeviceModes::of(shared, encoder)?;
let capability = capability_with(encoder, &device.extent);
test_encode(shared, encoder)?;
Ok(capability)
}
pub(super) fn preferred_capability(
shared: &SharedD3D12Context,
) -> Result<HardwareEncodeCapability, String> {
let mut reasons = Vec::new();
for codec in [
NativeVideoCodec::H264,
NativeVideoCodec::Hevc,
NativeVideoCodec::Av1,
] {
match capability(shared, codec) {
Ok(capability) => return Ok(capability),
Err(reason) => reasons.push(reason),
}
}
Err(reasons.join("; "))
}
fn capability_with(encoder: Encoder, extent: &D3D12EncodeExtent) -> HardwareEncodeCapability {
HardwareEncodeCapability {
backend: HardwareEncodeBackend::D3D12Va,
encoder: encoder.name,
codec: encoder.codec,
gpu_input_only: true,
input_layout: EncoderInputLayout::Nv12,
handoff: EncoderInputHandoff::GpuCopy,
shares_renderer_device: true,
minimum: extent.minimum,
maximum: extent.maximum,
granularity: extent.granularity,
max_p_picture_l0_references: Some(extent.max_p_picture_l0_references),
max_b_picture_l0_references: Some(extent.max_b_picture_l0_references),
max_l1_references: Some(extent.max_l1_references),
rate_control_modes: extent.rate_control_modes.clone(),
plane_ring_slots: alpha2_native_gpu::D3D12EncodeCopier::slots(),
handoff_retained_surfaces: alpha2_native_gpu::D3D12EncodeCopier::slots(),
codec_retained_surfaces: CODEC_RETAINED_SURFACES,
}
}
const CODEC_RETAINED_SURFACES: usize = 0;
fn test_encode(shared: &SharedD3D12Context, encoder: Encoder) -> Result<(), String> {
if let Some(reason) = TIMED_OUT.get() {
return Err(reason.clone());
}
let key = (shared.identity().device, encoder.d3d12);
let mut probed = PROBED
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some((_, answer)) = probed.iter().find(|(probed, _)| *probed == key) {
return accepted(answer.clone(), &shared.gpu().capabilities().adapter_name);
}
let adapter = shared.gpu().capabilities().adapter_name.clone();
if let Some(runner) = RUNNER.get() {
let answer = runner(shared, encoder.codec).map_err(|error| {
format!(
"{} did not open and encode on {adapter}: {error}",
encoder.name
)
});
probed.push((key, answer.clone()));
return accepted(answer, &adapter);
}
let (sender, receiver) = std::sync::mpsc::channel();
let worker = shared.clone();
let codec = encoder.codec;
std::thread::Builder::new()
.name(format!("alpha2 {} probe", encoder.name))
.spawn(move || {
let _ = sender.send(
probe_d3d12va_encode(
&worker,
codec,
D3D12VA_TEST_ENCODE_SIZE.0,
D3D12VA_TEST_ENCODE_SIZE.1,
)
.map_err(|error| error.to_string()),
);
})
.map_err(|error| format!("{}: the test encode did not start: {error}", encoder.name))?;
let timeout = alpha2_native_gpu::d3d12_wait_timeout();
let answer = match receiver.recv_timeout(timeout) {
Ok(answer) => answer.map_err(|error| {
format!(
"{} did not open and encode on {adapter}: {error}",
encoder.name
)
}),
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
let reason = format!(
"{}'s test encode on {adapter} did not finish within {timeout:?}; the D3D12VA path \
is off for the rest of this process",
encoder.name
);
let _ = TIMED_OUT.set(reason.clone());
return Err(reason);
}
Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => Err(format!(
"{}'s test encode on {adapter} ended without an answer",
encoder.name
)),
};
probed.push((key, answer.clone()));
accepted(answer, &adapter)
}
fn accepted(answer: Result<D3D12EncodeProbe, String>, adapter: &str) -> Result<(), String> {
let probe = answer?;
let decodes = probe
.first_packet_decodes()
.map_err(|reason| format!("the D3D12VA test encode on {adapter} is unusable: {reason}"));
#[cfg(test)]
if decodes.is_err() && tests::accepts_undecodable_packets() {
return Ok(());
}
decodes
}
struct DeviceModes {
adapter: String,
encoder: Encoder,
extent: D3D12EncodeExtent,
}
impl DeviceModes {
fn of(shared: &SharedD3D12Context, encoder: Encoder) -> Result<Self, String> {
Ok(Self {
adapter: shared.gpu().capabilities().adapter_name.clone(),
encoder,
extent: shared.encode_support(encoder.d3d12)?,
})
}
fn lists(&self, mode: &str) -> bool {
self.extent.rate_control_modes.contains(&mode)
}
fn listed(&self) -> String {
self.extent.rate_control_modes.join(", ")
}
fn refusal(&self, mode: &'static str) -> Option<String> {
if self.lists(mode) {
return None;
}
let answer = self
.extent
.refused_rate_control_modes
.iter()
.find(|(refused, _)| *refused == mode)
.map_or_else(
|| "does not list it".to_owned(),
|(_, status)| {
if *status == 0 {
"refuses it".to_owned()
} else {
format!("refuses it with HRESULT {status:#010X}")
}
},
);
Some(format!(
"{}: {} has no {mode} rate control here (D3D12 Video Encode {answer}; it encodes with {}){}",
self.adapter,
self.encoder.name,
self.listed(),
if self.lists("disabled") {
"; ask for a quantizer instead"
} else {
""
}
))
}
}
pub(super) fn open(
shared: &SharedD3D12Context,
request: &HardwareEncodeRequest<'_>,
generation: u64,
) -> Result<(D3D12EncodeSession, D3D12EncodeInput), HardwareEncodeOpenError> {
let refused = |error: EncodeError| HardwareEncodeOpenError::Refused(error.to_string());
let (width, height) = (request.width, request.height);
let capability = capability(shared, request.codec).map_err(HardwareEncodeOpenError::Refused)?;
capability
.accepts(width, height)
.map_err(HardwareEncodeOpenError::Refused)?;
let encoder = Encoder::for_codec(request.codec).map_err(HardwareEncodeOpenError::Refused)?;
let device = DeviceModes::of(shared, encoder).map_err(HardwareEncodeOpenError::Refused)?;
let frame_rate = super::session_frame_rate(request.frame_rate).map_err(refused)?;
let (frames, context, selection) = open_encoder(
&shared.ffmpeg_device(),
encoder,
width,
height,
request.rate_control,
frame_rate,
request.color,
&device,
)
.map_err(refused)?;
let time_base = ra(frame_rate.1, frame_rate.0);
let bitstream = EncodedVideoFilter::new(&context.extract_codecpar(), request.color, time_base)
.map_err(refused)?;
let copier = alpha2_native_gpu::D3D12EncodeCopier::new(shared)
.map_err(|error| HardwareEncodeOpenError::Refused(format!("{error:?}")))?;
let gpu = shared.gpu();
let planes = (0..alpha2_native_gpu::D3D12EncodeCopier::slots())
.map(|_| Nv12PlanePair {
luma: plane_texture(
gpu.device(),
"alpha2 NV12 luma",
width,
height,
wgpu::TextureFormat::R8Unorm,
),
chroma: plane_texture(
gpu.device(),
"alpha2 NV12 chroma",
width / 2,
height / 2,
wgpu::TextureFormat::Rg8Unorm,
),
})
.collect();
let input = D3D12EncodeInput {
copier,
planes,
frames,
size: (width, height),
time_base,
color: request.color,
stats: EncoderPoolStats::default(),
textures: HashSet::new(),
#[cfg(debug_assertions)]
thread: std::thread::current().id(),
};
let muxer = Muxer::open_deferred(
request.output,
request.container,
&context,
time_base,
request.audio,
)
.map_err(HardwareEncodeOpenError::Failed)?;
Ok((
D3D12EncodeSession {
context,
time_base,
bitstream,
muxer,
stages: EncodeStageTimings::default(),
selection,
generation,
},
input,
))
}
fn plane_texture(
device: &wgpu::Device,
label: &'static str,
width: u32,
height: u32,
format: wgpu::TextureFormat,
) -> wgpu::Texture {
device.create_texture(&wgpu::TextureDescriptor {
label: Some(label),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
})
}
struct Nv12PlanePair {
luma: wgpu::Texture,
chroma: wgpu::Texture,
}
#[allow(clippy::too_many_arguments)]
fn open_encoder(
device: &AVHWDeviceContext,
encoder: Encoder,
width: u32,
height: u32,
rate_control: VideoRateControl,
frame_rate: (i32, i32),
color: ColorMetadata,
modes: &DeviceModes,
) -> Result<
(
AVHWFramesContext,
AVCodecContext,
crate::NativeVideoEncoderSelection,
),
EncodeError,
> {
let name = encoder.name;
let codec = encoder.find().map_err(EncodeError::Adapter)?;
let coded_width =
i32::try_from(width).map_err(|_| EncodeError::Adapter("width is out of range".into()))?;
let coded_height =
i32::try_from(height).map_err(|_| EncodeError::Adapter("height is out of range".into()))?;
let rate = u32::try_from(frame_rate.0)
.ok()
.zip(u32::try_from(frame_rate.1).ok())
.and_then(|(num, den)| alpha2_time::FrameRate::new(num, den).ok())
.ok_or_else(|| EncodeError::Adapter("frame rate is out of range".into()))?;
let bitrate_mode = if rate_control.mode == BitrateMode::Constant {
"cbr"
} else {
"vbr"
};
let bitrate_refused = modes.refusal(bitrate_mode);
let frames = d3d12va_encode_frames(device, coded_width, coded_height)?;
let open = |quantized: Option<&crate::rate_control::MechanismRequest>| {
let mut context = AVCodecContext::new(&codec);
context.set_width(coded_width);
context.set_height(coded_height);
context.set_pix_fmt(ffi::AV_PIX_FMT_D3D12);
context.set_time_base(ra(frame_rate.1, frame_rate.0));
context.set_framerate(ra(frame_rate.0, frame_rate.1));
let bitrate = crate::rate_control::mechanism_request(
name,
if rate_control.mode == BitrateMode::Constant {
RateControlMechanism::ConstantBitrate
} else {
RateControlMechanism::VariableBitrate
},
rate_control.bitrate,
0,
)
.ok_or_else(|| EncodeError::Adapter(format!("{name} has no known bitrate option")))?;
let request = quantized.unwrap_or(&bitrate);
context.set_bit_rate(request.bit_rate);
context.set_gop_size(crate::capabilities::keyframe_interval(rate));
context.set_max_b_frames(0);
context.set_hw_frames_ctx(frames.clone());
context.set_flags(
context.flags | i32::try_from(ffi::AV_CODEC_FLAG_GLOBAL_HEADER).unwrap_or(0),
);
let options = crate::convert::color_options(color).set(c"profile", encoder.profile, 0);
let unused =
alpha2_ffmpeg_bridge::open_codec_context(&mut context, request.dictionary(options))
.map_err(|error| {
EncodeError::Adapter(format!(
"{name} session could not open: {error}{}",
match quantized {
Some(_) => "",
None => crate::rate_control::constant_rate_hint(name, rate_control),
}
))
})?;
crate::convert::reject_unused_encode_options(unused)?;
Ok::<_, EncodeError>(context)
};
let open_bitrate = || match &bitrate_refused {
Some(reason) => Err(EncodeError::Adapter(reason.clone())),
None => open(None),
};
let (context, applied, quantizer_refused) =
match crate::rate_control::quantizer_request(name, rate_control) {
Some(Ok(quantized)) => match modes.refusal("disabled") {
Some(reason) => (
open_bitrate()?,
crate::AppliedRateControl::Bitrate,
Some(format!("{} refused: {reason}", quantized.describe())),
),
None => match open(Some(&quantized)) {
Ok(context) => (context, crate::AppliedRateControl::Quantizer, None),
Err(error) => (
open_bitrate()?,
crate::AppliedRateControl::Bitrate,
Some(format!("{} refused: {error}", quantized.describe())),
),
},
},
Some(Err(reason)) => (
open_bitrate()?,
crate::AppliedRateControl::Bitrate,
Some(reason),
),
None => (open_bitrate()?, crate::AppliedRateControl::Bitrate, None),
};
if let Some(reason) = &quantizer_refused {
alpha2_os::diagnostic!("alpha2 native export: {name} runs the bitrate fallback: {reason}");
}
let selection = crate::NativeVideoEncoderSelection {
encoder_name: name.to_owned(),
class: crate::NativeEncoderClass::Hardware,
hardware_accelerated: true,
rejected: Vec::new(),
rate_control: applied,
quantizer_refused,
software_threads: None,
};
Ok((frames, context, selection))
}
pub(super) struct D3D12EncodeSession {
context: AVCodecContext,
time_base: AVRational,
bitstream: EncodedVideoFilter,
muxer: Muxer,
stages: EncodeStageTimings,
selection: crate::NativeVideoEncoderSelection,
generation: u64,
}
pub(super) struct D3D12EncodeInput {
copier: alpha2_native_gpu::D3D12EncodeCopier,
planes: Vec<Nv12PlanePair>,
frames: AVHWFramesContext,
size: (u32, u32),
time_base: AVRational,
color: ColorMetadata,
stats: EncoderPoolStats,
textures: HashSet<u64>,
#[cfg(debug_assertions)]
thread: std::thread::ThreadId,
}
pub(super) struct D3D12EncoderFrame {
frame: AVFrame,
packed: bool,
}
impl D3D12EncoderFrame {
pub(super) fn set_pts(&mut self, pts: i64) {
self.frame.set_pts(pts);
}
}
impl D3D12EncodeInput {
pub(super) fn descriptor(&self, generation: u64) -> EncoderSurfaceDescriptor {
EncoderSurfaceDescriptor {
backend: HardwareEncodeBackend::D3D12Va,
pool_generation: generation,
layout: EncoderInputLayout::Nv12,
visible: self.size,
color: self.color,
}
}
pub(super) fn acquire_frame(&mut self) -> Result<D3D12EncoderFrame, EncodeError> {
let mut frame = AVFrame::new();
self.frames
.get_buffer(&mut frame)
.map_err(encode_adapter_error)?;
let texture = alpha2_ffmpeg_bridge::d3d12_frame_info(&frame)
.map_err(encode_adapter_error)?
.resource;
self.textures.insert(texture);
self.stats.acquired = self.stats.acquired.saturating_add(1);
self.stats.allocated = u64::try_from(self.textures.len()).unwrap_or(u64::MAX);
Ok(D3D12EncoderFrame {
frame,
packed: false,
})
}
pub(super) fn begin_frame(&mut self) -> Result<InputFrame, EncodeError> {
let slot = self.copier.await_slot().map_err(encode_adapter_error)?;
if slot >= self.planes.len() {
return Err(EncodeError::Adapter(format!(
"encode copier reported slot {slot}"
)));
}
Ok(InputFrame { slot })
}
pub(super) fn planes(&self, frame: &InputFrame) -> (&wgpu::Texture, &wgpu::Texture) {
let pair = &self.planes[frame.slot];
(&pair.luma, &pair.chroma)
}
#[allow(clippy::needless_pass_by_value)]
pub(super) fn submit(
&mut self,
frame: InputFrame,
backing: &mut D3D12EncoderFrame,
) -> Result<(), EncodeError> {
#[cfg(debug_assertions)]
debug_assert_eq!(
std::thread::current().id(),
self.thread,
"the D3D12 encode copy must be submitted by the thread that rendered its planes"
);
let InputFrame { slot } = frame;
let pair = &self.planes[slot];
let mut access = alpha2_ffmpeg_bridge::D3D12EncodeFrameAccess::begin(&mut backing.frame)
.map_err(encode_adapter_error)?;
self.copier
.submit(&mut access, &pair.luma, &pair.chroma, self.size)
.map_err(encode_adapter_error)?;
backing.packed = true;
self.stats.copy_operations = self.stats.copy_operations.saturating_add(2);
self.stats.copy_bytes = self
.stats
.copy_bytes
.saturating_add(EncoderInputLayout::Nv12.frame_bytes(self.size.0, self.size.1));
Ok(())
}
pub(super) fn timestamp(&self, time: MediaTime) -> i64 {
av_rescale_q(time.ticks(), tick_base(), self.time_base)
}
pub(super) const fn stats(&self) -> EncoderPoolStats {
self.stats
}
pub(super) fn finish(self) -> Result<(), EncodeError> {
let Self { copier, planes, .. } = self;
copier.finish().map_err(encode_adapter_error)?;
drop(planes);
Ok(())
}
}
impl D3D12EncodeSession {
pub(super) const fn selection(&self) -> &crate::NativeVideoEncoderSelection {
&self.selection
}
pub(super) fn stage_timings(&self) -> EncodeStageTimings {
self.muxer.with_audio_timings(self.stages)
}
pub(super) fn encode(
&mut self,
generation: u64,
frame: &D3D12EncoderFrame,
) -> Result<(), EncodeError> {
if generation != self.generation {
return Err(EncodeError::Adapter(format!(
"encoder surface from pool {generation} reached session {}",
self.generation
)));
}
if !frame.packed {
return Err(EncodeError::Adapter(
"an encoder surface reached the encoder unpacked".into(),
));
}
timed(&mut self.stages.video_send_frame, || {
self.context.send_frame(Some(&frame.frame))
})
.map_err(encode_adapter_error)?;
self.drain()
}
pub(super) fn write_audio(
&mut self,
block: &alpha2_media::AudioBlock,
) -> Result<(), EncodeError> {
self.muxer.write_audio(block, &mut self.stages)
}
pub(super) fn finish(&mut self) -> Result<(), EncodeError> {
self.muxer.finish_audio()?;
crate::encode::beat_encode_call();
self.context
.send_frame(None)
.map_err(encode_adapter_error)?;
self.drain()?;
let tail = self.bitstream.flush()?;
self.write_video(tail)?;
self.muxer.write_ready_audio(true, &mut self.stages)?;
self.muxer.close()
}
pub(super) fn abort(&mut self) {
self.muxer.abort();
}
fn drain(&mut self) -> Result<(), EncodeError> {
loop {
let received = timed(&mut self.stages.video_receive_packet, || {
self.context.receive_packet()
});
match received {
Ok(packet) => {
let filtered = self.bitstream.filter(packet)?;
self.write_video(filtered)?;
}
Err(RsmpegError::EncoderDrainError | RsmpegError::EncoderFlushedError) => {
return self.muxer.write_ready_audio(false, &mut self.stages);
}
Err(error) => return Err(encode_adapter_error(error)),
}
}
}
fn write_video(&mut self, packets: Vec<AVPacket>) -> Result<(), EncodeError> {
for mut packet in packets {
self.muxer.write_video(&mut packet, self.time_base)?;
}
Ok(())
}
}
fn d3d12va_encode_frames(
device: &AVHWDeviceContext,
width: i32,
height: i32,
) -> Result<AVHWFramesContext, EncodeError> {
let mut frames = device.hwframe_ctx_alloc();
{
let data = frames.data();
data.format = ffi::AV_PIX_FMT_D3D12;
data.sw_format = ffi::AV_PIX_FMT_NV12;
data.width = width;
data.height = height;
data.initial_pool_size = 0;
}
alpha2_ffmpeg_bridge::d3d12_frames_configure(
&mut frames,
alpha2_ffmpeg_bridge::DXGI_FORMAT_NV12,
0,
)
.map_err(encode_adapter_error)?;
frames.init().map_err(encode_adapter_error)?;
Ok(frames)
}
#[cfg(test)]
#[path = "d3d12va_tests.rs"]
mod tests;