use std::time::{Duration, Instant};
use windows::Win32::Graphics::Direct3D11::{
D3D11_CPU_ACCESS_READ, D3D11_MAP_READ, D3D11_MAPPED_SUBRESOURCE, D3D11_TEXTURE2D_DESC, D3D11_USAGE_STAGING,
ID3D11Device, ID3D11DeviceContext, ID3D11Texture2D,
};
use windows::Win32::Graphics::Dxgi::{
DXGI_ERROR_ACCESS_LOST, DXGI_ERROR_WAIT_TIMEOUT, DXGI_OUTDUPL_FRAME_INFO, IDXGIAdapter, IDXGIDevice, IDXGIOutput1,
IDXGIOutputDuplication, IDXGIResource,
};
use windows::core::Interface;
use super::channel::FrameChannel;
use super::pump::{self, Geometry};
use super::{Config, FrameStream};
use crate::Error;
use crate::frame::{Frame, I420, d3d11};
const DEFAULT_FRAMERATE: u32 = 30;
fn err(ctx: &str, e: windows::core::Error) -> Error {
Error::Codec(anyhow::anyhow!("{ctx}: {e}"))
}
pub(super) async fn open(config: &Config, device: Option<&str>) -> Result<FrameStream, Error> {
let config = config.clone();
let device = device.map(str::to_string);
let chan = FrameChannel::new();
let (geo, guard) = pump::spawn(
chan.clone(),
move || {
let cap = Duplicator::open(&config, device.as_deref())?;
let geometry = Geometry {
width: cap.width,
height: cap.height,
framerate: Some(cap.framerate),
device: cap.device_name.clone(),
};
Ok((cap, geometry))
},
Duplicator::read,
)
.await?;
Ok(FrameStream::new(
chan,
geo.width,
geo.height,
geo.framerate,
geo.device,
None,
Box::new(guard),
))
}
struct Duplicator {
device: ID3D11Device,
context: ID3D11DeviceContext,
output: IDXGIOutput1,
dupl: IDXGIOutputDuplication,
staging: Option<ID3D11Texture2D>,
width: u32,
height: u32,
framerate: u32,
interval: Duration,
next_deadline: Option<Instant>,
last: Option<I420>,
device_name: String,
}
impl Duplicator {
fn open(config: &Config, selector: Option<&str>) -> Result<Self, Error> {
let device = d3d11::create_device()?;
let context = unsafe {
device
.GetImmediateContext()
.map_err(|e| err("GetImmediateContext", e))?
};
let index = select_output(selector)?;
let output = enumerate_output(&device, index)?;
let device_name = format!("display:{index}");
let dupl = duplicate(&output, &device)?;
let desc = unsafe { dupl.GetDesc() };
let width = desc.ModeDesc.Width & !1;
let height = desc.ModeDesc.Height & !1;
if width == 0 || height == 0 {
return Err(Error::Codec(anyhow::anyhow!(
"display {index} reported an unusable size {}x{}",
desc.ModeDesc.Width,
desc.ModeDesc.Height
)));
}
let framerate = config.framerate.unwrap_or(DEFAULT_FRAMERATE).max(1);
let mut cap = Self {
device,
context,
output,
dupl,
staging: None,
width,
height,
framerate,
interval: Duration::from_micros(1_000_000 / framerate as u64),
next_deadline: None,
last: None,
device_name,
};
for _ in 0..20 {
if cap.capture_once(100)? {
break;
}
}
if cap.last.is_none() {
return Err(Error::Codec(anyhow::anyhow!("no desktop frame within timeout")));
}
tracing::info!(
display = %cap.device_name,
width = cap.width,
height = cap.height,
framerate = cap.framerate,
"opened Desktop Duplication capture"
);
Ok(cap)
}
fn reduplicate(&mut self) -> Result<(), Error> {
self.dupl = duplicate(&self.output, &self.device)?;
self.staging = None;
Ok(())
}
fn capture_once(&mut self, timeout_ms: u32) -> Result<bool, Error> {
let mut info = DXGI_OUTDUPL_FRAME_INFO::default();
let mut resource: Option<IDXGIResource> = None;
match unsafe { self.dupl.AcquireNextFrame(timeout_ms, &mut info, &mut resource) } {
Ok(()) => {}
Err(e) if e.code() == DXGI_ERROR_WAIT_TIMEOUT => return Ok(false),
Err(e) if e.code() == DXGI_ERROR_ACCESS_LOST => {
self.reduplicate()?;
return Ok(false);
}
Err(e) => return Err(err("AcquireNextFrame", e)),
}
let resource = resource.ok_or_else(|| Error::Codec(anyhow::anyhow!("AcquireNextFrame returned no surface")))?;
let texture = resource
.cast::<ID3D11Texture2D>()
.map_err(|e| err("desktop surface is not a texture", e))?;
let result = self.copy_to_last(&texture);
unsafe {
let _ = self.dupl.ReleaseFrame();
}
result?;
Ok(true)
}
fn copy_to_last(&mut self, texture: &ID3D11Texture2D) -> Result<(), Error> {
let staging = self.ensure_staging(texture)?;
unsafe { self.context.CopyResource(&staging, texture) };
let mut mapped = D3D11_MAPPED_SUBRESOURCE::default();
unsafe {
self.context
.Map(&staging, 0, D3D11_MAP_READ, 0, Some(&mut mapped))
.map_err(|e| err("Map (staging)", e))?;
}
let _guard = UnmapGuard {
context: &self.context,
resource: &staging,
};
let pitch = mapped.RowPitch;
let len = pitch as usize * self.height as usize;
let bgra = unsafe { std::slice::from_raw_parts(mapped.pData as *const u8, len) };
self.last = Some(I420::from_bgra(bgra, pitch, self.width, self.height)?);
Ok(())
}
fn ensure_staging(&mut self, texture: &ID3D11Texture2D) -> Result<ID3D11Texture2D, Error> {
if let Some(staging) = &self.staging {
return Ok(staging.clone());
}
let mut desc = D3D11_TEXTURE2D_DESC::default();
unsafe { texture.GetDesc(&mut desc) };
desc.Usage = D3D11_USAGE_STAGING;
desc.BindFlags = 0;
desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ.0 as u32;
desc.MiscFlags = 0;
desc.ArraySize = 1;
desc.MipLevels = 1;
let mut staging: Option<ID3D11Texture2D> = None;
unsafe {
self.device
.CreateTexture2D(&desc, None, Some(&mut staging))
.map_err(|e| err("CreateTexture2D (staging)", e))?;
}
let staging = staging.ok_or_else(|| Error::Codec(anyhow::anyhow!("CreateTexture2D returned null")))?;
self.staging = Some(staging.clone());
Ok(staging)
}
fn read(&mut self) -> Result<Option<Frame>, Error> {
let deadline = *self.next_deadline.get_or_insert_with(|| Instant::now() + self.interval);
loop {
let now = Instant::now();
if now >= deadline {
break;
}
let remaining = (deadline - now).as_millis().max(1) as u32;
self.capture_once(remaining)?;
}
let next = deadline + self.interval;
self.next_deadline = Some(next.max(Instant::now()));
Ok(self.last.clone().map(Frame::I420))
}
}
struct UnmapGuard<'a> {
context: &'a ID3D11DeviceContext,
resource: &'a ID3D11Texture2D,
}
impl Drop for UnmapGuard<'_> {
fn drop(&mut self) {
unsafe { self.context.Unmap(self.resource, 0) };
}
}
fn select_output(selector: Option<&str>) -> Result<u32, Error> {
match selector {
None => Ok(0),
Some(spec) => spec
.strip_prefix("display:")
.unwrap_or(spec)
.parse::<u32>()
.map_err(|_| Error::Codec(anyhow::anyhow!("invalid display selector {spec:?}"))),
}
}
fn enumerate_output(device: &ID3D11Device, index: u32) -> Result<IDXGIOutput1, Error> {
let dxgi = device
.cast::<IDXGIDevice>()
.map_err(|e| err("device is not a DXGI device", e))?;
let adapter: IDXGIAdapter = unsafe { dxgi.GetAdapter().map_err(|e| err("GetAdapter", e))? };
let output = unsafe {
adapter
.EnumOutputs(index)
.map_err(|_| Error::Codec(anyhow::anyhow!("no display at index {index}")))?
};
output
.cast::<IDXGIOutput1>()
.map_err(|e| err("output is not IDXGIOutput1", e))
}
fn duplicate(output: &IDXGIOutput1, device: &ID3D11Device) -> Result<IDXGIOutputDuplication, Error> {
unsafe { output.DuplicateOutput(device) }.map_err(|e| err("DuplicateOutput", e))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::capture::Config;
use crate::frame::Frame;
#[test]
#[ignore]
fn duplicates_primary_display() {
let mut cap = match Duplicator::open(&Config::default(), None) {
Ok(cap) => cap,
Err(e) => {
eprintln!("skipping: no Desktop Duplication available: {e}");
return;
}
};
assert!(cap.width >= 2 && cap.width % 2 == 0, "bad width {}", cap.width);
assert!(cap.height >= 2 && cap.height % 2 == 0, "bad height {}", cap.height);
for i in 0..5 {
let frame = cap.read().expect("read frame");
let Some(Frame::I420(i420)) = frame else {
panic!("frame {i} was not I420");
};
assert_eq!(i420.width, cap.width);
assert_eq!(i420.height, cap.height);
assert_eq!(i420.data.len(), I420::len(cap.width, cap.height));
}
eprintln!("captured 5 frames at {}x{}", cap.width, cap.height);
}
}