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)]
18pub enum Error {
20 #[error("Invalid crop size")]
22 InvalidSize,
23 #[error("Invalid title bar height")]
25 InvalidTitleBarSize,
26 #[error("This color format is not supported for saving as an image")]
28 UnsupportedFormat,
29 #[error("DirectX error: {0}")]
31 DirectXError(#[from] crate::d3d11::Error),
32 #[error("Failed to encode the image buffer to image bytes with the specified format: {0}")]
36 ImageEncoderError(#[from] encoder::ImageEncoderError),
37 #[error("I/O error: {0}")]
41 IoError(#[from] io::Error),
42 #[error("Windows API error: {0}")]
46 WindowsError(#[from] windows::core::Error),
47}
48
49#[derive(Clone, Copy, Debug, PartialEq, Eq)]
51pub struct DirtyRegion {
52 pub x: i32,
54 pub y: i32,
56 pub width: i32,
58 pub height: i32,
60}
61
62pub 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 #[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 #[inline]
101 #[must_use]
102 pub const fn width(&self) -> u32 {
103 self.desc.Width
104 }
105 #[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 #[inline]
118 #[must_use]
119 pub const fn height(&self) -> u32 {
120 self.desc.Height
121 }
122
123 #[inline]
125 pub fn timestamp(&self) -> Result<TimeSpan, windows::core::Error> {
126 self.capture_frame.SystemRelativeTime()
127 }
128
129 #[inline]
131 #[must_use]
132 pub const fn color_format(&self) -> ColorFormat {
133 self.color_format
134 }
135
136 #[inline]
138 #[must_use]
139 pub const fn as_raw_surface(&self) -> &IDirect3DSurface {
140 &self.frame_surface
141 }
142
143 #[inline]
145 #[must_use]
146 pub const fn as_raw_texture(&self) -> &ID3D11Texture2D {
147 &self.frame_texture
148 }
149
150 #[inline]
152 #[must_use]
153 pub const fn device(&self) -> &ID3D11Device {
154 self.d3d_device
155 }
156
157 #[inline]
159 #[must_use]
160 pub const fn device_context(&self) -> &ID3D11DeviceContext {
161 self.context
162 }
163
164 #[inline]
166 #[must_use]
167 pub const fn desc(&self) -> &D3D11_TEXTURE2D_DESC {
168 &self.desc
169 }
170
171 #[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 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 #[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 let resource_box = D3D11_BOX { left: start_x, top: start_y, front: 0, right: end_x, bottom: end_y, back: 1 };
206
207 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 #[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 #[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
272pub 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 #[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 #[inline]
325 #[must_use]
326 pub const fn width(&self) -> u32 {
327 self.width
328 }
329
330 #[inline]
332 #[must_use]
333 pub const fn height(&self) -> u32 {
334 self.height
335 }
336
337 #[inline]
339 #[must_use]
340 pub const fn row_pitch(&self) -> u32 {
341 self.row_pitch
342 }
343
344 #[inline]
346 #[must_use]
347 pub const fn depth_pitch(&self) -> u32 {
348 self.depth_pitch
349 }
350
351 #[inline]
353 #[must_use]
354 pub const fn color_format(&self) -> ColorFormat {
355 self.color_format
356 }
357
358 #[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 #[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 #[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 #[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}