Skip to content

Commit ef52113

Browse files
authored
Resolve WGC technical limitation (#303)
1 parent bd6cf54 commit ef52113

File tree

8 files changed

+290
-106
lines changed

8 files changed

+290
-106
lines changed

Diff for: docs/tutorial.md

-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@
4343

4444
- **Windows Graphics Capture** (fast, most compatible, capped at 60fps)
4545
Only available in Windows 10.0.17134 and up.
46-
Due to current technical limitations, Windows versions below 10.0.0.17763 require having at least one audio or video Capture Device connected and enabled.
4746
Allows recording UWP apps, Hardware Accelerated and Exclusive Fullscreen windows.
4847
Adds a yellow border on Windows 10 (not on Windows 11).
4948
Caps at around 60 FPS.

Diff for: ruff.toml

+4
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,10 @@ max-branches = 15
146146
# Issues with using a star-imported name will be caught by type-checkers.
147147
"F405", # may be undefined, or defined from star imports
148148
]
149+
"src/d3d11.py" = [
150+
# Following windows API/ctypes like naming conventions
151+
"N801", # invalid-class-name
152+
]
149153

150154
[lint.flake8-tidy-imports.banned-api]
151155
"cv2.imread".msg = """\

Diff for: scripts/requirements.txt

+10-8
Original file line numberDiff line numberDiff line change
@@ -17,26 +17,28 @@ PySide6-Essentials>=6.6.0 # Python 3.12 support
1717
scipy>=1.11.2 # Python 3.12 support
1818
tomli-w>=1.1.0 # Typing fixes
1919
typing-extensions>=4.4.0 # @override decorator support
20+
2021
#
2122
# Build and compile resources
2223
pyinstaller>=5.13 # Python 3.12 support
24+
2325
#
2426
# https://peps.python.org/pep-0508/#environment-markers
2527
#
2628
# Windows-only dependencies:
2729
comtypes<1.4.5 ; sys_platform == 'win32' # https://github.com/pyinstaller/pyinstaller-hooks-contrib/issues/807
2830
pygrabber>=0.2 ; sys_platform == 'win32' # Completed types
2931
pywin32>=301 ; sys_platform == 'win32'
30-
winrt-Windows.AI.MachineLearning>=2.2.0 ; sys_platform == 'win32' # Python 3.13 support
32+
typed-D3DShot[numpy]>=1.0.1 ; sys_platform == 'win32'
3133
winrt-Windows.Foundation>=2.2.0 ; sys_platform == 'win32' # Python 3.13 support
32-
winrt-Windows.Graphics.Capture>=2.2.0 ; sys_platform == 'win32' # Python 3.13 support
33-
winrt-Windows.Graphics.Capture.Interop>=2.2.0 ; sys_platform == 'win32' # Python 3.13 support
34-
winrt-Windows.Graphics.DirectX>=2.2.0 ; sys_platform == 'win32' # Python 3.13 support
35-
winrt-Windows.Graphics.DirectX.Direct3D11>=2.2.0 ; sys_platform == 'win32' # Python 3.13 support
36-
winrt-Windows.Graphics.Imaging>=2.2.0 ; sys_platform == 'win32' # Python 3.13 support
34+
winrt-Windows.Graphics.Capture>=2.3.0 ; sys_platform == 'win32' # Python 3.13 support
35+
winrt-Windows.Graphics.Capture.Interop>=2.3.0 ; sys_platform == 'win32' # Python 3.13 support
36+
winrt-Windows.Graphics.DirectX>=2.3.0 ; sys_platform == 'win32' # Python 3.13 support
37+
winrt-Windows.Graphics.DirectX.Direct3D11>=2.3.0 ; sys_platform == 'win32' # Python 3.13 support
38+
winrt-Windows.Graphics.DirectX.Direct3D11.Interop>=2.3.0 ; sys_platform == 'win32'
3739
winrt-Windows.Graphics>=2.2.0 ; sys_platform == 'win32' # Python 3.13 support
38-
winrt-Windows.Media.Capture>=2.2.0 ; sys_platform == 'win32' # Python 3.13 support
39-
typed-D3DShot[numpy]>=1.0.1 ; sys_platform == 'win32'
40+
winrt-Windows.Graphics.Imaging>=2.3.0 ; sys_platform == 'win32' # Python 3.13 support
41+
4042
#
4143
# Linux-only dependencies
4244
PyScreeze ; sys_platform == 'linux'

Diff for: src/capture_method/WindowsGraphicsCaptureMethod.py

+15-14
Original file line numberDiff line numberDiff line change
@@ -14,23 +14,19 @@
1414
from winrt.windows.graphics.capture.interop import create_for_window
1515
from winrt.windows.graphics.directx import DirectXPixelFormat
1616
from winrt.windows.graphics.directx.direct3d11 import IDirect3DSurface
17+
from winrt.windows.graphics.directx.direct3d11.interop import (
18+
create_direct3d11_device_from_dxgi_device,
19+
)
1720
from winrt.windows.graphics.imaging import BitmapBufferAccessMode, SoftwareBitmap
1821

1922
from capture_method.CaptureMethodBase import CaptureMethodBase
20-
from utils import (
21-
BGRA_CHANNEL_COUNT,
22-
WGC_MIN_BUILD,
23-
WINDOWS_BUILD_NUMBER,
24-
get_direct3d_device,
25-
is_valid_hwnd,
26-
)
23+
from d3d11 import D3D11_CREATE_DEVICE_FLAG, D3D_DRIVER_TYPE, D3D11CreateDevice
24+
from utils import BGRA_CHANNEL_COUNT, WGC_MIN_BUILD, WINDOWS_BUILD_NUMBER, is_valid_hwnd
2725

2826
if TYPE_CHECKING:
2927
from AutoSplit import AutoSplit
3028

3129
WGC_NO_BORDER_MIN_BUILD = 20348
32-
LEARNING_MODE_DEVICE_BUILD = 17763
33-
"""https://learn.microsoft.com/en-us/uwp/api/windows.ai.machinelearning.learningmodeldevice"""
3430

