dxcam 0.2.0.dev1__tar.gz → 0.3.0.dev1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {dxcam-0.2.0.dev1/dxcam.egg-info → dxcam-0.3.0.dev1}/PKG-INFO +38 -60
- {dxcam-0.2.0.dev1 → dxcam-0.3.0.dev1}/README.md +37 -59
- {dxcam-0.2.0.dev1 → dxcam-0.3.0.dev1}/dxcam/__init__.py +8 -6
- {dxcam-0.2.0.dev1 → dxcam-0.3.0.dev1}/dxcam/_libs/dxgi.py +2 -0
- {dxcam-0.2.0.dev1 → dxcam-0.3.0.dev1}/dxcam/core/backend.py +24 -0
- dxcam-0.3.0.dev1/dxcam/core/capture_loop.py +85 -0
- dxcam-0.3.0.dev1/dxcam/core/capture_runtime.py +123 -0
- {dxcam-0.2.0.dev1 → dxcam-0.3.0.dev1}/dxcam/core/device.py +10 -2
- dxcam-0.3.0.dev1/dxcam/core/display_recovery.py +143 -0
- {dxcam-0.2.0.dev1 → dxcam-0.3.0.dev1}/dxcam/core/dxgi_duplicator.py +60 -42
- dxcam-0.3.0.dev1/dxcam/core/dxgi_errors.py +154 -0
- dxcam-0.3.0.dev1/dxcam/core/output_recovery.py +143 -0
- {dxcam-0.2.0.dev1 → dxcam-0.3.0.dev1}/dxcam/dxcam.py +186 -201
- {dxcam-0.2.0.dev1 → dxcam-0.3.0.dev1}/dxcam/processor/__init__.py +2 -0
- {dxcam-0.2.0.dev1 → dxcam-0.3.0.dev1}/dxcam/processor/_numpy_kernels.c +152 -152
- {dxcam-0.2.0.dev1 → dxcam-0.3.0.dev1}/dxcam/processor/base.py +36 -1
- {dxcam-0.2.0.dev1 → dxcam-0.3.0.dev1}/dxcam/util/io.py +15 -3
- {dxcam-0.2.0.dev1 → dxcam-0.3.0.dev1/dxcam.egg-info}/PKG-INFO +38 -60
- {dxcam-0.2.0.dev1 → dxcam-0.3.0.dev1}/dxcam.egg-info/SOURCES.txt +7 -1
- {dxcam-0.2.0.dev1 → dxcam-0.3.0.dev1}/pyproject.toml +2 -1
- dxcam-0.3.0.dev1/tests/test_dxgi_transient_policy.py +60 -0
- {dxcam-0.2.0.dev1 → dxcam-0.3.0.dev1}/LICENSE +0 -0
- {dxcam-0.2.0.dev1 → dxcam-0.3.0.dev1}/MANIFEST.in +0 -0
- {dxcam-0.2.0.dev1 → dxcam-0.3.0.dev1}/dxcam/_libs/__init__.py +0 -0
- {dxcam-0.2.0.dev1 → dxcam-0.3.0.dev1}/dxcam/_libs/d3d11.py +0 -0
- {dxcam-0.2.0.dev1 → dxcam-0.3.0.dev1}/dxcam/_libs/user32.py +0 -0
- {dxcam-0.2.0.dev1 → dxcam-0.3.0.dev1}/dxcam/core/__init__.py +0 -0
- {dxcam-0.2.0.dev1 → dxcam-0.3.0.dev1}/dxcam/core/duplicator.py +0 -0
- {dxcam-0.2.0.dev1 → dxcam-0.3.0.dev1}/dxcam/core/output.py +0 -0
- {dxcam-0.2.0.dev1 → dxcam-0.3.0.dev1}/dxcam/core/stagesurf.py +0 -0
- {dxcam-0.2.0.dev1 → dxcam-0.3.0.dev1}/dxcam/core/winrt_duplicator.py +0 -0
- {dxcam-0.2.0.dev1 → dxcam-0.3.0.dev1}/dxcam/processor/_numpy_kernels.pyx +0 -0
- {dxcam-0.2.0.dev1 → dxcam-0.3.0.dev1}/dxcam/processor/cv2_processor.py +0 -0
- {dxcam-0.2.0.dev1 → dxcam-0.3.0.dev1}/dxcam/processor/numpy_processor.py +0 -0
- {dxcam-0.2.0.dev1 → dxcam-0.3.0.dev1}/dxcam/types.py +0 -0
- {dxcam-0.2.0.dev1 → dxcam-0.3.0.dev1}/dxcam/util/__init__.py +0 -0
- {dxcam-0.2.0.dev1 → dxcam-0.3.0.dev1}/dxcam/util/timer.py +0 -0
- {dxcam-0.2.0.dev1 → dxcam-0.3.0.dev1}/dxcam.egg-info/dependency_links.txt +0 -0
- {dxcam-0.2.0.dev1 → dxcam-0.3.0.dev1}/dxcam.egg-info/requires.txt +0 -0
- {dxcam-0.2.0.dev1 → dxcam-0.3.0.dev1}/dxcam.egg-info/top_level.txt +0 -0
- {dxcam-0.2.0.dev1 → dxcam-0.3.0.dev1}/setup.cfg +0 -0
- {dxcam-0.2.0.dev1 → dxcam-0.3.0.dev1}/setup.py +0 -0
- {dxcam-0.2.0.dev1 → dxcam-0.3.0.dev1}/tests/test_bgra_conversion_parity.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dxcam
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0.dev1
|
|
4
4
|
Summary: A Python high-performance screenshot library for Windows using Desktop Duplication API
|
|
5
5
|
Author: ra1nty
|
|
6
6
|
License-Expression: MIT
|
|
@@ -49,19 +49,22 @@ Dynamic: license-file
|
|
|
49
49
|
```python
|
|
50
50
|
import dxcam
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
frame = camera.grab()
|
|
52
|
+
with dxcam.create() as camera:
|
|
53
|
+
frame = camera.grab()
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
+
> **Live API Docs:** [https://ra1nty.github.io/DXcam/](https://ra1nty.github.io/DXcam/)
|
|
57
|
+
|
|
56
58
|
## Introduction
|
|
57
59
|
DXcam is a high-performance python screenshot and capture library for Windows based on the Desktop Duplication API.
|
|
58
60
|
It is designed for low-latency, high-FPS capture pipelines (including full-screen Direct3D applications).
|
|
59
61
|
|
|
60
62
|
Compared with common Python alternatives, DXcam focuses on:
|
|
61
|
-
- Higher capture throughput
|
|
63
|
+
- Higher capture throughput (240+fps on 1080p)
|
|
62
64
|
- Stable capture for full-screen exclusive Direct3D apps
|
|
63
|
-
-
|
|
64
|
-
-
|
|
65
|
+
- Better FPS pacing for continuous video capture
|
|
66
|
+
- Support DXGI / Windows Graphics Capture dual backend
|
|
67
|
+
- Seamless integration for AI Agent / Computer Vision use cases.
|
|
65
68
|
|
|
66
69
|
## Installation
|
|
67
70
|
### From PyPI (pip)
|
|
@@ -79,39 +82,12 @@ Notes:
|
|
|
79
82
|
- Official Windows wheels are built for CPython `3.10` to `3.14`.
|
|
80
83
|
- Binary wheels include the Cython kernels used by processor backends.
|
|
81
84
|
|
|
82
|
-
### From source
|
|
83
|
-
|
|
84
|
-
uv sync
|
|
85
|
-
# include OpenCV conversion backend
|
|
86
|
-
uv sync --extra cv2
|
|
87
|
-
# include optional Cython tooling
|
|
88
|
-
uv sync --extra cython
|
|
89
|
-
# include WinRT backend
|
|
90
|
-
uv sync --extra winrt
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
Build local Cython kernels from source:
|
|
94
|
-
```bash
|
|
95
|
-
set DXCAM_BUILD_CYTHON=1
|
|
96
|
-
uv pip install -e .[cython] --no-build-isolation
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
### Dev environment (uv + ruff + ty)
|
|
100
|
-
```bash
|
|
101
|
-
uv venv --python 3.11 .venv
|
|
102
|
-
uv sync --dev
|
|
103
|
-
uv run ruff check dxcam
|
|
104
|
-
uv run ty check dxcam
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
### API docs (pdoc)
|
|
108
|
-
Generate autodocs for the public API surface:
|
|
109
|
-
```bash
|
|
110
|
-
uv run pdoc -d google -o site dxcam dxcam.dxcam dxcam.types
|
|
111
|
-
```
|
|
85
|
+
### From source
|
|
86
|
+
Please refer to [CONTRIBUTING](CONTRIBUTING.md).
|
|
112
87
|
|
|
113
|
-
|
|
114
|
-
|
|
88
|
+
### Contributing / Dev
|
|
89
|
+
Contributions are welcome!
|
|
90
|
+
Development setup and contributor workflow are documented in [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
115
91
|
|
|
116
92
|
## Usage
|
|
117
93
|
Each output (monitor) is associated with one `DXCamera` instance.
|
|
@@ -121,7 +97,7 @@ import dxcam
|
|
|
121
97
|
camera = dxcam.create() # primary output on device 0
|
|
122
98
|
```
|
|
123
99
|
|
|
124
|
-
To specify
|
|
100
|
+
To specify backends:
|
|
125
101
|
```python
|
|
126
102
|
camera = dxcam.create(
|
|
127
103
|
backend="dxgi", # default Desktop Duplication backend
|
|
@@ -133,7 +109,7 @@ camera = dxcam.create(
|
|
|
133
109
|
```python
|
|
134
110
|
frame = camera.grab()
|
|
135
111
|
```
|
|
136
|
-
`grab()` returns a `numpy.ndarray`. `None` if no new frame is available since the last capture (for backward compatibility); use `new_frame_only=False` to
|
|
112
|
+
`grab()` returns a `numpy.ndarray`. `None` if no new frame is available since the last capture (for backward compatibility); use `camera.grab(new_frame_only=False)` to make dxcam always return the latest frame.
|
|
137
113
|
|
|
138
114
|
Use `copy=False` (or `camera.grab_view()`) for a zero-copy view. This is faster, but the returned buffer can be overwritten by later captures.
|
|
139
115
|
|
|
@@ -167,6 +143,24 @@ Useful variants:
|
|
|
167
143
|
|
|
168
144
|
> When `start()` capture is running, calling `grab()` reads from the in-memory ring buffer instead of directly polling DXGI.
|
|
169
145
|
|
|
146
|
+
### Safely Releasing Resources
|
|
147
|
+
`release()` stops capture, frees buffers, and releases capture resources.
|
|
148
|
+
After `release()`, the same instance cannot be reused.
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
camera = dxcam.create(output_idx=0, output_color="BGR")
|
|
152
|
+
camera.release()
|
|
153
|
+
# camera.start() # raises RuntimeError
|
|
154
|
+
```
|
|
155
|
+
Equivalently you can use context manager:
|
|
156
|
+
```python
|
|
157
|
+
with dxcam.create() as camera:
|
|
158
|
+
frame = camera.grab()
|
|
159
|
+
# resource released automatically
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
**Full API Docs:** [https://ra1nty.github.io/DXcam/](https://ra1nty.github.io/DXcam/)
|
|
163
|
+
|
|
170
164
|
## Advanced Usage and Remarks
|
|
171
165
|
### Multiple monitors / GPUs
|
|
172
166
|
```python
|
|
@@ -257,12 +251,13 @@ DXcam supports two capture backends:
|
|
|
257
251
|
|
|
258
252
|
Use it like this:
|
|
259
253
|
```python
|
|
260
|
-
camera = dxcam.create(backend="dxgi")
|
|
254
|
+
camera = dxcam.create(backend="dxgi")
|
|
261
255
|
camera = dxcam.create(backend="winrt")
|
|
262
256
|
```
|
|
263
257
|
|
|
264
258
|
Guideline:
|
|
265
|
-
-
|
|
259
|
+
- If you need cursor rendering, use `winrt`.
|
|
260
|
+
- Start with `dxgi` for most workloads, especially one-shot grab.
|
|
266
261
|
- Try `winrt` if it performs better on your machine or fits your app constraints.
|
|
267
262
|
|
|
268
263
|
### Processor Backend
|
|
@@ -293,16 +288,6 @@ If `processor_backend="numpy"` is selected but compiled kernels are unavailable,
|
|
|
293
288
|
DXcam logs a warning and falls back to `cv2` behavior. In that fallback path,
|
|
294
289
|
install OpenCV for non-`BGRA` output modes.
|
|
295
290
|
|
|
296
|
-
### Safely Releasing Resources
|
|
297
|
-
`release()` stops capture, frees buffers, and releases capture resources.
|
|
298
|
-
After `release()`, the same instance cannot be reused.
|
|
299
|
-
|
|
300
|
-
```python
|
|
301
|
-
camera = dxcam.create(output_idx=0, output_color="BGR")
|
|
302
|
-
camera.release()
|
|
303
|
-
# camera.start() # raises RuntimeError
|
|
304
|
-
```
|
|
305
|
-
|
|
306
291
|
## Benchmarks
|
|
307
292
|
When using a similar logic (only capture newly rendered frames) running on a 240fps output, ```DXCam, python-mss, D3DShot``` benchmarked as follow:
|
|
308
293
|
|
|
@@ -319,16 +304,9 @@ The benchmark is across 5 runs, with a light-moderate usage on my PC (5900X + 30
|
|
|
319
304
|
| 60fps | 61.71, 0.26 :checkered_flag: | N/A | 47.11, 1.33 |
|
|
320
305
|
| 30fps | 30.08, 0.02 :checkered_flag: | N/A | 21.24, 0.17 |
|
|
321
306
|
|
|
322
|
-
Processor backend comparison helper:
|
|
323
|
-
```bash
|
|
324
|
-
python benchmarks/dxcam_processor_compare.py --backend dxgi --target-fps 120 --target-frames 1000
|
|
325
|
-
python benchmarks/dxcam_capture.py --backend dxgi --processor-backend numpy
|
|
326
|
-
python benchmarks/numpy_processor_micro.py --width 3840 --height 2160 --modes RGB --variants process into --processor-backends cv2 numpy
|
|
327
|
-
```
|
|
328
|
-
|
|
329
307
|
|
|
330
308
|
## Work Referenced
|
|
331
309
|
|
|
332
310
|
[OBS Studio](https://github.com/obsproject/obs-studio) - implementation ideas and references.
|
|
333
311
|
|
|
334
|
-
[D3DShot](https://github.com/SerpentAI/D3DShot/) : DXcam
|
|
312
|
+
[D3DShot](https://github.com/SerpentAI/D3DShot/) : DXcam borrowed some ctypes header from the no-longer maintained D3DShot.
|
|
@@ -4,19 +4,22 @@
|
|
|
4
4
|
```python
|
|
5
5
|
import dxcam
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
frame = camera.grab()
|
|
7
|
+
with dxcam.create() as camera:
|
|
8
|
+
frame = camera.grab()
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
+
> **Live API Docs:** [https://ra1nty.github.io/DXcam/](https://ra1nty.github.io/DXcam/)
|
|
12
|
+
|
|
11
13
|
## Introduction
|
|
12
14
|
DXcam is a high-performance python screenshot and capture library for Windows based on the Desktop Duplication API.
|
|
13
15
|
It is designed for low-latency, high-FPS capture pipelines (including full-screen Direct3D applications).
|
|
14
16
|
|
|
15
17
|
Compared with common Python alternatives, DXcam focuses on:
|
|
16
|
-
- Higher capture throughput
|
|
18
|
+
- Higher capture throughput (240+fps on 1080p)
|
|
17
19
|
- Stable capture for full-screen exclusive Direct3D apps
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
+
- Better FPS pacing for continuous video capture
|
|
21
|
+
- Support DXGI / Windows Graphics Capture dual backend
|
|
22
|
+
- Seamless integration for AI Agent / Computer Vision use cases.
|
|
20
23
|
|
|
21
24
|
## Installation
|
|
22
25
|
### From PyPI (pip)
|
|
@@ -34,39 +37,12 @@ Notes:
|
|
|
34
37
|
- Official Windows wheels are built for CPython `3.10` to `3.14`.
|
|
35
38
|
- Binary wheels include the Cython kernels used by processor backends.
|
|
36
39
|
|
|
37
|
-
### From source
|
|
38
|
-
|
|
39
|
-
uv sync
|
|
40
|
-
# include OpenCV conversion backend
|
|
41
|
-
uv sync --extra cv2
|
|
42
|
-
# include optional Cython tooling
|
|
43
|
-
uv sync --extra cython
|
|
44
|
-
# include WinRT backend
|
|
45
|
-
uv sync --extra winrt
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
Build local Cython kernels from source:
|
|
49
|
-
```bash
|
|
50
|
-
set DXCAM_BUILD_CYTHON=1
|
|
51
|
-
uv pip install -e .[cython] --no-build-isolation
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
### Dev environment (uv + ruff + ty)
|
|
55
|
-
```bash
|
|
56
|
-
uv venv --python 3.11 .venv
|
|
57
|
-
uv sync --dev
|
|
58
|
-
uv run ruff check dxcam
|
|
59
|
-
uv run ty check dxcam
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
### API docs (pdoc)
|
|
63
|
-
Generate autodocs for the public API surface:
|
|
64
|
-
```bash
|
|
65
|
-
uv run pdoc -d google -o site dxcam dxcam.dxcam dxcam.types
|
|
66
|
-
```
|
|
40
|
+
### From source
|
|
41
|
+
Please refer to [CONTRIBUTING](CONTRIBUTING.md).
|
|
67
42
|
|
|
68
|
-
|
|
69
|
-
|
|
43
|
+
### Contributing / Dev
|
|
44
|
+
Contributions are welcome!
|
|
45
|
+
Development setup and contributor workflow are documented in [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
70
46
|
|
|
71
47
|
## Usage
|
|
72
48
|
Each output (monitor) is associated with one `DXCamera` instance.
|
|
@@ -76,7 +52,7 @@ import dxcam
|
|
|
76
52
|
camera = dxcam.create() # primary output on device 0
|
|
77
53
|
```
|
|
78
54
|
|
|
79
|
-
To specify
|
|
55
|
+
To specify backends:
|
|
80
56
|
```python
|
|
81
57
|
camera = dxcam.create(
|
|
82
58
|
backend="dxgi", # default Desktop Duplication backend
|
|
@@ -88,7 +64,7 @@ camera = dxcam.create(
|
|
|
88
64
|
```python
|
|
89
65
|
frame = camera.grab()
|
|
90
66
|
```
|
|
91
|
-
`grab()` returns a `numpy.ndarray`. `None` if no new frame is available since the last capture (for backward compatibility); use `new_frame_only=False` to
|
|
67
|
+
`grab()` returns a `numpy.ndarray`. `None` if no new frame is available since the last capture (for backward compatibility); use `camera.grab(new_frame_only=False)` to make dxcam always return the latest frame.
|
|
92
68
|
|
|
93
69
|
Use `copy=False` (or `camera.grab_view()`) for a zero-copy view. This is faster, but the returned buffer can be overwritten by later captures.
|
|
94
70
|
|
|
@@ -122,6 +98,24 @@ Useful variants:
|
|
|
122
98
|
|
|
123
99
|
> When `start()` capture is running, calling `grab()` reads from the in-memory ring buffer instead of directly polling DXGI.
|
|
124
100
|
|
|
101
|
+
### Safely Releasing Resources
|
|
102
|
+
`release()` stops capture, frees buffers, and releases capture resources.
|
|
103
|
+
After `release()`, the same instance cannot be reused.
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
camera = dxcam.create(output_idx=0, output_color="BGR")
|
|
107
|
+
camera.release()
|
|
108
|
+
# camera.start() # raises RuntimeError
|
|
109
|
+
```
|
|
110
|
+
Equivalently you can use context manager:
|
|
111
|
+
```python
|
|
112
|
+
with dxcam.create() as camera:
|
|
113
|
+
frame = camera.grab()
|
|
114
|
+
# resource released automatically
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**Full API Docs:** [https://ra1nty.github.io/DXcam/](https://ra1nty.github.io/DXcam/)
|
|
118
|
+
|
|
125
119
|
## Advanced Usage and Remarks
|
|
126
120
|
### Multiple monitors / GPUs
|
|
127
121
|
```python
|
|
@@ -212,12 +206,13 @@ DXcam supports two capture backends:
|
|
|
212
206
|
|
|
213
207
|
Use it like this:
|
|
214
208
|
```python
|
|
215
|
-
camera = dxcam.create(backend="dxgi")
|
|
209
|
+
camera = dxcam.create(backend="dxgi")
|
|
216
210
|
camera = dxcam.create(backend="winrt")
|
|
217
211
|
```
|
|
218
212
|
|
|
219
213
|
Guideline:
|
|
220
|
-
-
|
|
214
|
+
- If you need cursor rendering, use `winrt`.
|
|
215
|
+
- Start with `dxgi` for most workloads, especially one-shot grab.
|
|
221
216
|
- Try `winrt` if it performs better on your machine or fits your app constraints.
|
|
222
217
|
|
|
223
218
|
### Processor Backend
|
|
@@ -248,16 +243,6 @@ If `processor_backend="numpy"` is selected but compiled kernels are unavailable,
|
|
|
248
243
|
DXcam logs a warning and falls back to `cv2` behavior. In that fallback path,
|
|
249
244
|
install OpenCV for non-`BGRA` output modes.
|
|
250
245
|
|
|
251
|
-
### Safely Releasing Resources
|
|
252
|
-
`release()` stops capture, frees buffers, and releases capture resources.
|
|
253
|
-
After `release()`, the same instance cannot be reused.
|
|
254
|
-
|
|
255
|
-
```python
|
|
256
|
-
camera = dxcam.create(output_idx=0, output_color="BGR")
|
|
257
|
-
camera.release()
|
|
258
|
-
# camera.start() # raises RuntimeError
|
|
259
|
-
```
|
|
260
|
-
|
|
261
246
|
## Benchmarks
|
|
262
247
|
When using a similar logic (only capture newly rendered frames) running on a 240fps output, ```DXCam, python-mss, D3DShot``` benchmarked as follow:
|
|
263
248
|
|
|
@@ -274,16 +259,9 @@ The benchmark is across 5 runs, with a light-moderate usage on my PC (5900X + 30
|
|
|
274
259
|
| 60fps | 61.71, 0.26 :checkered_flag: | N/A | 47.11, 1.33 |
|
|
275
260
|
| 30fps | 30.08, 0.02 :checkered_flag: | N/A | 21.24, 0.17 |
|
|
276
261
|
|
|
277
|
-
Processor backend comparison helper:
|
|
278
|
-
```bash
|
|
279
|
-
python benchmarks/dxcam_processor_compare.py --backend dxgi --target-fps 120 --target-frames 1000
|
|
280
|
-
python benchmarks/dxcam_capture.py --backend dxgi --processor-backend numpy
|
|
281
|
-
python benchmarks/numpy_processor_micro.py --width 3840 --height 2160 --modes RGB --variants process into --processor-backends cv2 numpy
|
|
282
|
-
```
|
|
283
|
-
|
|
284
262
|
|
|
285
263
|
## Work Referenced
|
|
286
264
|
|
|
287
265
|
[OBS Studio](https://github.com/obsproject/obs-studio) - implementation ideas and references.
|
|
288
266
|
|
|
289
|
-
[D3DShot](https://github.com/SerpentAI/D3DShot/) : DXcam
|
|
267
|
+
[D3DShot](https://github.com/SerpentAI/D3DShot/) : DXcam borrowed some ctypes header from the no-longer maintained D3DShot.
|
|
@@ -82,9 +82,9 @@ class Singleton(type):
|
|
|
82
82
|
class DXFactory(metaclass=Singleton):
|
|
83
83
|
"""Factory that owns device/output discovery and camera singletons."""
|
|
84
84
|
|
|
85
|
-
_camera_instances: weakref.WeakValueDictionary[
|
|
86
|
-
|
|
87
|
-
)
|
|
85
|
+
_camera_instances: weakref.WeakValueDictionary[
|
|
86
|
+
tuple[int, int, CaptureBackend], DXCamera
|
|
87
|
+
] = weakref.WeakValueDictionary()
|
|
88
88
|
|
|
89
89
|
def __init__(self) -> None:
|
|
90
90
|
p_adapters = enum_dxgi_adapters()
|
|
@@ -104,7 +104,7 @@ class DXFactory(metaclass=Singleton):
|
|
|
104
104
|
output_idx: int | None = None,
|
|
105
105
|
region: Region | None = None,
|
|
106
106
|
output_color: ColorMode = "RGB",
|
|
107
|
-
max_buffer_len: int =
|
|
107
|
+
max_buffer_len: int = 8,
|
|
108
108
|
backend: CaptureBackend = "dxgi",
|
|
109
109
|
processor_backend: ProcessorBackend = "cv2",
|
|
110
110
|
) -> DXCamera:
|
|
@@ -122,7 +122,9 @@ class DXFactory(metaclass=Singleton):
|
|
|
122
122
|
if metadata and metadata[1]
|
|
123
123
|
]
|
|
124
124
|
if not primary_output_indices:
|
|
125
|
-
raise RuntimeError(
|
|
125
|
+
raise RuntimeError(
|
|
126
|
+
f"No primary output found for device index {device_idx}"
|
|
127
|
+
)
|
|
126
128
|
output_idx = primary_output_indices[0]
|
|
127
129
|
instance_key = (device_idx, output_idx, backend)
|
|
128
130
|
existing_camera = self._camera_instances.get(instance_key)
|
|
@@ -225,7 +227,7 @@ def create(
|
|
|
225
227
|
output_idx: int | None = None,
|
|
226
228
|
region: Region | None = None,
|
|
227
229
|
output_color: ColorMode = "RGB",
|
|
228
|
-
max_buffer_len: int =
|
|
230
|
+
max_buffer_len: int = 8,
|
|
229
231
|
backend: CaptureBackend = "dxgi",
|
|
230
232
|
processor_backend: ProcessorBackend = "cv2",
|
|
231
233
|
) -> DXCamera:
|
|
@@ -5,8 +5,10 @@ from .d3d11 import ID3D11Device
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
DXGI_ERROR_ACCESS_LOST = 0x887A0026
|
|
8
|
+
DXGI_ERROR_DEVICE_REMOVED = 0x887A0005
|
|
8
9
|
DXGI_ERROR_NOT_FOUND = 0x887A0002
|
|
9
10
|
DXGI_ERROR_SESSION_DISCONNECTED = 0x887A0028
|
|
11
|
+
DXGI_ERROR_UNSUPPORTED = 0x887A0004
|
|
10
12
|
DXGI_ERROR_WAIT_TIMEOUT = 0x887A0027
|
|
11
13
|
|
|
12
14
|
DXGI_OUTDUPL_FLAG_NONE = 0x0
|
|
@@ -29,6 +29,17 @@ _BACKEND_CREATORS: dict[
|
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
def normalize_backend_name(backend: str) -> CaptureBackend:
|
|
32
|
+
"""Normalize and validate a capture backend name.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
backend: Backend name provided by user input.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Lower-cased validated backend literal (``"dxgi"`` or ``"winrt"``).
|
|
39
|
+
|
|
40
|
+
Raises:
|
|
41
|
+
ValueError: If ``backend`` is not a supported capture backend.
|
|
42
|
+
"""
|
|
32
43
|
normalized = backend.lower()
|
|
33
44
|
if normalized not in _SUPPORTED_BACKENDS:
|
|
34
45
|
supported = ", ".join(_SUPPORTED_BACKENDS)
|
|
@@ -42,6 +53,19 @@ def create_backend_duplicator(
|
|
|
42
53
|
output: Output,
|
|
43
54
|
device: Device,
|
|
44
55
|
) -> Any:
|
|
56
|
+
"""Create a backend-specific duplicator instance.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
backend: Selected capture backend.
|
|
60
|
+
output: Output descriptor to capture from.
|
|
61
|
+
device: Device descriptor associated with ``output``.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
A duplicator instance that implements the capture backend contract.
|
|
65
|
+
|
|
66
|
+
Raises:
|
|
67
|
+
ValueError: If ``backend`` has no registered factory.
|
|
68
|
+
"""
|
|
45
69
|
creator = _BACKEND_CREATORS.get(backend)
|
|
46
70
|
if creator is None:
|
|
47
71
|
# Defensive fallback in case literals are expanded without wiring.
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from _thread import LockType
|
|
4
|
+
from threading import Event
|
|
5
|
+
from typing import Callable
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from dxcam.core.capture_runtime import CaptureRuntime
|
|
10
|
+
from dxcam.types import Frame, Region
|
|
11
|
+
|
|
12
|
+
GrabIntoFn = Callable[[Region, Frame], tuple[bool, int, int, int]]
|
|
13
|
+
ProcessStagingFrameFn = Callable[[int, int], Frame]
|
|
14
|
+
HandleFrameSizeChangeFn = Callable[[int, int], None]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CaptureLoopRunner:
|
|
18
|
+
"""Runs one capture-loop iteration and updates the ring buffer."""
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
*,
|
|
23
|
+
lock: LockType,
|
|
24
|
+
frame_available_event: Event,
|
|
25
|
+
runtime: CaptureRuntime,
|
|
26
|
+
grab_into: GrabIntoFn,
|
|
27
|
+
process_staging_frame: ProcessStagingFrameFn,
|
|
28
|
+
handle_frame_size_change: HandleFrameSizeChangeFn,
|
|
29
|
+
) -> None:
|
|
30
|
+
self._lock = lock
|
|
31
|
+
self._frame_available_event = frame_available_event
|
|
32
|
+
self._runtime = runtime
|
|
33
|
+
self._grab_into = grab_into
|
|
34
|
+
self._process_staging_frame = process_staging_frame
|
|
35
|
+
self._handle_frame_size_change = handle_frame_size_change
|
|
36
|
+
|
|
37
|
+
def run_once(self, *, region: Region, video_mode: bool) -> None:
|
|
38
|
+
with self._lock:
|
|
39
|
+
write_slot = self._runtime.reserve_write_slot()
|
|
40
|
+
if write_slot is None:
|
|
41
|
+
return
|
|
42
|
+
write_idx, write_dst = write_slot
|
|
43
|
+
|
|
44
|
+
captured, frame_ticks, frame_width, frame_height = self._grab_into(
|
|
45
|
+
region,
|
|
46
|
+
write_dst,
|
|
47
|
+
)
|
|
48
|
+
if captured:
|
|
49
|
+
with self._lock:
|
|
50
|
+
if self._runtime.commit_write(write_idx, frame_ticks):
|
|
51
|
+
self._frame_available_event.set()
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
if frame_width > 0 and frame_height > 0:
|
|
55
|
+
frame = self._process_staging_frame(frame_width, frame_height)
|
|
56
|
+
with self._lock:
|
|
57
|
+
current_shape = self._runtime.current_frame_shape()
|
|
58
|
+
if current_shape is None:
|
|
59
|
+
return
|
|
60
|
+
current_height, current_width = current_shape
|
|
61
|
+
if frame.shape[0] != current_height or frame.shape[1] != current_width:
|
|
62
|
+
self._handle_frame_size_change(frame.shape[0], frame.shape[1])
|
|
63
|
+
|
|
64
|
+
write_slot = self._runtime.reserve_write_slot()
|
|
65
|
+
if write_slot is None:
|
|
66
|
+
return
|
|
67
|
+
write_idx, write_dst = write_slot
|
|
68
|
+
|
|
69
|
+
np.copyto(write_dst, frame)
|
|
70
|
+
with self._lock:
|
|
71
|
+
if self._runtime.commit_write(write_idx, frame_ticks):
|
|
72
|
+
self._frame_available_event.set()
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
if video_mode:
|
|
76
|
+
with self._lock:
|
|
77
|
+
duplicate_copy = self._runtime.reserve_duplicate_copy()
|
|
78
|
+
if duplicate_copy is None:
|
|
79
|
+
return
|
|
80
|
+
write_idx, write_dst, previous_dst, frame_ticks = duplicate_copy
|
|
81
|
+
np.copyto(write_dst, previous_dst)
|
|
82
|
+
with self._lock:
|
|
83
|
+
if self._runtime.commit_write(write_idx, frame_ticks):
|
|
84
|
+
self._frame_available_event.set()
|
|
85
|
+
return
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from numpy.typing import NDArray
|
|
7
|
+
|
|
8
|
+
from dxcam.types import Frame
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class CaptureRuntime:
|
|
13
|
+
"""Ring-buffer runtime state for threaded capture.
|
|
14
|
+
|
|
15
|
+
In threaded mode, producer flow is:
|
|
16
|
+
1) reserve slot under lock
|
|
17
|
+
2) copy frame data outside lock
|
|
18
|
+
3) commit metadata under lock
|
|
19
|
+
|
|
20
|
+
Callers must ensure frame-buffer clear/realloc never runs concurrently from
|
|
21
|
+
a non-producer thread while producer is active.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
max_buffer_len: int
|
|
25
|
+
channel_size: int
|
|
26
|
+
frame_buffer: Frame | None = None
|
|
27
|
+
frame_time_ticks: NDArray[np.int64] | None = None
|
|
28
|
+
head: int = 0
|
|
29
|
+
tail: int = 0
|
|
30
|
+
full: bool = False
|
|
31
|
+
has_frame: bool = False
|
|
32
|
+
frame_count: int = 0
|
|
33
|
+
latest_frame_ticks: int | None = None
|
|
34
|
+
|
|
35
|
+
def allocate_for_shape(self, frame_height: int, frame_width: int) -> None:
|
|
36
|
+
frame_shape = (frame_height, frame_width, self.channel_size)
|
|
37
|
+
self.frame_buffer = np.empty(
|
|
38
|
+
(self.max_buffer_len, *frame_shape),
|
|
39
|
+
dtype=np.uint8,
|
|
40
|
+
)
|
|
41
|
+
self.frame_time_ticks = np.zeros(self.max_buffer_len, dtype=np.int64)
|
|
42
|
+
self.head = 0
|
|
43
|
+
self.tail = 0
|
|
44
|
+
self.full = False
|
|
45
|
+
self.has_frame = False
|
|
46
|
+
self.frame_count = 0
|
|
47
|
+
self.latest_frame_ticks = None
|
|
48
|
+
|
|
49
|
+
def clear(self) -> None:
|
|
50
|
+
self.frame_buffer = None
|
|
51
|
+
self.frame_time_ticks = None
|
|
52
|
+
self.head = 0
|
|
53
|
+
self.tail = 0
|
|
54
|
+
self.full = False
|
|
55
|
+
self.has_frame = False
|
|
56
|
+
self.frame_count = 0
|
|
57
|
+
self.latest_frame_ticks = None
|
|
58
|
+
|
|
59
|
+
def current_frame_shape(self) -> tuple[int, int] | None:
|
|
60
|
+
if self.frame_buffer is None:
|
|
61
|
+
return None
|
|
62
|
+
return self.frame_buffer.shape[1], self.frame_buffer.shape[2]
|
|
63
|
+
|
|
64
|
+
def reserve_write_slot(self) -> tuple[int, Frame] | None:
|
|
65
|
+
if self.frame_buffer is None:
|
|
66
|
+
return None
|
|
67
|
+
write_idx = self.head
|
|
68
|
+
return write_idx, self.frame_buffer[write_idx]
|
|
69
|
+
|
|
70
|
+
def reserve_duplicate_copy(
|
|
71
|
+
self,
|
|
72
|
+
) -> tuple[int, Frame, Frame, int] | None:
|
|
73
|
+
if (
|
|
74
|
+
self.frame_buffer is None
|
|
75
|
+
or self.frame_time_ticks is None
|
|
76
|
+
or not self.has_frame
|
|
77
|
+
):
|
|
78
|
+
return None
|
|
79
|
+
write_idx = self.head
|
|
80
|
+
previous_idx = (self.head - 1) % self.max_buffer_len
|
|
81
|
+
dst = self.frame_buffer[write_idx]
|
|
82
|
+
src = self.frame_buffer[previous_idx]
|
|
83
|
+
frame_ticks = int(self.frame_time_ticks[previous_idx])
|
|
84
|
+
return write_idx, dst, src, frame_ticks
|
|
85
|
+
|
|
86
|
+
def commit_write(self, write_idx: int, frame_ticks: int) -> bool:
|
|
87
|
+
if self.frame_buffer is None or self.frame_time_ticks is None:
|
|
88
|
+
return False
|
|
89
|
+
if write_idx != self.head:
|
|
90
|
+
return False
|
|
91
|
+
if self.full:
|
|
92
|
+
self.tail = (self.tail + 1) % self.max_buffer_len
|
|
93
|
+
self.frame_time_ticks[write_idx] = frame_ticks
|
|
94
|
+
self.head = (write_idx + 1) % self.max_buffer_len
|
|
95
|
+
self.latest_frame_ticks = frame_ticks
|
|
96
|
+
self.frame_count += 1
|
|
97
|
+
self.full = self.head == self.tail
|
|
98
|
+
self.has_frame = True
|
|
99
|
+
return True
|
|
100
|
+
|
|
101
|
+
def peek_latest(self, copy: bool = True) -> Frame | None:
|
|
102
|
+
if self.frame_buffer is None or not self.has_frame:
|
|
103
|
+
return None
|
|
104
|
+
latest_idx = (self.head - 1) % self.max_buffer_len
|
|
105
|
+
frame = self.frame_buffer[latest_idx]
|
|
106
|
+
return np.array(frame, copy=True) if copy else frame
|
|
107
|
+
|
|
108
|
+
def peek_latest_with_ticks(
|
|
109
|
+
self,
|
|
110
|
+
copy: bool = True,
|
|
111
|
+
) -> tuple[Frame, int] | None:
|
|
112
|
+
if (
|
|
113
|
+
self.frame_buffer is None
|
|
114
|
+
or self.frame_time_ticks is None
|
|
115
|
+
or not self.has_frame
|
|
116
|
+
):
|
|
117
|
+
return None
|
|
118
|
+
latest_idx = (self.head - 1) % self.max_buffer_len
|
|
119
|
+
frame = self.frame_buffer[latest_idx]
|
|
120
|
+
frame_ticks = int(self.frame_time_ticks[latest_idx])
|
|
121
|
+
if copy:
|
|
122
|
+
return np.array(frame, copy=True), frame_ticks
|
|
123
|
+
return frame, frame_ticks
|
|
@@ -14,9 +14,13 @@ from dxcam._libs.d3d11 import (
|
|
|
14
14
|
)
|
|
15
15
|
from dxcam._libs.dxgi import (
|
|
16
16
|
DXGI_ADAPTER_DESC1,
|
|
17
|
-
DXGI_ERROR_NOT_FOUND,
|
|
18
17
|
IDXGIOutput1,
|
|
19
18
|
)
|
|
19
|
+
from dxcam.core.dxgi_errors import (
|
|
20
|
+
DXGITransientContext,
|
|
21
|
+
com_error_hresult_u32,
|
|
22
|
+
is_transient_hresult,
|
|
23
|
+
)
|
|
20
24
|
|
|
21
25
|
|
|
22
26
|
@dataclass
|
|
@@ -70,7 +74,11 @@ class Device:
|
|
|
70
74
|
p_outputs.append(p_output)
|
|
71
75
|
i += 1
|
|
72
76
|
except comtypes.COMError as ce:
|
|
73
|
-
|
|
77
|
+
hresult_u32 = com_error_hresult_u32(ce)
|
|
78
|
+
if is_transient_hresult(
|
|
79
|
+
hresult_u32,
|
|
80
|
+
DXGITransientContext.ENUM_OUTPUTS,
|
|
81
|
+
):
|
|
74
82
|
break
|
|
75
83
|
raise
|
|
76
84
|
return p_outputs
|