use super::super::core::{self, Capture, CaptureBudget, SurfaceGeometry, TargetSpec};
use super::window;
use anyhow::anyhow;
use std::time::{Duration, Instant};
use windows::Graphics::Capture::{
Direct3D11CaptureFrame, Direct3D11CaptureFramePool, GraphicsCaptureItem,
};
use windows::Graphics::DirectX::Direct3D11::IDirect3DDevice;
use windows::Graphics::DirectX::DirectXPixelFormat;
use windows::Win32::Foundation::{HMODULE, HWND, LPARAM, RECT};
use windows::Win32::Graphics::Direct3D::{
D3D_DRIVER_TYPE_HARDWARE, D3D_DRIVER_TYPE_WARP, 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::{IDXGIAdapter, IDXGIDevice};
use windows::Win32::Graphics::Gdi::{
BI_RGB, BITMAPINFO, BITMAPINFOHEADER, BitBlt, CAPTUREBLT, CreateCompatibleDC, CreateDIBSection,
DIB_RGB_COLORS, DeleteDC, DeleteObject, EnumDisplayMonitors, GdiFlush, GetDC, GetMonitorInfoW,
HBITMAP, HDC, HGDIOBJ, HMONITOR, MONITORINFO, ReleaseDC, SRCCOPY, SelectObject,
};
use windows::Win32::System::WinRT::Direct3D11::{
CreateDirect3D11DeviceFromDXGIDevice, IDirect3DDxgiInterfaceAccess,
};
use windows::Win32::System::WinRT::Graphics::Capture::IGraphicsCaptureItemInterop;
use windows::Win32::System::WinRT::RoGetActivationFactory;
use windows::core::{BOOL, HSTRING, IInspectable, Interface as _};
const FRAME_WAIT: Duration = Duration::from_millis(2_500);
const SCREEN_BUDGET: Duration = Duration::from_secs(12);
const FRAME_POLL: Duration = Duration::from_millis(10);
const BLACK_LEVEL: u8 = 8;
const EXTENT_TOLERANCE: u32 = 2;
const MAX_CAPTURE_EDGE: f64 = 16_384.0;
const BLANK_CAUSE: &str = "an all-black frame (a minimized window, protected content, or a surface \
that refuses readback)";
#[derive(Default)]
pub(super) struct CaptureContext {
device: Option<Gpu>,
}
impl CaptureContext {
pub(super) fn capture(
&mut self,
target: &TargetSpec,
budget: &CaptureBudget,
) -> anyhow::Result<Capture> {
match target {
TargetSpec::Screen => self.screen(budget),
other => {
let resolved = window::resolve(other)?;
self.window_frame(&resolved, budget)
}
}
}
fn screen(&mut self, budget: &CaptureBudget) -> anyhow::Result<Capture> {
let geometry = window::screen_geometry()?;
let mut causes = Vec::new();
let graphics = self.with_device(|gpu| screen_graphics(gpu, &geometry, budget));
if let Some(frame) = take_route(graphics, "Windows.Graphics.Capture", &mut causes) {
return Ok(frame);
}
if let Some(frame) = take_route(blit_screen(&geometry, budget), "BitBlt", &mut causes) {
return Ok(frame);
}
Err(no_frame_error("the screen", &causes))
}
fn window_frame(
&mut self,
resolved: &window::TargetWindow,
budget: &CaptureBudget,
) -> anyhow::Result<Capture> {
let mut causes = Vec::new();
let graphics = self.with_device(|gpu| window_graphics(gpu, resolved, budget));
if let Some(frame) = take_route(graphics, "Windows.Graphics.Capture", &mut causes) {
return Ok(frame);
}
if let Some(frame) = take_route(
blit_window(&resolved.surface, budget),
"BitBlt",
&mut causes,
) {
return Ok(frame);
}
Err(no_frame_error(
&core::window_description(&resolved.app_name, &resolved.title),
&causes,
))
}
fn with_device(
&mut self,
route: impl Fn(&Gpu) -> anyhow::Result<Capture>,
) -> anyhow::Result<Capture> {
let result = route(self.gpu()?);
if result.is_err() {
self.device = None;
}
result
}
fn gpu(&mut self) -> anyhow::Result<&Gpu> {
let gpu = match self.device.take() {
Some(gpu) => gpu,
None => Gpu::create()?,
};
Ok(self.device.insert(gpu))
}
}
fn window_graphics(
gpu: &Gpu,
resolved: &window::TargetWindow,
budget: &CaptureBudget,
) -> anyhow::Result<Capture> {
let item: GraphicsCaptureItem =
unsafe { capture_interop()?.CreateForWindow(resolved.hwnd) }?;
let frame = grab(gpu, &item, FRAME_WAIT, budget)?;
align_to_surface(frame, resolved.hwnd, &resolved.surface, budget)
}
fn screen_graphics(
gpu: &Gpu,
geometry: &SurfaceGeometry,
budget: &CaptureBudget,
) -> anyhow::Result<Capture> {
let displays = monitors()?;
let width = pixels(geometry.width)?;
let height = pixels(geometry.height)?;
let mut canvas = opaque_canvas(width, height, budget)?;
let deadline = Instant::now() + SCREEN_BUDGET;
for (monitor, rect) in displays {
let item: GraphicsCaptureItem =
unsafe { capture_interop()?.CreateForMonitor(monitor) }?;
let remaining = deadline.saturating_duration_since(Instant::now());
if remaining.is_zero() {
return Err(anyhow!("the displays did not produce frames in time"));
}
let frame = grab(gpu, &item, remaining.min(FRAME_WAIT), budget)?;
let extent = SurfaceGeometry {
x: f64::from(rect.left),
y: f64::from(rect.top),
width: f64::from(rect.right - rect.left),
height: f64::from(rect.bottom - rect.top),
};
let (expected_width, expected_height) = edge(&extent)?;
if !matches_extent(&frame, expected_width, expected_height) {
return Err(anyhow!(
"a display's frame is {}x{} but the display is {expected_width}x{expected_height} — \
it changed during the capture",
frame.width,
frame.height
));
}
let dx = extent.x - geometry.x;
let dy = extent.y - geometry.y;
if dx < 0.0 || dy < 0.0 {
return Err(anyhow!("a display lies outside the virtual desktop"));
}
core::blit_rgba(
&mut canvas.rgba,
width as usize,
height as usize,
&frame,
dx as usize,
dy as usize,
);
}
Ok(canvas)
}
fn grab(
gpu: &Gpu,
item: &GraphicsCaptureItem,
wait: Duration,
budget: &CaptureBudget,
) -> anyhow::Result<Capture> {
let size = item.Size()?;
if size.Width <= 0 || size.Height <= 0 {
return Err(anyhow!(
"the target has no capture area ({}x{}) — it was minimized, resized or closed \
during the capture",
size.Width,
size.Height
));
}
let pool = Direct3D11CaptureFramePool::CreateFreeThreaded(
&gpu.winrt,
DirectXPixelFormat::B8G8R8A8UIntNormalized,
2,
size,
)
.or_else(|_| {
Direct3D11CaptureFramePool::Create(
&gpu.winrt,
DirectXPixelFormat::B8G8R8A8UIntNormalized,
2,
size,
)
})?;
let session = pool.CreateCaptureSession(item)?;
let _ = session.SetIsCursorCaptureEnabled(true);
session.StartCapture()?;
let frame = match wait_for_frame(&pool, wait) {
Ok(frame) => frame,
Err(e) => {
let _ = session.Close();
let _ = pool.Close();
return Err(e);
}
};
let pixels = read_frame(gpu, &frame, budget);
let _ = frame.Close();
let _ = session.Close();
let _ = pool.Close();
pixels
}
fn read_frame(
gpu: &Gpu,
frame: &Direct3D11CaptureFrame,
budget: &CaptureBudget,
) -> anyhow::Result<Capture> {
let surface = frame.Surface()?;
let access: IDirect3DDxgiInterfaceAccess = surface.cast()?;
let texture: ID3D11Texture2D = unsafe { access.GetInterface()? };
read_texture(gpu, &texture, budget)
}
fn wait_for_frame(
pool: &Direct3D11CaptureFramePool,
wait: Duration,
) -> anyhow::Result<Direct3D11CaptureFrame> {
let deadline = Instant::now() + wait;
loop {
match pool.TryGetNextFrame() {
Ok(frame) => return Ok(frame),
Err(_) if Instant::now() >= deadline => {
return Err(anyhow!("no frame arrived within {}ms", wait.as_millis()));
}
Err(_) => std::thread::sleep(FRAME_POLL),
}
}
}
fn read_texture(
gpu: &Gpu,
texture: &ID3D11Texture2D,
budget: &CaptureBudget,
) -> anyhow::Result<Capture> {
let mut source = D3D11_TEXTURE2D_DESC::default();
unsafe { texture.GetDesc(&raw mut source) };
if source.Format != DXGI_FORMAT_B8G8R8A8_UNORM {
return Err(anyhow!(
"the capture facility returned an unexpected pixel format ({}) — an HDR or \
wide-gamut surface; use observe instead",
source.Format.0
));
}
let staging = D3D11_TEXTURE2D_DESC {
Width: source.Width,
Height: source.Height,
MipLevels: 1,
ArraySize: 1,
Format: source.Format,
SampleDesc: DXGI_SAMPLE_DESC {
Count: 1,
Quality: 0,
},
Usage: D3D11_USAGE_STAGING,
BindFlags: 0,
CPUAccessFlags: D3D11_CPU_ACCESS_READ.0.cast_unsigned(),
MiscFlags: 0,
};
let mut staging_texture = None;
unsafe {
gpu.device
.CreateTexture2D(&raw const staging, None, Some(&raw mut staging_texture))
}?;
let staging_texture =
staging_texture.ok_or_else(|| anyhow!("the staging texture could not be created"))?;
unsafe { gpu.context.CopyResource(&staging_texture, texture) };
let width = source.Width;
let height = source.Height;
let mut capture = Capture::alloc(budget, "a display frame", width, height)?;
let mut mapped = D3D11_MAPPED_SUBRESOURCE::default();
unsafe {
gpu.context.Map(
&staging_texture,
0,
D3D11_MAP_READ,
0,
Some(&raw mut mapped),
)
}?;
let stride = width as usize * 4;
for row in 0..height as usize {
let start = (mapped.pData as usize) + row * mapped.RowPitch as usize;
let line = unsafe { std::slice::from_raw_parts(start as *const u8, stride) };
bgra_to_rgba(&mut capture.rgba[row * stride..(row + 1) * stride], line);
}
unsafe { gpu.context.Unmap(&staging_texture, 0) };
Ok(capture)
}
struct Gpu {
device: ID3D11Device,
context: ID3D11DeviceContext,
winrt: IDirect3DDevice,
}
impl Gpu {
fn create() -> anyhow::Result<Self> {
let levels = [
D3D_FEATURE_LEVEL_11_0,
D3D_FEATURE_LEVEL_10_1,
D3D_FEATURE_LEVEL_10_0,
];
let mut device = None;
let mut context = None;
let mut created = unsafe {
D3D11CreateDevice(
None::<&IDXGIAdapter>,
D3D_DRIVER_TYPE_HARDWARE,
HMODULE::default(),
D3D11_CREATE_DEVICE_BGRA_SUPPORT,
Some(&levels),
D3D11_SDK_VERSION,
Some(&raw mut device),
None,
Some(&raw mut context),
)
};
if created.is_err() {
created = unsafe {
D3D11CreateDevice(
None::<&IDXGIAdapter>,
D3D_DRIVER_TYPE_WARP,
HMODULE::default(),
D3D11_CREATE_DEVICE_BGRA_SUPPORT,
Some(&levels),
D3D11_SDK_VERSION,
Some(&raw mut device),
None,
Some(&raw mut context),
)
};
}
created?;
let device = device.ok_or_else(|| anyhow!("no D3D11 device was returned"))?;
let context = context.ok_or_else(|| anyhow!("no D3D11 context was returned"))?;
let dxgi: IDXGIDevice = device.cast()?;
let inspectable: IInspectable = unsafe { CreateDirect3D11DeviceFromDXGIDevice(&dxgi) }?;
let winrt: IDirect3DDevice = inspectable.cast()?;
Ok(Self {
device,
context,
winrt,
})
}
}
fn capture_interop() -> anyhow::Result<IGraphicsCaptureItemInterop> {
let factory = HSTRING::from("Windows.Graphics.Capture.GraphicsCaptureItem");
Ok(unsafe { RoGetActivationFactory(&factory) }?)
}
fn monitors() -> anyhow::Result<Vec<(HMONITOR, RECT)>> {
let mut out: Vec<(HMONITOR, RECT)> = Vec::new();
let enumerated = unsafe {
EnumDisplayMonitors(
None,
None,
Some(collect_monitor),
LPARAM(std::ptr::from_mut(&mut out).cast::<std::ffi::c_void>() as isize),
)
};
if !enumerated.as_bool() {
return Err(anyhow!("the displays could not be enumerated"));
}
if out.is_empty() {
return Err(anyhow!("no display was reported"));
}
Ok(out)
}
unsafe extern "system" fn collect_monitor(
monitor: HMONITOR,
_dc: HDC,
_rect: *mut RECT,
data: LPARAM,
) -> BOOL {
let mut info = MONITORINFO {
cbSize: std::mem::size_of::<MONITORINFO>() as u32,
..Default::default()
};
if unsafe { GetMonitorInfoW(monitor, &raw mut info) }.as_bool() {
let out = unsafe { &mut *(data.0 as *mut Vec<(HMONITOR, RECT)>) };
out.push((monitor, info.rcMonitor));
}
BOOL(1)
}
struct Dib {
dc: HDC,
bitmap: HBITMAP,
previous: HGDIOBJ,
bits: *mut std::ffi::c_void,
width: u32,
height: u32,
}
impl Dib {
fn create(width: u32, height: u32) -> anyhow::Result<Self> {
let info = BITMAPINFO {
bmiHeader: BITMAPINFOHEADER {
biSize: std::mem::size_of::<BITMAPINFOHEADER>() as u32,
biWidth: i32::try_from(width).unwrap_or(i32::MAX),
biHeight: -i32::try_from(height).unwrap_or(i32::MAX),
biPlanes: 1,
biBitCount: 32,
biCompression: BI_RGB.0,
..Default::default()
},
..Default::default()
};
let dc = unsafe { CreateCompatibleDC(None) };
if dc.is_invalid() {
return Err(anyhow!("no GDI memory device context"));
}
let mut bits = std::ptr::null_mut();
let created = unsafe {
CreateDIBSection(
None,
&raw const info,
DIB_RGB_COLORS,
&raw mut bits,
None,
0,
)
};
let bitmap = match created {
Ok(bitmap) if !bits.is_null() => bitmap,
_ => {
unsafe {
let _ = DeleteDC(dc);
}
return Err(anyhow!("no GDI DIB section"));
}
};
let previous = unsafe { SelectObject(dc, HGDIOBJ(bitmap.0)) };
Ok(Self {
dc,
bitmap,
previous,
bits,
width,
height,
})
}
fn read_into(&self, rgba: &mut [u8]) -> anyhow::Result<()> {
if self.bits.is_null() {
return Err(anyhow!("the DIB section has no pixels"));
}
let bytes = unsafe {
std::slice::from_raw_parts(
self.bits.cast::<u8>(),
self.width as usize * self.height as usize * 4,
)
};
bgra_to_rgba(rgba, bytes);
Ok(())
}
}
impl Drop for Dib {
fn drop(&mut self) {
unsafe {
SelectObject(self.dc, self.previous);
let _ = DeleteObject(HGDIOBJ(self.bitmap.0));
let _ = DeleteDC(self.dc);
}
}
}
fn blit_window(surface: &SurfaceGeometry, budget: &CaptureBudget) -> anyhow::Result<Capture> {
let desktop = window::screen_geometry()?;
let (buffer_x, buffer_y, width, height) = core::window_capture_rect(surface, &desktop)?;
let (full_width, full_height) = edge(surface)?;
let (source_x, source_y) = (
buffer_x as i32 + desktop.x.round() as i32,
buffer_y as i32 + desktop.y.round() as i32,
);
blit_rect(
(
source_x - surface.x.round() as i32,
source_y - surface.y.round() as i32,
),
(full_width, full_height),
(source_x, source_y),
(width, height),
budget,
"the window still",
)
}
fn blit_screen(geometry: &SurfaceGeometry, budget: &CaptureBudget) -> anyhow::Result<Capture> {
let width = pixels(geometry.width)?;
let height = pixels(geometry.height)?;
let (x, y) = (geometry.x.round() as i32, geometry.y.round() as i32);
blit_rect(
(0, 0),
(width, height),
(x, y),
(width, height),
budget,
"the screen still",
)
}
fn blit_rect(
dest: (i32, i32),
canvas: (u32, u32),
source: (i32, i32),
region: (u32, u32),
budget: &CaptureBudget,
what: &str,
) -> anyhow::Result<Capture> {
let mut capture = Capture::alloc(budget, what, canvas.0, canvas.1)?;
let dib = Dib::create(canvas.0, canvas.1)?;
let screen = unsafe { GetDC(None) };
if screen.is_invalid() {
return Err(anyhow!("no screen device context"));
}
let copied = unsafe {
BitBlt(
dib.dc,
dest.0,
dest.1,
i32::try_from(region.0).unwrap_or(i32::MAX),
i32::try_from(region.1).unwrap_or(i32::MAX),
Some(screen),
source.0,
source.1,
SRCCOPY | CAPTUREBLT,
)
};
unsafe { ReleaseDC(None, screen) };
copied?;
let _ = unsafe { GdiFlush() };
dib.read_into(&mut capture.rgba)?;
Ok(capture)
}
fn opaque_canvas(width: u32, height: u32, budget: &CaptureBudget) -> anyhow::Result<Capture> {
let mut canvas = Capture::alloc(budget, "the screen composite", width, height)?;
for pixel in canvas.rgba.as_chunks_mut::<4>().0 {
pixel[3] = 255;
}
Ok(canvas)
}
fn bgra_to_rgba(out: &mut [u8], src: &[u8]) {
for (out, pixel) in out
.as_chunks_mut::<4>()
.0
.iter_mut()
.zip(src.as_chunks::<4>().0)
{
out.copy_from_slice(&[pixel[2], pixel[1], pixel[0], 255]);
}
}
fn align_to_surface(
frame: Capture,
hwnd: HWND,
surface: &SurfaceGeometry,
budget: &CaptureBudget,
) -> anyhow::Result<Capture> {
let (width, height) = edge(surface)?;
if matches_extent(&frame, width, height) {
return Ok(frame);
}
let outer = window::window_rect(hwnd)?;
let (outer_width, outer_height) = edge(&outer)?;
if matches_extent(&frame, outer_width, outer_height) {
return core::crop_rgba(
&frame,
(
crop_offset(surface.x - outer.x)?,
crop_offset(surface.y - outer.y)?,
width,
height,
),
budget,
"the window still",
);
}
Err(anyhow!(
"the frame is {}x{} but the window surface is {width}x{height} — the window changed size \
during the capture",
frame.width,
frame.height
))
}
fn crop_offset(value: f64) -> anyhow::Result<u32> {
let value = value.round();
if !(0.0..=f64::from(u32::MAX)).contains(&value) {
return Err(anyhow!(
"the visible frame starts outside the window rect — the window changed size during the \
capture"
));
}
Ok(value as u32)
}
fn matches_extent(frame: &Capture, width: u32, height: u32) -> bool {
frame.width.abs_diff(width) <= EXTENT_TOLERANCE
&& frame.height.abs_diff(height) <= EXTENT_TOLERANCE
}
struct Cause {
text: String,
ceiling: bool,
}
fn take_route(
route: anyhow::Result<Capture>,
label: &str,
causes: &mut Vec<Cause>,
) -> Option<Capture> {
match route {
Ok(frame) if !is_blank(&frame) => return Some(frame),
Ok(_) => causes.push(Cause {
text: format!("{label}: {BLANK_CAUSE}"),
ceiling: false,
}),
Err(e) => causes.push(Cause {
text: format!("{label}: {e:#}"),
ceiling: core::is_ceiling_refusal(&e),
}),
}
None
}
fn is_blank(capture: &Capture) -> bool {
if capture.width == 0 || capture.height == 0 {
return true;
}
let expected = capture.width as usize * capture.height as usize * 4;
if capture.rgba.len() < expected {
return true;
}
!capture
.rgba
.as_chunks::<4>()
.0
.iter()
.any(|pixel| pixel[0] > BLACK_LEVEL || pixel[1] > BLACK_LEVEL || pixel[2] > BLACK_LEVEL)
}
fn no_frame_error(target: &str, causes: &[Cause]) -> anyhow::Error {
let kind = if causes.iter().any(|cause| cause.ceiling) {
core::ERR_DEGRADED
} else {
core::ERR_UNSUPPORTED
};
let listed: Vec<&str> = causes.iter().map(|cause| cause.text.as_str()).collect();
core::taxonomy_error(
kind,
format!(
"capture of {target} produced no usable frame — every route failed: {}; use observe for \
the structure, or ask the owner to restore/focus the window",
listed.join("; ")
),
)
}
fn edge(surface: &SurfaceGeometry) -> anyhow::Result<(u32, u32)> {
if surface.width < 1.0 || surface.height < 1.0 {
return Err(core::taxonomy_error(
core::ERR_NOT_MATCHED,
"the target has no visible area — re-enumerate windows",
));
}
if surface.width > MAX_CAPTURE_EDGE || surface.height > MAX_CAPTURE_EDGE {
return Err(anyhow!(
"the target is larger than a capture can hold ({}x{})",
surface.width,
surface.height
));
}
Ok((pixels(surface.width)?, pixels(surface.height)?))
}
fn pixels(value: f64) -> anyhow::Result<u32> {
if !value.is_finite() || value < 1.0 || value > MAX_CAPTURE_EDGE {
return Err(anyhow!("invalid capture extent {value}"));
}
Ok(value.round() as u32)
}