dxcam 0.1.0.dev1__tar.gz → 0.1.0.dev2__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.
Files changed (28) hide show
  1. {dxcam-0.1.0.dev1 → dxcam-0.1.0.dev2}/PKG-INFO +10 -6
  2. {dxcam-0.1.0.dev1 → dxcam-0.1.0.dev2}/README.md +8 -4
  3. {dxcam-0.1.0.dev1 → dxcam-0.1.0.dev2}/dxcam/core/duplicator.py +1 -1
  4. {dxcam-0.1.0.dev1 → dxcam-0.1.0.dev2}/dxcam/dxcam.py +73 -10
  5. {dxcam-0.1.0.dev1 → dxcam-0.1.0.dev2}/dxcam/util/timer.py +4 -0
  6. {dxcam-0.1.0.dev1 → dxcam-0.1.0.dev2}/dxcam.egg-info/PKG-INFO +10 -6
  7. {dxcam-0.1.0.dev1 → dxcam-0.1.0.dev2}/pyproject.toml +3 -3
  8. {dxcam-0.1.0.dev1 → dxcam-0.1.0.dev2}/LICENSE +0 -0
  9. {dxcam-0.1.0.dev1 → dxcam-0.1.0.dev2}/dxcam/__init__.py +0 -0
  10. {dxcam-0.1.0.dev1 → dxcam-0.1.0.dev2}/dxcam/_libs/__init__.py +0 -0
  11. {dxcam-0.1.0.dev1 → dxcam-0.1.0.dev2}/dxcam/_libs/d3d11.py +0 -0
  12. {dxcam-0.1.0.dev1 → dxcam-0.1.0.dev2}/dxcam/_libs/dxgi.py +0 -0
  13. {dxcam-0.1.0.dev1 → dxcam-0.1.0.dev2}/dxcam/_libs/user32.py +0 -0
  14. {dxcam-0.1.0.dev1 → dxcam-0.1.0.dev2}/dxcam/core/__init__.py +0 -0
  15. {dxcam-0.1.0.dev1 → dxcam-0.1.0.dev2}/dxcam/core/device.py +0 -0
  16. {dxcam-0.1.0.dev1 → dxcam-0.1.0.dev2}/dxcam/core/output.py +0 -0
  17. {dxcam-0.1.0.dev1 → dxcam-0.1.0.dev2}/dxcam/core/stagesurf.py +0 -0
  18. {dxcam-0.1.0.dev1 → dxcam-0.1.0.dev2}/dxcam/processor/__init__.py +0 -0
  19. {dxcam-0.1.0.dev1 → dxcam-0.1.0.dev2}/dxcam/processor/base.py +0 -0
  20. {dxcam-0.1.0.dev1 → dxcam-0.1.0.dev2}/dxcam/processor/numpy_processor.py +0 -0
  21. {dxcam-0.1.0.dev1 → dxcam-0.1.0.dev2}/dxcam/types.py +0 -0
  22. {dxcam-0.1.0.dev1 → dxcam-0.1.0.dev2}/dxcam/util/__init__.py +0 -0
  23. {dxcam-0.1.0.dev1 → dxcam-0.1.0.dev2}/dxcam/util/io.py +0 -0
  24. {dxcam-0.1.0.dev1 → dxcam-0.1.0.dev2}/dxcam.egg-info/SOURCES.txt +0 -0
  25. {dxcam-0.1.0.dev1 → dxcam-0.1.0.dev2}/dxcam.egg-info/dependency_links.txt +0 -0
  26. {dxcam-0.1.0.dev1 → dxcam-0.1.0.dev2}/dxcam.egg-info/requires.txt +0 -0
  27. {dxcam-0.1.0.dev1 → dxcam-0.1.0.dev2}/dxcam.egg-info/top_level.txt +0 -0
  28. {dxcam-0.1.0.dev1 → dxcam-0.1.0.dev2}/setup.cfg +0 -0
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dxcam
3
- Version: 0.1.0.dev1
3
+ Version: 0.1.0.dev2
4
4
  Summary: A Python high-performance screenshot library for Windows using Desktop Duplication API