3531

3632
async def convert_d3d_surface_to_software_bitmap(surface: IDirect3DSurface | None):
@@ -42,13 +38,11 @@ class WindowsGraphicsCaptureMethod(CaptureMethodBase):
4238
short_description = "fast, most compatible, capped at 60fps"
4339
description = f"""
4440
Only available in Windows 10.0.{WGC_MIN_BUILD} and up.
45-
Due to current technical limitations, Windows versions below 10.0.0.{LEARNING_MODE_DEVICE_BUILD}
46-
require having at least one audio or video Capture Device connected and enabled.
4741
Allows recording UWP apps, Hardware Accelerated and Exclusive Fullscreen windows.
4842
Adds a yellow border on Windows 10 (not on Windows 11).
4943
Caps at around 60 FPS."""
5044

51-
size: SizeInt32
45+
size: "SizeInt32"
5246
frame_pool: Direct3D11CaptureFramePool | None = None
5347
session: GraphicsCaptureSession | None = None
5448
"""This is stored to prevent session from being garbage collected"""
@@ -59,11 +53,16 @@ def __init__(self, autosplit: "AutoSplit"):
5953
if not is_valid_hwnd(autosplit.hwnd):
6054
return
6155

56+
dxgi, *_ = D3D11CreateDevice(
57+
DriverType=D3D_DRIVER_TYPE.HARDWARE,
58+
Flags=D3D11_CREATE_DEVICE_FLAG.BGRA_SUPPORT,
59+
)
60+
direct3d_device = create_direct3d11_device_from_dxgi_device(dxgi.value)
6261
item = create_for_window(autosplit.hwnd)
6362
frame_pool = Direct3D11CaptureFramePool.create_free_threaded(
64-
get_direct3d_device(),
63+
direct3d_device,
6564
DirectXPixelFormat.B8_G8_R8_A8_UINT_NORMALIZED,
66-
1,
65+
1, # number_of_buffers
6766
item.size,
6867
)
6968
if not frame_pool:
@@ -114,6 +113,8 @@ def get_frame(self) -> MatLike | None:
114113
return None
115114

