use std::path::Path;
use std::{fs, io};
use rayon::iter::{IntoParallelIterator, ParallelIterator};
use windows::Win32::Foundation::E_ACCESSDENIED;
use windows::Win32::Graphics::Direct3D11::{
D3D11_BOX, D3D11_TEXTURE2D_DESC, ID3D11Device, ID3D11DeviceContext, ID3D11Texture2D,
};
use windows::Win32::Graphics::Dxgi::Common::{
DXGI_FORMAT, DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_R8G8B8A8_UNORM, DXGI_FORMAT_R16G16B16A16_FLOAT,
};
use windows::Win32::Graphics::Dxgi::{
DXGI_ERROR_ACCESS_LOST, DXGI_ERROR_NOT_FOUND, DXGI_ERROR_WAIT_TIMEOUT, DXGI_OUTDUPL_DESC, DXGI_OUTDUPL_FRAME_INFO,
IDXGIDevice4, IDXGIOutput6, IDXGIOutputDuplication,
};
use windows::Win32::UI::HiDpi::{DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2, SetProcessDpiAwarenessContext};
use windows::core::Interface;
use crate::d3d11::{MappedStagingTexture, StagingTexture, create_d3d_device, unmap_staging_texture};
use crate::encoder::{ImageEncoder, ImageEncoderError, ImageEncoderPixelFormat, ImageFormat};
use crate::monitor::Monitor;
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Invalid crop size")]
InvalidSize,
#[error("Failed to find DXGI output for the specified monitor")]
OutputNotFound,
#[error("AcquireNextFrame timed out")]
Timeout,
#[error("Duplication access lost; the duplication must be recreated")]
AccessLost,
#[error("DirectX error: {0}")]
DirectXError(#[from] crate::d3d11::Error),
#[error("Invalid staging texture: {0}")]
InvalidStagingTexture(&'static str),
#[error("Windows API succeeded but did not return {0}")]
UnexpectedNullResult(&'static str),
#[error("Failed to encode the image buffer to image bytes with the specified format: {0}")]
ImageEncoderError(#[from] crate::encoder::ImageEncoderError),
#[error("I/O error: {0}")]
IoError(#[from] io::Error),
#[error("Windows API error: {0}")]
WindowsError(#[from] windows::core::Error),
}
#[derive(Eq, PartialEq, Clone, Copy, Debug)]
pub enum DxgiDuplicationFormat {
Rgba16F,
Rgba8,
Bgra8,
}
const DEFAULT_DUPLICATION_FORMATS: [DXGI_FORMAT; 3] =
[DXGI_FORMAT_R16G16B16A16_FLOAT, DXGI_FORMAT_R8G8B8A8_UNORM, DXGI_FORMAT_B8G8R8A8_UNORM];
pub struct DxgiDuplicationApi {
d3d_device: ID3D11Device,
d3d_device_context: ID3D11DeviceContext,
duplication: IDXGIOutputDuplication,
duplication_desc: DXGI_OUTDUPL_DESC,
dxgi_device: IDXGIDevice4,
output: IDXGIOutput6,
is_holding_frame: bool,
}
fn enable_per_monitor_dpi_awareness() -> Result<(), Error> {
match unsafe { SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) } {
Ok(()) => Ok(()),
Err(error) if error.code() == E_ACCESSDENIED => Ok(()),
Err(error) => Err(Error::WindowsError(error)),
}
}
fn find_output_for_monitor(dxgi_device: &IDXGIDevice4, monitor: Monitor) -> Result<IDXGIOutput6, Error> {
let adapter = unsafe { dxgi_device.GetAdapter()? };
let mut index = 0u32;
loop {
match unsafe { adapter.EnumOutputs(index) } {
Ok(output) => {
let desc = unsafe { output.GetDesc()? };
if desc.Monitor.0 == monitor.as_raw_hmonitor() {
return Ok(output.cast::<IDXGIOutput6>()?);
}
index += 1;
}
Err(error) if error.code() == DXGI_ERROR_NOT_FOUND => return Err(Error::OutputNotFound),
Err(error) => return Err(Error::WindowsError(error)),
}
}
}
fn map_supported_formats(supported_formats: &[DxgiDuplicationFormat]) -> Vec<DXGI_FORMAT> {
let mut supported_formats = supported_formats
.iter()
.map(|format| match format {
DxgiDuplicationFormat::Rgba16F => DXGI_FORMAT_R16G16B16A16_FLOAT,
DxgiDuplicationFormat::Rgba8 => DXGI_FORMAT_R8G8B8A8_UNORM,
DxgiDuplicationFormat::Bgra8 => DXGI_FORMAT_B8G8R8A8_UNORM,
})
.collect::<Vec<_>>();
if !supported_formats.contains(&DXGI_FORMAT_B8G8R8A8_UNORM) {
supported_formats.push(DXGI_FORMAT_B8G8R8A8_UNORM);
}
supported_formats
}
impl DxgiDuplicationApi {
fn release_frame_if_needed(&mut self) -> Result<(), Error> {
if !self.is_holding_frame {
return Ok(());
}
match unsafe { self.duplication.ReleaseFrame() } {
Ok(()) => {
self.is_holding_frame = false;
Ok(())
}
Err(error) if error.code() == DXGI_ERROR_ACCESS_LOST => Err(Error::AccessLost),
Err(error) => Err(Error::WindowsError(error)),
}
}
fn recreate_with_formats(mut self, supported_formats: &[DXGI_FORMAT]) -> Result<Self, Error> {
let _ = self.release_frame_if_needed();
let d3d_device = self.d3d_device.clone();
let d3d_device_context = self.d3d_device_context.clone();
let dxgi_device = self.dxgi_device.clone();
let output = self.output.clone();
drop(self);
let duplication = unsafe { output.DuplicateOutput1(&d3d_device, 0, supported_formats)? };
let duplication_desc = unsafe { duplication.GetDesc() };
Ok(Self {
d3d_device,
d3d_device_context,
duplication,
duplication_desc,
dxgi_device,
output,
is_holding_frame: false,
})
}
pub fn new(monitor: Monitor) -> Result<Self, Error> {
let (d3d_device, d3d_device_context) = create_d3d_device()?;
let dxgi_device = d3d_device.cast::<IDXGIDevice4>()?;
let output = find_output_for_monitor(&dxgi_device, monitor)?;
enable_per_monitor_dpi_awareness()?;
let duplication = unsafe { output.DuplicateOutput1(&d3d_device, 0, &DEFAULT_DUPLICATION_FORMATS)? };
let duplication_desc = unsafe { duplication.GetDesc() };
Ok(Self {
d3d_device,
d3d_device_context,
duplication,
duplication_desc,
dxgi_device,
output,
is_holding_frame: false,
})
}
pub fn new_options(monitor: Monitor, supported_formats: &[DxgiDuplicationFormat]) -> Result<Self, Error> {
let (d3d_device, d3d_device_context) = create_d3d_device()?;
let dxgi_device = d3d_device.cast::<IDXGIDevice4>()?;
let output = find_output_for_monitor(&dxgi_device, monitor)?;
let supported_formats = map_supported_formats(supported_formats);
enable_per_monitor_dpi_awareness()?;
let duplication = unsafe { output.DuplicateOutput1(&d3d_device, 0, &supported_formats)? };
let duplication_desc = unsafe { duplication.GetDesc() };
Ok(Self {
d3d_device,
d3d_device_context,
duplication,
duplication_desc,
dxgi_device,
output,
is_holding_frame: false,
})
}
pub fn recreate(self) -> Result<Self, Error> {
self.recreate_with_formats(&DEFAULT_DUPLICATION_FORMATS)
}
pub fn recreate_options(self, supported_formats: &[DxgiDuplicationFormat]) -> Result<Self, Error> {
let supported_formats = map_supported_formats(supported_formats);
self.recreate_with_formats(&supported_formats)
}
#[inline]
#[must_use]
pub const fn device(&self) -> &ID3D11Device {
&self.d3d_device
}
#[inline]
#[must_use]
pub const fn device_context(&self) -> &ID3D11DeviceContext {
&self.d3d_device_context
}
#[inline]
#[must_use]
pub const fn duplication(&self) -> &IDXGIOutputDuplication {
&self.duplication
}
#[inline]
#[must_use]
pub const fn duplication_desc(&self) -> &DXGI_OUTDUPL_DESC {
&self.duplication_desc
}
#[inline]
#[must_use]
pub const fn dxgi_device(&self) -> &IDXGIDevice4 {
&self.dxgi_device
}
#[inline]
#[must_use]
pub const fn output(&self) -> &IDXGIOutput6 {
&self.output
}
#[inline]
#[must_use]
pub const fn width(&self) -> u32 {
self.duplication_desc.ModeDesc.Width
}
#[inline]
#[must_use]
pub const fn height(&self) -> u32 {
self.duplication_desc.ModeDesc.Height
}
#[inline]
#[must_use]
pub const fn format(&self) -> DxgiDuplicationFormat {
match self.duplication_desc.ModeDesc.Format {
DXGI_FORMAT_R16G16B16A16_FLOAT => DxgiDuplicationFormat::Rgba16F,
DXGI_FORMAT_R8G8B8A8_UNORM => DxgiDuplicationFormat::Rgba8,
DXGI_FORMAT_B8G8R8A8_UNORM => DxgiDuplicationFormat::Bgra8,
_ => unreachable!(),
}
}
#[inline]
#[must_use]
pub const fn refresh_rate(&self) -> (u32, u32) {
(self.duplication_desc.ModeDesc.RefreshRate.Numerator, self.duplication_desc.ModeDesc.RefreshRate.Denominator)
}
#[inline]
pub fn acquire_next_frame(&mut self, timeout_ms: u32) -> Result<DxgiDuplicationFrame<'_>, Error> {
let mut frame_info = DXGI_OUTDUPL_FRAME_INFO::default();
let mut resource = None;
self.release_frame_if_needed()?;
match unsafe { self.duplication.AcquireNextFrame(timeout_ms, &mut frame_info, &mut resource) } {
Ok(()) => (),
Err(e) => {
if e.code() == DXGI_ERROR_WAIT_TIMEOUT {
return Err(Error::Timeout);
} else if e.code() == DXGI_ERROR_ACCESS_LOST {
return Err(Error::AccessLost);
} else {
return Err(Error::WindowsError(e));
}
}
}
self.is_holding_frame = true;
let resource = resource.ok_or(Error::UnexpectedNullResult("an acquired DXGI frame resource"))?;
let frame_texture = resource.cast::<ID3D11Texture2D>()?;
let mut frame_desc = D3D11_TEXTURE2D_DESC::default();
unsafe { frame_texture.GetDesc(&mut frame_desc) };
Ok(DxgiDuplicationFrame {
d3d_device: &self.d3d_device,
d3d_device_context: &self.d3d_device_context,
duplication: &self.duplication,
texture: frame_texture,
texture_desc: frame_desc,
frame_info,
})
}
}
impl Drop for DxgiDuplicationApi {
fn drop(&mut self) {
let _ = self.release_frame_if_needed();
}
}
pub struct DxgiDuplicationFrame<'a> {
d3d_device: &'a ID3D11Device,
d3d_device_context: &'a ID3D11DeviceContext,
duplication: &'a IDXGIOutputDuplication,
texture: ID3D11Texture2D,
texture_desc: D3D11_TEXTURE2D_DESC,
frame_info: DXGI_OUTDUPL_FRAME_INFO,
}
impl<'a> DxgiDuplicationFrame<'a> {
#[inline]
#[must_use]
pub const fn width(&self) -> u32 {
self.texture_desc.Width
}
#[inline]
#[must_use]
pub const fn height(&self) -> u32 {
self.texture_desc.Height
}
#[inline]
#[must_use]
pub const fn format(&self) -> DxgiDuplicationFormat {
match self.texture_desc.Format {
DXGI_FORMAT_R16G16B16A16_FLOAT => DxgiDuplicationFormat::Rgba16F,
DXGI_FORMAT_R8G8B8A8_UNORM => DxgiDuplicationFormat::Rgba8,
DXGI_FORMAT_B8G8R8A8_UNORM => DxgiDuplicationFormat::Bgra8,
_ => unreachable!(),
}
}
#[inline]
#[must_use]
pub const fn device(&self) -> &ID3D11Device {
self.d3d_device
}
#[inline]
#[must_use]
pub const fn device_context(&self) -> &ID3D11DeviceContext {
self.d3d_device_context
}
#[inline]
#[must_use]
pub const fn duplication(&self) -> &IDXGIOutputDuplication {
self.duplication
}
#[inline]
#[must_use]
pub const fn texture(&self) -> &ID3D11Texture2D {
&self.texture
}
#[inline]
#[must_use]
pub const fn texture_desc(&self) -> &D3D11_TEXTURE2D_DESC {
&self.texture_desc
}
#[inline]
#[must_use]
pub const fn frame_info(&self) -> &DXGI_OUTDUPL_FRAME_INFO {
&self.frame_info
}
#[inline]
pub fn buffer<'b>(&'b mut self) -> Result<DxgiDuplicationFrameBuffer<'b>, Error> {
let staging = StagingTexture::new(
self.d3d_device,
self.texture_desc.Width,
self.texture_desc.Height,
self.texture_desc.Format,
)?;
unsafe {
self.d3d_device_context.CopyResource(staging.texture(), &self.texture);
}
let mapped_texture = MappedStagingTexture::map_owned(self.d3d_device_context, staging)?;
Ok(DxgiDuplicationFrameBuffer::from_mapped(
mapped_texture,
self.texture_desc.Width,
self.texture_desc.Height,
self.format(),
))
}
#[inline]
pub fn buffer_crop<'b>(
&'b mut self,
start_x: u32,
start_y: u32,
end_x: u32,
end_y: u32,
) -> Result<DxgiDuplicationFrameBuffer<'b>, Error> {
if start_x >= end_x || start_y >= end_y {
return Err(Error::InvalidSize);
}
let texture_width = end_x - start_x;
let texture_height = end_y - start_y;
let staging = StagingTexture::new(self.d3d_device, texture_width, texture_height, self.texture_desc.Format)?;
let src_box = D3D11_BOX { left: start_x, top: start_y, front: 0, right: end_x, bottom: end_y, back: 1 };
unsafe {
self.d3d_device_context.CopySubresourceRegion(
staging.texture(),
0,
0,
0,
0,
&self.texture,
0,
Some(&src_box),
);
}
let mapped_texture = MappedStagingTexture::map_owned(self.d3d_device_context, staging)?;
Ok(DxgiDuplicationFrameBuffer::from_mapped(mapped_texture, texture_width, texture_height, self.format()))
}
#[inline]
pub fn buffer_with<'s>(
&'s mut self,
staging: &'s mut StagingTexture,
) -> Result<DxgiDuplicationFrameBuffer<'s>, Error> {
let desc = staging.desc();
if desc.Width != self.texture_desc.Width || desc.Height != self.texture_desc.Height {
return Err(Error::InvalidStagingTexture("geometry must match the frame"));
}
if desc.Format != self.texture_desc.Format {
return Err(Error::InvalidStagingTexture("format must match the frame"));
}
unmap_staging_texture(self.d3d_device_context, staging);
unsafe {
self.d3d_device_context.CopyResource(staging.texture(), &self.texture);
}
let mapped_texture = MappedStagingTexture::map_borrowed(self.d3d_device_context, staging)?;
Ok(DxgiDuplicationFrameBuffer::from_mapped(
mapped_texture,
self.texture_desc.Width,
self.texture_desc.Height,
self.format(),
))
}
#[inline]
pub fn buffer_crop_with<'s>(
&'s mut self,
staging: &'s mut StagingTexture,
start_x: u32,
start_y: u32,
end_x: u32,
end_y: u32,
) -> Result<DxgiDuplicationFrameBuffer<'s>, Error> {
if start_x >= end_x || start_y >= end_y {
return Err(Error::InvalidSize);
}
let crop_width = end_x - start_x;
let crop_height = end_y - start_y;
let desc = staging.desc();
if desc.Format != self.texture_desc.Format {
return Err(Error::InvalidStagingTexture("format must match the frame"));
}
if desc.Width < crop_width || desc.Height < crop_height {
return Err(Error::InvalidStagingTexture("staging texture too small for crop region"));
}
unmap_staging_texture(self.d3d_device_context, staging);
let src_box = D3D11_BOX { left: start_x, top: start_y, front: 0, right: end_x, bottom: end_y, back: 1 };
unsafe {
self.d3d_device_context.CopySubresourceRegion(
staging.texture(),
0,
0,
0,
0,
&self.texture,
0,
Some(&src_box),
);
}
let mapped_texture = MappedStagingTexture::map_borrowed(self.d3d_device_context, staging)?;
Ok(DxgiDuplicationFrameBuffer::from_mapped(mapped_texture, crop_width, crop_height, self.format()))
}
#[inline]
pub fn save_as_image<T: AsRef<Path>>(&mut self, path: T, format: ImageFormat) -> Result<(), Error> {
let mut frame_buffer = self.buffer()?;
frame_buffer.save_as_image(path, format)?;
Ok(())
}
}
enum DxgiDuplicationFrameBufferBacking<'a> {
Borrowed(&'a mut [u8]),
Mapped(MappedStagingTexture<'a>),
}
impl DxgiDuplicationFrameBufferBacking<'_> {
const fn as_slice(&self, height: u32) -> &[u8] {
match self {
Self::Borrowed(buffer) => buffer,
Self::Mapped(texture) => texture.as_slice(height),
}
}
const fn as_mut_slice(&mut self, height: u32) -> &mut [u8] {
match self {
Self::Borrowed(buffer) => buffer,
Self::Mapped(texture) => texture.as_mut_slice(height),
}
}
}
pub struct DxgiDuplicationFrameBuffer<'a> {
backing: DxgiDuplicationFrameBufferBacking<'a>,
width: u32,
height: u32,
row_pitch: u32,
depth_pitch: u32,
format: DxgiDuplicationFormat,
}
impl<'a> DxgiDuplicationFrameBuffer<'a> {
#[inline]
#[must_use]
pub const fn new(
raw_buffer: &'a mut [u8],
width: u32,
height: u32,
row_pitch: u32,
depth_pitch: u32,
format: DxgiDuplicationFormat,
) -> Self {
Self {
backing: DxgiDuplicationFrameBufferBacking::Borrowed(raw_buffer),
width,
height,
row_pitch,
depth_pitch,
format,
}
}
const fn from_mapped(
mapped_texture: MappedStagingTexture<'a>,
width: u32,
height: u32,
format: DxgiDuplicationFormat,
) -> Self {
let row_pitch = mapped_texture.row_pitch();
let depth_pitch = mapped_texture.depth_pitch();
Self {
backing: DxgiDuplicationFrameBufferBacking::Mapped(mapped_texture),
width,
height,
row_pitch,
depth_pitch,
format,
}
}
#[inline]
#[must_use]
pub const fn width(&self) -> u32 {
self.width
}
#[inline]
#[must_use]
pub const fn height(&self) -> u32 {
self.height
}
#[inline]
#[must_use]
pub const fn row_pitch(&self) -> u32 {
self.row_pitch
}
#[inline]
#[must_use]
pub const fn depth_pitch(&self) -> u32 {
self.depth_pitch
}
#[inline]
#[must_use]
pub const fn format(&self) -> DxgiDuplicationFormat {
self.format
}
#[inline]
#[must_use]
pub const fn has_padding(&self) -> bool {
self.width * self.bytes_per_pixel() != self.row_pitch
}
#[inline]
#[must_use]
pub fn as_nopadding_buffer<'b>(&'b self, buffer: &'b mut Vec<u8>) -> &'b [u8] {
let raw_buffer = self.backing.as_slice(self.height);
if !self.has_padding() {
return raw_buffer;
}
let width = self.width;
let height = self.height;
let row_pitch = self.row_pitch;
let multiplier = self.bytes_per_pixel();
let frame_size = (width * height * multiplier) as usize;
if buffer.len() < frame_size {
buffer.resize(frame_size, 0);
}
let width_size = (width * multiplier) as usize;
let buffer_address = buffer.as_mut_ptr() as usize;
let raw_buffer_address = raw_buffer.as_ptr() as usize;
(0..height).into_par_iter().for_each(|y| {
let index = (y * row_pitch) as usize;
let src = raw_buffer_address as *const u8;
let dst = buffer_address as *mut u8;
unsafe {
std::ptr::copy_nonoverlapping(src.add(index), dst.add(y as usize * width_size), width_size);
}
});
&buffer[0..frame_size]
}
#[inline]
#[must_use]
pub const fn as_raw_buffer(&mut self) -> &mut [u8] {
self.backing.as_mut_slice(self.height)
}
#[inline]
pub fn save_as_image<T: AsRef<Path>>(&mut self, path: T, format: ImageFormat) -> Result<(), Error> {
let width = self.width;
let height = self.height;
let pixel_format = match self.format {
DxgiDuplicationFormat::Rgba8 => ImageEncoderPixelFormat::Rgba8,
DxgiDuplicationFormat::Bgra8 => ImageEncoderPixelFormat::Bgra8,
_ => return Err(ImageEncoderError::UnsupportedFormat.into()),
};
let mut buffer = Vec::new();
let bytes =
ImageEncoder::new(format, pixel_format)?.encode(self.as_nopadding_buffer(&mut buffer), width, height)?;
fs::write(path, bytes)?;
Ok(())
}
#[inline]
#[must_use]
const fn bytes_per_pixel(&self) -> u32 {
match self.format {
DxgiDuplicationFormat::Rgba16F => 8,
DxgiDuplicationFormat::Rgba8 | DxgiDuplicationFormat::Bgra8 => 4,
}
}
}