1use std::sync::Arc;
2use std::sync::atomic::{self, AtomicBool, AtomicI32};
3
4use parking_lot::Mutex;
5use windows::Foundation::Metadata::ApiInformation;
6use windows::Foundation::TypedEventHandler;
7use windows::Graphics::Capture::{
8 Direct3D11CaptureFramePool, GraphicsCaptureDirtyRegionMode, GraphicsCaptureItem, GraphicsCaptureSession,
9};
10use windows::Graphics::DirectX::Direct3D11::IDirect3DDevice;
11use windows::Graphics::DirectX::DirectXPixelFormat;
12use windows::Win32::Foundation::{LPARAM, WPARAM};
13use windows::Win32::Graphics::Direct3D11::{D3D11_TEXTURE2D_DESC, ID3D11Device, ID3D11DeviceContext, ID3D11Texture2D};
14use windows::Win32::System::WinRT::Direct3D11::IDirect3DDxgiInterfaceAccess;
15use windows::Win32::UI::WindowsAndMessaging::{PostThreadMessageW, WM_QUIT};
16use windows::core::{HSTRING, IInspectable, Interface};
17
18use crate::capture::GraphicsCaptureApiHandler;
19use crate::d3d11::{self, SendDirectX, create_direct3d_device};
20use crate::frame::Frame;
21use crate::settings::{
22 ColorFormat, CursorCaptureSettings, DirtyRegionSettings, DrawBorderSettings, GraphicsCaptureItemType,
23 MinimumUpdateIntervalSettings, SecondaryWindowSettings,
24};
25
26#[derive(thiserror::Error, Eq, PartialEq, Clone, Debug)]
27pub enum Error {
29 #[error("The Graphics Capture API is not supported on this platform.")]
31 Unsupported,
32 #[error("Toggling cursor capture is not supported by the Graphics Capture API on this platform.")]
34 CursorConfigUnsupported,
35 #[error("Toggling the capture border is not supported by the Graphics Capture API on this platform.")]
37 BorderConfigUnsupported,
38 #[error("Capturing secondary windows is not supported by the Graphics Capture API on this platform.")]
40 SecondaryWindowsUnsupported,
41 #[error("Setting a minimum update interval is not supported by the Graphics Capture API on this platform.")]
43 MinimumUpdateIntervalUnsupported,
44 #[error("Dirty region tracking is not supported by the Graphics Capture API on this platform.")]
46 DirtyRegionUnsupported,
47 #[error("The capture has already been started.")]
49 AlreadyStarted,
50 #[error("DirectX error: {0}")]
54 DirectXError(#[from] d3d11::Error),
55 #[error("Window error: {0}")]
59 WindowError(#[from] crate::window::Error),
60 #[error("Windows API error: {0}")]
64 WindowsError(#[from] windows::core::Error),
65}
66
67pub struct InternalCaptureControl {
69 stop: Arc<AtomicBool>,
70}
71
72impl InternalCaptureControl {
73 #[inline]
75 #[must_use]
76 pub const fn new(stop: Arc<AtomicBool>) -> Self {
77 Self { stop }
78 }
79
80 #[inline]
82 pub fn stop(self) {
83 self.stop.store(true, atomic::Ordering::Relaxed);
84 }
85}
86
87pub struct GraphicsCaptureApi {
89 item_with_details: GraphicsCaptureItemType,
92 _d3d_device: ID3D11Device,
94 _direct3d_device: IDirect3DDevice,
96 _d3d_device_context: ID3D11DeviceContext,
98 frame_pool: Option<Arc<Direct3D11CaptureFramePool>>,
100 session: Option<GraphicsCaptureSession>,
102 halt: Arc<AtomicBool>,
104 active: bool,
106 capture_closed_event_token: i64,
108 frame_arrived_event_token: i64,
110}
111
112impl GraphicsCaptureApi {
113 #[allow(clippy::too_many_arguments)]
118 #[inline]
119 pub fn new<T: GraphicsCaptureApiHandler<Error = E> + Send + 'static, E: Send + Sync + 'static>(
120 d3d_device: ID3D11Device,
121 d3d_device_context: ID3D11DeviceContext,
122 item_with_details: GraphicsCaptureItemType,
123 callback: Arc<Mutex<T>>,
124 cursor_capture_settings: CursorCaptureSettings,
125 draw_border_settings: DrawBorderSettings,
126 secondary_window_settings: SecondaryWindowSettings,
127 minimum_update_interval_settings: MinimumUpdateIntervalSettings,
128 dirty_region_settings: DirtyRegionSettings,
129 color_format: ColorFormat,
130 thread_id: u32,
131 result: Arc<Mutex<Option<E>>>,
132 ) -> Result<Self, Error> {
133 if !Self::is_supported()? {
135 return Err(Error::Unsupported);
136 }
137
138 if cursor_capture_settings != CursorCaptureSettings::Default && !Self::is_cursor_settings_supported()? {
139 return Err(Error::CursorConfigUnsupported);
140 }
141
142 if draw_border_settings != DrawBorderSettings::Default && !Self::is_border_settings_supported()? {
143 return Err(Error::BorderConfigUnsupported);
144 }
145
146 if secondary_window_settings != SecondaryWindowSettings::Default && !Self::is_secondary_windows_supported()? {
147 return Err(Error::SecondaryWindowsUnsupported);
148 }
149
150 if minimum_update_interval_settings != MinimumUpdateIntervalSettings::Default
151 && !Self::is_minimum_update_interval_supported()?
152 {
153 return Err(Error::MinimumUpdateIntervalUnsupported);
154 }
155
156 if dirty_region_settings != DirtyRegionSettings::Default && !Self::is_dirty_region_supported()? {
157 return Err(Error::DirtyRegionUnsupported);
158 }
159
160 let title_bar_height = match item_with_details {
162 GraphicsCaptureItemType::Window((_, window)) => Some(window.title_bar_height()?),
163 GraphicsCaptureItemType::Monitor(_) => None,
164 GraphicsCaptureItemType::Unknown(_) => None,
165 };
166
167 let item = match &item_with_details {
168 GraphicsCaptureItemType::Window((item, _)) => item,
169 GraphicsCaptureItemType::Monitor((item, _)) => item,
170 GraphicsCaptureItemType::Unknown((item, _)) => item,
171 };
172
173 let direct3d_device = create_direct3d_device(&d3d_device)?;
175
176 let pixel_format = DirectXPixelFormat(color_format as i32);
177
178 let frame_pool = Direct3D11CaptureFramePool::Create(&direct3d_device, pixel_format, 1, item.Size()?)?;
180 let frame_pool = Arc::new(frame_pool);
181
182 let session = frame_pool.CreateCaptureSession(item)?;
184
185 let halt = Arc::new(AtomicBool::new(false));
187
188 let capture_closed_event_token =
190 item.Closed(&TypedEventHandler::<GraphicsCaptureItem, IInspectable>::new({
191 let callback_closed = callback.clone();
193 let halt_closed = halt.clone();
194 let result_closed = result.clone();
195
196 move |_, _| {
197 halt_closed.store(true, atomic::Ordering::Relaxed);
198
199 let callback_closed = callback_closed.lock().on_closed();
201 if let Err(e) = callback_closed {
202 *result_closed.lock() = Some(e);
203 }
204
205 unsafe {
207 PostThreadMessageW(thread_id, WM_QUIT, WPARAM::default(), LPARAM::default())?;
208 };
209
210 Result::Ok(())
211 }
212 }))?;
213
214 let frame_arrived_event_token = frame_pool.FrameArrived(&TypedEventHandler::<
216 Direct3D11CaptureFramePool,
217 IInspectable,
218 >::new({
219 let frame_pool_recreate = frame_pool.clone();
221 let halt_frame_pool = halt.clone();
222 let d3d_device_frame_pool = d3d_device.clone();
223 let context = d3d_device_context.clone();
224 let result_frame_pool = result;
225
226 let last_size = item.Size()?;
227 let last_size = Arc::new((AtomicI32::new(last_size.Width), AtomicI32::new(last_size.Height)));
228 let callback_frame_pool = callback;
229 let direct3d_device_recreate = SendDirectX::new(direct3d_device.clone());
230
231 move |frame, _| {
232 if halt_frame_pool.load(atomic::Ordering::Relaxed) {
234 return Ok(());
235 }
236
237 let Some(frame_pool) = frame.as_ref() else {
239 return Ok(());
240 };
241 let frame = frame_pool.TryGetNextFrame()?;
242
243 let frame_content_size = frame.ContentSize()?;
245
246 if frame_content_size.Width != last_size.0.load(atomic::Ordering::Relaxed)
250 || frame_content_size.Height != last_size.1.load(atomic::Ordering::Relaxed)
251 {
252 drop(frame);
253
254 let direct3d_device_recreate = &direct3d_device_recreate;
255 frame_pool_recreate.Recreate(&direct3d_device_recreate.0, pixel_format, 1, frame_content_size)?;
256
257 last_size.0.store(frame_content_size.Width, atomic::Ordering::Relaxed);
258 last_size.1.store(frame_content_size.Height, atomic::Ordering::Relaxed);
259
260 return Ok(());
261 }
262
263 let frame_surface = frame.Surface()?;
265
266 let frame_dxgi_interface = frame_surface.cast::<IDirect3DDxgiInterfaceAccess>()?;
268 let frame_texture = unsafe { frame_dxgi_interface.GetInterface::<ID3D11Texture2D>()? };
269
270 let mut desc = D3D11_TEXTURE2D_DESC::default();
272 unsafe { frame_texture.GetDesc(&mut desc) }
273
274 let mut frame = Frame::new(
276 frame,
277 &d3d_device_frame_pool,
278 frame_surface,
279 frame_texture,
280 &context,
281 desc,
282 color_format,
283 title_bar_height,
284 );
285
286 let stop = Arc::new(AtomicBool::new(false));
288 let internal_capture_control = InternalCaptureControl::new(stop.clone());
289
290 let result = callback_frame_pool.lock().on_frame_arrived(&mut frame, internal_capture_control);
292
293 if stop.load(atomic::Ordering::Relaxed) || result.is_err() {
295 if let Err(e) = result {
296 *result_frame_pool.lock() = Some(e);
297 }
298
299 halt_frame_pool.store(true, atomic::Ordering::Relaxed);
300
301 unsafe {
303 PostThreadMessageW(thread_id, WM_QUIT, WPARAM::default(), LPARAM::default())?;
304 };
305 }
306
307 Result::Ok(())
308 }
309 }))?;
310
311 if cursor_capture_settings != CursorCaptureSettings::Default {
312 if Self::is_cursor_settings_supported()? {
313 match cursor_capture_settings {
314 CursorCaptureSettings::Default => (),
315 CursorCaptureSettings::WithCursor => session.SetIsCursorCaptureEnabled(true)?,
316 CursorCaptureSettings::WithoutCursor => session.SetIsCursorCaptureEnabled(false)?,
317 };
318 } else {
319 return Err(Error::CursorConfigUnsupported);
320 }
321 }
322
323 if draw_border_settings != DrawBorderSettings::Default {
324 if Self::is_border_settings_supported()? {
325 match draw_border_settings {
326 DrawBorderSettings::Default => (),
327 DrawBorderSettings::WithBorder => {
328 session.SetIsBorderRequired(true)?;
329 }
330 DrawBorderSettings::WithoutBorder => session.SetIsBorderRequired(false)?,
331 }
332 } else {
333 return Err(Error::BorderConfigUnsupported);
334 }
335 }
336
337 if secondary_window_settings != SecondaryWindowSettings::Default {
338 if Self::is_secondary_windows_supported()? {
339 match secondary_window_settings {
340 SecondaryWindowSettings::Default => (),
341 SecondaryWindowSettings::Include => session.SetIncludeSecondaryWindows(true)?,
342 SecondaryWindowSettings::Exclude => session.SetIncludeSecondaryWindows(false)?,
343 }
344 } else {
345 return Err(Error::SecondaryWindowsUnsupported);
346 }
347 }
348
349 if minimum_update_interval_settings != MinimumUpdateIntervalSettings::Default {
350 if Self::is_minimum_update_interval_supported()? {
351 match minimum_update_interval_settings {
352 MinimumUpdateIntervalSettings::Default => (),
353 MinimumUpdateIntervalSettings::Custom(duration) => {
354 session.SetMinUpdateInterval(duration.into())?;
355 }
356 }
357 } else {
358 return Err(Error::MinimumUpdateIntervalUnsupported);
359 }
360 }
361
362 if dirty_region_settings != DirtyRegionSettings::Default {
363 if Self::is_dirty_region_supported()? {
364 match dirty_region_settings {
365 DirtyRegionSettings::Default => (),
366 DirtyRegionSettings::ReportOnly => {
367 session.SetDirtyRegionMode(GraphicsCaptureDirtyRegionMode::ReportOnly)?
368 }
369 DirtyRegionSettings::ReportAndRender => {
370 session.SetDirtyRegionMode(GraphicsCaptureDirtyRegionMode::ReportAndRender)?
371 }
372 }
373 } else {
374 return Err(Error::DirtyRegionUnsupported);
375 }
376 }
377
378 Ok(Self {
379 item_with_details,
380 _d3d_device: d3d_device,
381 _direct3d_device: direct3d_device,
382 _d3d_device_context: d3d_device_context,
383 frame_pool: Some(frame_pool),
384 session: Some(session),
385 halt,
386 active: false,
387 frame_arrived_event_token,
388 capture_closed_event_token,
389 })
390 }
391
392 #[inline]
399 pub fn start_capture(&mut self) -> Result<(), Error> {
400 if self.active {
401 return Err(Error::AlreadyStarted);
402 }
403
404 if let Some(session) = &self.session {
405 session.StartCapture()?;
406 }
407 self.active = true;
408
409 Ok(())
410 }
411
412 #[inline]
414 pub fn stop_capture(mut self) {
415 self.cleanup();
416 }
417
418 #[inline]
424 #[must_use]
425 pub fn halt_handle(&self) -> Arc<AtomicBool> {
426 self.halt.clone()
427 }
428
429 #[inline]
431 pub fn is_supported() -> Result<bool, Error> {
432 Ok(ApiInformation::IsApiContractPresentByMajor(&HSTRING::from("Windows.Foundation.UniversalApiContract"), 8)?
433 && GraphicsCaptureSession::IsSupported()?)
434 }
435
436 #[inline]
438 pub fn is_cursor_settings_supported() -> Result<bool, Error> {
439 Ok(ApiInformation::IsPropertyPresent(
440 &HSTRING::from("Windows.Graphics.Capture.GraphicsCaptureSession"),
441 &HSTRING::from("IsCursorCaptureEnabled"),
442 )? && Self::is_supported()?)
443 }
444
445 #[inline]
447 pub fn is_border_settings_supported() -> Result<bool, Error> {
448 Ok(ApiInformation::IsPropertyPresent(
449 &HSTRING::from("Windows.Graphics.Capture.GraphicsCaptureSession"),
450 &HSTRING::from("IsBorderRequired"),
451 )? && Self::is_supported()?)
452 }
453
454 #[inline]
456 pub fn is_secondary_windows_supported() -> Result<bool, Error> {
457 Ok(ApiInformation::IsPropertyPresent(
458 &HSTRING::from("Windows.Graphics.Capture.GraphicsCaptureSession"),
459 &HSTRING::from("IncludeSecondaryWindows"),
460 )? && Self::is_supported()?)
461 }
462
463 #[inline]
465 pub fn is_minimum_update_interval_supported() -> Result<bool, Error> {
466 Ok(ApiInformation::IsPropertyPresent(
467 &HSTRING::from("Windows.Graphics.Capture.GraphicsCaptureSession"),
468 &HSTRING::from("MinUpdateInterval"),
469 )? && Self::is_supported()?)
470 }
471
472 #[inline]
474 pub fn is_dirty_region_supported() -> Result<bool, Error> {
475 Ok(ApiInformation::IsPropertyPresent(
476 &HSTRING::from("Windows.Graphics.Capture.GraphicsCaptureSession"),
477 &HSTRING::from("DirtyRegionMode"),
478 )? && Self::is_supported()?)
479 }
480
481 fn cleanup(&mut self) {
482 if let Some(frame_pool) = self.frame_pool.take() {
483 let _ = frame_pool.RemoveFrameArrived(self.frame_arrived_event_token);
484 let _ = frame_pool.Close();
485 }
486
487 if let Some(session) = self.session.take() {
488 let _ = session.Close();
489 }
490
491 let item = match &self.item_with_details {
492 GraphicsCaptureItemType::Window((item, _)) => item,
493 GraphicsCaptureItemType::Monitor((item, _)) => item,
494 GraphicsCaptureItemType::Unknown((item, _)) => item,
495 };
496
497 let _ = item.RemoveClosed(self.capture_closed_event_token);
498 self.active = false;
499 }
500}
501
502impl Drop for GraphicsCaptureApi {
503 fn drop(&mut self) {
504 self.cleanup();
505 }
506}