use std::{
collections::VecDeque,
error::Error,
ffi::{CStr, c_void},
fmt,
ptr::NonNull,
sync::{
Arc, Mutex, Weak,
atomic::{AtomicBool, Ordering},
},
};
use alpha2_ffmpeg_bridge::{
BridgeError, VulkanDeviceInfo, VulkanFrameAccess, VulkanFrameAccessGuard, VulkanFrameLease,
VulkanQueueLock, create_vulkan_device_context,
};
use alpha2_gpu::{ExternalQueueLock, GpuContext};
use ash::vk::{self, Handle};
use rsmpeg::avutil::AVHWDeviceContext;
use wgpu_hal::Instance as _;
use crate::{NativeGpuIdentity, NativePlaneLayout};
unsafe extern "C" {
fn alpha2_native_gpu_vulkan_probe(access: *mut VulkanFrameAccess, status: *mut i32) -> i32;
fn alpha2_native_gpu_vulkan_acquire_check(
access: *const VulkanFrameAccess,
status: *mut i32,
) -> i32;
fn alpha2_native_gpu_vulkan_ticket_create(
device: u64,
allocator: u64,
queue_family: u32,
output: *mut *mut c_void,
status: *mut i32,
) -> i32;
fn alpha2_native_gpu_vulkan_ticket_poll(ticket: *const c_void, status: *mut i32) -> i32;
fn alpha2_native_gpu_vulkan_ticket_reset(ticket: *mut c_void, status: *mut i32) -> i32;
fn alpha2_native_gpu_vulkan_acquire_submit(
access: *mut VulkanFrameAccess,
ticket: *mut c_void,
status: *mut i32,
) -> i32;
fn alpha2_native_gpu_vulkan_release(
access: *mut VulkanFrameAccess,
ticket: *mut c_void,
status: *mut i32,
) -> i32;
fn alpha2_native_gpu_vulkan_signal(
access: *mut VulkanFrameAccess,
semaphore: u64,
wait_value: u64,
signal_value: u64,
status: *mut i32,
) -> i32;
fn alpha2_native_gpu_vulkan_import_destroy(ticket: *mut c_void);
fn alpha2_native_gpu_vulkan_import_retire(ticket: *mut c_void, timeout_ns: u64) -> i32;
#[cfg(test)]
fn alpha2_native_gpu_vulkan_retained_helpers() -> u64;
fn alpha2_native_gpu_vulkan_encode_copier_create(
device: u64,
queue_family: u32,
allocator: u64,
output: *mut *mut c_void,
status: *mut i32,
) -> i32;
fn alpha2_native_gpu_vulkan_encode_copier_destroy(copier: *mut c_void, status: *mut i32)
-> i32;
fn alpha2_native_gpu_vulkan_encode_copier_slots() -> u32;
fn alpha2_native_gpu_vulkan_encode_copier_await_slot(
copier: *mut c_void,
slot: *mut u32,
status: *mut i32,
) -> i32;
fn alpha2_native_gpu_vulkan_encode_copier_submit(
copier: *mut c_void,
access: *mut VulkanFrameAccess,
copy: *const VulkanEncodeCopy,
status: *mut i32,
) -> i32;
fn alpha2_native_gpu_vulkan_set_wait_timeout(nanoseconds: u64);
fn alpha2_native_gpu_vulkan_wait_timeout() -> u64;
}
pub(crate) fn set_wait_timeout(timeout: std::time::Duration) {
let nanoseconds = u64::try_from(timeout.as_nanos()).unwrap_or(u64::MAX);
unsafe { alpha2_native_gpu_vulkan_set_wait_timeout(nanoseconds) };
}
fn wait_timeout() -> std::time::Duration {
std::time::Duration::from_nanos(unsafe { alpha2_native_gpu_vulkan_wait_timeout() })
}
fn helper_error(operation: &'static str, step: i32, status: i32) -> BridgeError {
crate::status::helper_failure(
alpha2_ffmpeg_bridge::NativeGpuBackend::Vulkan,
operation,
step,
status,
)
}
#[cfg(test)]
pub(crate) mod inject {
use std::cell::Cell;
#[derive(Clone, Copy, Debug)]
pub(crate) enum Hook {
AcquireSubmit,
TicketPoll,
TicketReset,
Release,
}
thread_local! {
static COUNTDOWNS: [Cell<u32>; 4] =
const { [Cell::new(0), Cell::new(0), Cell::new(0), Cell::new(0)] };
static STATUSES: [Cell<i32>; 4] =
const { [Cell::new(0), Cell::new(0), Cell::new(0), Cell::new(0)] };
}
pub(crate) fn fail_nth(hook: Hook, nth: u32) {
fail_nth_with(hook, nth, super::vk::Result::ERROR_DEVICE_LOST.as_raw());
}
pub(crate) fn fail_nth_with(hook: Hook, nth: u32, status: i32) {
COUNTDOWNS.with(|countdowns| countdowns[hook as usize].set(nth));
STATUSES.with(|statuses| statuses[hook as usize].set(status));
}
pub(super) fn fails(hook: Hook, step: i32, status: &mut i32) -> Option<i32> {
let due = COUNTDOWNS.with(|countdowns| {
let countdown = &countdowns[hook as usize];
match countdown.get() {
0 => false,
1 => {
countdown.set(0);
true
}
left => {
countdown.set(left - 1);
false
}
}
});
due.then(|| {
*status = STATUSES.with(|statuses| statuses[hook as usize].get());
step
})
}
}
struct LeaseAccess<'a> {
frame: VulkanFrameAccessGuard<'a>,
ticket: NonNull<c_void>,
}
unsafe fn acquire_submit(lease: &mut LeaseAccess<'_>, status: &mut i32) -> i32 {
#[cfg(test)]
if let Some(step) = inject::fails(
inject::Hook::AcquireSubmit,
crate::interop::ALPHA2_GPU_STEP_QUEUE_SUBMIT,
status,
) {
return step;
}
unsafe {
alpha2_native_gpu_vulkan_acquire_submit(
lease.frame.access_mut(),
lease.ticket.as_ptr(),
status,
)
}
}
unsafe fn release(lease: &mut LeaseAccess<'_>, status: &mut i32) -> i32 {
#[cfg(test)]
if let Some(step) = inject::fails(
inject::Hook::Release,
crate::interop::ALPHA2_GPU_STEP_QUEUE_SUBMIT,
status,
) {
return step;
}
unsafe {
alpha2_native_gpu_vulkan_release(lease.frame.access_mut(), lease.ticket.as_ptr(), status)
}
}
unsafe fn ticket_poll(ticket: *const c_void, status: &mut i32) -> i32 {
#[cfg(test)]
if let Some(step) = inject::fails(
inject::Hook::TicketPoll,
crate::interop::ALPHA2_GPU_STEP_FENCE_STATUS,
status,
) {
return step;
}
unsafe { alpha2_native_gpu_vulkan_ticket_poll(ticket, status) }
}
unsafe fn ticket_reset(ticket: *mut c_void, status: &mut i32) -> i32 {
#[cfg(test)]
if let Some(step) = inject::fails(
inject::Hook::TicketReset,
crate::interop::ALPHA2_GPU_STEP_RESET_FENCE,
status,
) {
return step;
}
unsafe { alpha2_native_gpu_vulkan_ticket_reset(ticket, status) }
}
use crate::interop::Alpha2VulkanEncodeCopy as VulkanEncodeCopy;
pub struct VulkanEncodeCopier {
copier: *mut c_void,
queue_lock: alpha2_ffmpeg_bridge::VulkanQueueLock,
retained: Option<Box<RetainedEncodeWork>>,
close_failed: bool,
}
struct RetainedEncodeWork {
copies: Vec<Option<EncodeCopyInputs>>,
_context: SharedVulkanContext,
#[cfg(test)]
lifetime_probe: Option<Arc<()>>,
}
struct EncodeCopyInputs {
_luma: wgpu::Texture,
_chroma: wgpu::Texture,
_frame: rsmpeg::avutil::AVFrame,
}
unsafe impl Send for VulkanEncodeCopier {}
impl fmt::Debug for VulkanEncodeCopier {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("VulkanEncodeCopier")
.finish_non_exhaustive()
}
}
impl Drop for VulkanEncodeCopier {
fn drop(&mut self) {
if !self.copier.is_null() && !self.close_failed {
let _ = self.try_close();
}
if !self.copier.is_null() {
std::mem::forget(self.retained.take());
}
}
}
impl VulkanEncodeCopier {
pub fn finish(mut self) -> Result<(), BridgeError> {
self.try_close()
}
fn try_close(&mut self) -> Result<(), BridgeError> {
let mut status = 0;
let step =
unsafe { alpha2_native_gpu_vulkan_encode_copier_destroy(self.copier, &raw mut status) };
if step != 0 {
self.close_failed = true;
return Err(helper_error("encode copier teardown", step, status));
}
self.copier = std::ptr::null_mut();
Ok(())
}
}
impl VulkanEncodeCopier {
pub fn new(context: &SharedVulkanContext) -> Result<Self, BridgeError> {
let gpu = context.gpu();
let device = unsafe { gpu.device().as_hal::<wgpu_hal::api::Vulkan>() }
.map(|device| device.raw_device().handle().as_raw())
.ok_or(BridgeError::InvalidInput("renderer is not Vulkan"))?;
let mut copier = std::ptr::null_mut();
let mut status = 0;
let step = unsafe {
alpha2_native_gpu_vulkan_encode_copier_create(
device,
0,
0,
&raw mut copier,
&raw mut status,
)
};
if step != 0 {
return Err(helper_error("encode copier creation", step, status));
}
Ok(Self {
copier,
queue_lock: alpha2_ffmpeg_bridge::VulkanQueueLock::new(context.ffmpeg_device(), 0)?,
retained: Some(Box::new(RetainedEncodeWork {
copies: (0..Self::slots()).map(|_| None).collect(),
_context: context.clone(),
#[cfg(test)]
lifetime_probe: None,
})),
close_failed: false,
})
}
#[must_use]
pub fn slots() -> usize {
unsafe { alpha2_native_gpu_vulkan_encode_copier_slots() as usize }
}
pub fn await_slot(&mut self) -> Result<usize, BridgeError> {
let (mut slot, mut status) = (0_u32, 0);
let step = unsafe {
alpha2_native_gpu_vulkan_encode_copier_await_slot(
self.copier,
&raw mut slot,
&raw mut status,
)
};
if step != 0 {
return Err(helper_error("encode copy slot", step, status));
}
if let Some(retained) = self.retained.as_mut() {
retained.copies[slot as usize] = None;
}
Ok(slot as usize)
}
pub fn submit(
&mut self,
frame: &mut alpha2_ffmpeg_bridge::VulkanEncodeFrameAccess<'_>,
luma: &wgpu::Texture,
chroma: &wgpu::Texture,
size: (u32, u32),
) -> Result<(), BridgeError> {
let slot = self.await_slot()?;
let inputs = EncodeCopyInputs {
_luma: luma.clone(),
_chroma: chroma.clone(),
_frame: frame.retain_frame()?,
};
let kept = self
.retained
.as_mut()
.and_then(|work| work.copies.get_mut(slot))
.ok_or(BridgeError::InvalidInput("invalid encode copier slot"))?;
let luma_image = unsafe { luma.as_hal::<wgpu_hal::api::Vulkan>() }
.map(|texture| unsafe { texture.raw_handle() }.as_raw())
.ok_or(BridgeError::InvalidInput("luma plane is not Vulkan"))?;
let chroma_image = unsafe { chroma.as_hal::<wgpu_hal::api::Vulkan>() }
.map(|texture| unsafe { texture.raw_handle() }.as_raw())
.ok_or(BridgeError::InvalidInput("chroma plane is not Vulkan"))?;
let copy = VulkanEncodeCopy {
luma_image,
chroma_image,
width: size.0,
height: size.1,
};
let queue = self.queue_lock.lock();
let mut status = 0;
let step = unsafe {
alpha2_native_gpu_vulkan_encode_copier_submit(
self.copier,
frame.access_mut(),
&raw const copy,
&raw mut status,
)
};
drop(queue);
if step != 0 {
return Err(helper_error("encode copy", step, status));
}
*kept = Some(inputs);
frame.commit();
Ok(())
}
}
pub fn probe_vulkan_submission(lease: &VulkanFrameLease) -> Result<(), BridgeError> {
let mut guard = lease.lock_access()?;
let mut status = 0;
let step = unsafe { alpha2_native_gpu_vulkan_probe(guard.access_mut(), &raw mut status) };
guard.commit();
if step != 0 {
return Err(helper_error("decode probe", step, status));
}
Ok(())
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct VideoEncodeExtent {
pub profile: &'static str,
pub minimum: (u32, u32),
pub maximum: (u32, u32),
pub granularity: (u32, u32),
pub max_p_picture_l0_references: u32,
pub max_b_picture_l0_references: u32,
pub max_l1_references: u32,
pub rate_control_modes: Vec<&'static str>,
}
impl VideoEncodeExtent {
#[must_use]
pub const fn supports_b_pictures(&self) -> bool {
self.max_b_picture_l0_references > 0 || self.max_l1_references > 0
}
}
impl VideoEncodeExtent {
#[must_use]
pub const fn accepts(&self, width: u32, height: u32) -> bool {
let coded = self.coded(width, height);
coded.0 >= self.minimum.0
&& coded.1 >= self.minimum.1
&& coded.0 <= self.maximum.0
&& coded.1 <= self.maximum.1
}
#[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),
)
}
}
const fn align_up(value: u32, granularity: u32) -> u32 {
if granularity <= 1 {
return value;
}
value.div_ceil(granularity).saturating_mul(granularity)
}
const H264_ENCODE_PROFILES: [(vk::native::StdVideoH264ProfileIdc, &str); 3] = [
(
vk::native::StdVideoH264ProfileIdc_STD_VIDEO_H264_PROFILE_IDC_BASELINE,
"constrained_baseline",
),
(
vk::native::StdVideoH264ProfileIdc_STD_VIDEO_H264_PROFILE_IDC_MAIN,
"main",
),
(
vk::native::StdVideoH264ProfileIdc_STD_VIDEO_H264_PROFILE_IDC_HIGH,
"high",
),
];
fn last_supported_profile<T>(
mut query: impl FnMut(vk::native::StdVideoH264ProfileIdc) -> Option<T>,
) -> Option<(&'static str, T)> {
H264_ENCODE_PROFILES
.iter()
.filter_map(|&(idc, name)| query(idc).map(|answer| (name, answer)))
.last()
}
fn h264_encode_extent(
entry: &ash::Entry,
instance: &ash::Instance,
physical: vk::PhysicalDevice,
) -> Option<VideoEncodeExtent> {
let video = ash::khr::video_queue::Instance::new(entry, instance);
let (profile, mut extent) = last_supported_profile(|idc| {
let mut h264 = vk::VideoEncodeH264ProfileInfoKHR::default().std_profile_idc(idc);
let profile = vk::VideoProfileInfoKHR::default()
.video_codec_operation(vk::VideoCodecOperationFlagsKHR::ENCODE_H264)
.chroma_subsampling(vk::VideoChromaSubsamplingFlagsKHR::TYPE_420)
.luma_bit_depth(vk::VideoComponentBitDepthFlagsKHR::TYPE_8)
.chroma_bit_depth(vk::VideoComponentBitDepthFlagsKHR::TYPE_8)
.push_next(&mut h264);
let mut h264_capabilities = vk::VideoEncodeH264CapabilitiesKHR::default();
let mut encode = vk::VideoEncodeCapabilitiesKHR::default();
let mut capabilities = vk::VideoCapabilitiesKHR::default()
.push_next(&mut encode)
.push_next(&mut h264_capabilities);
let result = unsafe {
(video.fp().get_physical_device_video_capabilities_khr)(
physical,
&raw const profile,
&raw mut capabilities,
)
};
if result != vk::Result::SUCCESS {
return None;
}
let coded = (capabilities.min_coded_extent, capabilities.max_coded_extent);
Some(encode_extent(coded, &encode, &h264_capabilities))
})?;
extent.profile = profile;
Some(extent)
}
fn encode_extent(
(minimum, maximum): (vk::Extent2D, vk::Extent2D),
encode: &vk::VideoEncodeCapabilitiesKHR<'_>,
h264: &vk::VideoEncodeH264CapabilitiesKHR<'_>,
) -> VideoEncodeExtent {
VideoEncodeExtent {
profile: "",
minimum: (minimum.width, minimum.height),
maximum: (maximum.width, maximum.height),
granularity: (
encode.encode_input_picture_granularity.width.max(1),
encode.encode_input_picture_granularity.height.max(1),
),
max_p_picture_l0_references: h264.max_p_picture_l0_reference_count,
max_b_picture_l0_references: h264.max_b_picture_l0_reference_count,
max_l1_references: h264.max_l1_reference_count,
rate_control_modes: [
(vk::VideoEncodeRateControlModeFlagsKHR::CBR, "cbr"),
(vk::VideoEncodeRateControlModeFlagsKHR::VBR, "vbr"),
(vk::VideoEncodeRateControlModeFlagsKHR::DISABLED, "disabled"),
]
.into_iter()
.filter(|(flag, _)| encode.rate_control_modes.contains(*flag))
.map(|(_, name)| name)
.collect(),
}
}
#[derive(Copy, Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub struct VulkanLayerVersion {
pub major: u32,
pub minor: u32,
pub patch: u32,
}
impl VulkanLayerVersion {
pub const VIDEO_SESSIONS: Self = Self {
major: 1,
minor: 4,
patch: 0,
};
#[must_use]
pub const fn from_packed(version: u32) -> Self {
Self {
major: vk::api_version_major(version),
minor: vk::api_version_minor(version),
patch: vk::api_version_patch(version),
}
}
}
impl fmt::Display for VulkanLayerVersion {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{}.{}.{}", self.major, self.minor, self.patch)
}
}
const KHRONOS_VALIDATION: &str = "VK_LAYER_KHRONOS_validation";
fn activated_validation_layer(
instance: &ash::Instance,
physical: vk::PhysicalDevice,
) -> Option<VulkanLayerVersion> {
let layers = unsafe { instance.enumerate_device_layer_properties(physical) }.ok()?;
layers
.iter()
.find(|layer| {
layer
.layer_name_as_c_str()
.is_ok_and(|name| name.to_bytes() == KHRONOS_VALIDATION.as_bytes())
})
.map(|layer| VulkanLayerVersion::from_packed(layer.spec_version))
}
fn hal_instance() -> Result<wgpu_hal::vulkan::Instance, SharedVulkanError> {
let hal_desc = wgpu_hal::InstanceDescriptor {
name: "alpha2 shared Vulkan decode",
flags: wgpu::InstanceFlags::from_env_or_default(),
memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
backend_options: wgpu::BackendOptions::from_env_or_default(),
telemetry: None,
display: None,
};
unsafe {
wgpu_hal::vulkan::Instance::init(&hal_desc)
.map_err(|error| SharedVulkanError(format!("Vulkan instance: {error}")))
}
}
#[derive(Debug)]
struct SharedQueueLock(VulkanQueueLock);
impl ExternalQueueLock for SharedQueueLock {
fn lock(&self) {
unsafe { self.0.raw_lock() };
}
fn unlock(&self) {
unsafe { self.0.raw_unlock() };
}
}
#[derive(Debug)]
pub struct SharedVulkanError(String);
impl fmt::Display for SharedVulkanError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.0)
}
}
impl Error for SharedVulkanError {}
#[derive(Debug)]
pub struct ImportedVulkanFrame {
pub y: wgpu::TextureView,
pub uv: wgpu::TextureView,
pub layout: NativePlaneLayout,
pub coded_size: [u32; 2],
pub lease: Arc<VulkanImportLease>,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct ImportTicketStats {
pub created: u64,
pub reused: u64,
pub idle: usize,
pub pending: usize,
pub abandoned: u64,
}
struct PooledTicket {
ticket: NonNull<c_void>,
device: u64,
queue_family: u32,
}
#[derive(Default)]
struct ImportTicketPool {
state: Mutex<ImportTicketPoolState>,
}
#[derive(Default)]
struct ImportTicketPoolState {
idle: Vec<PooledTicket>,
pending: VecDeque<PooledTicket>,
created: u64,
reused: u64,
abandoned: u64,
}
const MAX_IDLE_IMPORT_TICKETS: usize = 8;
const MAX_PENDING_IMPORT_TICKETS: usize = 32;
unsafe impl Send for ImportTicketPool {}
unsafe impl Sync for ImportTicketPool {}
impl ImportTicketPool {
fn take(
&self,
device: u64,
allocator: u64,
queue_family: u32,
) -> Result<PooledTicket, BridgeError> {
let mut state = self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
state.reclaim();
if let Some(index) = state
.idle
.iter()
.position(|idle| idle.device == device && idle.queue_family == queue_family)
{
state.reused = state.reused.saturating_add(1);
return Ok(state.idle.swap_remove(index));
}
state.created = state.created.saturating_add(1);
drop(state);
let (mut raw, mut status) = (std::ptr::null_mut(), 0);
let step = unsafe {
alpha2_native_gpu_vulkan_ticket_create(
device,
allocator,
queue_family,
&raw mut raw,
&raw mut status,
)
};
if step != 0 {
return Err(helper_error("import ticket creation", step, status));
}
NonNull::new(raw)
.map(|ticket| PooledTicket {
ticket,
device,
queue_family,
})
.ok_or(BridgeError::InvalidInput(
"Vulkan import ticket creation returned no ticket",
))
}
fn give_back(&self, ticket: PooledTicket) {
let mut state = self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
state.pending.push_back(ticket);
if state.pending.len() > MAX_PENDING_IMPORT_TICKETS {
state.reclaim();
}
let excess = state
.pending
.len()
.saturating_sub(MAX_PENDING_IMPORT_TICKETS);
let oldest = state.pending.drain(..excess).collect::<Vec<_>>();
state.abandoned = state.abandoned.saturating_add(oldest.len() as u64);
drop(state);
retire(oldest);
}
fn stats(&self) -> ImportTicketStats {
let state = self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
ImportTicketStats {
created: state.created,
reused: state.reused,
idle: state.idle.len(),
pending: state.pending.len(),
abandoned: state.abandoned,
}
}
}
impl ImportTicketPoolState {
fn reclaim(&mut self) {
let mut index = 0;
while index < self.pending.len() {
let ticket = self.pending[index].ticket;
let mut status = 0;
match unsafe { ticket_poll(ticket.as_ptr(), &mut status) } {
0 => index += 1,
1 => {
let Some(finished) = self.pending.remove(index) else {
break;
};
let reset = unsafe { ticket_reset(ticket.as_ptr(), &mut status) };
if reset != 0 {
self.abandoned = self.abandoned.saturating_add(1);
} else if self.idle.len() < MAX_IDLE_IMPORT_TICKETS {
self.idle.push(finished);
} else {
unsafe { alpha2_native_gpu_vulkan_import_destroy(ticket.as_ptr()) };
}
}
_ => {
self.pending.remove(index);
self.abandoned = self.abandoned.saturating_add(1);
}
}
}
}
}
impl Drop for ImportTicketPool {
fn drop(&mut self) {
let state = self
.state
.get_mut()
.unwrap_or_else(std::sync::PoisonError::into_inner);
for idle in state.idle.drain(..) {
unsafe { alpha2_native_gpu_vulkan_import_destroy(idle.ticket.as_ptr()) };
}
retire(state.pending.drain(..));
}
}
fn retire(tickets: impl IntoIterator<Item = PooledTicket>) {
let deadline = std::time::Instant::now().checked_add(wait_timeout());
for pooled in tickets {
let left = deadline.map_or(std::time::Duration::MAX, |deadline| {
deadline.saturating_duration_since(std::time::Instant::now())
});
let nanoseconds = u64::try_from(left.as_nanos()).unwrap_or(u64::MAX);
unsafe { alpha2_native_gpu_vulkan_import_retire(pooled.ticket.as_ptr(), nanoseconds) };
}
}
pub struct VulkanImportLease {
frame: Arc<VulkanFrameLease>,
_texture: wgpu::Texture,
gpu: GpuContext,
batch: Arc<VulkanSubmissionBatch>,
tickets: Arc<ImportTicketPool>,
ticket: Option<PooledTicket>,
staged_signals: Mutex<Vec<StagedSignal>>,
queued: AtomicBool,
acquired: AtomicBool,
released: AtomicBool,
retained_bytes: u64,
}
#[derive(Clone, Copy, Debug)]
struct StagedSignal {
semaphore: u64,
acquired_value: u64,
signal_value: u64,
}
#[derive(Default)]
struct VulkanSubmissionBatch {
pending: Mutex<Vec<Weak<VulkanImportLease>>>,
}
impl VulkanSubmissionBatch {
fn enqueue(&self, lease: &Arc<VulkanImportLease>) {
if !lease.queued.swap(true, Ordering::AcqRel) {
self.pending
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(Arc::downgrade(lease));
}
}
fn flush(&self) -> Result<(), BridgeError> {
let leases = self
.pending
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.drain(..)
.filter_map(|lease| lease.upgrade())
.filter(|lease| !lease.acquired.load(Ordering::Acquire))
.collect::<Vec<_>>();
if leases.is_empty() {
return Ok(());
}
let result = flush_leases(&leases);
if result.is_err() {
for lease in &leases {
if !lease.acquired.load(Ordering::Acquire) {
lease.queued.store(false, Ordering::Release);
}
}
}
result
}
}
fn flush_leases(leases: &[Arc<VulkanImportLease>]) -> Result<(), BridgeError> {
for lease in leases {
let mut access = lease.lock()?;
if lease.acquired.load(Ordering::Acquire) {
continue;
}
let mut status = 0;
let step = unsafe { acquire_submit(&mut access, &mut status) };
if step != 0 {
return Err(helper_error("import acquire", step, status));
}
lease.acquired.store(true, Ordering::Release);
access.frame.commit();
lease.stage_submission_signal(unsafe { access.frame.access_mut() })?;
}
Ok(())
}
unsafe impl Send for VulkanImportLease {}
unsafe impl Sync for VulkanImportLease {}
impl fmt::Debug for VulkanImportLease {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("VulkanImportLease")
.field("released", &self.released.load(Ordering::Acquire))
.field("retained_bytes", &self.retained_bytes)
.finish_non_exhaustive()
}
}
impl VulkanImportLease {
fn lock(&self) -> Result<LeaseAccess<'_>, BridgeError> {
let ticket = self
.ticket
.as_ref()
.map(|pooled| pooled.ticket)
.ok_or(BridgeError::InvalidInput("missing Vulkan import ticket"))?;
Ok(LeaseAccess {
frame: self.frame.lock_access()?,
ticket,
})
}
#[must_use]
pub const fn retained_bytes(&self) -> u64 {
self.retained_bytes
}
#[must_use]
pub fn staged_signals(&self) -> usize {
self.staged_signals
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.len()
}
pub fn prepare_submission(self: &Arc<Self>) {
self.batch.enqueue(self);
}
pub fn flush_submission(&self) -> Result<(), BridgeError> {
self.batch.flush()
}
fn stage_submission_signal(&self, access: &mut VulkanFrameAccess) -> Result<(), BridgeError> {
let mut staged = self
.staged_signals
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if !staged.is_empty() || self.released.load(Ordering::Acquire) {
return Ok(());
}
let queue = unsafe { self.gpu.queue().as_hal::<wgpu_hal::api::Vulkan>() }
.ok_or(BridgeError::InvalidInput("renderer queue is not Vulkan"))?;
let descriptor = access.frame_mut();
for index in 0..descriptor.images.len() {
if descriptor.images[index] == 0 {
continue;
}
let semaphore = descriptor.semaphores[index];
let acquired_value = descriptor.semaphore_values[index];
if semaphore == 0 {
return Err(BridgeError::InvalidInput("frame has no timeline semaphore"));
}
if let Some(existing) = staged
.iter_mut()
.find(|existing| existing.semaphore == semaphore)
{
existing.acquired_value = existing.acquired_value.max(acquired_value);
existing.signal_value = existing.acquired_value.saturating_add(1);
} else {
staged.push(StagedSignal {
semaphore,
acquired_value,
signal_value: acquired_value.saturating_add(1),
});
}
}
for signal in &*staged {
queue.add_signal_semaphore(
vk::Semaphore::from_raw(signal.semaphore),
Some(signal.signal_value),
);
for index in 0..descriptor.images.len() {
if descriptor.semaphores[index] == signal.semaphore {
descriptor.semaphore_values[index] = signal.signal_value;
}
}
}
Ok(())
}
pub fn submitted(&self) -> Result<(), BridgeError> {
if self.released.load(Ordering::Acquire) {
return Ok(());
}
if !self
.staged_signals
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.is_empty()
{
self.released.store(true, Ordering::Release);
return Ok(());
}
let mut access = self.lock()?;
if self.released.load(Ordering::Acquire) {
return Ok(());
}
let mut status = 0;
let step = unsafe { release(&mut access, &mut status) };
if step != 0 {
return Err(helper_error("import release", step, status));
}
access.frame.commit();
self.released.store(true, Ordering::Release);
Ok(())
}
}
impl Drop for VulkanImportLease {
fn drop(&mut self) {
let Some(pooled) = self.ticket.take() else {
return;
};
if self.acquired.load(Ordering::Acquire) && !self.released.load(Ordering::Acquire) {
let staged = std::mem::take(
self.staged_signals
.get_mut()
.unwrap_or_else(std::sync::PoisonError::into_inner),
);
if let Ok(frame) = self.frame.lock_access() {
let mut lease = LeaseAccess {
frame,
ticket: pooled.ticket,
};
if staged.is_empty() {
let mut status = 0;
if unsafe { release(&mut lease, &mut status) } == 0 {
lease.frame.commit();
}
} else {
let queue = unsafe { self.gpu.queue().as_hal::<wgpu_hal::api::Vulkan>() };
for signal in &staged {
let unsubmitted = queue.as_ref().is_some_and(|queue| {
queue.remove_signal_semaphore(vk::Semaphore::from_raw(signal.semaphore))
});
if unsubmitted {
let mut status = 0;
let _ = unsafe {
alpha2_native_gpu_vulkan_signal(
lease.frame.access_mut(),
signal.semaphore,
signal.acquired_value,
signal.signal_value,
&raw mut status,
)
};
}
}
}
}
}
self.tickets.give_back(pooled);
}
}
#[derive(Clone, Copy, Debug, Default)]
struct FfmpegDeviceFeatures {
synchronization2: bool,
sampler_ycbcr_conversion: bool,
video_maintenance1: bool,
}
fn ffmpeg_device_features(
instance: &ash::Instance,
physical: vk::PhysicalDevice,
video_maintenance1: bool,
) -> FfmpegDeviceFeatures {
let mut synchronization2 = vk::PhysicalDeviceSynchronization2Features::default();
let mut ycbcr = vk::PhysicalDeviceSamplerYcbcrConversionFeatures::default();
let mut maintenance = vk::PhysicalDeviceVideoMaintenance1FeaturesKHR::default();
let mut features = vk::PhysicalDeviceFeatures2::default()
.push_next(&mut synchronization2)
.push_next(&mut ycbcr);
if video_maintenance1 {
features = features.push_next(&mut maintenance);
}
unsafe { instance.get_physical_device_features2(physical, &mut features) };
FfmpegDeviceFeatures {
synchronization2: synchronization2.synchronization2 == vk::TRUE,
sampler_ycbcr_conversion: ycbcr.sampler_ycbcr_conversion == vk::TRUE,
video_maintenance1: video_maintenance1 && maintenance.video_maintenance1 == vk::TRUE,
}
}
impl FfmpegDeviceFeatures {
fn enable(self, create_info: &mut vk::DeviceCreateInfo<'_>) {
let mut info = std::mem::take(create_info);
if self.synchronization2 {
info = info.push_next(Box::leak(Box::new(
vk::PhysicalDeviceSynchronization2Features::default().synchronization2(true),
)));
}
if self.sampler_ycbcr_conversion {
info = info.push_next(Box::leak(Box::new(
vk::PhysicalDeviceSamplerYcbcrConversionFeatures::default()
.sampler_ycbcr_conversion(true),
)));
}
if self.video_maintenance1 {
info = info.push_next(Box::leak(Box::new(
vk::PhysicalDeviceVideoMaintenance1FeaturesKHR::default().video_maintenance1(true),
)));
}
*create_info = info;
}
}
#[derive(Clone)]
pub struct SharedVulkanContext {
import_tickets: Arc<ImportTicketPool>,
ffmpeg_device: AVHWDeviceContext,
gpu: GpuContext,
submission_batch: Arc<VulkanSubmissionBatch>,
identity: NativeGpuIdentity,
decode_codecs: Vec<&'static str>,
encode_codecs: Vec<&'static str>,
encode_extent: Option<VideoEncodeExtent>,
validation_layer: Option<VulkanLayerVersion>,
}
impl fmt::Debug for SharedVulkanContext {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("SharedVulkanContext")
.field("identity", &self.identity)
.field("decode_codecs", &self.decode_codecs)
.field("encode_codecs", &self.encode_codecs)
.finish_non_exhaustive()
}
}
impl SharedVulkanContext {
#[allow(clippy::too_many_lines)]
pub fn create() -> Result<Self, SharedVulkanError> {
let _turn = alpha2_gpu::serialize_acquisition();
let hal_instance = hal_instance()?;
let mut adapters = unsafe { hal_instance.enumerate_adapters(None) };
let position = choose_adapter(
&mut adapters,
std::env::var("ALPHA2_VULKAN_DEVICE").ok().as_deref(),
)?;
let exposed = adapters.swap_remove(position);
let physical = exposed.adapter.raw_physical_device();
let shared_instance = exposed.adapter.shared_instance();
let raw_instance = shared_instance.raw_instance().handle();
let validation_layer = activated_validation_layer(shared_instance.raw_instance(), physical);
let queue_properties = unsafe {
shared_instance
.raw_instance()
.get_physical_device_queue_family_properties(physical)
};
let graphics_family = u32::try_from(
queue_properties
.iter()
.position(|family| {
family.queue_flags.contains(vk::QueueFlags::GRAPHICS) && family.queue_count > 0
})
.ok_or_else(|| SharedVulkanError("Vulkan adapter has no graphics queue".into()))?,
)
.map_err(|_| SharedVulkanError("graphics queue index is out of range".into()))?;
if graphics_family != 0 {
return Err(SharedVulkanError(
"wgpu-hal shared-device path requires graphics queue family 0".into(),
));
}
let graphics_flags = queue_properties[0].queue_flags;
if !graphics_flags.contains(vk::QueueFlags::COMPUTE | vk::QueueFlags::TRANSFER) {
return Err(SharedVulkanError(
"graphics queue family lacks FFmpeg compute/transfer capabilities".into(),
));
}
let decode_family = u32::try_from(
queue_properties
.iter()
.position(|family| {
family
.queue_flags
.contains(vk::QueueFlags::VIDEO_DECODE_KHR)
&& family.queue_count > 0
})
.ok_or_else(|| {
SharedVulkanError("Vulkan adapter has no video-decode queue".into())
})?,
)
.map_err(|_| SharedVulkanError("video-decode queue index is out of range".into()))?;
if decode_family == graphics_family {
return Err(SharedVulkanError(
"shared Vulkan device requires a decode queue separate from wgpu graphics".into(),
));
}
let encode_family = queue_properties
.iter()
.position(|family| {
family
.queue_flags
.contains(vk::QueueFlags::VIDEO_ENCODE_KHR)
&& family.queue_count > 0
})
.and_then(|index| u32::try_from(index).ok());
let extension_properties = unsafe {
shared_instance
.raw_instance()
.enumerate_device_extension_properties(physical)
}
.map_err(|error| SharedVulkanError(format!("Vulkan extensions: {error}")))?;
let supports = |name: &CStr| {
extension_properties.iter().any(|property| {
property
.extension_name_as_c_str()
.is_ok_and(|candidate| candidate == name)
})
};
let mut video_extensions = vec![
ash::khr::video_queue::NAME,
ash::khr::video_decode_queue::NAME,
];
for extension in [
ash::khr::video_decode_h264::NAME,
ash::khr::video_decode_h265::NAME,
ash::khr::video_decode_av1::NAME,
] {
if supports(extension) {
video_extensions.push(extension);
}
}
if !video_extensions.iter().all(|extension| supports(extension))
|| !video_extensions.contains(&ash::khr::video_decode_h264::NAME)
{
return Err(SharedVulkanError(
"Vulkan H.264 video decode is unavailable".into(),
));
}
let encode_extensions = [
ash::khr::video_encode_queue::NAME,
ash::khr::video_maintenance1::NAME,
ash::khr::video_encode_h264::NAME,
];
let encode_family = encode_family.filter(|_| {
encode_extensions
.iter()
.all(|extension| supports(extension))
});
let h264_extent = encode_family.and_then(|_| {
h264_encode_extent(
shared_instance.entry(),
shared_instance.raw_instance(),
physical,
)
});
if let Some(family) = encode_family {
video_extensions.extend_from_slice(&encode_extensions);
let _ = family;
}
let mut decode_caps = vk::VideoCodecOperationFlagsKHR::empty();
let mut decode_codecs = Vec::new();
for (extension, cap, label) in [
(
ash::khr::video_decode_h264::NAME,
vk::VideoCodecOperationFlagsKHR::DECODE_H264,
"h264",
),
(
ash::khr::video_decode_h265::NAME,
vk::VideoCodecOperationFlagsKHR::DECODE_H265,
"hevc",
),
(
ash::khr::video_decode_av1::NAME,
vk::VideoCodecOperationFlagsKHR::DECODE_AV1,
"av1",
),
] {
if video_extensions.contains(&extension) {
decode_caps |= cap;
decode_codecs.push(label);
}
}
let required_features = (exposed.features
& (wgpu::Features::TIMESTAMP_QUERY
| wgpu::Features::TEXTURE_FORMAT_P010
| wgpu::Features::TEXTURE_FORMAT_16BIT_NORM))
| wgpu::Features::TEXTURE_FORMAT_NV12;
let required_limits = wgpu::Limits::downlevel_webgl2_defaults()
.using_resolution(exposed.capabilities.limits.clone());
let enabled_extensions = exposed
.adapter
.required_device_extensions(required_features);
let core = exposed
.adapter
.physical_device_features(&enabled_extensions, required_features)
.get_core();
let ffmpeg_features = ffmpeg_device_features(
shared_instance.raw_instance(),
physical,
video_extensions.contains(&ash::khr::video_maintenance1::NAME),
);
let extra_family = decode_family;
let extra_encode_family =
encode_family.filter(|family| *family != graphics_family && *family != decode_family);
let extra_extensions = video_extensions.clone();
let open = unsafe {
exposed.adapter.open_with_callback(
required_features,
&required_limits,
&wgpu::MemoryHints::Performance,
Some(Box::new(move |args| {
ffmpeg_features.enable(args.create_info);
for extension in extra_extensions {
if !args.extensions.contains(&extension) {
args.extensions.push(extension);
}
}
if extra_family != 0 {
args.queue_create_infos.push(
vk::DeviceQueueCreateInfo::default()
.queue_family_index(extra_family)
.queue_priorities(&[1.0]),
);
}
if let Some(family) = extra_encode_family {
args.queue_create_infos.push(
vk::DeviceQueueCreateInfo::default()
.queue_family_index(family)
.queue_priorities(&[1.0]),
);
}
})),
)
}
.map_err(|error| SharedVulkanError(format!("shared Vulkan device: {error}")))?;
let raw_device = open.device.raw_device().handle();
let device_extensions = open
.device
.enabled_device_extensions()
.iter()
.map(|name| name.to_string_lossy().into_owned())
.collect::<Vec<_>>();
let instance_extensions = shared_instance
.extensions()
.iter()
.map(|name| name.to_string_lossy().into_owned())
.collect::<Vec<_>>();
let ffmpeg_device = unsafe {
create_vulkan_device_context(&VulkanDeviceInfo {
instance: raw_instance.as_raw(),
physical_device: physical.as_raw(),
device: raw_device.as_raw(),
graphics_queue_family: 0,
graphics_queue_count: 1,
decode_queue_family: decode_family,
decode_queue_count: 1,
decode_video_caps: decode_caps.as_raw(),
encode_queue_family: encode_family.unwrap_or(0),
encode_queue_count: u32::from(encode_family.is_some()),
encode_video_caps: encode_family
.map_or(0, |_| vk::VideoCodecOperationFlagsKHR::ENCODE_H264.as_raw()),
core_features: [
core.shader_image_gather_extended,
core.fragment_stores_and_atomics,
core.shader_int64,
core.vertex_pipeline_stores_and_atomics,
],
synchronization2: u32::from(ffmpeg_features.synchronization2),
sampler_ycbcr_conversion: u32::from(ffmpeg_features.sampler_ycbcr_conversion),
video_maintenance1: u32::from(ffmpeg_features.video_maintenance1),
instance_extensions,
device_extensions,
})
}
.map_err(|error| SharedVulkanError(format!("FFmpeg Vulkan device: {error}")))?;
let wgpu_instance =
unsafe { wgpu::Instance::from_hal::<wgpu_hal::api::Vulkan>(hal_instance) };
let adapter = unsafe { wgpu_instance.create_adapter_from_hal(exposed) };
let descriptor = wgpu::DeviceDescriptor {
label: Some("alpha2 shared Vulkan decode"),
required_features,
required_limits,
..Default::default()
};
let (device, queue) = unsafe { adapter.create_device_from_hal(open, &descriptor) }
.map_err(|error| SharedVulkanError(format!("wgpu adoption: {error}")))?;
let queue_lock: Arc<dyn ExternalQueueLock> = Arc::new(SharedQueueLock(
VulkanQueueLock::new(ffmpeg_device.clone(), 0)
.map_err(|error| SharedVulkanError(format!("FFmpeg Vulkan queue lock: {error}")))?,
));
let gpu = GpuContext::from_parts_with_queue_lock(adapter, device, queue, Some(queue_lock));
Ok(Self {
ffmpeg_device,
gpu,
submission_batch: Arc::new(VulkanSubmissionBatch::default()),
import_tickets: Arc::new(ImportTicketPool::default()),
identity: NativeGpuIdentity {
physical_device: physical.as_raw(),
device: raw_device.as_raw(),
epoch: 1,
},
decode_codecs,
encode_extent: h264_extent,
encode_codecs: if encode_family.is_some() {
vec!["h264"]
} else {
Vec::new()
},
validation_layer,
})
}
#[must_use]
pub fn gpu(&self) -> GpuContext {
self.gpu.clone()
}
#[must_use]
pub fn ffmpeg_device(&self) -> AVHWDeviceContext {
self.ffmpeg_device.clone()
}
#[must_use]
pub const fn identity(&self) -> NativeGpuIdentity {
self.identity
}
#[must_use]
pub fn supports_ten_bit(&self) -> bool {
self.gpu
.device()
.features()
.contains(wgpu::Features::TEXTURE_FORMAT_P010)
}
#[must_use]
pub fn decode_codecs(&self) -> &[&'static str] {
&self.decode_codecs
}
#[must_use]
pub const fn validation_layer(&self) -> Option<VulkanLayerVersion> {
self.validation_layer
}
#[must_use]
pub fn video_session_hazard(&self) -> Option<String> {
self.validation_layer
.filter(|layer| *layer < VulkanLayerVersion::VIDEO_SESSIONS)
.map(|layer| {
format!(
"the loaded Vulkan validation layer is {layer}, and layers older than {} \
crash in FFmpeg's video session setup",
VulkanLayerVersion::VIDEO_SESSIONS
)
})
}
#[must_use]
pub fn encode_codecs(&self) -> &[&'static str] {
&self.encode_codecs
}
#[must_use]
pub const fn encode_extent(&self) -> Option<&VideoEncodeExtent> {
self.encode_extent.as_ref()
}
#[must_use]
pub fn import_ticket_stats(&self) -> ImportTicketStats {
self.import_tickets.stats()
}
#[allow(clippy::too_many_lines)]
pub fn import_surface(
&self,
frame: Arc<VulkanFrameLease>,
) -> Result<ImportedVulkanFrame, BridgeError> {
let info = frame.info()?;
if info.device != self.identity.device
|| info.physical_device != self.identity.physical_device
{
return Err(BridgeError::InvalidInput(
"decoded frame belongs to another Vulkan device",
));
}
let layout = match (info.software_format, info.formats[0]) {
(rsmpeg::ffi::AV_PIX_FMT_NV12, format)
if format
== vk::Format::G8_B8R8_2PLANE_420_UNORM
.as_raw()
.cast_unsigned() =>
{
NativePlaneLayout::Nv12
}
(rsmpeg::ffi::AV_PIX_FMT_P010LE, format)
if format
== vk::Format::G10X6_B10X6R10X6_2PLANE_420_UNORM_3PACK16
.as_raw()
.cast_unsigned() =>
{
NativePlaneLayout::P010
}
_ => {
return Err(BridgeError::InvalidInput(
"only sampled single-image NV12 or P010 is importable",
));
}
};
if layout == NativePlaneLayout::P010
&& !self
.gpu
.device()
.features()
.contains(wgpu::Features::TEXTURE_FORMAT_P010)
{
return Err(BridgeError::InvalidInput(
"shared Vulkan device does not expose P010 plane views",
));
}
if info.plane_count != 1 || !info.sampled || info.width <= 0 || info.height <= 0 {
return Err(BridgeError::InvalidInput(
"only sampled single-image NV12 or P010 is importable",
));
}
if info.coded_width < info.width || info.coded_height < info.height {
return Err(BridgeError::InvalidInput(
"decoded frame is larger than its allocated image",
));
}
let coded_width = u32::try_from(info.coded_width)
.map_err(|_| BridgeError::InvalidInput("invalid coded width"))?;
let coded_height = u32::try_from(info.coded_height)
.map_err(|_| BridgeError::InvalidInput("invalid coded height"))?;
let access = frame.lock_access()?;
let mut status = 0;
let step =
unsafe { alpha2_native_gpu_vulkan_acquire_check(access.access(), &raw mut status) };
if step != 0 {
return Err(helper_error("import check", step, status));
}
let (device, allocator, queue_family) = {
let access = access.access();
(
access.frame().device,
access.allocator(),
access.queue_family(),
)
};
drop(access);
let ticket = self.import_tickets.take(device, allocator, queue_family)?;
let (texture_format, luma_format, chroma_format) = match layout {
NativePlaneLayout::P010 => (
wgpu::TextureFormat::P010,
wgpu::TextureFormat::R16Unorm,
wgpu::TextureFormat::Rg16Unorm,
),
_ => (
wgpu::TextureFormat::NV12,
wgpu::TextureFormat::R8Unorm,
wgpu::TextureFormat::Rg8Unorm,
),
};
let hal_descriptor = wgpu_hal::TextureDescriptor {
label: Some("alpha2 FFmpeg decode surface"),
size: wgpu::Extent3d {
width: coded_width,
height: coded_height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: texture_format,
usage: wgpu::TextureUses::RESOURCE,
memory_flags: wgpu_hal::MemoryFlags::empty(),
view_formats: Vec::new(),
};
let hal_device = unsafe { self.gpu.device().as_hal::<wgpu_hal::api::Vulkan>() }
.ok_or(BridgeError::InvalidInput("renderer is not Vulkan"))?;
let drop_owner = Arc::clone(&frame);
let hal_texture = unsafe {
hal_device.texture_from_raw(
vk::Image::from_raw(info.images[0]),
&hal_descriptor,
Some(Box::new(move || drop(drop_owner))),
wgpu_hal::vulkan::TextureMemory::External,
)
};
let descriptor = wgpu::TextureDescriptor {
label: Some("alpha2 FFmpeg decode surface"),
size: hal_descriptor.size,
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: texture_format,
usage: wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
};
let texture = unsafe {
self.gpu
.device()
.create_texture_from_hal::<wgpu_hal::api::Vulkan>(
hal_texture,
&descriptor,
wgpu::TextureUses::RESOURCE,
)
};
let y = texture.create_view(&wgpu::TextureViewDescriptor {
label: Some("alpha2 FFmpeg decode Y"),
format: Some(luma_format),
aspect: wgpu::TextureAspect::Plane0,
..Default::default()
});
let uv = texture.create_view(&wgpu::TextureViewDescriptor {
label: Some("alpha2 FFmpeg decode UV"),
format: Some(chroma_format),
aspect: wgpu::TextureAspect::Plane1,
..Default::default()
});
let pixels = u64::from(coded_width).saturating_mul(u64::from(coded_height));
let lease = Arc::new(VulkanImportLease {
frame,
_texture: texture,
gpu: self.gpu.clone(),
batch: Arc::clone(&self.submission_batch),
tickets: Arc::clone(&self.import_tickets),
ticket: Some(ticket),
staged_signals: Mutex::new(Vec::new()),
queued: AtomicBool::new(false),
acquired: AtomicBool::new(false),
released: AtomicBool::new(false),
retained_bytes: pixels
.saturating_mul(layout.bytes_per_pixel_numerator())
.div_ceil(2),
});
Ok(ImportedVulkanFrame {
y,
uv,
layout,
coded_size: [coded_width, coded_height],
lease,
})
}
}
fn choose_adapter(
adapters: &mut [wgpu_hal::ExposedAdapter<wgpu_hal::api::Vulkan>],
selector: Option<&str>,
) -> Result<usize, SharedVulkanError> {
let preference = wgpu::PowerPreference::from_env();
adapters.sort_by_key(|adapter| adapter_rank(adapter.info.device_type, preference));
adapters
.iter()
.position(|adapter| {
selector.is_none_or(|needle| adapter.info.name.contains(needle))
&& adapter
.features
.contains(wgpu::Features::TEXTURE_FORMAT_NV12)
})
.ok_or_else(|| {
SharedVulkanError(selector.map_or_else(
|| "no Vulkan adapter supports NV12 plane views".to_owned(),
|needle| {
format!(
"no Vulkan adapter whose name contains ALPHA2_VULKAN_DEVICE={needle:?} \
supports NV12 plane views"
)
},
))
})
}
pub fn shared_device_index(selector: Option<&str>) -> Result<usize, SharedVulkanError> {
let _turn = alpha2_gpu::serialize_acquisition();
let hal_instance = hal_instance()?;
let mut adapters = unsafe { hal_instance.enumerate_adapters(None) };
let position = choose_adapter(&mut adapters, selector)?;
let chosen = &adapters[position];
let physical = chosen.adapter.raw_physical_device();
let order = unsafe {
chosen
.adapter
.shared_instance()
.raw_instance()
.enumerate_physical_devices()
}
.map_err(|error| SharedVulkanError(format!("Vulkan device enumeration: {error}")))?;
order
.iter()
.position(|device| *device == physical)
.ok_or_else(|| SharedVulkanError("the chosen adapter is not in the device list".into()))
}
fn adapter_rank(device_type: wgpu::DeviceType, preference: Option<wgpu::PowerPreference>) -> u8 {
let low_power = preference == Some(wgpu::PowerPreference::LowPower);
match device_type {
wgpu::DeviceType::IntegratedGpu if low_power => 0,
wgpu::DeviceType::DiscreteGpu if low_power => 1,
wgpu::DeviceType::DiscreteGpu => 0,
wgpu::DeviceType::IntegratedGpu => 1,
_ => 2,
}
}
#[cfg(test)]
#[path = "vulkan_failure_tests.rs"]
mod failure_tests;
#[cfg(test)]
#[path = "vulkan_tests.rs"]
mod tests;