#![cfg(target_os = "windows")]
use std::time::Duration;
use anyhow::{Context, Result, anyhow, bail};
use windows::Win32::Foundation::{HMODULE, RECT};
use windows::Win32::Graphics::Direct3D::{
D3D_DRIVER_TYPE, D3D_DRIVER_TYPE_HARDWARE, D3D_FEATURE_LEVEL, D3D_FEATURE_LEVEL_10_0,
D3D_FEATURE_LEVEL_10_1, D3D_FEATURE_LEVEL_11_0,
};
use windows::Win32::Graphics::Direct3D11::{
D3D11_CPU_ACCESS_READ, D3D11_CREATE_DEVICE_BGRA_SUPPORT, D3D11_MAP_READ,
D3D11_MAPPED_SUBRESOURCE, D3D11_SDK_VERSION, D3D11_TEXTURE2D_DESC, D3D11_USAGE_STAGING,
D3D11CreateDevice, ID3D11Device, ID3D11DeviceContext, ID3D11Texture2D,
};
use windows::Win32::Graphics::Dxgi::Common::{DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_SAMPLE_DESC};
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;
pub const BYTES_PER_PIXEL: usize = 4;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DirtyRect {
pub left: i32,
pub top: i32,
pub right: i32,
pub bottom: i32,
}
impl DirtyRect {
pub fn area_px(&self) -> u64 {
let w = (self.right - self.left).max(0) as u64;
let h = (self.bottom - self.top).max(0) as u64;
w * h
}
}
impl From<RECT> for DirtyRect {
fn from(r: RECT) -> Self {
Self {
left: r.left,
top: r.top,
right: r.right,
bottom: r.bottom,
}
}
}
#[derive(Debug, Clone)]
pub struct Frame {
pub width: u32,
pub height: u32,
pub bgra: Vec<u8>,
pub dirty_rects: Vec<DirtyRect>,
pub accumulated_frames: u32,
}
impl Frame {
pub fn dirty_area_px(&self) -> u64 {
self.dirty_rects.iter().map(DirtyRect::area_px).sum()
}
pub fn total_px(&self) -> u64 {
u64::from(self.width) * u64::from(self.height)
}
}
#[derive(Debug)]
pub enum Capture {
Frame(Box<Frame>),
Idle,
Unavailable(String),
}
pub struct CaptureSession {
device: ID3D11Device,
context: ID3D11DeviceContext,
dupl: IDXGIOutputDuplication,
staging: Option<ID3D11Texture2D>,
width: u32,
height: u32,
output_index: u32,
frame_held: bool,
needs_rebuild: bool,
}
impl CaptureSession {
pub fn new(output_index: u32) -> Result<Self> {
let (device, context) = create_device()?;
let dupl = duplicate_output(&device, output_index)?;
Ok(Self {
device,
context,
dupl,
staging: None,
width: 0,
height: 0,
output_index,
frame_held: false,
needs_rebuild: false,
})
}
pub fn dimensions(&self) -> (u32, u32) {
(self.width, self.height)
}
fn unavailable(timeout_ms: u32, reason: String) -> Capture {
std::thread::sleep(Duration::from_millis(u64::from(timeout_ms)));
Capture::Unavailable(reason)
}
pub fn next_frame(&mut self, timeout_ms: u32) -> Result<Capture> {
if self.needs_rebuild {
match duplicate_output(&self.device, self.output_index) {
Ok(dupl) => {
self.dupl = dupl;
self.needs_rebuild = false;
self.staging = None;
}
Err(e) => {
return Ok(Self::unavailable(
timeout_ms,
format!("desktop still gone: {e}"),
));
}
}
}
let mut info = DXGI_OUTDUPL_FRAME_INFO::default();
let mut resource: Option<IDXGIResource> = None;
let acquired = unsafe {
self.dupl
.AcquireNextFrame(timeout_ms, &mut info, &mut resource)
};
if let Err(e) = acquired {
return match e.code() {
DXGI_ERROR_WAIT_TIMEOUT => Ok(Capture::Idle),
DXGI_ERROR_ACCESS_LOST => {
self.mark_lost();
Ok(Self::unavailable(
timeout_ms,
format!("desktop access lost: {e}"),
))
}
_ => Err(anyhow!("AcquireNextFrame failed: {e}")),
};
}
self.frame_held = true;
let result = self.process_frame(&info, resource);
self.release_frame();
result
}
fn process_frame(
&mut self,
info: &DXGI_OUTDUPL_FRAME_INFO,
resource: Option<IDXGIResource>,
) -> Result<Capture> {
if info.LastPresentTime == 0 {
return Ok(Capture::Idle);
}
let resource = resource
.ok_or_else(|| anyhow!("AcquireNextFrame succeeded but returned no resource"))?;
let frame_tex: ID3D11Texture2D = resource
.cast()
.context("desktop resource is not an ID3D11Texture2D")?;
let mut desc = D3D11_TEXTURE2D_DESC::default();
unsafe { frame_tex.GetDesc(&mut desc) };
self.ensure_staging(desc.Width, desc.Height)?;
let staging = self
.staging
.as_ref()
.ok_or_else(|| anyhow!("staging texture missing after ensure_staging"))?;
unsafe { self.context.CopyResource(staging, &frame_tex) };
let bgra = self.read_staging()?;
let dirty_rects = self.dirty_rects()?;
Ok(Capture::Frame(Box::new(Frame {
width: self.width,
height: self.height,
bgra,
dirty_rects,
accumulated_frames: info.AccumulatedFrames,
})))
}
fn read_staging(&self) -> Result<Vec<u8>> {
let staging = self
.staging
.as_ref()
.ok_or_else(|| anyhow!("read_staging called with no staging texture"))?;
let mut mapped = D3D11_MAPPED_SUBRESOURCE::default();
unsafe {
self.context
.Map(staging, 0, D3D11_MAP_READ, 0, Some(&mut mapped))
.context("map staging texture")?;
}
let row_bytes = self.width as usize * BYTES_PER_PIXEL;
let pitch = mapped.RowPitch as usize;
let height = self.height as usize;
let mut out = vec![0u8; row_bytes * height];
if pitch < row_bytes {
unsafe { self.context.Unmap(staging, 0) };
bail!("staging RowPitch {pitch} is narrower than a {row_bytes}-byte row");
}
unsafe {
let src = mapped.pData as *const u8;
for y in 0..height {
std::ptr::copy_nonoverlapping(
src.add(y * pitch),
out.as_mut_ptr().add(y * row_bytes),
row_bytes,
);
}
self.context.Unmap(staging, 0);
}
Ok(out)
}
fn dirty_rects(&self) -> Result<Vec<DirtyRect>> {
let mut buf = vec![RECT::default(); 256];
let mut required: u32 = 0;
let buf_bytes = std::mem::size_of_val(buf.as_slice()) as u32;
let r = unsafe {
self.dupl
.GetFrameDirtyRects(buf_bytes, buf.as_mut_ptr(), &mut required)
};
match r {
Ok(()) => {
let n = required as usize / std::mem::size_of::<RECT>();
Ok(buf.into_iter().take(n).map(DirtyRect::from).collect())
}
Err(_) if required as usize > buf_bytes as usize => {
let n = required as usize / std::mem::size_of::<RECT>();
let mut big = vec![RECT::default(); n];
let big_bytes = std::mem::size_of_val(big.as_slice()) as u32;
let mut required2: u32 = 0;
let r2 = unsafe {
self.dupl
.GetFrameDirtyRects(big_bytes, big.as_mut_ptr(), &mut required2)
};
if r2.is_err() {
return Ok(Vec::new());
}
let n2 = required2 as usize / std::mem::size_of::<RECT>();
Ok(big.into_iter().take(n2).map(DirtyRect::from).collect())
}
Err(_) => Ok(Vec::new()),
}
}
fn ensure_staging(&mut self, width: u32, height: u32) -> Result<()> {
if self.staging.is_some() && self.width == width && self.height == height {
return Ok(());
}
let desc = D3D11_TEXTURE2D_DESC {
Width: width,
Height: height,
MipLevels: 1,
ArraySize: 1,
Format: DXGI_FORMAT_B8G8R8A8_UNORM,
SampleDesc: DXGI_SAMPLE_DESC {
Count: 1,
Quality: 0,
},
Usage: D3D11_USAGE_STAGING,
BindFlags: 0,
CPUAccessFlags: D3D11_CPU_ACCESS_READ.0 as u32,
MiscFlags: 0,
};
let mut tex: Option<ID3D11Texture2D> = None;
unsafe {
self.device
.CreateTexture2D(&desc, None, Some(&mut tex))
.context("create staging texture")?;
}
self.staging = tex;
self.width = width;
self.height = height;
Ok(())
}
fn release_frame(&mut self) {
if !self.frame_held {
return;
}
let _ = unsafe { self.dupl.ReleaseFrame() };
self.frame_held = false;
}
fn mark_lost(&mut self) {
self.frame_held = false;
self.needs_rebuild = true;
}
}
impl Drop for CaptureSession {
fn drop(&mut self) {
self.release_frame();
}
}
fn create_device() -> Result<(ID3D11Device, ID3D11DeviceContext)> {
let levels = [
D3D_FEATURE_LEVEL_11_0,
D3D_FEATURE_LEVEL_10_1,
D3D_FEATURE_LEVEL_10_0,
];
try_create_device(D3D_DRIVER_TYPE_HARDWARE, &levels)
}
fn try_create_device(
driver: D3D_DRIVER_TYPE,
levels: &[D3D_FEATURE_LEVEL],
) -> Result<(ID3D11Device, ID3D11DeviceContext)> {
let mut device: Option<ID3D11Device> = None;
let mut context: Option<ID3D11DeviceContext> = None;
unsafe {
D3D11CreateDevice(
None,
driver,
HMODULE::default(),
D3D11_CREATE_DEVICE_BGRA_SUPPORT,
Some(levels),
D3D11_SDK_VERSION,
Some(&mut device),
None,
Some(&mut context),
)
.with_context(|| format!("D3D11CreateDevice({driver:?})"))?;
}
let device = device.ok_or_else(|| anyhow!("D3D11CreateDevice returned no device"))?;
let context = context.ok_or_else(|| anyhow!("D3D11CreateDevice returned no context"))?;
Ok((device, context))
}
fn duplicate_output(device: &ID3D11Device, output_index: u32) -> Result<IDXGIOutputDuplication> {
let dxgi_device: IDXGIDevice = device
.cast()
.context("D3D11 device does not implement IDXGIDevice")?;
let adapter: IDXGIAdapter = unsafe { dxgi_device.GetAdapter() }.context("get DXGI adapter")?;
let output = unsafe { adapter.EnumOutputs(output_index) }
.with_context(|| format!("no display output at index {output_index}"))?;
let output1: IDXGIOutput1 = output
.cast()
.context("display output does not implement IDXGIOutput1 (needs Windows 8+)")?;
unsafe { output1.DuplicateOutput(device) }.context(
"DuplicateOutput failed — another process may already be duplicating this output, \
or this process is not running in the interactive desktop session",
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dirty_rect_area_is_width_times_height() {
let r = DirtyRect {
left: 10,
top: 20,
right: 110,
bottom: 70,
};
assert_eq!(r.area_px(), 100 * 50);
}
#[test]
fn degenerate_dirty_rect_has_zero_area() {
let r = DirtyRect {
left: 100,
top: 100,
right: 10,
bottom: 10,
};
assert_eq!(r.area_px(), 0);
}
#[test]
fn empty_dirty_rect_has_zero_area() {
let r = DirtyRect {
left: 5,
top: 5,
right: 5,
bottom: 5,
};
assert_eq!(r.area_px(), 0);
}
#[test]
fn frame_dirty_area_sums_every_rect() {
let f = Frame {
width: 1920,
height: 1080,
bgra: Vec::new(),
dirty_rects: vec![
DirtyRect {
left: 0,
top: 0,
right: 10,
bottom: 10,
},
DirtyRect {
left: 100,
top: 100,
right: 120,
bottom: 110,
},
],
accumulated_frames: 1,
};
assert_eq!(f.dirty_area_px(), 100 + 200);
assert_eq!(f.total_px(), 1920 * 1080);
}
#[test]
fn frame_with_no_dirty_rects_reports_zero_area() {
let f = Frame {
width: 640,
height: 480,
bgra: Vec::new(),
dirty_rects: Vec::new(),
accumulated_frames: 1,
};
assert_eq!(f.dirty_area_px(), 0);
}
#[test]
fn rect_converts_from_win32_rect() {
let r: DirtyRect = RECT {
left: 1,
top: 2,
right: 3,
bottom: 4,
}
.into();
assert_eq!(
r,
DirtyRect {
left: 1,
top: 2,
right: 3,
bottom: 4
}
);
}
}