Skip to main content

windows_capture/
frame.rs

1use std::fs::{self};
2use std::path::Path;
3use std::{io, ptr};
4
5use rayon::iter::{IntoParallelIterator, ParallelIterator};
6use windows::Foundation::TimeSpan;
7use windows::Graphics::Capture::Direct3D11CaptureFrame;
8use windows::Graphics::DirectX::Direct3D11::IDirect3DSurface;
9use windows::Win32::Graphics::Direct3D11::{
10    D3D11_BOX, D3D11_TEXTURE2D_DESC, ID3D11Device, ID3D11DeviceContext, ID3D11Texture2D,
11};
12
13use crate::d3d11::{MappedStagingTexture, StagingTexture};
14use crate::encoder::{self, ImageEncoder, ImageEncoderError, ImageEncoderPixelFormat, ImageFormat};
15use crate::settings::ColorFormat;
16
17#[derive(thiserror::Error, Debug)]
18/// Errors that can occur while working with captured frames and buffers.
19pub enum Error {
20    /// The crop rectangle is invalid (start >= end on either axis).
21    #[error("Invalid crop size")]
22    InvalidSize,
23    /// The configured title bar height is invalid (greater than or equal to the frame height).
24    #[error("Invalid title bar height")]
25    InvalidTitleBarSize,
26    /// The current [`ColorFormat`] cannot be saved as an image.
27    #[error("This color format is not supported for saving as an image")]
28    UnsupportedFormat,
29    /// Direct3D staging/mapping failed.
30    #[error("DirectX error: {0}")]
31    DirectXError(#[from] crate::d3d11::Error),
32    /// Image encoding failed.
33    ///
34    /// Wraps [`crate::encoder::ImageEncoderError`].
35    #[error("Failed to encode the image buffer to image bytes with the specified format: {0}")]
36    ImageEncoderError(#[from] encoder::ImageEncoderError),
37    /// An I/O error occurred while writing the image to disk.
38    ///
39    /// Wraps [`std::io::Error`].
40    #[error("I/O error: {0}")]
41    IoError(#[from] io::Error),
42    /// A Windows API call failed.
43    ///
44    /// Wraps [`windows::core::Error`].
45    #[error("Windows API error: {0}")]
46    WindowsError(#[from] windows::core::Error),
47}
48
49/// Represents a rectangular dirty region within a frame.
50#[derive(Clone, Copy, Debug, PartialEq, Eq)]
51pub struct DirtyRegion {
52    /// The left coordinate (in pixels) of the region.
53    pub x: i32,
54    /// The top coordinate (in pixels) of the region.
55    pub y: i32,
56    /// The width (in pixels) of the region.
57    pub width: i32,
58    /// The height (in pixels) of the region.
59    pub height: i32,
60}
61
62/// Represents a frame captured from a graphics capture item.
63///
64/// # Example
65/// ```ignore
66/// // Get a frame from the capture session
67/// let mut buffer = frame.buffer()?;
68/// buffer.save_as_image("screenshot.png", ImageFormat::Png)?;
69/// ```
70pub struct Frame<'a> {
71    capture_frame: Direct3D11CaptureFrame,
72    d3d_device: &'a ID3D11Device,
73    frame_surface: IDirect3DSurface,
74    frame_texture: ID3D11Texture2D,
75    context: &'a ID3D11DeviceContext,
76    desc: D3D11_TEXTURE2D_DESC,
77    color_format: ColorFormat,
78    title_bar_height: Option<u32>,
79}
80
81impl<'a> Frame<'a> {
82    /// Constructs a new `Frame`.
83    #[allow(clippy::too_many_arguments)]
84    #[inline]
85    #[must_use]
86    pub const fn new(
87        capture_frame: Direct3D11CaptureFrame,
88        d3d_device: &'a ID3D11Device,
89        frame_surface: IDirect3DSurface,
90        frame_texture: ID3D11Texture2D,
91        context: &'a ID3D11DeviceContext,
92        desc: D3D11_TEXTURE2D_DESC,
93        color_format: ColorFormat,
94        title_bar_height: Option<u32>,
95    ) -> Self {
96        Self { capture_frame, d3d_device, frame_surface, frame_texture, context, desc, color_format, title_bar_height }
97    }
98
99    /// Gets the width of the frame.
100    #[inline]
101    #[must_use]
102    pub const fn width(&self) -> u32 {
103        self.desc.Width
104    }
105    /// Gets the dirty regions of the frame.
106    #[inline]
107    pub fn dirty_regions(&self) -> Result<Vec<DirtyRegion>, windows::core::Error> {
108        Ok(self
109            .capture_frame
110            .DirtyRegions()?
111            .into_iter()
112            .map(|r| DirtyRegion { x: r.X, y: r.Y, width: r.Width, height: r.Height })
113            .collect())
114    }
115
116    /// Gets the height of the frame.
117    #[inline]
118    #[must_use]
119    pub const fn height(&self) -> u32 {
120        self.desc.Height
121    }
122
123    /// Gets the timestamp of the frame.
124    #[inline]
125    pub fn timestamp(&self) -> Result<TimeSpan, windows::core::Error> {
126        self.capture_frame.SystemRelativeTime()
127    }
128
129    /// Gets the color format of the frame.
130    #[inline]
131    #[must_use]
132    pub const fn color_format(&self) -> ColorFormat {
133        self.color_format
134    }
135
136    /// Gets the raw surface of the frame.
137    #[inline]
138    #[must_use]
139    pub const fn as_raw_surface(&self) -> &IDirect3DSurface {
140        &self.frame_surface
141    }
142
143    /// Gets the raw texture of the frame.
144    #[inline]
145    #[must_use]
146    pub const fn as_raw_texture(&self) -> &ID3D11Texture2D {
147        &self.frame_texture
148    }
149
150    /// Gets the underlying Direct3D device associated with this frame.
151    #[inline]
152    #[must_use]
153    pub const fn device(&self) -> &ID3D11Device {
154        self.d3d_device
155    }
156
157    /// Gets the device context used for GPU operations on this frame.
158    #[inline]
159    #[must_use]
160    pub const fn device_context(&self) -> &ID3D11DeviceContext {
161        self.context
162    }
163
164    /// Gets the texture description of the frame.
165    #[inline]
166    #[must_use]
167    pub const fn desc(&self) -> &D3D11_TEXTURE2D_DESC {
168        &self.desc
169    }
170
171    /// Gets the frame buffer.
172    #[inline]
173    pub fn buffer(&'_ mut self) -> Result<FrameBuffer<'_>, Error> {
174        let staging = StagingTexture::new(self.d3d_device, self.width(), self.height(), self.desc.Format)?;
175
176        // Copy the GPU texture into a CPU-readable staging texture before mapping it.
177        unsafe {
178            self.context.CopyResource(staging.texture(), &self.frame_texture);
179        }
180
181        let mapped_texture = MappedStagingTexture::map_owned(self.context, staging)?;
182
183        Ok(FrameBuffer::from_mapped(mapped_texture, self.width(), self.height(), self.color_format))
184    }
185
186    /// Gets a cropped frame buffer.
187    #[inline]
188    pub fn buffer_crop(
189        &'_ mut self,
190        start_x: u32,
191        start_y: u32,
192        end_x: u32,
193        end_y: u32,
194    ) -> Result<FrameBuffer<'_>, Error> {
195        if start_x >= end_x || start_y >= end_y {
196            return Err(Error::InvalidSize);
197        }
198
199        let texture_width = end_x - start_x;
200        let texture_height = end_y - start_y;
201
202        let staging = StagingTexture::new(self.d3d_device, texture_width, texture_height, self.desc.Format)?;
203
204        // Box settings
205        let resource_box = D3D11_BOX { left: start_x, top: start_y, front: 0, right: end_x, bottom: end_y, back: 1 };
206
207        // Copy the requested sub-rectangle into a CPU-readable staging texture.
208        unsafe {
209            self.context.CopySubresourceRegion(
210                staging.texture(),
211                0,
212                0,
213                0,
214                0,
215                &self.frame_texture,
216                0,
217                Some(&resource_box),
218            );
219        }
220
221        let mapped_texture = MappedStagingTexture::map_owned(self.context, staging)?;
222
223        Ok(FrameBuffer::from_mapped(mapped_texture, texture_width, texture_height, self.color_format))
224    }
225
226    /// Gets the frame buffer without the title bar.
227    #[inline]
228    pub fn buffer_without_title_bar(&'_ mut self) -> Result<FrameBuffer<'_>, Error> {
229        if let Some(title_bar_height) = self.title_bar_height {
230            if title_bar_height >= self.height() {
231                return Err(Error::InvalidTitleBarSize);
232            }
233
234            self.buffer_crop(0, title_bar_height, self.width(), self.height())
235        } else {
236            self.buffer()
237        }
238    }
239
240    /// Saves the frame buffer as an image to the specified path.
241    #[inline]
242    pub fn save_as_image<T: AsRef<Path>>(&mut self, path: T, format: ImageFormat) -> Result<(), Error> {
243        let mut frame_buffer = self.buffer()?;
244
245        frame_buffer.save_as_image(path, format)?;
246
247        Ok(())
248    }
249}
250
251enum FrameBufferBacking<'a> {
252    Borrowed(&'a mut [u8]),
253    Mapped(MappedStagingTexture<'a>),
254}
255
256impl FrameBufferBacking<'_> {
257    const fn as_slice(&self, height: u32) -> &[u8] {
258        match self {
259            Self::Borrowed(buffer) => buffer,
260            Self::Mapped(texture) => texture.as_slice(height),
261        }
262    }
263
264    const fn as_mut_slice(&mut self, height: u32) -> &mut [u8] {
265        match self {
266            Self::Borrowed(buffer) => buffer,
267            Self::Mapped(texture) => texture.as_mut_slice(height),
268        }
269    }
270}
271
272/// Represents a frame buffer containing pixel data.
273///
274/// # Example
275/// ```ignore
276/// // Get a frame from the capture session
277/// let mut buffer = frame.buffer()?;
278/// buffer.save_as_image("screenshot.png", ImageFormat::Png)?;
279/// ```
280pub struct FrameBuffer<'a> {
281    backing: FrameBufferBacking<'a>,
282    width: u32,
283    height: u32,
284    row_pitch: u32,
285    depth_pitch: u32,
286    color_format: ColorFormat,
287}
288
289impl<'a> FrameBuffer<'a> {
290    /// Constructs a new `FrameBuffer`.
291    #[inline]
292    #[must_use]
293    pub const fn new(
294        raw_buffer: &'a mut [u8],
295        width: u32,
296        height: u32,
297        row_pitch: u32,
298        depth_pitch: u32,
299        color_format: ColorFormat,
300    ) -> Self {
301        Self { backing: FrameBufferBacking::Borrowed(raw_buffer), width, height, row_pitch, depth_pitch, color_format }
302    }
303
304    const fn from_mapped(
305        mapped_texture: MappedStagingTexture<'a>,
306        width: u32,
307        height: u32,
308        color_format: ColorFormat,
309    ) -> Self {
310        let row_pitch = mapped_texture.row_pitch();
311        let depth_pitch = mapped_texture.depth_pitch();
312
313        Self {
314            backing: FrameBufferBacking::Mapped(mapped_texture),
315            width,
316            height,
317            row_pitch,
318            depth_pitch,
319            color_format,
320        }
321    }
322
323    /// Gets the width of the frame buffer.
324    #[inline]
325    #[must_use]
326    pub const fn width(&self) -> u32 {
327        self.width
328    }
329
330    /// Gets the height of the frame buffer.
331    #[inline]
332    #[must_use]
333    pub const fn height(&self) -> u32 {
334        self.height
335    }
336
337    /// Gets the row pitch of the frame buffer.
338    #[inline]
339    #[must_use]
340    pub const fn row_pitch(&self) -> u32 {
341        self.row_pitch
342    }
343
344    /// Gets the depth pitch of the frame buffer.
345    #[inline]
346    #[must_use]
347    pub const fn depth_pitch(&self) -> u32 {
348        self.depth_pitch
349    }
350
351    /// Gets the color format of the frame buffer.
352    #[inline]
353    #[must_use]
354    pub const fn color_format(&self) -> ColorFormat {
355        self.color_format
356    }
357
358    /// Checks if the buffer has padding.
359    #[inline]
360    #[must_use]
361    pub const fn has_padding(&self) -> bool {
362        self.width * self.bytes_per_pixel() != self.row_pitch
363    }
364
365    /// Gets the raw pixel data, which may include padding.
366    #[inline]
367    #[must_use]
368    pub const fn as_raw_buffer(&mut self) -> &mut [u8] {
369        self.backing.as_mut_slice(self.height)
370    }
371
372    /// Gets the pixel data without padding.
373    #[inline]
374    #[must_use]
375    pub fn as_nopadding_buffer<'b>(&'b self, buffer: &'b mut Vec<u8>) -> &'b [u8] {
376        let raw_buffer = self.backing.as_slice(self.height);
377
378        if !self.has_padding() {
379            return raw_buffer;
380        }
381
382        let width = self.width;
383        let height = self.height;
384        let row_pitch = self.row_pitch;
385        let multiplier = self.bytes_per_pixel();
386        let frame_size = (width * height * multiplier) as usize;
387        if buffer.len() < frame_size {
388            buffer.resize(frame_size, 0);
389        }
390
391        let width_size = (width * multiplier) as usize;
392        let buffer_address = buffer.as_mut_ptr() as usize;
393        let raw_buffer_address = raw_buffer.as_ptr() as usize;
394        (0..height).into_par_iter().for_each(|y| {
395            let index = (y * row_pitch) as usize;
396            let src = raw_buffer_address as *const u8;
397            let dst = buffer_address as *mut u8;
398
399            unsafe {
400                ptr::copy_nonoverlapping(src.add(index), dst.add(y as usize * width_size), width_size);
401            }
402        });
403
404        &buffer[0..frame_size]
405    }
406
407    /// Saves the frame buffer as an image to the specified path.
408    #[inline]
409    pub fn save_as_image<T: AsRef<Path>>(&mut self, path: T, format: ImageFormat) -> Result<(), Error> {
410        let width = self.width;
411        let height = self.height;
412
413        let pixel_format = match self.color_format {
414            ColorFormat::Rgba8 => ImageEncoderPixelFormat::Rgba8,
415            ColorFormat::Bgra8 => ImageEncoderPixelFormat::Bgra8,
416            _ => return Err(ImageEncoderError::UnsupportedFormat.into()),
417        };
418
419        let mut buffer = Vec::new();
420        let bytes =
421            ImageEncoder::new(format, pixel_format)?.encode(self.as_nopadding_buffer(&mut buffer), width, height)?;
422
423        fs::write(path, bytes)?;
424
425        Ok(())
426    }
427
428    #[inline]
429    #[must_use]
430    const fn bytes_per_pixel(&self) -> u32 {
431        match self.color_format {
432            ColorFormat::Rgba16F => 8,
433            ColorFormat::Rgba8 | ColorFormat::Bgra8 => 4,
434        }
435    }
436}