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};
31
32use rayon::iter::{IntoParallelIterator, ParallelIterator};
33use windows::Win32::Foundation::E_ACCESSDENIED;
34use windows::Win32::Graphics::Direct3D11::{
35    D3D11_BOX, D3D11_TEXTURE2D_DESC, ID3D11Device, ID3D11DeviceContext, ID3D11Texture2D,
36};
37use windows::Win32::Graphics::Dxgi::Common::{
38    DXGI_FORMAT, DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_R8G8B8A8_UNORM, DXGI_FORMAT_R16G16B16A16_FLOAT,
39};
40use windows::Win32::Graphics::Dxgi::{
41    DXGI_ERROR_ACCESS_LOST, DXGI_ERROR_NOT_FOUND, DXGI_ERROR_WAIT_TIMEOUT, DXGI_OUTDUPL_DESC, DXGI_OUTDUPL_FRAME_INFO,
42    IDXGIDevice4, IDXGIOutput6, IDXGIOutputDuplication,
43};
44use windows::Win32::UI::HiDpi::{DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2, SetProcessDpiAwarenessContext};
45use windows::core::Interface;
46
47use crate::d3d11::{MappedStagingTexture, StagingTexture, create_d3d_device, unmap_staging_texture};
48use crate::encoder::{ImageEncoder, ImageEncoderError, ImageEncoderPixelFormat, ImageFormat};
49use crate::monitor::Monitor;
50
51/// Errors that can occur while using the DXGI Desktop Duplication API wrapper.
52#[derive(thiserror::Error, Debug)]
53pub enum Error {
54    /// The crop rectangle is invalid (start >= end on either axis).
55    #[error("Invalid crop size")]
56    InvalidSize,
57    /// Failed to find a DXGI output that corresponds to the provided monitor.
58    #[error("Failed to find DXGI output for the specified monitor")]
59    OutputNotFound,
60    /// AcquireNextFrame timed out without a new frame becoming available.
61    #[error("AcquireNextFrame timed out")]
62    Timeout,
63    /// The duplication access was lost and must be recreated.
64    #[error("Duplication access lost; the duplication must be recreated")]
65    AccessLost,
66    /// DirectX device creation or related error.
67    #[error("DirectX error: {0}")]
68    DirectXError(#[from] crate::d3d11::Error),
69    /// Invalid or mismatched staging texture supplied to [`DxgiDuplicationFrame::buffer_with`].
70    #[error("Invalid staging texture: {0}")]
71    InvalidStagingTexture(&'static str),
72    /// A DXGI/D3D call reported success but did not populate the requested output value.
73    #[error("Windows API succeeded but did not return {0}")]
74    UnexpectedNullResult(&'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    /// 8-bit RGBA format.
96    Rgba8,
97    /// 8-bit BGRA format.
98    Bgra8,
99}
100
101const DEFAULT_DUPLICATION_FORMATS: [DXGI_FORMAT; 3] =
102    [DXGI_FORMAT_R16G16B16A16_FLOAT, DXGI_FORMAT_R8G8B8A8_UNORM, DXGI_FORMAT_B8G8R8A8_UNORM];
103
104/// A minimal, ergonomic wrapper around the DXGI Desktop Duplication API for capturing a monitor.
105///
106/// This wrapper focuses on staying close to the native API while providing a simple Rust interface.
107/// It integrates with [`crate::monitor::Monitor`] to select the target output.
108pub struct DxgiDuplicationApi {
109    /// Direct3D 11 device used for duplication operations.
110    d3d_device: ID3D11Device,
111    /// Direct3D 11 device context used for copy/map operations.
112    d3d_device_context: ID3D11DeviceContext,
113    /// The duplication interface used to acquire frames.
114    duplication: IDXGIOutputDuplication,
115    /// Description of the duplication, including format and dimensions.
116    duplication_desc: DXGI_OUTDUPL_DESC,
117    /// The DXGI device associated with the Direct3D device.
118    dxgi_device: IDXGIDevice4,
119    /// The DXGI output associated with this duplication.
120    output: IDXGIOutput6,
121    /// Whether the internal staging texture is currently holding a frame.
122    is_holding_frame: bool,
123}
124
125fn enable_per_monitor_dpi_awareness() -> Result<(), Error> {
126    match unsafe { SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) } {
127        Ok(()) => Ok(()),
128        Err(error) if error.code() == E_ACCESSDENIED => Ok(()),
129        Err(error) => Err(Error::WindowsError(error)),
130    }
131}
132
133fn find_output_for_monitor(dxgi_device: &IDXGIDevice4, monitor: Monitor) -> Result<IDXGIOutput6, Error> {
134    let adapter = unsafe { dxgi_device.GetAdapter()? };
135    let mut index = 0u32;
136
137    loop {
138        match unsafe { adapter.EnumOutputs(index) } {
139            Ok(output) => {
140                let desc = unsafe { output.GetDesc()? };
141                if desc.Monitor.0 == monitor.as_raw_hmonitor() {
142                    return Ok(output.cast::<IDXGIOutput6>()?);
143                }
144                index += 1;
145            }
146            Err(error) if error.code() == DXGI_ERROR_NOT_FOUND => return Err(Error::OutputNotFound),
147            Err(error) => return Err(Error::WindowsError(error)),
148        }
149    }
150}
151
152fn map_supported_formats(supported_formats: &[DxgiDuplicationFormat]) -> Vec<DXGI_FORMAT> {
153    let mut supported_formats = supported_formats
154        .iter()
155        .map(|format| match format {
156            DxgiDuplicationFormat::Rgba16F => DXGI_FORMAT_R16G16B16A16_FLOAT,
157            DxgiDuplicationFormat::Rgba8 => DXGI_FORMAT_R8G8B8A8_UNORM,
158            DxgiDuplicationFormat::Bgra8 => DXGI_FORMAT_B8G8R8A8_UNORM,
159        })
160        .collect::<Vec<_>>();
161
162    if !supported_formats.contains(&DXGI_FORMAT_B8G8R8A8_UNORM) {
163        supported_formats.push(DXGI_FORMAT_B8G8R8A8_UNORM);
164    }
165
166    supported_formats
167}
168
169impl DxgiDuplicationApi {
170    fn release_frame_if_needed(&mut self) -> Result<(), Error> {
171        if !self.is_holding_frame {
172            return Ok(());
173        }
174
175        match unsafe { self.duplication.ReleaseFrame() } {
176            Ok(()) => {
177                self.is_holding_frame = false;
178                Ok(())
179            }
180            Err(error) if error.code() == DXGI_ERROR_ACCESS_LOST => Err(Error::AccessLost),
181            Err(error) => Err(Error::WindowsError(error)),
182        }
183    }
184
185    fn recreate_with_formats(mut self, supported_formats: &[DXGI_FORMAT]) -> Result<Self, Error> {
186        let _ = self.release_frame_if_needed();
187
188        // Keep the device/output alive, but release the existing duplication interface before
189        // asking DXGI for its replacement. `DuplicateOutput1` may reject a second live
190        // duplication for the same output.
191        let d3d_device = self.d3d_device.clone();
192        let d3d_device_context = self.d3d_device_context.clone();
193        let dxgi_device = self.dxgi_device.clone();
194        let output = self.output.clone();
195        drop(self);
196
197        let duplication = unsafe { output.DuplicateOutput1(&d3d_device, 0, supported_formats)? };
198        let duplication_desc = unsafe { duplication.GetDesc() };
199
200        Ok(Self {
201            d3d_device,
202            d3d_device_context,
203            duplication,
204            duplication_desc,
205            dxgi_device,
206            output,
207            is_holding_frame: false,
208        })
209    }
210
211    /// Constructs a new duplication session for the specified monitor.
212    ///
213    /// Internally creates a Direct3D 11 device and immediate context using the crate's d3d11
214    /// module.
215    pub fn new(monitor: Monitor) -> Result<Self, Error> {
216        // Create D3D11 device and context.
217        let (d3d_device, d3d_device_context) = create_d3d_device()?;
218
219        let dxgi_device = d3d_device.cast::<IDXGIDevice4>()?;
220        let output = find_output_for_monitor(&dxgi_device, monitor)?;
221        enable_per_monitor_dpi_awareness()?;
222
223        // Restrict the duplication to the crate-supported DXGI formats.
224        let duplication = unsafe { output.DuplicateOutput1(&d3d_device, 0, &DEFAULT_DUPLICATION_FORMATS)? };
225
226        // Get the duplication description to determine the format for our internal texture.
227        let duplication_desc = unsafe { duplication.GetDesc() };
228
229        Ok(Self {
230            d3d_device,
231            d3d_device_context,
232            duplication,
233            duplication_desc,
234            dxgi_device,
235            output,
236            is_holding_frame: false,
237        })
238    }
239
240    /// Constructs a new duplication session for the specified monitor, using a custom list of
241    /// supported DXGI formats.
242    ///
243    /// This method lets callers prefer any subset of the crate-supported DXGI formats.
244    /// `Bgra8` is inserted because it is widely supported and serves as a reliable fallback.
245    pub fn new_options(monitor: Monitor, supported_formats: &[DxgiDuplicationFormat]) -> Result<Self, Error> {
246        // Create D3D11 device and context.
247        let (d3d_device, d3d_device_context) = create_d3d_device()?;
248
249        let dxgi_device = d3d_device.cast::<IDXGIDevice4>()?;
250        let output = find_output_for_monitor(&dxgi_device, monitor)?;
251        let supported_formats = map_supported_formats(supported_formats);
252        enable_per_monitor_dpi_awareness()?;
253
254        // Create the duplication for this output using the supplied D3D11 device.
255        let duplication = unsafe { output.DuplicateOutput1(&d3d_device, 0, &supported_formats)? };
256
257        // Get the duplication description to determine the format for our internal texture.
258        let duplication_desc = unsafe { duplication.GetDesc() };
259
260        Ok(Self {
261            d3d_device,
262            d3d_device_context,
263            duplication,
264            duplication_desc,
265            dxgi_device,
266            output,
267            is_holding_frame: false,
268        })
269    }
270
271    /// Recreates the duplication interface, mostly used after receiving an [`Error::AccessLost`]
272    /// error from [`DxgiDuplicationApi::acquire_next_frame`].
273    pub fn recreate(self) -> Result<Self, Error> {
274        self.recreate_with_formats(&DEFAULT_DUPLICATION_FORMATS)
275    }
276
277    /// Recreates the duplication interface with a custom list of supported DXGI formats, mostly
278    /// used after receiving an [`Error::AccessLost`] error from
279    /// [`DxgiDuplicationApi::acquire_next_frame`].
280    pub fn recreate_options(self, supported_formats: &[DxgiDuplicationFormat]) -> Result<Self, Error> {
281        let supported_formats = map_supported_formats(supported_formats);
282        self.recreate_with_formats(&supported_formats)
283    }
284
285    /// Gets the underlying [`windows::Win32::Graphics::Direct3D11::ID3D11Device`] associated with
286    /// this object.
287    #[inline]
288    #[must_use]
289    pub const fn device(&self) -> &ID3D11Device {
290        &self.d3d_device
291    }
292
293    /// Gets the underlying [`windows::Win32::Graphics::Direct3D11::ID3D11DeviceContext`] used for
294    /// GPU operations.
295    #[inline]
296    #[must_use]
297    pub const fn device_context(&self) -> &ID3D11DeviceContext {
298        &self.d3d_device_context
299    }
300
301    /// Gets the underlying [`windows::Win32::Graphics::Dxgi::IDXGIOutputDuplication`] interface.
302    #[inline]
303    #[must_use]
304    pub const fn duplication(&self) -> &IDXGIOutputDuplication {
305        &self.duplication
306    }
307
308    /// Gets the [`windows::Win32::Graphics::Dxgi::DXGI_OUTDUPL_DESC`] of the duplication.
309    #[inline]
310    #[must_use]
311    pub const fn duplication_desc(&self) -> &DXGI_OUTDUPL_DESC {
312        &self.duplication_desc
313    }
314
315    /// Gets the underlying [`windows::Win32::Graphics::Dxgi::IDXGIDevice4`] interface.
316    #[inline]
317    #[must_use]
318    pub const fn dxgi_device(&self) -> &IDXGIDevice4 {
319        &self.dxgi_device
320    }
321
322    /// Gets the underlying [`windows::Win32::Graphics::Dxgi::IDXGIOutput6`] interface.
323    #[inline]
324    #[must_use]
325    pub const fn output(&self) -> &IDXGIOutput6 {
326        &self.output
327    }
328
329    /// Gets the width of the duplication.
330    #[inline]
331    #[must_use]
332    pub const fn width(&self) -> u32 {
333        self.duplication_desc.ModeDesc.Width
334    }
335
336    /// Gets the height of the duplication.
337    #[inline]
338    #[must_use]
339    pub const fn height(&self) -> u32 {
340        self.duplication_desc.ModeDesc.Height
341    }
342
343    /// Gets the pixel format of the duplication.
344    #[inline]
345    #[must_use]
346    pub const fn format(&self) -> DxgiDuplicationFormat {
347        match self.duplication_desc.ModeDesc.Format {
348            DXGI_FORMAT_R16G16B16A16_FLOAT => DxgiDuplicationFormat::Rgba16F,
349            DXGI_FORMAT_R8G8B8A8_UNORM => DxgiDuplicationFormat::Rgba8,
350            DXGI_FORMAT_B8G8R8A8_UNORM => DxgiDuplicationFormat::Bgra8,
351            _ => unreachable!(),
352        }
353    }
354
355    /// Gets the refresh rate of the duplication as (numerator, denominator).
356    #[inline]
357    #[must_use]
358    pub const fn refresh_rate(&self) -> (u32, u32) {
359        (self.duplication_desc.ModeDesc.RefreshRate.Numerator, self.duplication_desc.ModeDesc.RefreshRate.Denominator)
360    }
361
362    /// Acquires the next frame and updates the internal texture.
363    ///
364    /// This call will block up to `timeout_ms` milliseconds. If no new frame arrives within
365    /// the timeout, [`Error::Timeout`] is returned. If duplication access is lost,
366    /// [`Error::AccessLost`] is returned and a new duplication should be recreated.
367    ///
368    /// Main reasons for [`Error::AccessLost`] include:
369    /// - The display mode of the output changed (e.g. resolution or color format change).
370    /// - The user switched to a different desktop (e.g. via Ctrl+Alt+Del or Fast User Switching).
371    /// - Switch from DWM on, DWM off, or other full-screen application
372    ///
373    /// The returned [`DxgiDuplicationFrame`] allows you to map the current full desktop image via
374    /// [`DxgiDuplicationFrame::buffer`]. It contains the list of dirty rectangles reported for this
375    /// frame.
376    ///
377    /// # Errors
378    /// - [`Error::Timeout`] when no frame arrives within `timeout_ms`
379    /// - [`Error::AccessLost`] when duplication access is lost and must be recreated
380    /// - [`Error::WindowsError`] for other Windows API failures during frame acquisition
381    #[inline]
382    pub fn acquire_next_frame(&mut self, timeout_ms: u32) -> Result<DxgiDuplicationFrame<'_>, Error> {
383        let mut frame_info = DXGI_OUTDUPL_FRAME_INFO::default();
384        let mut resource = None;
385
386        // Release the previous frame if we were holding one
387        self.release_frame_if_needed()?;
388
389        // Acquire frame
390        match unsafe { self.duplication.AcquireNextFrame(timeout_ms, &mut frame_info, &mut resource) } {
391            Ok(()) => (),
392            Err(e) => {
393                if e.code() == DXGI_ERROR_WAIT_TIMEOUT {
394                    return Err(Error::Timeout);
395                } else if e.code() == DXGI_ERROR_ACCESS_LOST {
396                    return Err(Error::AccessLost);
397                } else {
398                    return Err(Error::WindowsError(e));
399                }
400            }
401        }
402        self.is_holding_frame = true;
403
404        let resource = resource.ok_or(Error::UnexpectedNullResult("an acquired DXGI frame resource"))?;
405
406        // Convert the resource to an ID3D11Texture2D.
407        let frame_texture = resource.cast::<ID3D11Texture2D>()?;
408
409        // Obtain texture description to get size/format details.
410        let mut frame_desc = D3D11_TEXTURE2D_DESC::default();
411        unsafe { frame_texture.GetDesc(&mut frame_desc) };
412
413        Ok(DxgiDuplicationFrame {
414            d3d_device: &self.d3d_device,
415            d3d_device_context: &self.d3d_device_context,
416            duplication: &self.duplication,
417            texture: frame_texture,
418            texture_desc: frame_desc,
419            frame_info,
420        })
421    }
422}
423
424impl Drop for DxgiDuplicationApi {
425    fn drop(&mut self) {
426        let _ = self.release_frame_if_needed();
427    }
428}
429
430/// Represents a pre-assembled full desktop image for the current frame,
431/// backed by the internal GPU texture.
432/// Call [`DxgiDuplicationFrame::buffer`] to obtain a CPU-readable [`crate::frame::FrameBuffer`].
433pub struct DxgiDuplicationFrame<'a> {
434    d3d_device: &'a ID3D11Device,
435    d3d_device_context: &'a ID3D11DeviceContext,
436    duplication: &'a IDXGIOutputDuplication,
437    texture: ID3D11Texture2D,
438    texture_desc: D3D11_TEXTURE2D_DESC,
439    frame_info: DXGI_OUTDUPL_FRAME_INFO,
440}
441
442impl<'a> DxgiDuplicationFrame<'a> {
443    /// Gets the width of the frame.
444    #[inline]
445    #[must_use]
446    pub const fn width(&self) -> u32 {
447        self.texture_desc.Width
448    }
449
450    /// Gets the height of the frame.
451    #[inline]
452    #[must_use]
453    pub const fn height(&self) -> u32 {
454        self.texture_desc.Height
455    }
456
457    /// Gets the pixel format of the frame.
458    #[inline]
459    #[must_use]
460    pub const fn format(&self) -> DxgiDuplicationFormat {
461        match self.texture_desc.Format {
462            DXGI_FORMAT_R16G16B16A16_FLOAT => DxgiDuplicationFormat::Rgba16F,
463            DXGI_FORMAT_R8G8B8A8_UNORM => DxgiDuplicationFormat::Rgba8,
464            DXGI_FORMAT_B8G8R8A8_UNORM => DxgiDuplicationFormat::Bgra8,
465            _ => unreachable!(),
466        }
467    }
468
469    /// Gets the underlying Direct3D device associated with this frame.
470    #[inline]
471    #[must_use]
472    pub const fn device(&self) -> &ID3D11Device {
473        self.d3d_device
474    }
475
476    /// Gets the underlying Direct3D device context used for GPU operations.
477    #[inline]
478    #[must_use]
479    pub const fn device_context(&self) -> &ID3D11DeviceContext {
480        self.d3d_device_context
481    }
482
483    /// Gets the underlying IDXGIOutputDuplication interface.
484    #[inline]
485    #[must_use]
486    pub const fn duplication(&self) -> &IDXGIOutputDuplication {
487        self.duplication
488    }
489
490    /// Gets the underlying [`windows::Win32::Graphics::Direct3D11::ID3D11Texture2D`] interface.
491    #[inline]
492    #[must_use]
493    pub const fn texture(&self) -> &ID3D11Texture2D {
494        &self.texture
495    }
496
497    /// Gets the [`windows::Win32::Graphics::Direct3D11::D3D11_TEXTURE2D_DESC`] of the underlying
498    /// texture.
499    #[inline]
500    #[must_use]
501    pub const fn texture_desc(&self) -> &D3D11_TEXTURE2D_DESC {
502        &self.texture_desc
503    }
504
505    /// Gets the frame information for the current frame.
506    #[inline]
507    #[must_use]
508    pub const fn frame_info(&self) -> &DXGI_OUTDUPL_FRAME_INFO {
509        &self.frame_info
510    }
511
512    /// Maps the internal frame into CPU accessible memory and returns a
513    /// [`crate::frame::FrameBuffer`].
514    ///
515    /// This creates a staging texture, copies the internal texture into it,
516    /// and maps it for CPU read/write. The returned buffer may include row padding;
517    /// you can use [`crate::frame::FrameBuffer::as_nopadding_buffer`] to obtain a packed
518    /// representation.
519    #[inline]
520    pub fn buffer<'b>(&'b mut self) -> Result<DxgiDuplicationFrameBuffer<'b>, Error> {
521        let staging = StagingTexture::new(
522            self.d3d_device,
523            self.texture_desc.Width,
524            self.texture_desc.Height,
525            self.texture_desc.Format,
526        )?;
527
528        // Copy from the internal GPU texture into the staging texture
529        unsafe {
530            self.d3d_device_context.CopyResource(staging.texture(), &self.texture);
531        }
532
533        let mapped_texture = MappedStagingTexture::map_owned(self.d3d_device_context, staging)?;
534
535        Ok(DxgiDuplicationFrameBuffer::from_mapped(
536            mapped_texture,
537            self.texture_desc.Width,
538            self.texture_desc.Height,
539            self.format(),
540        ))
541    }
542
543    /// Gets a cropped frame buffer of the duplication frame.
544    #[inline]
545    pub fn buffer_crop<'b>(
546        &'b mut self,
547        start_x: u32,
548        start_y: u32,
549        end_x: u32,
550        end_y: u32,
551    ) -> Result<DxgiDuplicationFrameBuffer<'b>, Error> {
552        if start_x >= end_x || start_y >= end_y {
553            return Err(Error::InvalidSize);
554        }
555
556        let texture_width = end_x - start_x;
557        let texture_height = end_y - start_y;
558
559        let staging = StagingTexture::new(self.d3d_device, texture_width, texture_height, self.texture_desc.Format)?;
560
561        // Define the source box to copy from the duplication texture
562        let src_box = D3D11_BOX { left: start_x, top: start_y, front: 0, right: end_x, bottom: end_y, back: 1 };
563
564        // Copy the selected region into the staging texture at (0,0)
565        unsafe {
566            self.d3d_device_context.CopySubresourceRegion(
567                staging.texture(),
568                0,
569                0,
570                0,
571                0,
572                &self.texture,
573                0,
574                Some(&src_box),
575            );
576        }
577
578        let mapped_texture = MappedStagingTexture::map_owned(self.d3d_device_context, staging)?;
579
580        Ok(DxgiDuplicationFrameBuffer::from_mapped(mapped_texture, texture_width, texture_height, self.format()))
581    }
582
583    /// Advanced: reuse your own CPU staging texture ([`crate::d3d11::StagingTexture`]).
584    ///
585    /// This avoids per-frame allocations and lets you manage the texture’s lifetime.
586    /// The `staging` texture must be a `D3D11_USAGE_STAGING` 2D texture with CPU read/write access,
587    /// matching the frame’s width/height/format.
588    #[inline]
589    pub fn buffer_with<'s>(
590        &'s mut self,
591        staging: &'s mut StagingTexture,
592    ) -> Result<DxgiDuplicationFrameBuffer<'s>, Error> {
593        // Validate geometry/format match.
594        let desc = staging.desc();
595        if desc.Width != self.texture_desc.Width || desc.Height != self.texture_desc.Height {
596            return Err(Error::InvalidStagingTexture("geometry must match the frame"));
597        }
598        if desc.Format != self.texture_desc.Format {
599            return Err(Error::InvalidStagingTexture("format must match the frame"));
600        }
601
602        unmap_staging_texture(self.d3d_device_context, staging);
603
604        // Copy the acquired duplication texture into the provided staging texture
605        unsafe {
606            self.d3d_device_context.CopyResource(staging.texture(), &self.texture);
607        }
608
609        let mapped_texture = MappedStagingTexture::map_borrowed(self.d3d_device_context, staging)?;
610
611        Ok(DxgiDuplicationFrameBuffer::from_mapped(
612            mapped_texture,
613            self.texture_desc.Width,
614            self.texture_desc.Height,
615            self.format(),
616        ))
617    }
618
619    /// Advanced: cropped buffer using a preallocated staging texture.
620    /// The provided staging texture must be a D3D11_USAGE_STAGING 2D texture with CPU read/write
621    /// access, of the same format as the duplication frame, and large enough to contain the
622    /// crop region.
623    #[inline]
624    pub fn buffer_crop_with<'s>(
625        &'s mut self,
626        staging: &'s mut StagingTexture,
627        start_x: u32,
628        start_y: u32,
629        end_x: u32,
630        end_y: u32,
631    ) -> Result<DxgiDuplicationFrameBuffer<'s>, Error> {
632        // Validate crop rectangle
633        if start_x >= end_x || start_y >= end_y {
634            return Err(Error::InvalidSize);
635        }
636
637        let crop_width = end_x - start_x;
638        let crop_height = end_y - start_y;
639
640        // Validate format and capacity
641        let desc = staging.desc();
642        if desc.Format != self.texture_desc.Format {
643            return Err(Error::InvalidStagingTexture("format must match the frame"));
644        }
645        if desc.Width < crop_width || desc.Height < crop_height {
646            return Err(Error::InvalidStagingTexture("staging texture too small for crop region"));
647        }
648
649        unmap_staging_texture(self.d3d_device_context, staging);
650
651        // Define the source region to copy
652        let src_box = D3D11_BOX { left: start_x, top: start_y, front: 0, right: end_x, bottom: end_y, back: 1 };
653
654        // Copy the selected region to the top-left of the staging texture
655        unsafe {
656            self.d3d_device_context.CopySubresourceRegion(
657                staging.texture(),
658                0,
659                0,
660                0,
661                0,
662                &self.texture,
663                0,
664                Some(&src_box),
665            );
666        }
667
668        let mapped_texture = MappedStagingTexture::map_borrowed(self.d3d_device_context, staging)?;
669
670        Ok(DxgiDuplicationFrameBuffer::from_mapped(mapped_texture, crop_width, crop_height, self.format()))
671    }
672
673    /// Saves the frame buffer as an image to the specified path.
674    #[inline]
675    pub fn save_as_image<T: AsRef<Path>>(&mut self, path: T, format: ImageFormat) -> Result<(), Error> {
676        let mut frame_buffer = self.buffer()?;
677
678        frame_buffer.save_as_image(path, format)?;
679
680        Ok(())
681    }
682}
683
684/// Represents a frame buffer containing pixel data.
685///
686/// # Example
687/// ```ignore
688/// // Get a frame from the capture session
689/// let mut buffer = frame.buffer()?;
690/// buffer.save_as_image("screenshot.png", ImageFormat::Png)?;
691/// ```
692enum DxgiDuplicationFrameBufferBacking<'a> {
693    Borrowed(&'a mut [u8]),
694    Mapped(MappedStagingTexture<'a>),
695}
696
697impl DxgiDuplicationFrameBufferBacking<'_> {
698    const fn as_slice(&self, height: u32) -> &[u8] {
699        match self {
700            Self::Borrowed(buffer) => buffer,
701            Self::Mapped(texture) => texture.as_slice(height),
702        }
703    }
704
705    const fn as_mut_slice(&mut self, height: u32) -> &mut [u8] {
706        match self {
707            Self::Borrowed(buffer) => buffer,
708            Self::Mapped(texture) => texture.as_mut_slice(height),
709        }
710    }
711}
712
713/// Represents a CPU-readable frame buffer produced from a duplication frame.
714pub struct DxgiDuplicationFrameBuffer<'a> {
715    backing: DxgiDuplicationFrameBufferBacking<'a>,
716    width: u32,
717    height: u32,
718    row_pitch: u32,
719    depth_pitch: u32,
720    format: DxgiDuplicationFormat,
721}
722
723impl<'a> DxgiDuplicationFrameBuffer<'a> {
724    /// Constructs a new `FrameBuffer`.
725    #[inline]
726    #[must_use]
727    pub const fn new(
728        raw_buffer: &'a mut [u8],
729        width: u32,
730        height: u32,
731        row_pitch: u32,
732        depth_pitch: u32,
733        format: DxgiDuplicationFormat,
734    ) -> Self {
735        Self {
736            backing: DxgiDuplicationFrameBufferBacking::Borrowed(raw_buffer),
737            width,
738            height,
739            row_pitch,
740            depth_pitch,
741            format,
742        }
743    }
744
745    const fn from_mapped(
746        mapped_texture: MappedStagingTexture<'a>,
747        width: u32,
748        height: u32,
749        format: DxgiDuplicationFormat,
750    ) -> Self {
751        let row_pitch = mapped_texture.row_pitch();
752        let depth_pitch = mapped_texture.depth_pitch();
753
754        Self {
755            backing: DxgiDuplicationFrameBufferBacking::Mapped(mapped_texture),
756            width,
757            height,
758            row_pitch,
759            depth_pitch,
760            format,
761        }
762    }
763
764    /// Gets the width of the frame buffer.
765    #[inline]
766    #[must_use]
767    pub const fn width(&self) -> u32 {
768        self.width
769    }
770
771    /// Gets the height of the frame buffer.
772    #[inline]
773    #[must_use]
774    pub const fn height(&self) -> u32 {
775        self.height
776    }
777
778    /// Gets the row pitch of the frame buffer.
779    #[inline]
780    #[must_use]
781    pub const fn row_pitch(&self) -> u32 {
782        self.row_pitch
783    }
784
785    /// Gets the depth pitch of the frame buffer.
786    #[inline]
787    #[must_use]
788    pub const fn depth_pitch(&self) -> u32 {
789        self.depth_pitch
790    }
791
792    /// Gets the color format of the frame buffer.
793    #[inline]
794    #[must_use]
795    pub const fn format(&self) -> DxgiDuplicationFormat {
796        self.format
797    }
798
799    /// Checks if the buffer has padding.
800    #[inline]
801    #[must_use]
802    pub const fn has_padding(&self) -> bool {
803        self.width * self.bytes_per_pixel() != self.row_pitch
804    }
805
806    /// Gets the pixel data without padding.
807    #[inline]
808    #[must_use]
809    pub fn as_nopadding_buffer<'b>(&'b self, buffer: &'b mut Vec<u8>) -> &'b [u8] {
810        let raw_buffer = self.backing.as_slice(self.height);
811
812        if !self.has_padding() {
813            return raw_buffer;
814        }
815
816        let width = self.width;
817        let height = self.height;
818        let row_pitch = self.row_pitch;
819        let multiplier = self.bytes_per_pixel();
820        let frame_size = (width * height * multiplier) as usize;
821        if buffer.len() < frame_size {
822            buffer.resize(frame_size, 0);
823        }
824
825        let width_size = (width * multiplier) as usize;
826        let buffer_address = buffer.as_mut_ptr() as usize;
827        let raw_buffer_address = raw_buffer.as_ptr() as usize;
828        (0..height).into_par_iter().for_each(|y| {
829            let index = (y * row_pitch) as usize;
830            let src = raw_buffer_address as *const u8;
831            let dst = buffer_address as *mut u8;
832
833            unsafe {
834                std::ptr::copy_nonoverlapping(src.add(index), dst.add(y as usize * width_size), width_size);
835            }
836        });
837
838        &buffer[0..frame_size]
839    }
840
841    /// Gets the raw pixel data, which may include padding.
842    #[inline]
843    #[must_use]
844    pub const fn as_raw_buffer(&mut self) -> &mut [u8] {
845        self.backing.as_mut_slice(self.height)
846    }
847
848    /// Saves the frame buffer as an image to the specified path.
849    #[inline]
850    pub fn save_as_image<T: AsRef<Path>>(&mut self, path: T, format: ImageFormat) -> Result<(), Error> {
851        let width = self.width;
852        let height = self.height;
853
854        let pixel_format = match self.format {
855            DxgiDuplicationFormat::Rgba8 => ImageEncoderPixelFormat::Rgba8,
856            DxgiDuplicationFormat::Bgra8 => ImageEncoderPixelFormat::Bgra8,
857            _ => return Err(ImageEncoderError::UnsupportedFormat.into()),
858        };
859
860        let mut buffer = Vec::new();
861        let bytes =
862            ImageEncoder::new(format, pixel_format)?.encode(self.as_nopadding_buffer(&mut buffer), width, height)?;
863
864        fs::write(path, bytes)?;
865
866        Ok(())
867    }
868
869    #[inline]
870    #[must_use]
871    const fn bytes_per_pixel(&self) -> u32 {
872        match self.format {
873            DxgiDuplicationFormat::Rgba16F => 8,
874            DxgiDuplicationFormat::Rgba8 | DxgiDuplicationFormat::Bgra8 => 4,
875        }
876    }
877}