Skip to main content

windows_capture/
frame.rs

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