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::DirectX::Direct3D11::IDirect3DSurface;
8use windows::Win32::Graphics::Direct3D11::{
9 D3D11_BOX, D3D11_CPU_ACCESS_READ, D3D11_CPU_ACCESS_WRITE, D3D11_MAP_READ_WRITE,
10 D3D11_MAPPED_SUBRESOURCE, D3D11_TEXTURE2D_DESC, D3D11_USAGE_STAGING, ID3D11Device,
11 ID3D11DeviceContext, ID3D11Texture2D,
12};
13use windows::Win32::Graphics::Dxgi::Common::{DXGI_FORMAT, DXGI_SAMPLE_DESC};
14
15use crate::encoder::{self, ImageEncoder};
16use crate::settings::ColorFormat;
17
18#[derive(thiserror::Error, Debug)]
19pub enum Error {
20 #[error("Invalid crop size")]
21 InvalidSize,
22 #[error("Invalid title bar height")]
23 InvalidTitleBarSize,
24 #[error("This color format is not supported for saving as an image")]
25 UnsupportedFormat,
26 #[error("Failed to encode the image buffer to image bytes with the specified format: {0}")]
27 ImageEncoderError(#[from] encoder::ImageEncoderError),
28 #[error("I/O error: {0}")]
29 IoError(#[from] io::Error),
30 #[error("Windows API error: {0}")]
31 WindowsError(#[from] windows::core::Error),
32}
33
34#[derive(Eq, PartialEq, Clone, Copy, Debug)]
35pub enum ImageFormat {
36 Jpeg,
37 Png,
38 Gif,
39 Tiff,
40 Bmp,
41 JpegXr,
42}
43
44pub struct Frame<'a> {
53 d3d_device: &'a ID3D11Device,
54 frame_surface: IDirect3DSurface,
55 frame_texture: ID3D11Texture2D,
56 timestamp: TimeSpan,
57 context: &'a ID3D11DeviceContext,
58 buffer: &'a mut Vec<u8>,
59 width: u32,
60 height: u32,
61 color_format: ColorFormat,
62 title_bar_height: Option<u32>,
63}
64
65impl<'a> Frame<'a> {
66 #[allow(clippy::too_many_arguments)]
85 #[must_use]
86 #[inline]
87 pub const fn new(
88 d3d_device: &'a ID3D11Device,
89 frame_surface: IDirect3DSurface,
90 frame_texture: ID3D11Texture2D,
91 timestamp: TimeSpan,
92 context: &'a ID3D11DeviceContext,
93 buffer: &'a mut Vec<u8>,
94 width: u32,
95 height: u32,
96 color_format: ColorFormat,
97 title_bar_height: Option<u32>,
98 ) -> Self {
99 Self {
100 d3d_device,
101 frame_surface,
102 frame_texture,
103 timestamp,
104 context,
105 buffer,
106 width,
107 height,
108 color_format,
109 title_bar_height,
110 }
111 }
112
113 #[must_use]
119 #[inline]
120 pub const fn width(&self) -> u32 {
121 self.width
122 }
123
124 #[must_use]
130 #[inline]
131 pub const fn height(&self) -> u32 {
132 self.height
133 }
134
135 #[must_use]
141 #[inline]
142 pub const fn timestamp(&self) -> TimeSpan {
143 self.timestamp
144 }
145
146 #[must_use]
152 #[inline]
153 pub const fn color_format(&self) -> ColorFormat {
154 self.color_format
155 }
156
157 #[allow(clippy::missing_safety_doc)]
167 #[must_use]
168 #[inline]
169 pub const unsafe fn as_raw_surface(&self) -> &IDirect3DSurface {
170 &self.frame_surface
171 }
172
173 #[allow(clippy::missing_safety_doc)]
183 #[must_use]
184 #[inline]
185 pub const unsafe fn as_raw_texture(&self) -> &ID3D11Texture2D {
186 &self.frame_texture
187 }
188
189 #[inline]
195 pub fn buffer(&mut self) -> Result<FrameBuffer, Error> {
196 let texture_desc = D3D11_TEXTURE2D_DESC {
198 Width: self.width,
199 Height: self.height,
200 MipLevels: 1,
201 ArraySize: 1,
202 Format: DXGI_FORMAT(self.color_format as i32),
203 SampleDesc: DXGI_SAMPLE_DESC { Count: 1, Quality: 0 },
204 Usage: D3D11_USAGE_STAGING,
205 BindFlags: 0,
206 CPUAccessFlags: D3D11_CPU_ACCESS_READ.0 as u32 | D3D11_CPU_ACCESS_WRITE.0 as u32,
207 MiscFlags: 0,
208 };
209
210 let mut texture = None;
212 unsafe {
213 self.d3d_device.CreateTexture2D(&texture_desc, None, Some(&mut texture))?;
214 };
215
216 let texture = texture.unwrap();
217
218 unsafe {
220 self.context.CopyResource(&texture, &self.frame_texture);
221 };
222
223 let mut mapped_resource = D3D11_MAPPED_SUBRESOURCE::default();
225 unsafe {
226 self.context.Map(&texture, 0, D3D11_MAP_READ_WRITE, 0, Some(&mut mapped_resource))?;
227 };
228
229 let mapped_frame_data = unsafe {
231 slice::from_raw_parts_mut(
232 mapped_resource.pData.cast(),
233 (self.height * mapped_resource.RowPitch) as usize,
234 )
235 };
236
237 let frame_buffer = FrameBuffer::new(
239 mapped_frame_data,
240 self.buffer,
241 self.width,
242 self.height,
243 mapped_resource.RowPitch,
244 mapped_resource.DepthPitch,
245 self.color_format,
246 );
247
248 Ok(frame_buffer)
249 }
250
251 #[inline]
264 pub fn buffer_crop(
265 &mut self,
266 start_width: u32,
267 start_height: u32,
268 end_width: u32,
269 end_height: u32,
270 ) -> Result<FrameBuffer, Error> {
271 if start_width >= end_width || start_height >= end_height {
272 return Err(Error::InvalidSize);
273 }
274
275 let texture_width = end_width - start_width;
276 let texture_height = end_height - start_height;
277
278 let texture_desc = D3D11_TEXTURE2D_DESC {
280 Width: texture_width,
281 Height: texture_height,
282 MipLevels: 1,
283 ArraySize: 1,
284 Format: DXGI_FORMAT(self.color_format as i32),
285 SampleDesc: DXGI_SAMPLE_DESC { Count: 1, Quality: 0 },
286 Usage: D3D11_USAGE_STAGING,
287 BindFlags: 0,
288 CPUAccessFlags: D3D11_CPU_ACCESS_READ.0 as u32 | D3D11_CPU_ACCESS_WRITE.0 as u32,
289 MiscFlags: 0,
290 };
291
292 let mut texture = None;
294 unsafe {
295 self.d3d_device.CreateTexture2D(&texture_desc, None, Some(&mut texture))?;
296 };
297 let texture = texture.unwrap();
298
299 let resource_box = D3D11_BOX {
301 left: start_width,
302 top: start_height,
303 front: 0,
304 right: end_width,
305 bottom: end_height,
306 back: 1,
307 };
308
309 unsafe {
311 self.context.CopySubresourceRegion(
312 &texture,
313 0,
314 0,
315 0,
316 0,
317 &self.frame_texture,
318 0,
319 Some(&resource_box),
320 );
321 };
322
323 let mut mapped_resource = D3D11_MAPPED_SUBRESOURCE::default();
325 unsafe {
326 self.context.Map(&texture, 0, D3D11_MAP_READ_WRITE, 0, Some(&mut mapped_resource))?;
327 };
328
329 let mapped_frame_data = unsafe {
331 slice::from_raw_parts_mut(
332 mapped_resource.pData.cast(),
333 (texture_height * mapped_resource.RowPitch) as usize,
334 )
335 };
336
337 let frame_buffer = FrameBuffer::new(
339 mapped_frame_data,
340 self.buffer,
341 texture_width,
342 texture_height,
343 mapped_resource.RowPitch,
344 mapped_resource.DepthPitch,
345 self.color_format,
346 );
347
348 Ok(frame_buffer)
349 }
350
351 #[inline]
357 pub fn buffer_without_title_bar(&mut self) -> Result<FrameBuffer, Error> {
358 if let Some(title_bar_height) = self.title_bar_height {
359 if title_bar_height >= self.height {
360 return Err(Error::InvalidTitleBarSize);
361 }
362
363 self.buffer_crop(0, title_bar_height, self.width, self.height)
364 } else {
365 self.buffer()
366 }
367 }
368
369 #[inline]
380 pub fn save_as_image<T: AsRef<Path>>(
381 &mut self,
382 path: T,
383 format: ImageFormat,
384 ) -> Result<(), Error> {
385 let mut frame_buffer = self.buffer()?;
386
387 frame_buffer.save_as_image(path, format)?;
388
389 Ok(())
390 }
391}
392
393pub struct FrameBuffer<'a> {
402 raw_buffer: &'a mut [u8],
403 buffer: &'a mut Vec<u8>,
404 width: u32,
405 height: u32,
406 row_pitch: u32,
407 depth_pitch: u32,
408 color_format: ColorFormat,
409}
410
411impl<'a> FrameBuffer<'a> {
412 #[must_use]
428 #[inline]
429 pub const fn new(
430 raw_buffer: &'a mut [u8],
431 buffer: &'a mut Vec<u8>,
432 width: u32,
433 height: u32,
434 row_pitch: u32,
435 depth_pitch: u32,
436 color_format: ColorFormat,
437 ) -> Self {
438 Self { raw_buffer, buffer, width, height, row_pitch, depth_pitch, color_format }
439 }
440
441 #[must_use]
443 #[inline]
444 pub const fn width(&self) -> u32 {
445 self.width
446 }
447
448 #[must_use]
450 #[inline]
451 pub const fn height(&self) -> u32 {
452 self.height
453 }
454
455 #[must_use]
457 #[inline]
458 pub const fn row_pitch(&self) -> u32 {
459 self.row_pitch
460 }
461
462 #[must_use]
464 #[inline]
465 pub const fn depth_pitch(&self) -> u32 {
466 self.depth_pitch
467 }
468
469 #[must_use]
471 #[inline]
472 pub const fn has_padding(&self) -> bool {
473 self.width * 4 != self.row_pitch
474 }
475
476 #[must_use]
478 #[inline]
479 pub const fn as_raw_buffer(&mut self) -> &mut [u8] {
480 self.raw_buffer
481 }
482
483 #[inline]
489 pub fn as_nopadding_buffer(&mut self) -> Result<&mut [u8], Error> {
490 if !self.has_padding() {
491 return Ok(self.raw_buffer);
492 }
493
494 let multiplier = match self.color_format {
495 ColorFormat::Rgba16F => 8,
496 ColorFormat::Rgba8 => 4,
497 ColorFormat::Bgra8 => 4,
498 };
499
500 let frame_size = (self.width * self.height * multiplier) as usize;
501 if self.buffer.capacity() < frame_size {
502 self.buffer.resize(frame_size, 0);
503 }
504
505 let width_size = (self.width * multiplier) as usize;
506 let buffer_address = self.buffer.as_mut_ptr() as isize;
507 (0..self.height).into_par_iter().for_each(|y| {
508 let index = (y * self.row_pitch) as usize;
509 let ptr = buffer_address as *mut u8;
510
511 unsafe {
512 ptr::copy_nonoverlapping(
513 self.raw_buffer.as_ptr().add(index),
514 ptr.add(y as usize * width_size),
515 width_size,
516 );
517 }
518 });
519
520 Ok(&mut self.buffer[0..frame_size])
521 }
522
523 #[inline]
534 pub fn save_as_image<T: AsRef<Path>>(
535 &mut self,
536 path: T,
537 format: ImageFormat,
538 ) -> Result<(), Error> {
539 let width = self.width;
540 let height = self.height;
541
542 let bytes = ImageEncoder::new(format, self.color_format).encode(
543 self.as_nopadding_buffer()?,
544 width,
545 height,
546 )?;
547
548 fs::write(path, bytes)?;
549
550 Ok(())
551 }
552}