116115
# We were too fast and the next frame wasn't ready yet
116+
# TODO: Consider "add_frame_arrive" instead !
117+
# https://github.com/pywinrt/pywinrt/blob/5bf1ac5ff4a77cf343e11d7c841c368fa9235d81/samples/screen_capture/__main__.py#L67-L78
117118
if not frame:
118119
return self.last_converted_frame
119120

Diff for: src/capture_method/__init__.py

+3-13
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,7 @@
1010

1111
from capture_method.CaptureMethodBase import CaptureMethodBase
1212
from capture_method.VideoCaptureDeviceCaptureMethod import VideoCaptureDeviceCaptureMethod
13-
from utils import (
14-
WGC_MIN_BUILD,
15-
WINDOWS_BUILD_NUMBER,
16-
first,
17-
get_input_device_resolution,
18-
try_get_direct3d_device,
19-
)
13+
from utils import WGC_MIN_BUILD, WINDOWS_BUILD_NUMBER, first, get_input_device_resolution
2014

2115
if sys.platform == "win32":
2216
from _ctypes import COMError # noqa: PLC2701 # comtypes is untyped
@@ -125,12 +119,8 @@ def get(self, key: CaptureMethodEnum, default: object = None, /):
125119

126120
CAPTURE_METHODS = CaptureMethodDict()
127121
if sys.platform == "win32":
128-
if ( # Windows Graphics Capture requires a minimum Windows Build
129-
WINDOWS_BUILD_NUMBER >= WGC_MIN_BUILD
130-
# Our current implementation of Windows Graphics Capture
131-
# does not ensure we can get an ID3DDevice
132-
and try_get_direct3d_device()
133-
):
122+
# Windows Graphics Capture requires a minimum Windows Build
123+
if WINDOWS_BUILD_NUMBER >= WGC_MIN_BUILD:
134124
CAPTURE_METHODS[CaptureMethodEnum.WINDOWS_GRAPHICS_CAPTURE] = WindowsGraphicsCaptureMethod
135125
CAPTURE_METHODS[CaptureMethodEnum.BITBLT] = BitBltCaptureMethod
136126
try: # Test for laptop cross-GPU Desktop Duplication issue

Diff for: src/d3d11.py

