Skip to main content

windows_capture/
dxgi_duplication_api.rs

1//! DXGI Desktop Duplication API wrapper.
2//!
3//! This module provides [`DxgiDuplicationApi`] to capture a monitor using the
4//! Windows DXGI Desktop Duplication API. It integrates with [`crate::monitor::Monitor`]
5//! to select the target output and exposes CPU-readable frames via [`crate::frame::FrameBuffer`].
6//!
7//! # Example
8//! ```no_run
9//! use windows_capture::dxgi_duplication_api::DxgiDuplicationApi;
10//! use windows_capture::encoder::ImageFormat;
11//! use windows_capture::monitor::Monitor;
12//!
13//! fn main() -> Result<(), Box<dyn std::error::Error>> {
14//!     // Select the primary monitor
15//!     let monitor = Monitor::primary()?;
16//!
17//!     // Create a duplication session for this monitor
18//!     let mut dup = DxgiDuplicationApi::new(monitor)?;
19//!
20//!     // Try to grab one frame within ~33ms (about 30 FPS budget)
21//!     let mut frame = dup.acquire_next_frame(33)?;
22//!
23//!     // Map the GPU image into CPU memory and save a PNG
24//!     let mut buffer = frame.buffer()?;
25//!     buffer.save_as_image("dup.png", ImageFormat::Png)?;
26//!     Ok(())
27//! }
28//! ```
29use std::path::Path;
30use std::{fs, io, slice};
31
32use rayon::iter::{IntoParallelIterator, ParallelIterator};
33use windows::Win32::Foundation::E_ACCESSDENIED;
34use windows::Win32::Graphics::Direct3D11::{
35    D3D11_BOX, D3D11_CPU_ACCESS_READ, D3D11_CPU_ACCESS_WRITE, D3D11_MAP_READ_WRITE, D3D11_MAPPED_SUBRESOURCE,
36    D3D11_TEXTURE2D_DESC, D3D11_USAGE_STAGING, ID3D11Device, ID3D11DeviceContext, ID3D11Texture2D,
37};
38use windows::Win32::Graphics::Dxgi::Common::{
39    DXGI_FORMAT, DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_B8G8R8A8_UNORM_SRGB, DXGI_FORMAT_R8G8B8A8_UNORM,
40    DXGI_FORMAT_R8G8B8A8_UNORM_SRGB, DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM, DXGI_FORMAT_R10G10B10A2_UNORM,
41    DXGI_FORMAT_R16G16B16A16_FLOAT, DXGI_SAMPLE_DESC,
42};
43use windows::Win32::Graphics::Dxgi::{
44    DXGI_ERROR_ACCESS_LOST, DXGI_ERROR_WAIT_TIMEOUT, DXGI_OUTDUPL_DESC, DXGI_OUTDUPL_FRAME_INFO, IDXGIDevice4,
45    IDXGIOutput6, IDXGIOutputDuplication,
46};
47use windows::Win32::UI::HiDpi::{DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2, SetProcessDpiAwarenessContext};
48use windows::core::Interface;
49
50use crate::d3d11::{StagingTexture, create_d3d_device};
51use crate::encoder::{ImageEncoder, ImageEncoderError, ImageEncoderPixelFormat, ImageFormat};
52use crate::monitor::Monitor;
53
54/// Errors that can occur while using the DXGI Desktop Duplication API wrapper.
55#[derive(thiserror::Error, Debug)]
56pub enum Error {
57    /// The crop rectangle is invalid (start >= end on either axis).
58    #[error("Invalid crop size")]
59    InvalidSize,
60    /// Failed to find a DXGI output that corresponds to the provided monitor.
61    #[error("Failed to find DXGI output for the specified monitor")]
62    OutputNotFound,
63    /// AcquireNextFrame timed out without a new frame becoming available.
64    #[error("AcquireNextFrame timed out")]
65    Timeout,
66    /// The duplication access was lost and must be recreated.
67    #[error("Duplication access lost; the duplication must be recreated")]
68    AccessLost,
69    /// DirectX device creation or related error.
70    #[error("DirectX error: {0}")]
71    DirectXError(#[from] crate::d3d11::Error),
72    /// Invalid or mismatched staging texture supplied to [`DxgiDuplicationFrame::buffer_with`].
73    #[error("Invalid staging texture: {0}")]
74    InvalidStagingTexture(&'static str),
75    /// Image encoding failed.
76    ///
77    /// Wraps [`crate::encoder::ImageEncoderError`].
78    #[error("Failed to encode the image buffer to image bytes with the specified format: {0}")]
79    ImageEncoderError(#[from] crate::encoder::ImageEncoderError),
80    /// An I/O error occurred while writing the image to disk.
81    ///
82    /// Wraps [`std::io::Error`].
83    #[error("I/O error: {0}")]
84    IoError(#[from] io::Error),
85    /// Windows API error.
86    #[error("Windows API error: {0}")]
87    WindowsError(#[from] windows::core::Error),
88}
89
90/// Supported DXGI formats for duplication.
91#[derive(Eq, PartialEq, Clone, Copy, Debug)]
92pub enum DxgiDuplicationFormat {
93    /// 16-bit float RGBA format.
94    Rgba16F,
95    /// 10-bit RGB with 2-bit alpha format.
96    Rgb10A2,
97    /// 10-bit RGB with 2-bit alpha format (biased).
98    Rgb10XrA2,
99    /// 8-bit RGBA format.
100    Rgba8,
101    /// 8-bit RGBA format (sRGB).
102    Rgba8Srgb,
103    /// 8-bit BGRA format.
104    Bgra8,
105    /// 8-bit BGRA format (sRGB).
106    Bgra8Srgb,
107}
108
109/// A minimal, ergonomic wrapper around the DXGI Desktop Duplication API for capturing a monitor.
110///
111/// This wrapper focuses on staying close to the native API while providing a simple Rust interface.
112/// It integrates with [`crate::monitor::Monitor`] to select the target output.
113pub struct DxgiDuplicationApi {
114    /// Direct3D 11 device used for duplication operations.
115    d3d_device: ID3D11Device,
116    /// Direct3D 11 device context used for copy/map operations.
117    d3d_device_context: ID3D11DeviceContext,
118    /// The duplication interface used to acquire frames.
119    duplication: IDXGIOutputDuplication,
120    /// Description of the duplication, including format and dimensions.
121    duplication_desc: DXGI_OUTDUPL_DESC,
122    /// The DXGI device associated with the Direct3D device.
123    dxgi_device: IDXGIDevice4,
124    /// The DXGI output associated with this duplication.
125    output: IDXGIOutput6,
126    /// Whether the internal staging texture is currently holding a frame.
127    is_holding_frame: bool,
128}
129
130impl DxgiDuplicationApi {
131    /// Constructs a new duplication session for the specified monitor.
132    ///
133    /// Internally creates a Direct3D 11 device and immediate context using the crate's d3d11
134    /// module.
135    pub fn new(monitor: Monitor) -> Result<Self, Error> {
136        // Create D3D11 device and context.
137        let (d3d_device, d3d_device_context) = create_d3d_device()?;
138
139        // Get the adapter used by the created device.
140        let dxgi_device = d3d_device.cast::<IDXGIDevice4>()?;
141        let adapter = unsafe { dxgi_device.GetAdapter()? };
142
143        // Find the DXGI output that corresponds to the provided HMONITOR.
144        let found_output;
145        let mut index = 0u32;
146        loop {
147            let output = unsafe { adapter.EnumOutputs(index) }?;
148            let desc = unsafe { output.GetDesc()? };
149            if desc.Monitor.0 == monitor.as_raw_hmonitor() {
150                found_output = Some(output);
151                break;
152            }
153            index += 1;
154        }
155
156        let Some(output) = found_output else {
157            return Err(Error::OutputNotFound);
158        };
159
160        // Get IDXGIOutput6 for DuplicateOutput.
161        let output = output.cast::<IDXGIOutput6>()?;
162
163        // Set the process to be per-monitor DPI aware to handle high-DPI monitors correctly.
164        match unsafe { SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) } {
165            Ok(()) => (),
166            Err(e) => {
167                // returns E_ACCESSDENIED when the default API awareness mode for the process has already been set
168                // (via a previous API call or within the application manifest)
169                if e.code() != E_ACCESSDENIED {
170                    return Err(Error::WindowsError(e));
171                }
172            }
173        }
174
175        // Create the duplication for this output using the supplied D3D11 device.
176        let duplication = unsafe { output.DuplicateOutput(&d3d_device)? };
177
178        // Get the duplication description to determine the format for our internal texture.
179        let duplication_desc = unsafe { duplication.GetDesc() };
180
181        Ok(Self {
182            d3d_device,
183            d3d_device_context,
184            duplication,
185            duplication_desc,
186            dxgi_device,
187            output,
188            is_holding_frame: false,
189        })
190    }
191
192    /// Constructs a new duplication session for the specified monitor, using a custom list of
193    /// supported DXGI formats.
194    ///
195    /// This method allows directly receiving the original back buffer format used by a running
196    /// fullscreen application.
197    ///
198    /// Bgra8 is inserted because it is widely supported and serves as a reliable fallback.
199    pub fn new_options(monitor: Monitor, supported_formats: &[DxgiDuplicationFormat]) -> Result<Self, Error> {
200        // Create D3D11 device and context.
201        let (d3d_device, d3d_device_context) = create_d3d_device()?;
202
203        // Get the adapter used by the created device.
204        let dxgi_device = d3d_device.cast::<IDXGIDevice4>()?;
205        let adapter = unsafe { dxgi_device.GetAdapter()? };
206
207        // Find the DXGI output that corresponds to the provided HMONITOR.
208        let found_output;
209        let mut index = 0u32;
210        loop {
211            let output = unsafe { adapter.EnumOutputs(index) }?;
212            let desc = unsafe { output.GetDesc()? };
213            if desc.Monitor.0 == monitor.as_raw_hmonitor() {
214                found_output = Some(output);
215                break;
216            }
217            index += 1;
218        }
219
220        let Some(output) = found_output else {
221            return Err(Error::OutputNotFound);
222        };
223
224        // Get IDXGIOutput6 for DuplicateOutput1.
225        let output = output.cast::<IDXGIOutput6>()?;
226
227        // Map the supported formats to DXGI_FORMAT values.
228        let mut supported_formats = supported_formats
229            .iter()
230            .map(|f| match f {
231                DxgiDuplicationFormat::Rgba16F => DXGI_FORMAT_R16G16B16A16_FLOAT,
232                DxgiDuplicationFormat::Rgb10A2 => DXGI_FORMAT_R10G10B10A2_UNORM,
233                DxgiDuplicationFormat::Rgb10XrA2 => DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM,
234                DxgiDuplicationFormat::Rgba8 => DXGI_FORMAT_R8G8B8A8_UNORM,
235                DxgiDuplicationFormat::Rgba8Srgb => DXGI_FORMAT_R8G8B8A8_UNORM_SRGB,
236                DxgiDuplicationFormat::Bgra8 => DXGI_FORMAT_B8G8R8A8_UNORM,
237                DxgiDuplicationFormat::Bgra8Srgb => DXGI_FORMAT_B8G8R8A8_UNORM_SRGB,
238            })
239            .collect::<Vec<DXGI_FORMAT>>();
240
241        if !supported_formats.contains(&DXGI_FORMAT_B8G8R8A8_UNORM) {
242            supported_formats.push(DXGI_FORMAT_B8G8R8A8_UNORM);
243        }
244
245        // Set the process to be per-monitor DPI aware to handle high-DPI monitors correctly.
246        match unsafe { SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) } {
247            Ok(()) => (),
248            Err(e) => {
249                // returns E_ACCESSDENIED when the default API awareness mode for the process has already been set
250                // (via a previous API call or within the application manifest)
251                if e.code() != E_ACCESSDENIED {
252                    return Err(Error::WindowsError(e));
253                }
254            }
255        }
256
257        // Create the duplication for this output using the supplied D3D11 device.
258        let duplication = unsafe { output.DuplicateOutput1(&d3d_device, 0, &supported_formats)? };
259
260        // Get the duplication description to determine the format for our internal texture.
261        let duplication_desc = unsafe { duplication.GetDesc() };
262
263        Ok(Self {
264            d3d_device,
265            d3d_device_context,
266            duplication,
267            duplication_desc,
268            dxgi_device,
269            output,
270            is_holding_frame: false,
271        })
272    }
273
274    /// Recreates the duplication interface, mostly used after receiving an [`Error::AccessLost`]
275    /// error from [`DxgiDuplicationApi::acquire_next_frame`].
276    pub fn recreate(self) -> Result<Self, Error> {
277        let Self {
278            d3d_device,
279            d3d_device_context,
280            duplication,
281            duplication_desc: _,
282            dxgi_device,
283            output,
284            is_holding_frame: _,
285        } = self;
286
287        drop(duplication);
288
289        let duplication = unsafe { output.DuplicateOutput(&d3d_device)? };
290        let duplication_desc = unsafe { duplication.GetDesc() };
291
292        Ok(Self {
293            d3d_device,
294            d3d_device_context,
295            duplication,
296            duplication_desc,
297            dxgi_device,
298            output,
299            is_holding_frame: false,
300        })
301    }
302
303    /// Recreates the duplication interface with a custom list of supported DXGI formats, mostly
304    /// used after receiving an [`Error::AccessLost`] error from
305    /// [`DxgiDuplicationApi::acquire_next_frame`].
306    pub fn recreate_options(self, supported_formats: &[DxgiDuplicationFormat]) -> Result<Self, Error> {
307        // Map the supported formats to DXGI_FORMAT values.
308        let mut supported_formats = supported_formats
309            .iter()
310            .map(|f| match f {
311                DxgiDuplicationFormat::Rgba16F => DXGI_FORMAT_R16G16B16A16_FLOAT,
312                DxgiDuplicationFormat::Rgb10A2 => DXGI_FORMAT_R10G10B10A2_UNORM,
313                DxgiDuplicationFormat::Rgb10XrA2 => DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM,
314                DxgiDuplicationFormat::Rgba8 => DXGI_FORMAT_R8G8B8A8_UNORM,
315                DxgiDuplicationFormat::Rgba8Srgb => DXGI_FORMAT_R8G8B8A8_UNORM_SRGB,
316                DxgiDuplicationFormat::Bgra8 => DXGI_FORMAT_B8G8R8A8_UNORM,
317                DxgiDuplicationFormat::Bgra8Srgb => DXGI_FORMAT_B8G8R8A8_UNORM_SRGB,
318            })
319            .collect::<Vec<DXGI_FORMAT>>();
320
321        if !supported_formats.contains(&DXGI_FORMAT_B8G8R8A8_UNORM) {
322            supported_formats.push(DXGI_FORMAT_B8G8R8A8_UNORM);
323        }
324
325        let Self {
326            d3d_device,
327            d3d_device_context,
328            duplication,
329            duplication_desc: _,
330            dxgi_device,
331            output,
332            is_holding_frame: _,
333        } = self;
334
335        drop(duplication);
336
337        let duplication = unsafe { output.DuplicateOutput1(&d3d_device, 0, &supported_formats)? };
338        let duplication_desc = unsafe { duplication.GetDesc() };
339
340        Ok(Self {
341            d3d_device,
342            d3d_device_context,
343            duplication,
344            duplication_desc,
345            dxgi_device,
346            output,
347            is_holding_frame: false,
348        })
349    }
350
351    /// Gets the underlying [`windows::Win32::Graphics::Direct3D11::ID3D11Device`] associated with
352    /// this object.
353    #[inline]
354    #[must_use]
355    pub const fn device(&self) -> &ID3D11Device {
356        &self.d3d_device
357    }
358
359    /// Gets the underlying [`windows::Win32::Graphics::Direct3D11::ID3D11DeviceContext`] used for
360    /// GPU operations.
361    #[inline]
362    #[must_use]
363    pub const fn device_context(&self) -> &ID3D11DeviceContext {
364        &self.d3d_device_context
365    }
366
367    /// Gets the underlying [`windows::Win32::Graphics::Dxgi::IDXGIOutputDuplication`] interface.
368    #[inline]
369    #[must_use]
370    pub const fn duplication(&self) -> &IDXGIOutputDuplication {
371        &self.duplication
372    }
373
374    /// Gets the [`windows::Win32::Graphics::Dxgi::DXGI_OUTDUPL_DESC`] of the duplication.
375    #[inline]
376    #[must_use]
377    pub const fn duplication_desc(&self) -> &DXGI_OUTDUPL_DESC {
378        &self.duplication_desc
379    }
380
381    /// Gets the underlying [`windows::Win32::Graphics::Dxgi::IDXGIDevice4`] interface.
382    #[inline]
383    #[must_use]
384    pub const fn dxgi_device(&self) -> &IDXGIDevice4 {
385        &self.dxgi_device
386    }
387
388    /// Gets the underlying [`windows::Win32::Graphics::Dxgi::IDXGIOutput6`] interface.
389    #[inline]
390    #[must_use]
391    pub const fn output(&self) -> &IDXGIOutput6 {
392        &self.output
393    }
394
395    /// Gets the width of the duplication.
396    #[inline]
397    #[must_use]
398    pub const fn width(&self) -> u32 {
399        self.duplication_desc.ModeDesc.Width
400    }
401
402    /// Gets the height of the duplication.
403    #[inline]
404    #[must_use]
405    pub const fn height(&self) -> u32 {
406        self.duplication_desc.ModeDesc.Height
407    }
408
409    /// Gets the pixel format of the duplication.
410    #[inline]
411    #[must_use]
412    pub const fn format(&self) -> DxgiDuplicationFormat {
413        match self.duplication_desc.ModeDesc.Format {
414            DXGI_FORMAT_R16G16B16A16_FLOAT => DxgiDuplicationFormat::Rgba16F,
415            DXGI_FORMAT_R10G10B10A2_UNORM => DxgiDuplicationFormat::Rgb10A2,
416            DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM => DxgiDuplicationFormat::Rgb10XrA2,
417            DXGI_FORMAT_R8G8B8A8_UNORM => DxgiDuplicationFormat::Rgba8,
418            DXGI_FORMAT_R8G8B8A8_UNORM_SRGB => DxgiDuplicationFormat::Rgba8Srgb,
419            DXGI_FORMAT_B8G8R8A8_UNORM => DxgiDuplicationFormat::Bgra8,
420            DXGI_FORMAT_B8G8R8A8_UNORM_SRGB => DxgiDuplicationFormat::Bgra8Srgb,
421            _ => unreachable!(),
422        }
423    }
424
425    /// Gets the refresh rate of the duplication as (numerator, denominator).
426    #[inline]
427    #[must_use]
428    pub const fn refresh_rate(&self) -> (u32, u32) {
429        (self.duplication_desc.ModeDesc.RefreshRate.Numerator, self.duplication_desc.ModeDesc.RefreshRate.Denominator)
430    }
431
432    /// Acquires the next frame and updates the internal texture.
433    ///
434    /// This call will block up to `timeout_ms` milliseconds. If no new frame arrives within
435    /// the timeout, [`Error::Timeout`] is returned. If duplication access is lost,
436    /// [`Error::AccessLost`] is returned and a new duplication should be recreated.
437    ///
438    /// Main reasons for [`Error::AccessLost`] include:
439    /// - The display mode of the output changed (e.g. resolution or color format change).
440    /// - The user switched to a different desktop (e.g. via Ctrl+Alt+Del or Fast User Switching).
441    /// - Switch from DWM on, DWM off, or other full-screen application
442    ///
443    /// The returned [`DxgiDuplicationFrame`] allows you to map the current full desktop image via
444    /// [`DxgiDuplicationFrame::buffer`]. It contains the list of dirty rectangles reported for this
445    /// frame.
446    ///
447    /// # Errors
448    /// - [`Error::Timeout`] when no frame arrives within `timeout_ms`
449    /// - [`Error::AccessLost`] when duplication access is lost and must be recreated
450    /// - [`Error::WindowsError`] for other Windows API failures during frame acquisition
451    #[inline]
452    pub fn acquire_next_frame(&mut self, timeout_ms: u32) -> Result<DxgiDuplicationFrame<'_>, Error> {
453        let mut frame_info = DXGI_OUTDUPL_FRAME_INFO::default();
454        let mut resource = None;
455
456        // Release the previous frame if we were holding one
457        if self.is_holding_frame {
458            match unsafe { self.duplication.ReleaseFrame() } {
459                Ok(()) => (),
460                Err(e) => {
461                    if e.code() == DXGI_ERROR_ACCESS_LOST {
462                        return Err(Error::AccessLost);
463                    } else {
464                        return Err(Error::WindowsError(e));
465                    }
466                }
467            }
468            self.is_holding_frame = false;
469        }
470
471        // Acquire frame
472        match unsafe { self.duplication.AcquireNextFrame(timeout_ms, &mut frame_info, &mut resource) } {
473            Ok(()) => (),
474            Err(e) => {
475                if e.code() == DXGI_ERROR_WAIT_TIMEOUT {
476                    return Err(Error::Timeout);
477                } else if e.code() == DXGI_ERROR_ACCESS_LOST {
478                    return Err(Error::AccessLost);
479                } else {
480                    return Err(Error::WindowsError(e));
481                }
482            }
483        }
484        self.is_holding_frame = true;
485
486        let resource = resource.unwrap();
487
488        // Convert the resource to an ID3D11Texture2D.
489        let frame_texture = resource.cast::<ID3D11Texture2D>()?;
490
491        // Obtain texture description to get size/format details.
492        let mut frame_desc = D3D11_TEXTURE2D_DESC::default();
493        unsafe { frame_texture.GetDesc(&mut frame_desc) };
494
495        Ok(DxgiDuplicationFrame {
496            d3d_device: &self.d3d_device,
497            d3d_device_context: &self.d3d_device_context,
498            duplication: &self.duplication,
499            texture: frame_texture,
500            texture_desc: frame_desc,
501            frame_info,
502        })
503    }
504}
505
506/// Represents a pre-assembled full desktop image for the current frame,
507/// backed by the internal GPU texture.
508/// Call [`DxgiDuplicationFrame::buffer`] to obtain a CPU-readable [`crate::frame::FrameBuffer`].
509pub struct DxgiDuplicationFrame<'a> {
510    d3d_device: &'a ID3D11Device,
511    d3d_device_context: &'a ID3D11DeviceContext,
512    duplication: &'a IDXGIOutputDuplication,
513    texture: ID3D11Texture2D,
514    texture_desc: D3D11_TEXTURE2D_DESC,
515    frame_info: DXGI_OUTDUPL_FRAME_INFO,
516}
517
518impl<'a> DxgiDuplicationFrame<'a> {
519    /// Gets the width of the frame.
520    #[inline]
521    #[must_use]
522    pub const fn width(&self) -> u32 {
523        self.texture_desc.Width
524    }
525
526    /// Gets the height of the frame.
527    #[inline]
528    #[must_use]
529    pub const fn height(&self) -> u32 {
530        self.texture_desc.Height
531    }
532
533    /// Gets the pixel format of the frame.
534    #[inline]
535    #[must_use]
536    pub const fn format(&self) -> DxgiDuplicationFormat {
537        match self.texture_desc.Format {
538            DXGI_FORMAT_R16G16B16A16_FLOAT => DxgiDuplicationFormat::Rgba16F,
539            DXGI_FORMAT_R10G10B10A2_UNORM => DxgiDuplicationFormat::Rgb10A2,
540            DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM => DxgiDuplicationFormat::Rgb10XrA2,
541            DXGI_FORMAT_R8G8B8A8_UNORM => DxgiDuplicationFormat::Rgba8,
542            DXGI_FORMAT_R8G8B8A8_UNORM_SRGB => DxgiDuplicationFormat::Rgba8Srgb,
543            DXGI_FORMAT_B8G8R8A8_UNORM => DxgiDuplicationFormat::Bgra8,
544            DXGI_FORMAT_B8G8R8A8_UNORM_SRGB => DxgiDuplicationFormat::Bgra8Srgb,
545            _ => unreachable!(),
546        }
547    }
548
549    /// Gets the underlying Direct3D device associated with this frame.
550    #[inline]
551    #[must_use]
552    pub const fn device(&self) -> &ID3D11Device {
553        self.d3d_device
554    }
555
556    /// Gets the underlying Direct3D device context used for GPU operations.
557    #[inline]
558    #[must_use]
559    pub const fn device_context(&self) -> &ID3D11DeviceContext {
560        self.d3d_device_context
561    }
562
563    /// Gets the underlying IDXGIOutputDuplication interface.
564    #[inline]
565    #[must_use]
566    pub const fn duplication(&self) -> &IDXGIOutputDuplication {
567        self.duplication
568    }
569
570    /// Gets the underlying [`windows::Win32::Graphics::Direct3D11::ID3D11Texture2D`] interface.
571    #[inline]
572    #[must_use]
573    pub const fn texture(&self) -> &ID3D11Texture2D {
574        &self.texture
575    }
576
577    /// Gets the [`windows::Win32::Graphics::Direct3D11::D3D11_TEXTURE2D_DESC`] of the underlying
578    /// texture.
579    #[inline]
580    #[must_use]
581    pub const fn texture_desc(&self) -> &D3D11_TEXTURE2D_DESC {
582        &self.texture_desc
583    }
584
585    /// Gets the frame information for the current frame.
586    #[inline]
587    #[must_use]
588    pub const fn frame_info(&self) -> &DXGI_OUTDUPL_FRAME_INFO {
589        &self.frame_info
590    }
591
592    /// Maps the internal frame into CPU accessible memory and returns a
593    /// [`crate::frame::FrameBuffer`].
594    ///
595    /// This creates a staging texture, copies the internal texture into it,
596    /// and maps it for CPU read/write. The returned buffer may include row padding;
597    /// you can use [`crate::frame::FrameBuffer::as_nopadding_buffer`] to obtain a packed
598    /// representation.
599    #[inline]
600    pub fn buffer<'b>(&'b mut self) -> Result<DxgiDuplicationFrameBuffer<'b>, Error> {
601        // Staging texture settings
602        let texture_desc = D3D11_TEXTURE2D_DESC {
603            Width: self.texture_desc.Width,
604            Height: self.texture_desc.Height,
605            MipLevels: 1,
606            ArraySize: 1,
607            Format: self.texture_desc.Format,
608            SampleDesc: DXGI_SAMPLE_DESC { Count: 1, Quality: 0 },
609            Usage: D3D11_USAGE_STAGING,
610            BindFlags: 0,
611            CPUAccessFlags: D3D11_CPU_ACCESS_READ.0 as u32 | D3D11_CPU_ACCESS_WRITE.0 as u32,
612            MiscFlags: 0,
613        };
614
615        // Create a CPU-readable staging texture
616        let mut staging = None;
617        unsafe {
618            self.d3d_device.CreateTexture2D(&texture_desc, None, Some(&mut staging))?;
619        };
620        let staging = staging.unwrap();
621
622        // Copy from the internal GPU texture into the staging texture
623        unsafe {
624            self.d3d_device_context.CopyResource(&staging, &self.texture);
625        };
626
627        // Map the staging texture for CPU access
628        let mut mapped = D3D11_MAPPED_SUBRESOURCE::default();
629        unsafe {
630            self.d3d_device_context.Map(&staging, 0, D3D11_MAP_READ_WRITE, 0, Some(&mut mapped))?;
631        };
632
633        // SAFETY: The staging texture remains alive for the scope of this function.
634        let mapped_frame_data = unsafe {
635            slice::from_raw_parts_mut(mapped.pData.cast(), (self.texture_desc.Height * mapped.RowPitch) as usize)
636        };
637
638        let format = match self.texture_desc.Format {
639            DXGI_FORMAT_R16G16B16A16_FLOAT => DxgiDuplicationFormat::Rgba16F,
640            DXGI_FORMAT_R10G10B10A2_UNORM => DxgiDuplicationFormat::Rgb10A2,
641            DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM => DxgiDuplicationFormat::Rgb10XrA2,
642            DXGI_FORMAT_R8G8B8A8_UNORM => DxgiDuplicationFormat::Rgba8,
643            DXGI_FORMAT_R8G8B8A8_UNORM_SRGB => DxgiDuplicationFormat::Rgba8Srgb,
644            DXGI_FORMAT_B8G8R8A8_UNORM => DxgiDuplicationFormat::Bgra8,
645            DXGI_FORMAT_B8G8R8A8_UNORM_SRGB => DxgiDuplicationFormat::Bgra8Srgb,
646            _ => unreachable!(),
647        };
648
649        Ok(DxgiDuplicationFrameBuffer::new(
650            mapped_frame_data,
651            self.texture_desc.Width,
652            self.texture_desc.Height,
653            mapped.RowPitch,
654            mapped.DepthPitch,
655            format,
656        ))
657    }
658
659    /// Gets a cropped frame buffer of the duplication frame.
660    #[inline]
661    pub fn buffer_crop<'b>(
662        &'b mut self,
663        start_x: u32,
664        start_y: u32,
665        end_x: u32,
666        end_y: u32,
667    ) -> Result<DxgiDuplicationFrameBuffer<'b>, Error> {
668        if start_x >= end_x || start_y >= end_y {
669            return Err(Error::InvalidSize);
670        }
671
672        let texture_width = end_x - start_x;
673        let texture_height = end_y - start_y;
674
675        // Staging texture settings for the cropped region
676        let texture_desc = D3D11_TEXTURE2D_DESC {
677            Width: texture_width,
678            Height: texture_height,
679            MipLevels: 1,
680            ArraySize: 1,
681            Format: self.texture_desc.Format,
682            SampleDesc: DXGI_SAMPLE_DESC { Count: 1, Quality: 0 },
683            Usage: D3D11_USAGE_STAGING,
684            BindFlags: 0,
685            CPUAccessFlags: D3D11_CPU_ACCESS_READ.0 as u32 | D3D11_CPU_ACCESS_WRITE.0 as u32,
686            MiscFlags: 0,
687        };
688
689        // Create a CPU-readable staging texture of the crop size
690        let mut staging = None;
691        unsafe {
692            self.d3d_device.CreateTexture2D(&texture_desc, None, Some(&mut staging))?;
693        };
694        let staging = staging.unwrap();
695
696        // Define the source box to copy from the duplication texture
697        let src_box = D3D11_BOX { left: start_x, top: start_y, front: 0, right: end_x, bottom: end_y, back: 1 };
698
699        // Copy the selected region into the staging texture at (0,0)
700        unsafe {
701            self.d3d_device_context.CopySubresourceRegion(&staging, 0, 0, 0, 0, &self.texture, 0, Some(&src_box));
702        }
703
704        // Map the staging texture for CPU access
705        let mut mapped = D3D11_MAPPED_SUBRESOURCE::default();
706        unsafe {
707            self.d3d_device_context.Map(&staging, 0, D3D11_MAP_READ_WRITE, 0, Some(&mut mapped))?;
708        }
709
710        // SAFETY: staging remains alive for the scope of this function.
711        let mapped_frame_data =
712            unsafe { slice::from_raw_parts_mut(mapped.pData.cast(), (texture_height * mapped.RowPitch) as usize) };
713
714        let format = match self.texture_desc.Format {
715            DXGI_FORMAT_R16G16B16A16_FLOAT => DxgiDuplicationFormat::Rgba16F,
716            DXGI_FORMAT_R10G10B10A2_UNORM => DxgiDuplicationFormat::Rgb10A2,
717            DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM => DxgiDuplicationFormat::Rgb10XrA2,
718            DXGI_FORMAT_R8G8B8A8_UNORM => DxgiDuplicationFormat::Rgba8,
719            DXGI_FORMAT_R8G8B8A8_UNORM_SRGB => DxgiDuplicationFormat::Rgba8Srgb,
720            DXGI_FORMAT_B8G8R8A8_UNORM => DxgiDuplicationFormat::Bgra8,
721            DXGI_FORMAT_B8G8R8A8_UNORM_SRGB => DxgiDuplicationFormat::Bgra8Srgb,
722            _ => unreachable!(),
723        };
724
725        Ok(DxgiDuplicationFrameBuffer::new(
726            mapped_frame_data,
727            texture_width,
728            texture_height,
729            mapped.RowPitch,
730            mapped.DepthPitch,
731            format,
732        ))
733    }
734
735    /// Advanced: reuse your own CPU staging texture ([`crate::d3d11::StagingTexture`]).
736    ///
737    /// This avoids per-frame allocations and lets you manage the texture’s lifetime.
738    /// The `staging` texture must be a `D3D11_USAGE_STAGING` 2D texture with CPU read/write access,
739    /// matching the frame’s width/height/format.
740    #[inline]
741    pub fn buffer_with<'s>(
742        &'s mut self,
743        staging: &'s mut StagingTexture,
744    ) -> Result<DxgiDuplicationFrameBuffer<'s>, Error> {
745        // Validate geometry/format match.
746        let desc = staging.desc();
747        if desc.Width != self.texture_desc.Width || desc.Height != self.texture_desc.Height {
748            return Err(Error::InvalidStagingTexture("geometry must match the frame"));
749        }
750        if desc.Format != self.texture_desc.Format {
751            return Err(Error::InvalidStagingTexture("format must match the frame"));
752        }
753
754        // Unmap if was previously mapped
755        if staging.is_mapped() {
756            unsafe { self.d3d_device_context.Unmap(staging.texture(), 0) };
757            staging.set_mapped(false);
758        }
759
760        // Copy the acquired duplication texture into the provided staging texture
761        unsafe {
762            self.d3d_device_context.CopyResource(staging.texture(), &self.texture);
763        }
764
765        // Map the staging texture for CPU access
766        let mut mapped = D3D11_MAPPED_SUBRESOURCE::default();
767        unsafe {
768            self.d3d_device_context.Map(staging.texture(), 0, D3D11_MAP_READ_WRITE, 0, Some(&mut mapped))?;
769        }
770        staging.set_mapped(true);
771
772        // SAFETY: staging lives for 's and remains alive while the FrameBuffer is borrowed.
773        let mapped_frame_data = unsafe {
774            slice::from_raw_parts_mut(mapped.pData.cast(), (self.texture_desc.Height * mapped.RowPitch) as usize)
775        };
776
777        let format = match self.texture_desc.Format {
778            DXGI_FORMAT_R16G16B16A16_FLOAT => DxgiDuplicationFormat::Rgba16F,
779            DXGI_FORMAT_R10G10B10A2_UNORM => DxgiDuplicationFormat::Rgb10A2,
780            DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM => DxgiDuplicationFormat::Rgb10XrA2,
781            DXGI_FORMAT_R8G8B8A8_UNORM => DxgiDuplicationFormat::Rgba8,
782            DXGI_FORMAT_R8G8B8A8_UNORM_SRGB => DxgiDuplicationFormat::Rgba8Srgb,
783            DXGI_FORMAT_B8G8R8A8_UNORM => DxgiDuplicationFormat::Bgra8,
784            DXGI_FORMAT_B8G8R8A8_UNORM_SRGB => DxgiDuplicationFormat::Bgra8Srgb,
785            _ => unreachable!(),
786        };
787
788        Ok(DxgiDuplicationFrameBuffer::new(
789            mapped_frame_data,
790            self.texture_desc.Width,
791            self.texture_desc.Height,
792            mapped.RowPitch,
793            mapped.DepthPitch,
794            format,
795        ))
796    }
797
798    /// Advanced: cropped buffer using a preallocated staging texture.
799    /// The provided staging texture must be a D3D11_USAGE_STAGING 2D texture with CPU read/write
800    /// access, of the same format as the duplication frame, and large enough to contain the
801    /// crop region.
802    #[inline]
803    pub fn buffer_crop_with<'s>(
804        &'s mut self,
805        staging: &'s mut StagingTexture,
806        start_x: u32,
807        start_y: u32,
808        end_x: u32,
809        end_y: u32,
810    ) -> Result<DxgiDuplicationFrameBuffer<'s>, Error> {
811        // Validate crop rectangle
812        if start_x >= end_x || start_y >= end_y {
813            return Err(Error::InvalidSize);
814        }
815
816        let crop_width = end_x - start_x;
817        let crop_height = end_y - start_y;
818
819        // Validate format and capacity
820        let desc = staging.desc();
821        if desc.Format != self.texture_desc.Format {
822            return Err(Error::InvalidStagingTexture("format must match the frame"));
823        }
824        if desc.Width < crop_width || desc.Height < crop_height {
825            return Err(Error::InvalidStagingTexture("staging texture too small for crop region"));
826        }
827
828        // Unmap if was previously mapped
829        if staging.is_mapped() {
830            unsafe { self.d3d_device_context.Unmap(staging.texture(), 0) };
831            staging.set_mapped(false);
832        }
833
834        // Define the source region to copy
835        let src_box = D3D11_BOX { left: start_x, top: start_y, front: 0, right: end_x, bottom: end_y, back: 1 };
836
837        // Copy the selected region to the top-left of the staging texture
838        unsafe {
839            self.d3d_device_context.CopySubresourceRegion(
840                staging.texture(),
841                0,
842                0,
843                0,
844                0,
845                &self.texture,
846                0,
847                Some(&src_box),
848            );
849        }
850
851        // Map the staging texture
852        let mut mapped = D3D11_MAPPED_SUBRESOURCE::default();
853        unsafe {
854            self.d3d_device_context.Map(staging.texture(), 0, D3D11_MAP_READ_WRITE, 0, Some(&mut mapped))?;
855        }
856        staging.set_mapped(true);
857
858        // SAFETY: staging lives for 's and remains alive while the FrameBuffer is borrowed.
859        let mapped_frame_data =
860            unsafe { slice::from_raw_parts_mut(mapped.pData.cast(), (crop_height * mapped.RowPitch) as usize) };
861
862        let format = match self.texture_desc.Format {
863            DXGI_FORMAT_R16G16B16A16_FLOAT => DxgiDuplicationFormat::Rgba16F,
864            DXGI_FORMAT_R10G10B10A2_UNORM => DxgiDuplicationFormat::Rgb10A2,
865            DXGI_FORMAT_R10G10B10_XR_BIAS_A2_UNORM => DxgiDuplicationFormat::Rgb10XrA2,
866            DXGI_FORMAT_R8G8B8A8_UNORM => DxgiDuplicationFormat::Rgba8,
867            DXGI_FORMAT_R8G8B8A8_UNORM_SRGB => DxgiDuplicationFormat::Rgba8Srgb,
868            DXGI_FORMAT_B8G8R8A8_UNORM => DxgiDuplicationFormat::Bgra8,
869            DXGI_FORMAT_B8G8R8A8_UNORM_SRGB => DxgiDuplicationFormat::Bgra8Srgb,
870            _ => unreachable!(),
871        };
872
873        Ok(DxgiDuplicationFrameBuffer::new(
874            mapped_frame_data,
875            crop_width,
876            crop_height,
877            mapped.RowPitch,
878            mapped.DepthPitch,
879            format,
880        ))
881    }
882
883    /// Saves the frame buffer as an image to the specified path.
884    #[inline]
885    pub fn save_as_image<T: AsRef<Path>>(&mut self, path: T, format: ImageFormat) -> Result<(), Error> {
886        let mut frame_buffer = self.buffer()?;
887
888        frame_buffer.save_as_image(path, format)?;
889
890        Ok(())
891    }
892}
893
894/// Represents a frame buffer containing pixel data.
895///
896/// # Example
897/// ```ignore
898/// // Get a frame from the capture session
899/// let mut buffer = frame.buffer()?;
900/// buffer.save_as_image("screenshot.png", ImageFormat::Png)?;
901/// ```
902pub struct DxgiDuplicationFrameBuffer<'a> {
903    raw_buffer: &'a mut [u8],
904    width: u32,
905    height: u32,
906    row_pitch: u32,
907    depth_pitch: u32,
908    format: DxgiDuplicationFormat,
909}
910
911impl<'a> DxgiDuplicationFrameBuffer<'a> {
912    /// Constructs a new `FrameBuffer`.
913    #[inline]
914    #[must_use]
915    pub const fn new(
916        raw_buffer: &'a mut [u8],
917        width: u32,
918        height: u32,
919        row_pitch: u32,
920        depth_pitch: u32,
921        format: DxgiDuplicationFormat,
922    ) -> Self {
923        Self { raw_buffer, width, height, row_pitch, depth_pitch, format }
924    }
925
926    /// Gets the width of the frame buffer.
927    #[inline]
928    #[must_use]
929    pub const fn width(&self) -> u32 {
930        self.width
931    }
932
933    /// Gets the height of the frame buffer.
934    #[inline]
935    #[must_use]
936    pub const fn height(&self) -> u32 {
937        self.height
938    }
939
940    /// Gets the row pitch of the frame buffer.
941    #[inline]
942    #[must_use]
943    pub const fn row_pitch(&self) -> u32 {
944        self.row_pitch
945    }
946
947    /// Gets the depth pitch of the frame buffer.
948    #[inline]
949    #[must_use]
950    pub const fn depth_pitch(&self) -> u32 {
951        self.depth_pitch
952    }
953
954    /// Gets the color format of the frame buffer.
955    #[inline]
956    #[must_use]
957    pub const fn format(&self) -> DxgiDuplicationFormat {
958        self.format
959    }
960
961    /// Checks if the buffer has padding.
962    #[inline]
963    #[must_use]
964    pub const fn has_padding(&self) -> bool {
965        self.width * 4 != self.row_pitch
966    }
967
968    /// Gets the pixel data without padding.
969    #[inline]
970    #[must_use]
971    pub fn as_nopadding_buffer<'b>(&'b self, buffer: &'b mut Vec<u8>) -> &'b [u8] {
972        if !self.has_padding() {
973            return self.raw_buffer;
974        }
975
976        let multiplier = match self.format {
977            DxgiDuplicationFormat::Rgba16F => 8,
978            DxgiDuplicationFormat::Rgb10A2 => 4,
979            DxgiDuplicationFormat::Rgb10XrA2 => 4,
980            DxgiDuplicationFormat::Rgba8 => 4,
981            DxgiDuplicationFormat::Rgba8Srgb => 4,
982            DxgiDuplicationFormat::Bgra8 => 4,
983            DxgiDuplicationFormat::Bgra8Srgb => 4,
984        };
985
986        let frame_size = (self.width * self.height * multiplier) as usize;
987        if buffer.capacity() < frame_size {
988            buffer.resize(frame_size, 0);
989        }
990
991        let width_size = (self.width * multiplier) as usize;
992        let buffer_address = buffer.as_mut_ptr() as isize;
993        (0..self.height).into_par_iter().for_each(|y| {
994            let index = (y * self.row_pitch) as usize;
995            let ptr = buffer_address as *mut u8;
996
997            unsafe {
998                std::ptr::copy_nonoverlapping(
999                    self.raw_buffer.as_ptr().add(index),
1000                    ptr.add(y as usize * width_size),
1001                    width_size,
1002                );
1003            }
1004        });
1005
1006        &buffer[0..frame_size]
1007    }
1008
1009    /// Gets the raw pixel data, which may include padding.
1010    #[inline]
1011    #[must_use]
1012    pub const fn as_raw_buffer(&mut self) -> &mut [u8] {
1013        self.raw_buffer
1014    }
1015
1016    /// Saves the frame buffer as an image to the specified path.
1017    #[inline]
1018    pub fn save_as_image<T: AsRef<Path>>(&mut self, path: T, format: ImageFormat) -> Result<(), Error> {
1019        let width = self.width;
1020        let height = self.height;
1021
1022        let pixel_format = match self.format {
1023            DxgiDuplicationFormat::Rgba8 => ImageEncoderPixelFormat::Rgba8,
1024            DxgiDuplicationFormat::Bgra8 => ImageEncoderPixelFormat::Bgra8,
1025            _ => return Err(ImageEncoderError::UnsupportedFormat.into()),
1026        };
1027
1028        let mut buffer = Vec::new();
1029        let bytes =
1030            ImageEncoder::new(format, pixel_format)?.encode(self.as_nopadding_buffer(&mut buffer), width, height)?;
1031
1032        fs::write(path, bytes)?;
1033
1034        Ok(())
1035    }
1036}