5
5
  Author: ra1nty
6
6
  License-Expression: MIT
7
7
  Project-URL: Source, https://github.com/ra1nty/DXcam
8
8
  Project-URL: Tracker, https://github.com/ra1nty/DXcam/issues
9
9
  Keywords: screen,screenshot,screencapture,screengrab,windows
10
- Classifier: Development Status :: 2 - Pre-Alpha
10
+ Classifier: Development Status :: 4 - Beta
11
11
  Classifier: Intended Audience :: Developers
12
12
  Classifier: Programming Language :: Python :: 3
13
13
  Classifier: Programming Language :: Python :: 3 :: Only
@@ -20,7 +20,7 @@ Classifier: Operating System :: Microsoft :: Windows :: Windows 11
20
20
  Classifier: Topic :: Multimedia :: Graphics
21
21
  Classifier: Topic :: Multimedia :: Graphics :: Capture
22
22
  Classifier: Topic :: Multimedia :: Graphics :: Capture :: Screen Capture
23
- Requires-Python: >=3.11
23
+ Requires-Python: >=3.10
24
24
  Description-Content-Type: text/markdown
25
25
  License-File: LICENSE
26
26
  Requires-Dist: comtypes
@@ -88,8 +88,9 @@ camera = dxcam.create() # primary output on device 0
88
88
  ```python
89
89
  frame = camera.grab()
90
90
  ```
91
+ `grab()` returns a `numpy.ndarray`. `None` if no new frame is available since the last capture, mainly for backward compatibility. You can use `new_frame_only=False` to change this behavior.
91
92
 
92
- `grab()` returns a `numpy.ndarray`. `None` if no new frame is available since the last capture.
93
+ Use `copy=False` (or `camera.grab_view()`) for a zero-copy view.
93
94
 
94
95
  To capture a region:
95
96
  ```python
@@ -117,6 +118,9 @@ for _ in range(1000):
117
118
  Useful variants:
118
119
  - `camera.get_latest_frame(with_timestamp=True)` -> `(frame, frame_timestamp)` -> return frame timestamp
119
120
  - `camera.get_latest_frame_view()` -> zero-copy view into the frame buffer
121
+ - `camera.grab(copy=False)` / `camera.grab_view()` -> zero-copy latest-frame snapshot
122
+
123
+ ** When `start()` capture is running, calling `grab()` reads from the in-memory ring buffer instead of directly polling DXGI.
120
124
 
121
125
  ## Advanced Usage and Remarks
122
126
  ### Multiple monitors / GPUs
@@ -211,14 +215,14 @@ camera.release()
211
215
  ```
212
216
 
213
217
  ## Benchmarks
214
- When using a similar logic (only captured newly rendered frames), ```DXCam, python-mss, D3DShot``` benchmarked as follow:
218
+ When using a similar logic (only capture newly rendered frames) running on a 240fps output, ```DXCam, python-mss, D3DShot``` benchmarked as follow:
215
219
 
216
220
  | | DXcam | python-mss | D3DShot |
217
221
  |-------------|--------|------------|---------|
218
222
  | Average FPS | 239.19 :checkered_flag: | 75.87 | 118.36 |
219
223
  | Std Dev | 1.25 | 0.5447 | 0.3224 |
220
224
 
