#![allow(unsafe_code)]
#![allow(clippy::redundant_pub_crate)]
use crate::{CaptureError, DeviceId};
use mediaway_common::{
Bytes, CodecKind, GpuBufferHandle, NativeHandle, PixelFormat, StreamInfo, VideoFrame,
VideoFrameStorage, VideoGeometry,
};
use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, OnceLock, PoisonError, Weak, mpsc};
use std::thread::JoinHandle;
use windows::Win32::Graphics::Direct3D11::{
D3D11_BIND_SHADER_RESOURCE, D3D11_TEXTURE2D_DESC, D3D11_USAGE_DEFAULT, ID3D11Device,
ID3D11Texture2D,
};
use windows::Win32::Graphics::Dxgi::Common::{DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_SAMPLE_DESC};
use windows::Win32::Graphics::Dxgi::{
DXGI_ERROR_ACCESS_LOST, DXGI_OUTDUPL_FRAME_INFO, IDXGIDevice, IDXGIOutput1,
IDXGIOutputDuplication, IDXGIResource,
};
use windows::core::Interface;
const POLL_TIMEOUT_MS: u32 = 16;
fn registry() -> &'static Mutex<HashMap<DeviceId, Weak<SharedDuplication>>> {
static REGISTRY: OnceLock<Mutex<HashMap<DeviceId, Weak<SharedDuplication>>>> = OnceLock::new();
REGISTRY.get_or_init(|| Mutex::new(HashMap::new()))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum SlotState {
Empty,
Pending,
Held,
}
struct ConsumerRecord {
id: u64,
raw_texture_ptr: usize,
state: SlotState,
}
enum ControlMsg {
Attach {
reply: mpsc::Sender<Result<u64, CaptureError>>,
},
Detach {
id: u64,
},
}
pub(crate) struct SharedDuplication {
consumers: Arc<Mutex<Vec<ConsumerRecord>>>,
control_tx: mpsc::Sender<ControlMsg>,
shutdown: Arc<AtomicBool>,
driver_thread: Mutex<Option<JoinHandle<()>>>,
stream_info: StreamInfo,
device_raw: usize,
}
impl Drop for SharedDuplication {
fn drop(&mut self) {
self.shutdown.store(true, Ordering::SeqCst);
let handle = self
.driver_thread
.lock()
.unwrap_or_else(PoisonError::into_inner)
.take();
if let Some(handle) = handle {
let _ = handle.join();
}
}
}
pub(crate) fn attach(
key: DeviceId,
device_raw: usize,
output_index: u32,
) -> Result<(Arc<SharedDuplication>, u64, StreamInfo), CaptureError> {
let mut map = registry().lock().unwrap_or_else(PoisonError::into_inner);
let existing = map.get(&key).and_then(Weak::upgrade);
let shared = if let Some(shared) = existing {
shared
} else {
let shared = spawn_driver(device_raw, output_index)?;
map.insert(key, Arc::downgrade(&shared));
shared
};
if shared.device_raw != device_raw {
return Err(CaptureError::InvalidInput);
}
let (reply_tx, reply_rx) = mpsc::channel();
shared
.control_tx
.send(ControlMsg::Attach { reply: reply_tx })
.map_err(|_| CaptureError::Backend)?;
let consumer_id = reply_rx.recv().map_err(|_| CaptureError::Backend)??;
let stream_info = shared.stream_info.clone();
drop(map);
Ok((shared, consumer_id, stream_info))
}
fn spawn_driver(
device_raw: usize,
output_index: u32,
) -> Result<Arc<SharedDuplication>, CaptureError> {
let consumers: Arc<Mutex<Vec<ConsumerRecord>>> = Arc::new(Mutex::new(Vec::new()));
let shutdown = Arc::new(AtomicBool::new(false));
let (control_tx, control_rx) = mpsc::channel();
let (ready_tx, ready_rx) = mpsc::channel::<Result<StreamInfo, CaptureError>>();
let thread_consumers = Arc::clone(&consumers);
let thread_shutdown = Arc::clone(&shutdown);
let handle = std::thread::Builder::new()
.name("mediaway-dxgi-shared".to_owned())
.spawn(move || {
driver_loop(
device_raw,
output_index,
&thread_consumers,
&thread_shutdown,
&control_rx,
&ready_tx,
);
})
.map_err(|_| CaptureError::Backend)?;
let stream_info = match ready_rx.recv() {
Ok(Ok(info)) => info,
Ok(Err(e)) => {
let _ = handle.join();
return Err(e);
}
Err(_) => {
let _ = handle.join();
return Err(CaptureError::Backend);
}
};
Ok(Arc::new(SharedDuplication {
consumers,
control_tx,
shutdown,
driver_thread: Mutex::new(Some(handle)),
stream_info,
device_raw,
}))
}
fn driver_loop(
device_raw: usize,
output_index: u32,
consumers: &Arc<Mutex<Vec<ConsumerRecord>>>,
shutdown: &Arc<AtomicBool>,
control_rx: &mpsc::Receiver<ControlMsg>,
ready_tx: &mpsc::Sender<Result<StreamInfo, CaptureError>>,
) {
let opened = open_duplication(device_raw, output_index);
let (device, duplication, stream_info) = match opened {
Ok(parts) => parts,
Err(e) => {
let _ = ready_tx.send(Err(e));
return;
}
};
if ready_tx.send(Ok(stream_info.clone())).is_err() {
return;
}
let mut textures: HashMap<u64, ID3D11Texture2D> = HashMap::new();
let mut next_id: u64 = 0;
loop {
if shutdown.load(Ordering::SeqCst) {
break;
}
while let Ok(msg) = control_rx.try_recv() {
match msg {
ControlMsg::Attach { reply } => {
let result = attach_consumer(
&device,
&stream_info,
&mut next_id,
&mut textures,
consumers,
);
let _ = reply.send(result);
}
ControlMsg::Detach { id } => {
textures.remove(&id);
let mut guard = consumers.lock().unwrap_or_else(PoisonError::into_inner);
guard.retain(|c| c.id != id);
}
}
}
let mut frame_info = DXGI_OUTDUPL_FRAME_INFO::default();
let mut desktop_resource: Option<IDXGIResource> = None;
let acquire = unsafe {
duplication.AcquireNextFrame(
POLL_TIMEOUT_MS,
&raw mut frame_info,
&raw mut desktop_resource,
)
};
if let Err(e) = acquire {
if e.code() == DXGI_ERROR_ACCESS_LOST {
break; }
continue;
}
let Some(desktop_resource) = desktop_resource else {
continue;
};
let Ok(source_texture) = desktop_resource.cast::<ID3D11Texture2D>() else {
let _ = unsafe { duplication.ReleaseFrame() };
continue;
};
copy_to_ready_consumers(&device, &source_texture, consumers, &textures);
let _ = unsafe { duplication.ReleaseFrame() };
}
}
fn open_duplication(
device_raw: usize,
output_index: u32,
) -> Result<(ID3D11Device, IDXGIOutputDuplication, StreamInfo), CaptureError> {
let raw = device_raw as *mut std::ffi::c_void;
let device_ref =
unsafe { ID3D11Device::from_raw_borrowed(&raw) }.ok_or(CaptureError::InvalidInput)?;
let device: ID3D11Device = device_ref.clone();
let dxgi_device: IDXGIDevice = device.cast().map_err(|_| CaptureError::Backend)?;
let adapter = unsafe { dxgi_device.GetAdapter() }.map_err(|_| CaptureError::Backend)?;
let output =
unsafe { adapter.EnumOutputs(output_index) }.map_err(|_| CaptureError::InvalidInput)?;
let output1: IDXGIOutput1 = output.cast().map_err(|_| CaptureError::Backend)?;
let duplication =
unsafe { output1.DuplicateOutput(&device) }.map_err(|_| CaptureError::AccessDenied)?;
let dup_desc = unsafe { duplication.GetDesc() };
let width = dup_desc.ModeDesc.Width;
let height = dup_desc.ModeDesc.Height;
if width == 0 || height == 0 {
return Err(CaptureError::Backend);
}
let stream_info = StreamInfo::Video {
id: 0,
codec: CodecKind::RawVideo,
time_base: mediaway_common::Rational::new(1, 60),
geometry: VideoGeometry { width, height },
extra_data: Bytes::new(),
};
Ok((device, duplication, stream_info))
}
fn attach_consumer(
device: &ID3D11Device,
stream_info: &StreamInfo,
next_id: &mut u64,
textures: &mut HashMap<u64, ID3D11Texture2D>,
consumers: &Arc<Mutex<Vec<ConsumerRecord>>>,
) -> Result<u64, CaptureError> {
let geometry = stream_info.geometry().unwrap_or(VideoGeometry {
width: 0,
height: 0,
});
let desc = D3D11_TEXTURE2D_DESC {
Width: geometry.width,
Height: geometry.height,
MipLevels: 1,
ArraySize: 1,
Format: DXGI_FORMAT_B8G8R8A8_UNORM,
SampleDesc: DXGI_SAMPLE_DESC {
Count: 1,
Quality: 0,
},
Usage: D3D11_USAGE_DEFAULT,
BindFlags: D3D11_BIND_SHADER_RESOURCE.0 as u32,
MiscFlags: 0,
CPUAccessFlags: 0,
};
let mut texture: Option<ID3D11Texture2D> = None;
unsafe {
device
.CreateTexture2D(&raw const desc, None, Some(&raw mut texture))
.map_err(|_| CaptureError::Backend)?;
}
let texture = texture.ok_or(CaptureError::Backend)?;
let raw_texture_ptr = Interface::as_raw(&texture) as usize;
let id = *next_id;
*next_id += 1;
textures.insert(id, texture);
consumers
.lock()
.unwrap_or_else(PoisonError::into_inner)
.push(ConsumerRecord {
id,
raw_texture_ptr,
state: SlotState::Empty,
});
Ok(id)
}
fn copy_to_ready_consumers(
device: &ID3D11Device,
source: &ID3D11Texture2D,
consumers: &Arc<Mutex<Vec<ConsumerRecord>>>,
textures: &HashMap<u64, ID3D11Texture2D>,
) {
let Ok(context) = (unsafe { device.GetImmediateContext() }) else {
return;
};
let mut guard = consumers.lock().unwrap_or_else(PoisonError::into_inner);
for record in guard.iter_mut() {
if record.state != SlotState::Empty {
continue;
}
let Some(dest) = textures.get(&record.id) else {
continue;
};
unsafe { context.CopyResource(dest, source) };
record.state = SlotState::Pending;
}
}
pub(crate) fn detach(shared: &SharedDuplication, consumer_id: u64) {
let _ = shared
.control_tx
.send(ControlMsg::Detach { id: consumer_id });
}
pub(crate) fn poll_shared_frame(
shared: &SharedDuplication,
consumer_id: u64,
next_pts: &mut i64,
) -> Result<Option<VideoFrame>, CaptureError> {
let mut guard = shared
.consumers
.lock()
.unwrap_or_else(PoisonError::into_inner);
let record = guard
.iter_mut()
.find(|c| c.id == consumer_id)
.ok_or(CaptureError::Closed)?;
let raw_texture_ptr = match record.state {
SlotState::Held => return Err(CaptureError::Backend),
SlotState::Empty => return Ok(None),
SlotState::Pending => {
record.state = SlotState::Held;
record.raw_texture_ptr
}
};
drop(guard);
let geometry = shared.stream_info.geometry().unwrap_or(VideoGeometry {
width: 0,
height: 0,
});
let texture_handle = NativeHandle::new(raw_texture_ptr).ok_or(CaptureError::Backend)?;
let pts = *next_pts;
*next_pts += 1;
Ok(Some(VideoFrame {
pts,
duration: 1,
width: geometry.width,
height: geometry.height,
format: PixelFormat::Bgra8,
storage: VideoFrameStorage::Gpu(GpuBufferHandle::DirectX11 {
texture: texture_handle,
subresource: 0,
}),
}))
}
pub(crate) fn release_shared_frame(
shared: &SharedDuplication,
consumer_id: u64,
) -> Result<(), CaptureError> {
let mut guard = shared
.consumers
.lock()
.unwrap_or_else(PoisonError::into_inner);
let record = guard
.iter_mut()
.find(|c| c.id == consumer_id)
.ok_or(CaptureError::Closed)?;
if record.state == SlotState::Held {
record.state = SlotState::Empty;
}
drop(guard);
Ok(())
}
#[cfg(test)]
#[path = "dxgi_shared_tests.rs"]
mod tests;