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)]
19pub enum Error {
21 #[error("Invalid crop size")]
23 InvalidSize,
24 #[error("Invalid title bar height")]
26 InvalidTitleBarSize,
27 #[error("This color format is not supported for saving as an image")]
29 UnsupportedFormat,
30 #[error("Failed to encode the image buffer to image bytes with the specified format: {0}")]
34 ImageEncoderError(#[from] encoder::ImageEncoderError),
35 #[error("I/O error: {0}")]
39 IoError(#[from] io::Error),
40 #[error("Windows API error: {0}")]
44 WindowsError(#[from] windows::core::Error),
45}
46
47#[derive(Clone, Copy, Debug, PartialEq, Eq)]
49pub struct DirtyRegion {
50 pub x: i32,
52 pub y: i32,
54 pub width: i32,
56 pub height: i32,
58}
59
60pub 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 #[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 #[inline]
99 #[must_use]
100 pub const fn width(&self) -> u32 {
101 self.desc.Width
102 }
103 #[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 #[inline]
116 #[must_use]
117 pub const fn height(&self) -> u32 {
118 self.desc.Height
119 }
120
121 #[inline]
123 pub fn timestamp(&self) -> Result<TimeSpan, windows::core::Error> {
124 self.capture_frame.SystemRelativeTime()
125 }
126
127 #[inline]
129 #[must_use]
130 pub const fn color_format(&self) -> ColorFormat {
131 self.color_format
132 }
133
134 #[inline]
136 #[must_use]
137 pub const fn as_raw_surface(&self) -> &IDirect3DSurface {
138 &self.frame_surface
139 }
140
141 #[inline]
143 #[must_use]
144 pub const fn as_raw_texture(&self) -> &ID3D11Texture2D {
145 &self.frame_texture
146 }
147
148 #[inline]
150 #[must_use]
151 pub const fn device(&self) -> &ID3D11Device {
152 self.d3d_device
153 }
154
155 #[inline]
157 #[must_use]
158 pub const fn device_context(&self) -> &ID3D11DeviceContext {
159 self.context
160 }
161
162 #[inline]
164 #[must_use]
165 pub const fn desc(&self) -> &D3D11_TEXTURE2D_DESC {
166 &self.desc
167 }
168
169 #[inline]
171 pub fn buffer(&'_ mut self) -> Result<FrameBuffer<'_>, Error> {
172 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 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 unsafe {
196 self.context.CopyResource(&texture, &self.frame_texture);
197 };
198
199 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 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 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 #[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 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 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 let resource_box = D3D11_BOX { left: start_x, top: start_y, front: 0, right: end_x, bottom: end_y, back: 1 };
262
263 unsafe {
265 self.context.CopySubresourceRegion(&texture, 0, 0, 0, 0, &self.frame_texture, 0, Some(&resource_box));
266 };
267
268 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 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 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 #[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 #[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
320pub 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 #[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 #[inline]
354 #[must_use]
355 pub const fn width(&self) -> u32 {
356 self.width
357 }
358
359 #[inline]
361 #[must_use]
362 pub const fn height(&self) -> u32 {
363 self.height
364 }
365
366 #[inline]
368 #[must_use]
369 pub const fn row_pitch(&self) -> u32 {
370 self.row_pitch
371 }
372
373 #[inline]
375 #[must_use]
376 pub const fn depth_pitch(&self) -> u32 {
377 self.depth_pitch
378 }
379
380 #[inline]
382 #[must_use]
383 pub const fn color_format(&self) -> ColorFormat {
384 self.color_format
385 }
386
387 #[inline]
389 #[must_use]
390 pub const fn has_padding(&self) -> bool {
391 self.width * 4 != self.row_pitch
392 }
393
394 #[inline]
396 #[must_use]
397 pub const fn as_raw_buffer(&mut self) -> &mut [u8] {
398 self.raw_buffer
399 }
400
401 #[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 #[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}