221
- The benchmark is across 5 runs, with a light-moderate usage on my PC (5900X + 3090; Chrome ~30tabs, VS Code opened, etc.), I used the [Blur Buster UFO test](https://www.testufo.com/framerates#count=5&background=stars&pps=960) to constantly render 240 fps on my monitor (Zowie 2546K). DXcam captured almost every frame rendered. You will see some benchmarks online claiming 1000+fps capture while most of them is busy-spinning a for loop on a staled frame (no new frame rendered on screen in test scenario).
225
+ The benchmark is across 5 runs, with a light-moderate usage on my PC (5900X + 3090; Chrome ~30tabs, VS Code opened, etc.), I used the [Blur Buster UFO test](https://www.testufo.com/framerates#count=5&background=stars&pps=960) to constantly render 240 fps on my monitor. DXcam captured almost every frame rendered. You will see some benchmarks online claiming 1000+fps capture while most of them is busy-spinning a for loop on a staled frame (no new frame rendered on screen in test scenario).
222
226
 
223
227
  ### For Targeting FPS:
224
228
  | (Target)\\(mean,std) | DXcam | python-mss | D3DShot |
@@ -57,8 +57,9 @@ camera = dxcam.create() # primary output on device 0
57
57
  ```python
58
58
  frame = camera.grab()
59
59
  ```
60
+ `grab()` returns a `numpy.ndarray`. `None` if no new frame is available since the last capture, mainly for backward compatibility. You can use `new_frame_only=False` to change this behavior.
60
61
 
61
- `grab()` returns a `numpy.ndarray`. `None` if no new frame is available since the last capture.
62
+ Use `copy=False` (or `camera.grab_view()`) for a zero-copy view.
62
63
 
63
64
  To capture a region:
64
65
  ```python
@@ -86,6 +87,9 @@ for _ in range(1000):
86
87
  Useful variants:
87
88
  - `camera.get_latest_frame(with_timestamp=True)` -> `(frame, frame_timestamp)` -> return frame timestamp
88
89
  - `camera.get_latest_frame_view()` -> zero-copy view into the frame buffer
90
+ - `camera.grab(copy=False)` / `camera.grab_view()` -> zero-copy latest-frame snapshot
91
+
92
+ ** When `start()` capture is running, calling `grab()` reads from the in-memory ring buffer instead of directly polling DXGI.
89
93
 
90
94
  ## Advanced Usage and Remarks
91
95
  ### Multiple monitors / GPUs
@@ -180,14 +184,14 @@ camera.release()
180
184
  ```
181
185
 
182
186
  ## Benchmarks
183
- When using a similar logic (only captured newly rendered frames), ```DXCam, python-mss, D3DShot``` benchmarked as follow:
187
+ When using a similar logic (only capture newly rendered frames) running on a 240fps output, ```DXCam, python-mss, D3DShot``` benchmarked as follow:
184
188
 
185
189
  | | DXcam | python-mss | D3DShot |
186
190
  |-------------|--------|------------|---------|
187
191
  | Average FPS | 239.19 :checkered_flag: | 75.87 | 118.36 |
188
192
  | Std Dev | 1.25 | 0.5447 | 0.3224 |
189
193
 
190
- The benchmark is across 5 runs, with a light-moderate usage on my PC (5900X + 3090; Chrome ~30tabs, VS Code opened, etc.), I used the [Blur Buster UFO test](https://www.testufo.com/framerates#count=5&background=stars&pps=960) to constantly render 240 fps on my monitor (Zowie 2546K). DXcam captured almost every frame rendered. You will see some benchmarks online claiming 1000+fps capture while most of them is busy-spinning a for loop on a staled frame (no new frame rendered on screen in test scenario).
194
+ The benchmark is across 5 runs, with a light-moderate usage on my PC (5900X + 3090; Chrome ~30tabs, VS Code opened, etc.), I used the [Blur Buster UFO test](https://www.testufo.com/framerates#count=5&background=stars&pps=960) to constantly render 240 fps on my monitor. DXcam captured almost every frame rendered. You will see some benchmarks online claiming 1000+fps capture while most of them is busy-spinning a for loop on a staled frame (no new frame rendered on screen in test scenario).
191
195
 
192
196
  ### For Targeting FPS:
193
197
  | (Target)\\(mean,std) | DXcam | python-mss | D3DShot |
@@ -200,4 +204,4 @@ The benchmark is across 5 runs, with a light-moderate usage on my PC (5900X + 30
200
204
 
201
205
  [OBS Studio](https://github.com/obsproject/obs-studio) - implementation ideas and references.
202
206
 
203
- [D3DShot](https://github.com/SerpentAI/D3DShot/) : DXcam borrows the ctypes header directly from the no-longer maintained D3DShot.
207
+ [D3DShot](https://github.com/SerpentAI/D3DShot/) : DXcam borrows the ctypes header directly from the no-longer maintained D3DShot.
@@ -124,7 +124,7 @@ class Duplicator:
124
124
  res = ctypes.POINTER(IDXGIResource)()
125
125
  try:
126
126
  self.duplicator.AcquireNextFrame(
127
- 1,
127
+ 0,
128
128
  ctypes.byref(info),
129
129
  ctypes.byref(res),
130
130
  )
@@ -87,32 +87,93 @@ class DXCamera:
87
87
  self.__frame_count = 0
88
88
  self.__capture_start_time = 0
89
89
  self.__latest_frame_ticks: int | None = None
90
+ self.__last_grab_entry: tuple[Region, Frame] | None = None
90
91
 
91
- def grab(self, region: Region | None = None) -> Frame | None:
92
+ def grab(
93
+ self,
94
+ region: Region | None = None,
95
+ copy: bool = True,
96
+ new_frame_only: bool = True,
97
+ ) -> Frame | None:
98
+ """Grab one frame.
99
+
100
+ Ownership contract:
101
+ - ``copy=True`` returns caller-owned memory.
102
+ - ``copy=False`` may return internal memory reused by future grabs.
103
+ - ``new_frame_only=True`` returns ``None`` when no new frame is available
104
+ in one-shot mode.
105
+ - ``new_frame_only=False`` falls back to the last successfully grabbed
106
+ frame for the same region in one-shot mode.
107
+
108
+ When capture is running (``start()``), this reads from the ring buffer
109
+ instead of touching DXGI objects directly. ``new_frame_only`` does not
110
+ apply while capture is running.
111
+ """
92
112
  self._ensure_not_released()
113
+ if self.is_capturing:
114
+ if region is not None and region != self.region:
115
+ raise ValueError(
116
+ "grab(region=...) is not supported while capture is running. "
117
+ "Use start(region=...) to configure capture region."
118
+ )
119
+ return self._peek_latest_buffered_frame(copy=copy)
93
120
  if region is None:
94
121
  region = self.region
95
122
  else:
96
123
  self._validate_region(region)
97
- frame = self._grab(region)
124
+ frame = self._grab(region, copy=copy, new_frame_only=new_frame_only)
98
125
  return frame
99
126
 
100
- def _grab(self, region: Region) -> Frame | None:
127
+ def grab_view(self, region: Region | None = None) -> Frame | None:
128
+ """Zero-copy variant of ``grab()``."""
129
+ return self.grab(region=region, copy=False)
130
+
131
+ def _peek_latest_buffered_frame(self, copy: bool = True) -> Frame | None:
132
+ with self.__lock:
133
+ if self.__frame_buffer is None or not self.__has_frame:
134
+ return None
135
+ latest_idx = (self.__head - 1) % self.max_buffer_len
136
+ ret = self.__frame_buffer[latest_idx]
137
+ return np.array(ret, copy=True) if copy else ret
138
+
139
+ def _set_cached_grab_frame(self, region: Region, frame: Frame) -> None:
140
+ self.__last_grab_entry = (region, frame)
141
+
142
+ def _get_cached_grab_frame(
143
+ self, region: Region, copy: bool = True
144
+ ) -> Frame | None:
145
+ entry = self.__last_grab_entry
146
+ if entry is None:
147
+ return None
148
+ cached_region, cached = entry
149
+ if cached_region != region:
150
+ return None
151
+ return np.array(cached, copy=True) if copy else cached
152
+
153
+ def _grab(
154
+ self,
155
+ region: Region,
156
+ copy: bool = True,
157
+ new_frame_only: bool = True,
158
+ ) -> Frame | None:
101
159
  if not self._duplicator.update_frame():
102
160
  self._on_output_change()
103
- return None
161
+ if new_frame_only:
162
+ return None
163
+ return self._get_cached_grab_frame(region=region, copy=copy)
104
164
  if not self._duplicator.updated:
105
- return None
165
+ if new_frame_only:
166
+ return None
167
+ return self._get_cached_grab_frame(region=region, copy=copy)
106
168
 
107
169
  frame_width, frame_height = self._copy_region_to_stage(region)
108
170
  frame = self._process_staging_frame(
109
171
  frame_width=frame_width, frame_height=frame_height
110
172
  )
111
-
112
- if not self.is_capturing:
113
- self._duplicator.release_frame()
114
- return np.array(frame, copy=True)
115
- return frame
173
+ result = np.array(frame, copy=True) if copy else frame
174
+ if not new_frame_only:
175
+ self._set_cached_grab_frame(region=region, frame=result)
176
+ return result
116
177
 
117
178
  def _grab_into(self, region: Region, dst: Frame) -> tuple[bool, int, int, int]:
118
179
  """Capture into ``dst`` and return ``(captured, frame_ticks, width, height)``.
@@ -203,6 +264,7 @@ class DXCamera:
203
264
  time.sleep(0.1) # Wait for Display mode change (Access Lost)
204
265
  self._duplicator.release()
205
266
  self._stagesurf.release()
267
+ self.__last_grab_entry = None
206
268
  self._output.update_desc()
207
269
  self.width, self.height = self._output.resolution
208
270
  if not self._region_set_by_user:
@@ -303,6 +365,7 @@ class DXCamera:
303
365
  self.__frame_time_ticks = None
304
366
  self.__frame_count = 0
305
367
  self.__latest_frame_ticks = None
368
+ self.__last_grab_entry = None
306
369
  self.__head = 0
307
370
  self.__tail = 0
308
371
  self.__full = False
@@ -8,6 +8,10 @@ class _Timer:
8
8
  def __init__(self):
9
9
  self.period_s: float = 0.0
10
10
  self._next_tick: Optional[float] = None
11
+ # Declared on the base type so static checkers can validate both
12
+ # modern and legacy timer paths through a shared interface.
13
+ self.cancelled: bool = False
14
+ self._handle: Optional[int] = None
11
15
 
12
16
 
13
17
  if sys.version_info >= (3, 11):
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dxcam
3
- Version: 0.1.0.dev1
3
+ Version: 0.1.0.dev2
4
4
  Summary: A Python high-performance screenshot library for Windows using Desktop Duplication API
5
5
  Author: ra1nty
6
6
  License-Expression: MIT
7
7
  Project-URL: Source, https://github.com/ra1nty/DXcam
8
8
  Project-URL: Tracker, https://github.com/ra1nty/DXcam/issues
9
9
  Keywords: screen,screenshot,screencapture,screengrab,windows
10
- Classifier: Development Status :: 2 - Pre-Alpha
10
+ Classifier: Development Status :: 4 - Beta
11
11
  Classifier: Intended Audience :: Developers
12
12
  Classifier: Programming Language :: Python :: 3
13
13
  Classifier: Programming Language :: Python :: 3 :: Only
@@ -20,7 +20,7 @@ Classifier: Operating System :: Microsoft :: Windows :: Windows 11
20
20
  Classifier: Topic :: Multimedia :: Graphics
21
21
  Classifier: Topic :: Multimedia :: Graphics :: Capture
22
22
  Classifier: Topic :: Multimedia :: Graphics :: Capture :: Screen Capture
23
- Requires-Python: >=3.11
23
+ Requires-Python: >=3.10
24
24
  Description-Content-Type: text/markdown
25
25
  License-File: LICENSE
26
26
  Requires-Dist: comtypes
@@ -88,8 +88,9 @@ camera = dxcam.create() # primary output on device 0
88
88
  ```python
89
89
  frame = camera.grab()
90
90
  ```
91
+ `grab()` returns a `numpy.ndarray`. `None` if no new frame is available since the last capture, mainly for backward compatibility. You can use `new_frame_only=False` to change this behavior.
91
92
 
92
- `grab()` returns a `numpy.ndarray`. `None` if no new frame is available since the last capture.
93
+ Use `copy=False` (or `camera.grab_view()`) for a zero-copy view.
93
94
 
94
95
  To capture a region:
95
96
  ```python
@@ -117,6 +118,9 @@ for _ in range(1000):
117
118
  Useful variants:
118
119
  - `camera.get_latest_frame(with_timestamp=True)` -> `(frame, frame_timestamp)` -> return frame timestamp
119
120
  - `camera.get_latest_frame_view()` -> zero-copy view into the frame buffer
121
+ - `camera.grab(copy=False)` / `camera.grab_view()` -> zero-copy latest-frame snapshot
122
+
123
+ ** When `start()` capture is running, calling `grab()` reads from the in-memory ring buffer instead of directly polling DXGI.
120
124
 
121
125
  ## Advanced Usage and Remarks
122
126
  ### Multiple monitors / GPUs
@@ -211,14 +215,14 @@ camera.release()
211
215
  ```
212
216
 
213
217
  ## Benchmarks
214
- When using a similar logic (only captured newly rendered frames), ```DXCam, python-mss, D3DShot``` benchmarked as follow:
218
+ When using a similar logic (only capture newly rendered frames) running on a 240fps output, ```DXCam, python-mss, D3DShot``` benchmarked as follow:
215
219
 
216
220
  | | DXcam | python-mss | D3DShot |
217
221
  |-------------|--------|------------|---------|
218
222
  | Average FPS | 239.19 :checkered_flag: | 75.87 | 118.36 |
219
223
  | Std Dev | 1.25 | 0.5447 | 0.3224 |
220
224
 
221
- The benchmark is across 5 runs, with a light-moderate usage on my PC (5900X + 3090; Chrome ~30tabs, VS Code opened, etc.), I used the [Blur Buster UFO test](https://www.testufo.com/framerates#count=5&background=stars&pps=960) to constantly render 240 fps on my monitor (Zowie 2546K). DXcam captured almost every frame rendered. You will see some benchmarks online claiming 1000+fps capture while most of them is busy-spinning a for loop on a staled frame (no new frame rendered on screen in test scenario).
225
+ The benchmark is across 5 runs, with a light-moderate usage on my PC (5900X + 3090; Chrome ~30tabs, VS Code opened, etc.), I used the [Blur Buster UFO test](https://www.testufo.com/framerates#count=5&background=stars&pps=960) to constantly render 240 fps on my monitor. DXcam captured almost every frame rendered. You will see some benchmarks online claiming 1000+fps capture while most of them is busy-spinning a for loop on a staled frame (no new frame rendered on screen in test scenario).
222
226
 
223
227
  ### For Targeting FPS:
224
228
  | (Target)\\(mean,std) | DXcam | python-mss | D3DShot |
@@ -4,15 +4,15 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "dxcam"
7
- version = "0.1.0.dev1"
7
+ version = "0.1.0.dev2"
8
8
  description = "A Python high-performance screenshot library for Windows using Desktop Duplication API"
9
9
  readme = "README.md"
10
- requires-python = ">=3.11"
10
+ requires-python = ">=3.10"
11
11
  license = "MIT"
12
12
  authors = [{ name = "ra1nty" }]
13
13
  keywords = ["screen", "screenshot", "screencapture", "screengrab", "windows"]
14
14
  classifiers = [
15
- "Development Status :: 2 - Pre-Alpha",
15
+ "Development Status :: 4 - Beta",
16
16
  "Intended Audience :: Developers",
17
17
  "Programming Language :: Python :: 3",
18
18
  "Programming Language :: Python :: 3 :: Only",
File without changes
File without changes
File without changes
File without changes
File without changes