+226
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
# SPDX-License-Identifier: MIT
2+
# Copyright (c) 2024 David Lechner <[email protected]>
3+
import sys
4+
5+
if sys.platform != "win32":
6+
raise OSError
7+
8+
import ctypes
9+
import enum
10+
import uuid
11+
from ctypes import wintypes
12+
from typing import TYPE_CHECKING
13+
14+
if TYPE_CHECKING:
15+
from ctypes import _FuncPointer # pyright: ignore[reportPrivateUsage]
16+
17+
18+
###
19+
# https://github.com/pywinrt/pywinrt/blob/main/samples/screen_capture/iunknown.py
20+
###
21+
22+
23+
class GUID(ctypes.Structure):
24+
_fields_ = (
25+
("Data1", ctypes.c_ulong),
26+
("Data2", ctypes.c_ushort),
27+
("Data3", ctypes.c_ushort),
28+
("Data4", ctypes.c_ubyte * 8),
29+
)
30+
31+
32+
class IUnknown(ctypes.c_void_p):
33+
QueryInterface = ctypes.WINFUNCTYPE(
34+
# _CData is incompatible with int
35+
int, # type: ignore[arg-type] # pyright: ignore[reportArgumentType]
36+
ctypes.POINTER(GUID),
37+
ctypes.POINTER(wintypes.LPVOID),
38+
)(0, "QueryInterface")
39+
AddRef = ctypes.WINFUNCTYPE(wintypes.ULONG)(1, "AddRef")
40+
Release = ctypes.WINFUNCTYPE(wintypes.ULONG)(2, "Release")
41+
42+
def query_interface(self, iid: uuid.UUID | str) -> "IUnknown":
43+
if isinstance(iid, str):
44+
iid = uuid.UUID(iid)
45+
46+
ppv = wintypes.LPVOID()
47+
_iid = GUID.from_buffer_copy(iid.bytes_le)
48+
ret = self.QueryInterface(self, ctypes.byref(_iid), ctypes.byref(ppv))
49+
50+
if ret:
51+
raise ctypes.WinError(ret)
52+
53+
return IUnknown(ppv.value)
54+
55+
def __del__(self):
56+
IUnknown.Release(self)
57+
58+
59+
###
60+
# https://github.com/pywinrt/pywinrt/blob/main/samples/screen_capture/d3d11.py
61+
###
62+
63+
64+
__all__ = [
65+
"D3D11_CREATE_DEVICE_FLAG",
66+
"D3D_DRIVER_TYPE",
67+
"D3D_FEATURE_LEVEL",
68+
"D3D11CreateDevice",
69+
]
70+
71+
IN = 1
72+
OUT = 2
73+
74+
# https://learn.microsoft.com/en-us/windows/win32/api/d3dcommon/ne-d3dcommon-d3d_driver_type
75+
#
76+
# typedef enum D3D_DRIVER_TYPE {
77+
# D3D_DRIVER_TYPE_UNKNOWN = 0,
78+
# D3D_DRIVER_TYPE_HARDWARE,
79+
# D3D_DRIVER_TYPE_REFERENCE,
80+
# D3D_DRIVER_TYPE_NULL,
81+
# D3D_DRIVER_TYPE_SOFTWARE,
82+
# D3D_DRIVER_TYPE_WARP
83+
# } ;
84+
85+
86+
class D3D_DRIVER_TYPE(enum.IntEnum):
87+
UNKNOWN = 0
88+
HARDWARE = 1
89+
REFERENCE = 2
90+
NULL = 3
91+
SOFTWARE = 4
92+
WARP = 5
93+
94+
95+
# https://learn.microsoft.com/en-us/windows/win32/api/d3d11/ne-d3d11-d3d11_create_device_flag
96+
#
97+
# typedef enum D3D11_CREATE_DEVICE_FLAG {
98+
# D3D11_CREATE_DEVICE_SINGLETHREADED = 0x1,
99+
# D3D11_CREATE_DEVICE_DEBUG = 0x2,
100+
# D3D11_CREATE_DEVICE_SWITCH_TO_REF = 0x4,
101+
# D3D11_CREATE_DEVICE_PREVENT_INTERNAL_THREADING_OPTIMIZATIONS = 0x8,
102+
# D3D11_CREATE_DEVICE_BGRA_SUPPORT = 0x20,
103+
# D3D11_CREATE_DEVICE_DEBUGGABLE = 0x40,
104+
# D3D11_CREATE_DEVICE_PREVENT_ALTERING_LAYER_SETTINGS_FROM_REGISTRY = 0x80,
105+
# D3D11_CREATE_DEVICE_DISABLE_GPU_TIMEOUT = 0x100,
106+
# D3D11_CREATE_DEVICE_VIDEO_SUPPORT = 0x800
107+
# } ;
108+
109+
110+
class D3D11_CREATE_DEVICE_FLAG(enum.IntFlag):
111+
SINGLETHREADED = 0x1
112+
DEBUG = 0x2
113+
SWITCH_TO_REF = 0x4
114+
PREVENT_INTERNAL_THREADING_OPTIMIZATIONS = 0x8
115+
BGRA_SUPPORT = 0x20
116+
DEBUGGABLE = 0x40
117+
PREVENT_ALTERING_LAYER_SETTINGS_FROM_REGISTRY = 0x80
118+
DISABLE_GPU_TIMEOUT = 0x100
119+
VIDEO_SUPPORT = 0x800
120+
121+
122+
# https://learn.microsoft.com/en-us/windows/win32/api/d3dcommon/ne-d3dcommon-d3d_feature_level
123+
#
124+
# typedef enum D3D_FEATURE_LEVEL {
125+
# D3D_FEATURE_LEVEL_1_0_GENERIC,
126+
# D3D_FEATURE_LEVEL_1_0_CORE,
127+
# D3D_FEATURE_LEVEL_9_1,
128+
# D3D_FEATURE_LEVEL_9_2,
129+
# D3D_FEATURE_LEVEL_9_3,
130+
# D3D_FEATURE_LEVEL_10_0,
131+
# D3D_FEATURE_LEVEL_10_1,
132+
# D3D_FEATURE_LEVEL_11_0,
133+
# D3D_FEATURE_LEVEL_11_1,
134+
# D3D_FEATURE_LEVEL_12_0,
135+
# D3D_FEATURE_LEVEL_12_1,
136+
# D3D_FEATURE_LEVEL_12_2
137+
# } ;
138+
139+
140+
class D3D_FEATURE_LEVEL(enum.IntEnum):
141+
LEVEL_1_0_GENERIC = 0x1000
142+
LEVEL_1_0_CORE = 0x1001
143+
LEVEL_9_1 = 0x9100
144+
LEVEL_9_2 = 0x9200
145+
LEVEL_9_3 = 0x9300
146+
LEVEL_10_0 = 0xA000
147+
LEVEL_10_1 = 0xA100
148+
LEVEL_11_0 = 0xB000
149+
LEVEL_11_1 = 0xB100
150+
LEVEL_12_0 = 0xC000
151+
LEVEL_12_1 = 0xC100
152+
LEVEL_12_2 = 0xC200
153+
154+
155+
# not sure where this is officially defined or if the value would ever change
156+
157+
D3D11_SDK_VERSION = 7
158+
159+
# https://learn.microsoft.com/en-us/windows/win32/api/d3d11/nf-d3d11-d3d11createdevice
160+
#
161+
# HRESULT D3D11CreateDevice(
162+
# [in, optional] IDXGIAdapter *pAdapter,
163+
# D3D_DRIVER_TYPE DriverType,
164+
# HMODULE Software,
165+
# UINT Flags,
166+
# [in, optional] const D3D_FEATURE_LEVEL *pFeatureLevels,
167+
# UINT FeatureLevels,
168+
# UINT SDKVersion,
169+
# [out, optional] ID3D11Device **ppDevice,
170+
# [out, optional] D3D_FEATURE_LEVEL *pFeatureLevel,
171+
# [out, optional] ID3D11DeviceContext **ppImmediateContext
172+
# );
173+
174+
175+
def errcheck(
176+
result: int,
177+
_func: "_FuncPointer", # Actually WinFunctionType but that's an internal class
178+
args: tuple[
179+
IUnknown | None, # IDXGIAdapter
180+
D3D_DRIVER_TYPE,
181+
wintypes.HMODULE | None,
182+
D3D11_CREATE_DEVICE_FLAG,
183+
D3D_FEATURE_LEVEL | None,
184+
int,
185+
int,
186+
IUnknown, # ID3D11Device
187+
wintypes.UINT,
188+
IUnknown, # ID3D11DeviceContext
189+
],
190+
):
191+
if result:
192+
raise ctypes.WinError(result)
193+
194+
return (args[7], D3D_FEATURE_LEVEL(args[8].value), args[9])
195+
196+
197+
D3D11CreateDevice = ctypes.WINFUNCTYPE(
198+
# _CData is incompatible with int
199+
int, # type: ignore[arg-type] # pyright: ignore[reportArgumentType]
200+
wintypes.LPVOID,
201+
wintypes.UINT,
202+
wintypes.LPVOID,
203+
wintypes.UINT,
204+
ctypes.POINTER(wintypes.UINT),
205+
wintypes.UINT,
206+
wintypes.UINT,
207+
ctypes.POINTER(IUnknown),
208+
ctypes.POINTER(wintypes.UINT),
209+
ctypes.POINTER(IUnknown),
210+
)(
211+
("D3D11CreateDevice", ctypes.windll.d3d11),
212+
(
213+
(IN, "pAdapter", None),
214+
(IN, "DriverType", D3D_DRIVER_TYPE.UNKNOWN),
215+
(IN, "Software", None),
216+
(IN, "Flags", 0),
217+
(IN, "pFeatureLevels", None),
218+
(IN, "FeatureLevels", 0),
219+
(IN, "SDKVersion", D3D11_SDK_VERSION),
220+
(OUT, "ppDevice"),
221+
(OUT, "pFeatureLevel"),
222+
(OUT, "ppImmediateContext"),
223+
),
224+
)
225+
# _CData is incompatible with int
226+
D3D11CreateDevice.errcheck = errcheck # type: ignore[assignment] # pyright: ignore[reportAttributeAccessIssue]

0 commit comments

Comments
 (0)