From a70ab3ae5921c65388dfc38839b8c1eccbea37ca Mon Sep 17 00:00:00 2001 From: Agade09 Date: Sat, 15 Apr 2023 10:04:14 +0200 Subject: [PATCH 01/20] Use numpy directly for BGRA->RGB and BGRA->BGR conversion. In profiling a benchmark code this takes processing overhead from ~14% to ~1.4% and ~0.06% respectively --- dxcam/processor/numpy_processor.py | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/dxcam/processor/numpy_processor.py b/dxcam/processor/numpy_processor.py index b781690..d6293f9 100644 --- a/dxcam/processor/numpy_processor.py +++ b/dxcam/processor/numpy_processor.py @@ -13,21 +13,17 @@ def process_cvtcolor(self, image): # only one time process if self.cvtcolor is None: - color_mapping = { - "RGB": cv2.COLOR_BGRA2RGB, - "RGBA": cv2.COLOR_BGRA2RGBA, - "BGR": cv2.COLOR_BGRA2BGR, - "GRAY": cv2.COLOR_BGRA2GRAY, - "BGRA": None, - } - cv2_code = color_mapping[self.color_mode] - if cv2_code is not None: - if cv2_code != cv2.COLOR_BGRA2GRAY: - self.cvtcolor = lambda image: cv2.cvtColor(image, cv2_code) + if self.color_mode!="BGRA": + if self.color_mode=="BGR": + self.cvtcolor = lambda image: image[:,:,:3] #BGRA -> BGR conversion + elif self.color_mode=='RGB': + self.cvtcolor = lambda image: np.flip(image[:,:,:3],axis=-1) #BGRA -> RGB conversion + elif self.color_mode=='RGBA': + self.cvtcolor = lambda image: cv2.cvtColor(image, cv2.COLOR_BGRA2RGBA) else: - self.cvtcolor = lambda image: cv2.cvtColor(image, cv2_code)[ + self.cvtcolor = lambda image: cv2.cvtColor(image, cv2.COLOR_BGRA2GRAY)[ ..., np.newaxis - ] + ] else: return image return self.cvtcolor(image) From b7ac4c2b23bddcf462357d486e3354d6c80ef73c Mon Sep 17 00:00:00 2001 From: Agade09 Date: Sat, 15 Apr 2023 15:06:47 +0200 Subject: [PATCH 02/20] Reduce overhead in ctypes.string_at() when grabbing from a region smaller than the whole screen. The idea is to ask ctypes.string_at for as little memory as possible. Since images are stored in memory with width being the fast index. If we want to grab a 480x640 region from a 1440x2560 screen we can ask ctypes.string_at() for a 480x2560 region. This reduces memory allocation and memcpy overhead in ctypes.string_at(). To grab a 480x640 region out of a 1440x2560 screen the profiler time spent went from ~24% to ~8%. --- dxcam/processor/numpy_processor.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/dxcam/processor/numpy_processor.py b/dxcam/processor/numpy_processor.py index d6293f9..102bbf4 100644 --- a/dxcam/processor/numpy_processor.py +++ b/dxcam/processor/numpy_processor.py @@ -30,13 +30,21 @@ def process_cvtcolor(self, image): def process(self, rect, width, height, region, rotation_angle): pitch = int(rect.Pitch) + ptr = rect.pBits + + if region[3] - region[1] != height: + if rotation_angle in (0, 180): + height = region[3] - region[1] + else: + width = region[3] - region[1] + ptr = ctypes.c_void_p(ctypes.addressof(ptr.contents)+region[1]*pitch)#Pointer arithmetic if rotation_angle in (0, 180): size = pitch * height else: size = pitch * width - buffer = ctypes.string_at(rect.pBits, size) + buffer = ctypes.string_at(ptr, size) pitch = pitch // 4 if rotation_angle in (0, 180): image = np.ndarray((height, pitch, 4), dtype=np.uint8, buffer=buffer) @@ -58,7 +66,7 @@ def process(self, rect, width, height, region, rotation_angle): elif rotation_angle in (90, 270) and pitch != height: image = image[:height, :, :] - if region[2] - region[0] != width or region[3] - region[1] != height: - image = image[region[1] : region[3], region[0] : region[2], :] + if region[2] - region[0] != image.shape[1]: + image = image[:, region[0] : region[2], :] return image From 2833caf9ff3da3d039696f10842cd55d09505056 Mon Sep 17 00:00:00 2001 From: Agade09 Date: Sun, 16 Apr 2023 12:47:26 +0200 Subject: [PATCH 03/20] Fixed regression where numpy_processor was no longer correct for rotations of (90,180,270) --- dxcam/processor/numpy_processor.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/dxcam/processor/numpy_processor.py b/dxcam/processor/numpy_processor.py index 102bbf4..dd14878 100644 --- a/dxcam/processor/numpy_processor.py +++ b/dxcam/processor/numpy_processor.py @@ -34,10 +34,12 @@ def process(self, rect, width, height, region, rotation_angle): if region[3] - region[1] != height: if rotation_angle in (0, 180): + offset = (region[1] if rotation_angle==0 else height-region[3])*pitch height = region[3] - region[1] else: - width = region[3] - region[1] - ptr = ctypes.c_void_p(ctypes.addressof(ptr.contents)+region[1]*pitch)#Pointer arithmetic + offset = (region[0] if rotation_angle==270 else width-region[2])*pitch + width = region[2] - region[0] + ptr = ctypes.c_void_p(ctypes.addressof(ptr.contents)+offset)#Pointer arithmetic if rotation_angle in (0, 180): size = pitch * height @@ -66,6 +68,8 @@ def process(self, rect, width, height, region, rotation_angle): elif rotation_angle in (90, 270) and pitch != height: image = image[:height, :, :] + if region[3] - region[1] != image.shape[0]: + image = image[region[1] : region[3], :, :] if region[2] - region[0] != image.shape[1]: image = image[:, region[0] : region[2], :] From 94bec3f0c3fde18de9a7cc1d70a5afe6b89a97e7 Mon Sep 17 00:00:00 2001 From: Agade09 Date: Sun, 16 Apr 2023 16:25:19 +0200 Subject: [PATCH 04/20] Fixed not applying performance optimization if a region is defined where its width region matches the screen's width. --- dxcam/processor/numpy_processor.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/dxcam/processor/numpy_processor.py b/dxcam/processor/numpy_processor.py index dd14878..054b038 100644 --- a/dxcam/processor/numpy_processor.py +++ b/dxcam/processor/numpy_processor.py @@ -30,16 +30,14 @@ def process_cvtcolor(self, image): def process(self, rect, width, height, region, rotation_angle): pitch = int(rect.Pitch) - ptr = rect.pBits - if region[3] - region[1] != height: - if rotation_angle in (0, 180): - offset = (region[1] if rotation_angle==0 else height-region[3])*pitch - height = region[3] - region[1] - else: - offset = (region[0] if rotation_angle==270 else width-region[2])*pitch - width = region[2] - region[0] - ptr = ctypes.c_void_p(ctypes.addressof(ptr.contents)+offset)#Pointer arithmetic + if rotation_angle in (0, 180): + offset = (region[1] if rotation_angle==0 else height-region[3])*pitch + height = region[3] - region[1] + else: + offset = (region[0] if rotation_angle==270 else width-region[2])*pitch + width = region[2] - region[0] + ptr = ctypes.c_void_p(ctypes.addressof(rect.pBits.contents)+offset)#Pointer arithmetic if rotation_angle in (0, 180): size = pitch * height From c9859c267cd5d471387f25250983c528a4d47594 Mon Sep 17 00:00:00 2001 From: Agade09 Date: Mon, 17 Apr 2023 11:00:56 +0200 Subject: [PATCH 05/20] Almost completely removed overhead of ctypes.string_at by using the from_address API instead. In profiling a 1440x2560 grab, total time spent went from 20% in string_at() to almost 0% in from_address. My understanding is that string_at uses memove which is slower than the memcpy I suspect from_address uses. --- dxcam/processor/numpy_processor.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dxcam/processor/numpy_processor.py b/dxcam/processor/numpy_processor.py index 054b038..1ca7d12 100644 --- a/dxcam/processor/numpy_processor.py +++ b/dxcam/processor/numpy_processor.py @@ -37,14 +37,13 @@ def process(self, rect, width, height, region, rotation_angle): else: offset = (region[0] if rotation_angle==270 else width-region[2])*pitch width = region[2] - region[0] - ptr = ctypes.c_void_p(ctypes.addressof(rect.pBits.contents)+offset)#Pointer arithmetic if rotation_angle in (0, 180): size = pitch * height else: size = pitch * width - buffer = ctypes.string_at(ptr, size) + buffer = (ctypes.c_char*size).from_address(ctypes.addressof(rect.pBits.contents)+offset)#Pointer arithmetic pitch = pitch // 4 if rotation_angle in (0, 180): image = np.ndarray((height, pitch, 4), dtype=np.uint8, buffer=buffer) From 87a81172a4d9a70d2786c93b0b452495073f1d89 Mon Sep 17 00:00:00 2001 From: Agade09 Date: Mon, 17 Apr 2023 12:41:55 +0200 Subject: [PATCH 06/20] Don't call _validate_region in grab if the region is None because in that case self.region is used, and self.region was already validated when it was defined. In profiling a max FPS benchmark with no region defined, this spares 3% of total execution time. --- dxcam/dxcam.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dxcam/dxcam.py b/dxcam/dxcam.py index b83f5f8..d37f446 100644 --- a/dxcam/dxcam.py +++ b/dxcam/dxcam.py @@ -66,7 +66,8 @@ def __init__( def grab(self, region: Tuple[int, int, int, int] = None): if region is None: region = self.region - self._validate_region(region) + else: + self._validate_region(region) frame = self._grab(region) return frame From bf02b69f7c14cce8b5aab0bf57ea066b00d3582b Mon Sep 17 00:00:00 2001 From: Agade09 Date: Mon, 17 Apr 2023 13:34:26 +0200 Subject: [PATCH 07/20] Translate 'BGRA' color_mode to None in NumpyProcessor so the existing if statement bypassed the call to self.process_cvtcolor(). Simplify code in process_cvtcolor since it no longer needs to handle 'BGRA''. In profiling this spares 0.4% of total execution time in 'BGRA' mode. --- dxcam/processor/numpy_processor.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/dxcam/processor/numpy_processor.py b/dxcam/processor/numpy_processor.py index 1ca7d12..465f511 100644 --- a/dxcam/processor/numpy_processor.py +++ b/dxcam/processor/numpy_processor.py @@ -7,25 +7,24 @@ class NumpyProcessor(Processor): def __init__(self, color_mode): self.cvtcolor = None self.color_mode = color_mode + if self.color_mode=='BGRA': + self.color_mode = None def process_cvtcolor(self, image): import cv2 # only one time process if self.cvtcolor is None: - if self.color_mode!="BGRA": - if self.color_mode=="BGR": - self.cvtcolor = lambda image: image[:,:,:3] #BGRA -> BGR conversion - elif self.color_mode=='RGB': - self.cvtcolor = lambda image: np.flip(image[:,:,:3],axis=-1) #BGRA -> RGB conversion - elif self.color_mode=='RGBA': - self.cvtcolor = lambda image: cv2.cvtColor(image, cv2.COLOR_BGRA2RGBA) - else: - self.cvtcolor = lambda image: cv2.cvtColor(image, cv2.COLOR_BGRA2GRAY)[ - ..., np.newaxis - ] + if self.color_mode=="BGR": + self.cvtcolor = lambda image: image[:,:,:3] #BGRA -> BGR conversion + elif self.color_mode=='RGB': + self.cvtcolor = lambda image: np.flip(image[:,:,:3],axis=-1) #BGRA -> RGB conversion + elif self.color_mode=='RGBA': + self.cvtcolor = lambda image: cv2.cvtColor(image, cv2.COLOR_BGRA2RGBA) else: - return image + self.cvtcolor = lambda image: cv2.cvtColor(image, cv2.COLOR_BGRA2GRAY)[ + ..., np.newaxis + ] return self.cvtcolor(image) def process(self, rect, width, height, region, rotation_angle): From 204a5588b4fd50acb99423dc529b0225ee5bf3c2 Mon Sep 17 00:00:00 2001 From: Agade09 Date: Mon, 17 Apr 2023 14:41:25 +0200 Subject: [PATCH 08/20] Revert using numpy for conversion of BGRA->BGR/RGB. Numpy was faster but it was producing a non-contiguous array, which changes the behavior of the library --- dxcam/processor/numpy_processor.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/dxcam/processor/numpy_processor.py b/dxcam/processor/numpy_processor.py index 465f511..16c392b 100644 --- a/dxcam/processor/numpy_processor.py +++ b/dxcam/processor/numpy_processor.py @@ -15,14 +15,17 @@ def process_cvtcolor(self, image): # only one time process if self.cvtcolor is None: - if self.color_mode=="BGR": - self.cvtcolor = lambda image: image[:,:,:3] #BGRA -> BGR conversion - elif self.color_mode=='RGB': - self.cvtcolor = lambda image: np.flip(image[:,:,:3],axis=-1) #BGRA -> RGB conversion - elif self.color_mode=='RGBA': - self.cvtcolor = lambda image: cv2.cvtColor(image, cv2.COLOR_BGRA2RGBA) + color_mapping = { + "RGB": cv2.COLOR_BGRA2RGB, + "RGBA": cv2.COLOR_BGRA2RGBA, + "BGR": cv2.COLOR_BGRA2BGR, + "GRAY": cv2.COLOR_BGRA2GRAY + } + cv2_code = color_mapping[self.color_mode] + if cv2_code != cv2.COLOR_BGRA2GRAY: + self.cvtcolor = lambda image: cv2.cvtColor(image, cv2_code) else: - self.cvtcolor = lambda image: cv2.cvtColor(image, cv2.COLOR_BGRA2GRAY)[ + self.cvtcolor = lambda image: cv2.cvtColor(image, cv2_code)[ ..., np.newaxis ] return self.cvtcolor(image) From 592acb7325e3a463ff1e186c5745dfdcec7e82b4 Mon Sep 17 00:00:00 2001 From: AI-M-BOT <93147937+AI-M-BOT@users.noreply.github.com> Date: Fri, 28 Jul 2023 21:57:53 -0400 Subject: [PATCH 09/20] Go for speed, but need many patches --- dxcam/_libs/dxgi.py | 1 + dxcam/core/duplicator.py | 8 +++- dxcam/core/stagesurf.py | 3 +- dxcam/dxcam.py | 61 +++++++++++++++++++++++++--- dxcam/processor/base.py | 3 ++ dxcam/processor/numpy_processor.py | 64 ++++++++++++++---------------- 6 files changed, 96 insertions(+), 44 deletions(-) diff --git a/dxcam/_libs/dxgi.py b/dxcam/_libs/dxgi.py index 513de2a..45afea3 100644 --- a/dxcam/_libs/dxgi.py +++ b/dxcam/_libs/dxgi.py @@ -7,6 +7,7 @@ DXGI_ERROR_ACCESS_LOST = 0x887A0026 DXGI_ERROR_NOT_FOUND = 0x887A0002 DXGI_ERROR_WAIT_TIMEOUT = 0x887A0027 +ABANDONED_MUTEX_EXCEPTION = -0x7785ffda # -2005270490 class LUID(ctypes.Structure): diff --git a/dxcam/core/duplicator.py b/dxcam/core/duplicator.py index 4c97bc7..a82d154 100644 --- a/dxcam/core/duplicator.py +++ b/dxcam/core/duplicator.py @@ -1,4 +1,5 @@ import ctypes +from time import sleep from dataclasses import dataclass, InitVar from dxcam._libs.d3d11 import * from dxcam._libs.dxgi import * @@ -23,12 +24,15 @@ def update_frame(self): res = ctypes.POINTER(IDXGIResource)() try: self.duplicator.AcquireNextFrame( - 0, + 10, ctypes.byref(info), ctypes.byref(res), ) except comtypes.COMError as ce: - if ctypes.c_int32(DXGI_ERROR_ACCESS_LOST).value == ce.args[0]: + if ctypes.c_int32(DXGI_ERROR_ACCESS_LOST).value == ce.args[0] or ctypes.c_int32(ABANDONED_MUTEX_EXCEPTION).value == ce.args[0]: + self.release() # Release resources before reinitializing + sleep(0.1) + self.__post_init__(self.output, self.device) return False if ctypes.c_int32(DXGI_ERROR_WAIT_TIMEOUT).value == ce.args[0]: self.updated = False diff --git a/dxcam/core/stagesurf.py b/dxcam/core/stagesurf.py index 8e05180..4cfea1b 100644 --- a/dxcam/core/stagesurf.py +++ b/dxcam/core/stagesurf.py @@ -27,7 +27,8 @@ def release(self): self.texture = None def rebuild(self, output: Output, device: Device): - self.width, self.height = output.surface_size + if self.width==0 or self.height==0: + self.width, self.height = output.surface_size if self.texture is None: self.desc.Width = self.width self.desc.Height = self.height diff --git a/dxcam/dxcam.py b/dxcam/dxcam.py index d37f446..690a853 100644 --- a/dxcam/dxcam.py +++ b/dxcam/dxcam.py @@ -5,6 +5,7 @@ import comtypes import numpy as np from dxcam.core import Device, Output, StageSurface, Duplicator +from dxcam._libs.d3d11 import D3D11_BOX from dxcam.processor import Processor from dxcam.util.timer import ( create_high_resolution_timer, @@ -34,8 +35,12 @@ def __init__( output=self._output, device=self._device ) self._processor: Processor = Processor(output_color=output_color) + self._sourceRegion: D3D11_BOX = D3D11_BOX() + self._sourceRegion.front = 0 + self._sourceRegion.back = 1 self.width, self.height = self._output.resolution + self.shot_w, self.shot_h = self.width, self.height self.channel_size = len(output_color) if output_color != "GRAY" else 1 self.rotation_angle: int = self._output.rotation_angle @@ -66,22 +71,60 @@ def __init__( def grab(self, region: Tuple[int, int, int, int] = None): if region is None: region = self.region - else: + + if not self.region==region: + self._validate_region(region) + + return self._grab(region) + + def shot(self, image_ptr, region: Tuple[int, int, int, int] = None): + if region is None: + region = self.region + + if not self.region==region: self._validate_region(region) - frame = self._grab(region) - return frame + + return self._shot(image_ptr, region) + + def _shot(self, image_ptr, region: Tuple[int, int, int, int]): + if self._duplicator.update_frame(): + if not self._duplicator.updated: + return None + + if self._stagesurf.width != region[2]-region[0] or self._stagesurf.height != region[3]-region[1]: + self._stagesurf.release() + self._stagesurf.width = region[2]-region[0] + self._stagesurf.height = region[3]-region[1] + self._stagesurf.rebuild(output=self._output, device=self._device) + self._device.im_context.CopySubresourceRegion( + self._stagesurf.texture, 0, 0, 0, 0, self._duplicator.texture, 0, ctypes.byref(self._sourceRegion) + ) + self._duplicator.release_frame() + rect = self._stagesurf.map() + self._processor.process2(image_ptr, rect, self.shot_w, self.shot_h) + self._stagesurf.unmap() + return True + else: + self._on_output_change() + return False def _grab(self, region: Tuple[int, int, int, int]): if self._duplicator.update_frame(): if not self._duplicator.updated: return None - self._device.im_context.CopyResource( - self._stagesurf.texture, self._duplicator.texture + + if self._stagesurf.width != region[2]-region[0] or self._stagesurf.height != region[3]-region[1]: + self._stagesurf.release() + self._stagesurf.width = region[2]-region[0] + self._stagesurf.height = region[3]-region[1] + self._stagesurf.rebuild(output=self._output, device=self._device) + self._device.im_context.CopySubresourceRegion( + self._stagesurf.texture, 0, 0, 0, 0, self._duplicator.texture, 0, ctypes.byref(self._sourceRegion) ) self._duplicator.release_frame() rect = self._stagesurf.map() frame = self._processor.process( - rect, self.width, self.height, region, self.rotation_angle + rect, self.shot_w, self.shot_h, region, self.rotation_angle ) self._stagesurf.unmap() return frame @@ -234,6 +277,12 @@ def _validate_region(self, region: Tuple[int, int, int, int]): raise ValueError( f"Invalid Region: Region should be in {self.width}x{self.height}" ) + self.region = region + self._sourceRegion.left = region[0] + self._sourceRegion.top = region[1] + self._sourceRegion.right = region[2] + self._sourceRegion.bottom = region[3] + self.shot_w, self.shot_h = region[2]-region[0], region[3]-region[1] def release(self): self.stop() diff --git a/dxcam/processor/base.py b/dxcam/processor/base.py index 5351616..e94d0a2 100644 --- a/dxcam/processor/base.py +++ b/dxcam/processor/base.py @@ -14,6 +14,9 @@ def __init__(self, backend=ProcessorBackends.NUMPY, output_color: str = "RGB"): def process(self, rect, width, height, region, rotation_angle): return self.backend.process(rect, width, height, region, rotation_angle) + def process2(self, image_ptr, rect, width, height): + self.backend.shot(image_ptr, rect, width, height) + def _initialize_backend(self, backend): if backend == ProcessorBackends.NUMPY: from dxcam.processor.numpy_processor import NumpyProcessor diff --git a/dxcam/processor/numpy_processor.py b/dxcam/processor/numpy_processor.py index 16c392b..4ea9b86 100644 --- a/dxcam/processor/numpy_processor.py +++ b/dxcam/processor/numpy_processor.py @@ -1,5 +1,6 @@ import ctypes -import numpy as np +from numpy import rot90, newaxis, uint8, zeros +from numpy.ctypeslib import as_array from .base import Processor @@ -7,8 +8,7 @@ class NumpyProcessor(Processor): def __init__(self, color_mode): self.cvtcolor = None self.color_mode = color_mode - if self.color_mode=='BGRA': - self.color_mode = None + self.PBYTE = ctypes.POINTER(ctypes.c_ubyte) def process_cvtcolor(self, image): import cv2 @@ -19,57 +19,51 @@ def process_cvtcolor(self, image): "RGB": cv2.COLOR_BGRA2RGB, "RGBA": cv2.COLOR_BGRA2RGBA, "BGR": cv2.COLOR_BGRA2BGR, - "GRAY": cv2.COLOR_BGRA2GRAY + "GRAY": cv2.COLOR_BGRA2GRAY, + "BGRA": None, } cv2_code = color_mapping[self.color_mode] - if cv2_code != cv2.COLOR_BGRA2GRAY: - self.cvtcolor = lambda image: cv2.cvtColor(image, cv2_code) + if cv2_code is not None: + if cv2_code != cv2.COLOR_BGRA2GRAY: + self.cvtcolor = lambda image: cv2.cvtColor(image, cv2_code) + else: + self.cvtcolor = lambda image: cv2.cvtColor(image, cv2_code)[ + ..., newaxis + ] else: - self.cvtcolor = lambda image: cv2.cvtColor(image, cv2_code)[ - ..., np.newaxis - ] + return image + return self.cvtcolor(image) + def shot(self, image_ptr, rect, width, height): + ctypes.memmove(image_ptr, rect.pBits, width*height*4) + def process(self, rect, width, height, region, rotation_angle): pitch = int(rect.Pitch) - - if rotation_angle in (0, 180): - offset = (region[1] if rotation_angle==0 else height-region[3])*pitch - height = region[3] - region[1] - else: - offset = (region[0] if rotation_angle==270 else width-region[2])*pitch - width = region[2] - region[0] - - if rotation_angle in (0, 180): - size = pitch * height - else: - size = pitch * width - - buffer = (ctypes.c_char*size).from_address(ctypes.addressof(rect.pBits.contents)+offset)#Pointer arithmetic + buffer = ctypes.cast(rect.pBits, self.PBYTE) pitch = pitch // 4 + if rotation_angle in (0, 180): - image = np.ndarray((height, pitch, 4), dtype=np.uint8, buffer=buffer) + image = as_array(buffer, (height, pitch, 4)) elif rotation_angle in (90, 270): - image = np.ndarray((width, pitch, 4), dtype=np.uint8, buffer=buffer) - - if not self.color_mode is None: - image = self.process_cvtcolor(image) + image = as_array(buffer, (width, pitch, 4)) if rotation_angle == 90: - image = np.rot90(image, axes=(1, 0)) + image = rot90(image, axes=(1, 0)) elif rotation_angle == 180: - image = np.rot90(image, k=2, axes=(0, 1)) + image = rot90(image, k=2, axes=(0, 1)) elif rotation_angle == 270: - image = np.rot90(image, axes=(0, 1)) + image = rot90(image, axes=(0, 1)) if rotation_angle in (0, 180) and pitch != width: image = image[:, :width, :] elif rotation_angle in (90, 270) and pitch != height: image = image[:height, :, :] - if region[3] - region[1] != image.shape[0]: - image = image[region[1] : region[3], :, :] - if region[2] - region[0] != image.shape[1]: - image = image[:, region[0] : region[2], :] + if region[2] - region[0] != width or region[3] - region[1] != height: + image = image[region[1]:region[3], region[0]:region[2]] + + if self.color_mode is not None: + return self.process_cvtcolor(image) return image From a32a43b92bb3c02962e102366eb84e4b70a5c7fb Mon Sep 17 00:00:00 2001 From: WhatEverNames <93147937+AI-M-BOT@users.noreply.github.com> Date: Wed, 9 Aug 2023 11:00:11 -0400 Subject: [PATCH 10/20] Integrate updates from https://github.com/Agade09/DXcam --- dxcam/core/stagesurf.py | 13 +++++++---- dxcam/dxcam.py | 33 +++++++++++++++++++++------- dxcam/processor/numpy_processor.py | 35 +++++++++++------------------- 3 files changed, 47 insertions(+), 34 deletions(-) diff --git a/dxcam/core/stagesurf.py b/dxcam/core/stagesurf.py index 4cfea1b..9ff9cac 100644 --- a/dxcam/core/stagesurf.py +++ b/dxcam/core/stagesurf.py @@ -26,9 +26,12 @@ def release(self): self.texture.Release() self.texture = None - def rebuild(self, output: Output, device: Device): - if self.width==0 or self.height==0: + def rebuild(self, output: Output, device: Device, dim:tuple[int]=None): + if dim is not None: + self.width, self.height = dim + else: self.width, self.height = output.surface_size + if self.texture is None: self.desc.Width = self.width self.desc.Height = self.height @@ -48,13 +51,15 @@ def rebuild(self, output: Output, device: Device): ctypes.byref(self.texture), ) + self.interface = self.texture.QueryInterface(IDXGISurface) # Caching to improve performance from https://github.com/Agade09/DXcam + def map(self): rect: DXGI_MAPPED_RECT = DXGI_MAPPED_RECT() - self.texture.QueryInterface(IDXGISurface).Map(ctypes.byref(rect), 1) + self.interface.Map(ctypes.byref(rect), 1) return rect def unmap(self): - self.texture.QueryInterface(IDXGISurface).Unmap() + self.interface.Unmap() def __repr__(self) -> str: repr = f"{self.width}, {self.height}, {self.dxgi_format}" diff --git a/dxcam/dxcam.py b/dxcam/dxcam.py index 690a853..18b8e59 100644 --- a/dxcam/dxcam.py +++ b/dxcam/dxcam.py @@ -68,6 +68,17 @@ def __init__( self.__frame_count = 0 self.__capture_start_time = 0 + # from https://github.com/Agade09/DXcam + def region_to_memory_region(self, region: Tuple[int, int, int, int], rotation_angle: int, output: Output): + if rotation_angle==0: + return region + elif rotation_angle==90: #Axes (X,Y) -> (-Y,X) + return (region[1], output.surface_size[1]-region[2], region[3], output.surface_size[1]-region[0]) + elif rotation_angle==180: #Axes (X,Y) -> (-X,-Y) + return (output.surface_size[0]-region[2], output.surface_size[1]-region[3], output.surface_size[0]-region[0], output.surface_size[1]-region[1]) + else: #rotation_angle==270 Axes (X,Y) -> (Y,-X) + return (output.surface_size[0]-region[3], region[0], output.surface_size[0]-region[1], region[2]) + def grab(self, region: Tuple[int, int, int, int] = None): if region is None: region = self.region @@ -91,11 +102,14 @@ def _shot(self, image_ptr, region: Tuple[int, int, int, int]): if not self._duplicator.updated: return None - if self._stagesurf.width != region[2]-region[0] or self._stagesurf.height != region[3]-region[1]: + _region = self.region_to_memory_region(region, self.rotation_angle, self._output) + _width = _region[2] - _region[0] + _height = _region[3] - _region[1] + + if self._stagesurf.width != _width or self._stagesurf.height != _height: self._stagesurf.release() - self._stagesurf.width = region[2]-region[0] - self._stagesurf.height = region[3]-region[1] - self._stagesurf.rebuild(output=self._output, device=self._device) + self._stagesurf.rebuild(output=self._output, device=self._device, dim=(_width, _height)) + self._device.im_context.CopySubresourceRegion( self._stagesurf.texture, 0, 0, 0, 0, self._duplicator.texture, 0, ctypes.byref(self._sourceRegion) ) @@ -113,11 +127,14 @@ def _grab(self, region: Tuple[int, int, int, int]): if not self._duplicator.updated: return None - if self._stagesurf.width != region[2]-region[0] or self._stagesurf.height != region[3]-region[1]: + _region = self.region_to_memory_region(region, self.rotation_angle, self._output) + _width = _region[2] - _region[0] + _height = _region[3] - _region[1] + + if self._stagesurf.width != _width or self._stagesurf.height != _height: self._stagesurf.release() - self._stagesurf.width = region[2]-region[0] - self._stagesurf.height = region[3]-region[1] - self._stagesurf.rebuild(output=self._output, device=self._device) + self._stagesurf.rebuild(output=self._output, device=self._device, dim=(_width, _height)) + self._device.im_context.CopySubresourceRegion( self._stagesurf.texture, 0, 0, 0, 0, self._duplicator.texture, 0, ctypes.byref(self._sourceRegion) ) diff --git a/dxcam/processor/numpy_processor.py b/dxcam/processor/numpy_processor.py index 4ea9b86..dbdbf19 100644 --- a/dxcam/processor/numpy_processor.py +++ b/dxcam/processor/numpy_processor.py @@ -1,5 +1,5 @@ import ctypes -from numpy import rot90, newaxis, uint8, zeros +from numpy import rot90, ndarray, newaxis, uint8, zeros from numpy.ctypeslib import as_array from .base import Processor @@ -36,32 +36,23 @@ def process_cvtcolor(self, image): return self.cvtcolor(image) def shot(self, image_ptr, rect, width, height): - ctypes.memmove(image_ptr, rect.pBits, width*height*4) + ctypes.memmove(image_ptr, rect.pBits, height*width*4) def process(self, rect, width, height, region, rotation_angle): - pitch = int(rect.Pitch) - buffer = ctypes.cast(rect.pBits, self.PBYTE) - pitch = pitch // 4 - - if rotation_angle in (0, 180): - image = as_array(buffer, (height, pitch, 4)) - elif rotation_angle in (90, 270): - image = as_array(buffer, (width, pitch, 4)) + width = region[2] - region[0] + height = region[3] - region[1] + if rotation_angle in (90, 270): + width, height = height, width - if rotation_angle == 90: - image = rot90(image, axes=(1, 0)) - elif rotation_angle == 180: - image = rot90(image, k=2, axes=(0, 1)) - elif rotation_angle == 270: - image = rot90(image, axes=(0, 1)) + buffer = ctypes.cast(rect.pBits, self.PBYTE) + image = as_array(buffer, (height, width, 4)) - if rotation_angle in (0, 180) and pitch != width: - image = image[:, :width, :] - elif rotation_angle in (90, 270) and pitch != height: - image = image[:height, :, :] + # Another approach from https://github.com/Agade09/DXcam + # buffer = (ctypes.c_char*height*width*4).from_address(ctypes.addressof(rect.pBits.contents)) + # image = ndarray((height, width, 4), dtype=uint8, buffer=buffer) - if region[2] - region[0] != width or region[3] - region[1] != height: - image = image[region[1]:region[3], region[0]:region[2]] + if rotation_angle != 0: + image = rot90(image, k=rotation_angle//90, axes=(1, 0)) if self.color_mode is not None: return self.process_cvtcolor(image) From a78fc1f03e41c80b11a9cd200c66631821656637 Mon Sep 17 00:00:00 2001 From: vimacs <6170744+scamiv@users.noreply.github.com> Date: Mon, 16 Oct 2023 18:35:02 +0200 Subject: [PATCH 11/20] Add basic pointer support: modified: dxcam/__init__.py modified: dxcam/_libs/dxgi.py modified: dxcam/core/duplicator.py modified: dxcam/dxcam.py modified: dxcam/processor/numpy_processor.py --- dxcam/__init__.py | 2 ++ dxcam/_libs/dxgi.py | 21 +++++++++++- dxcam/core/duplicator.py | 33 +++++++++++++++++- dxcam/dxcam.py | 5 +++ dxcam/processor/numpy_processor.py | 55 +++++++++++++++++++++++++++--- 5 files changed, 110 insertions(+), 6 deletions(-) diff --git a/dxcam/__init__.py b/dxcam/__init__.py index 1626f5b..67dc77e 100644 --- a/dxcam/__init__.py +++ b/dxcam/__init__.py @@ -89,7 +89,9 @@ def output_info(self) -> str: ret = "" for didx, outputs in enumerate(self.outputs): for idx, output in enumerate(outputs): + #print(output) ret += f"Device[{didx}] Output[{idx}]: " + ret += f"szDevice[{output.devicename}]: " ret += f"Res:{output.resolution} Rot:{output.rotation_angle}" ret += f" Primary:{self.output_metadata.get(output.devicename)[1]}\n" return ret diff --git a/dxcam/_libs/dxgi.py b/dxcam/_libs/dxgi.py index 45afea3..26b2b31 100644 --- a/dxcam/_libs/dxgi.py +++ b/dxcam/_libs/dxgi.py @@ -42,6 +42,20 @@ class DXGI_OUTPUT_DESC(ctypes.Structure): class DXGI_OUTDUPL_POINTER_POSITION(ctypes.Structure): _fields_ = [("Position", wintypes.POINT), ("Visible", wintypes.BOOL)] +class DXGI_OUTDUPL_POINTER_SHAPE_INFO(ctypes.Structure): + _fields_ = [('Type', wintypes.UINT), + ('Width', wintypes.UINT), + ('Height', wintypes.UINT), + ('Pitch', wintypes.UINT), + ('HotSpot', wintypes.POINT), + ] + +class ac_Cursor(): + def __init__(self): + self.PointerPositionInfo: DXGI_OUTDUPL_POINTER_POSITION = DXGI_OUTDUPL_POINTER_POSITION() + self.PointerShapeInfo: DXGI_OUTDUPL_POINTER_SHAPE_INFO = DXGI_OUTDUPL_POINTER_SHAPE_INFO() + self.Shape: bytes = None + class DXGI_OUTDUPL_FRAME_INFO(ctypes.Structure): _fields_ = [ @@ -113,7 +127,12 @@ class IDXGIOutputDuplication(IDXGIObject): ), comtypes.STDMETHOD(comtypes.HRESULT, "GetFrameDirtyRects"), comtypes.STDMETHOD(comtypes.HRESULT, "GetFrameMoveRects"), - comtypes.STDMETHOD(comtypes.HRESULT, "GetFramePointerShape"), + comtypes.STDMETHOD(comtypes.HRESULT, "GetFramePointerShape", [ + wintypes.UINT, + ctypes.c_void_p, + ctypes.POINTER(wintypes.UINT), + ctypes.POINTER(DXGI_OUTDUPL_POINTER_SHAPE_INFO), + ]), comtypes.STDMETHOD(comtypes.HRESULT, "MapDesktopSurface"), comtypes.STDMETHOD(comtypes.HRESULT, "UnMapDesktopSurface"), comtypes.STDMETHOD(comtypes.HRESULT, "ReleaseFrame"), diff --git a/dxcam/core/duplicator.py b/dxcam/core/duplicator.py index a82d154..8b45e4e 100644 --- a/dxcam/core/duplicator.py +++ b/dxcam/core/duplicator.py @@ -6,6 +6,9 @@ from dxcam.core.device import Device from dxcam.core.output import Output +#class ac_Cursor: +# _fields_ = [("PointerPositionInfo",DXGI_OUTDUPL_POINTER_POSITION), ("PointerShapeInfo", DXGI_OUTDUPL_POINTER_SHAPE_INFO), ("Shape", bytes)] + @dataclass class Duplicator: @@ -14,6 +17,10 @@ class Duplicator: updated: bool = False output: InitVar[Output] = None device: InitVar[Device] = None + #cursor: ctypes.POINTER(ID3D11Texture2D) = ctypes.POINTER(ID3D11Texture2D)() #use proper type + cursor = ac_Cursor() + cursor.shape: bytes = None + cursor.PointerShapeInfo: DXGI_OUTDUPL_POINTER_SHAPE_INFO = DXGI_OUTDUPL_POINTER_SHAPE_INFO() def __post_init__(self, output: Output, device: Device) -> None: self.duplicator = ctypes.POINTER(IDXGIOutputDuplication)() @@ -24,7 +31,7 @@ def update_frame(self): res = ctypes.POINTER(IDXGIResource)() try: self.duplicator.AcquireNextFrame( - 10, + 1000, ctypes.byref(info), ctypes.byref(res), ) @@ -43,6 +50,14 @@ def update_frame(self): self.texture = res.QueryInterface(ID3D11Texture2D) except comtypes.COMError as ce: self.duplicator.ReleaseFrame() + try: + new_PointerInfo, new_PointerShape = self.get_frame_pointer_shape(info) + if new_PointerShape != False: + self.cursor.Shape = new_PointerShape + self.cursor.PointerShapeInfo = new_PointerInfo + self.cursor.PointerPositionInfo = info.PointerPosition + except Exception as e: + print(e) self.updated = True return True @@ -54,6 +69,22 @@ def release(self): self.duplicator.Release() self.duplicator = None + def get_frame_pointer_shape(self, FrameInfo): + PointerShapeInfo = DXGI_OUTDUPL_POINTER_SHAPE_INFO() + buffer_size_required = ctypes.c_uint() + pPointerShapeBuffer = (ctypes.c_byte*FrameInfo.PointerShapeBufferSize)() + #try: + # Get shape + hr = self.duplicator.GetFramePointerShape(FrameInfo.PointerShapeBufferSize, ctypes.byref(pPointerShapeBuffer), ctypes.byref(buffer_size_required), ctypes.byref(PointerShapeInfo)) + if FrameInfo.PointerShapeBufferSize > 0: + print("T",PointerShapeInfo.Type,PointerShapeInfo.Width,"x",PointerShapeInfo.Height,"Pitch:",PointerShapeInfo.Pitch,"HS:",PointerShapeInfo.HotSpot.x,PointerShapeInfo.HotSpot.y) + if PointerShapeInfo.Type != 2: return False # cant handle others rn + return PointerShapeInfo, pPointerShapeBuffer + #except: + # return False + #return False + return False + def __repr__(self) -> str: return "<{} Initalized:{}>".format( self.__class__.__name__, diff --git a/dxcam/dxcam.py b/dxcam/dxcam.py index 18b8e59..badf3e7 100644 --- a/dxcam/dxcam.py +++ b/dxcam/dxcam.py @@ -87,6 +87,11 @@ def grab(self, region: Tuple[int, int, int, int] = None): self._validate_region(region) return self._grab(region) + + def grab_cursor(self): + #convert texture(bytearray) to surface + return self._duplicator.cursor + def shot(self, image_ptr, region: Tuple[int, int, int, int] = None): if region is None: diff --git a/dxcam/processor/numpy_processor.py b/dxcam/processor/numpy_processor.py index dbdbf19..32776c2 100644 --- a/dxcam/processor/numpy_processor.py +++ b/dxcam/processor/numpy_processor.py @@ -39,17 +39,18 @@ def shot(self, image_ptr, rect, width, height): ctypes.memmove(image_ptr, rect.pBits, height*width*4) def process(self, rect, width, height, region, rotation_angle): + #print(self, rect, width, height, region, rotation_angle) width = region[2] - region[0] height = region[3] - region[1] if rotation_angle in (90, 270): width, height = height, width - buffer = ctypes.cast(rect.pBits, self.PBYTE) - image = as_array(buffer, (height, width, 4)) + #buffer = ctypes.cast(rect.pBits, self.PBYTE) + #image = as_array(buffer, (height, width, 4)) # Another approach from https://github.com/Agade09/DXcam - # buffer = (ctypes.c_char*height*width*4).from_address(ctypes.addressof(rect.pBits.contents)) - # image = ndarray((height, width, 4), dtype=uint8, buffer=buffer) + buffer = (ctypes.c_char*height*width*4).from_address(ctypes.addressof(rect.pBits.contents)) + image = ndarray((height, width, 4), dtype=uint8, buffer=buffer) if rotation_angle != 0: image = rot90(image, k=rotation_angle//90, axes=(1, 0)) @@ -58,3 +59,49 @@ def process(self, rect, width, height, region, rotation_angle): return self.process_cvtcolor(image) return image + # def process(self, rect, width, height, region, rotation_angle): + # pitch = int(rect.Pitch) + # ptr = rect.pBits + + # if region[3] - region[1] != height: + # if rotation_angle in (0, 180): + # offset = (region[1] if rotation_angle==0 else height-region[3])*pitch + # height = region[3] - region[1] + # else: + # offset = (region[0] if rotation_angle==270 else width-region[2])*pitch + # width = region[2] - region[0] + # ptr = ctypes.c_void_p(ctypes.addressof(ptr.contents)+offset)#Pointer arithmetic + + # if rotation_angle in (0, 180): + # size = pitch * height + # else: + # size = pitch * width + + # buffer = ctypes.string_at(ptr, size) + # pitch = pitch // 4 + # if rotation_angle in (0, 180): + # image = np.ndarray((height, pitch, 4), dtype=np.uint8, buffer=buffer) + # elif rotation_angle in (90, 270): + # image = np.ndarray((width, pitch, 4), dtype=np.uint8, buffer=buffer) + + # if not self.color_mode is None: + # image = self.process_cvtcolor(image) + + # if rotation_angle == 90: + # image = np.rot90(image, axes=(1, 0)) + # elif rotation_angle == 180: + # image = np.rot90(image, k=2, axes=(0, 1)) + # elif rotation_angle == 270: + # image = np.rot90(image, axes=(0, 1)) + + # if rotation_angle in (0, 180) and pitch != width: + # image = image[:, :width, :] + # elif rotation_angle in (90, 270) and pitch != height: + # image = image[:height, :, :] + + # if region[3] - region[1] != image.shape[0]: + # image = image[region[1] : region[3], :, :] + # if region[2] - region[0] != image.shape[1]: + # image = image[:, region[0] : region[2], :] + + # return image \ No newline at end of file From 53c250167fa80623c6e55475fa969daca65f9871 Mon Sep 17 00:00:00 2001 From: vimacs <6170744+scamiv@users.noreply.github.com> Date: Mon, 16 Oct 2023 19:38:37 +0200 Subject: [PATCH 12/20] Doh modified: dxcam/core/duplicator.py --- dxcam/core/duplicator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dxcam/core/duplicator.py b/dxcam/core/duplicator.py index 8b45e4e..ff6b739 100644 --- a/dxcam/core/duplicator.py +++ b/dxcam/core/duplicator.py @@ -78,12 +78,12 @@ def get_frame_pointer_shape(self, FrameInfo): hr = self.duplicator.GetFramePointerShape(FrameInfo.PointerShapeBufferSize, ctypes.byref(pPointerShapeBuffer), ctypes.byref(buffer_size_required), ctypes.byref(PointerShapeInfo)) if FrameInfo.PointerShapeBufferSize > 0: print("T",PointerShapeInfo.Type,PointerShapeInfo.Width,"x",PointerShapeInfo.Height,"Pitch:",PointerShapeInfo.Pitch,"HS:",PointerShapeInfo.HotSpot.x,PointerShapeInfo.HotSpot.y) - if PointerShapeInfo.Type != 2: return False # cant handle others rn + if PointerShapeInfo.Type != 2: return False, False # cant handle others rn return PointerShapeInfo, pPointerShapeBuffer #except: # return False #return False - return False + return False, False def __repr__(self) -> str: return "<{} Initalized:{}>".format( From 547a8302b4bf76268503a55f6b01a54dda34b823 Mon Sep 17 00:00:00 2001 From: scamiv <6170744+scamiv@users.noreply.github.com> Date: Mon, 16 Oct 2023 20:40:24 +0200 Subject: [PATCH 13/20] Update README.md --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 18d3e8b..78dab40 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,6 @@ +This fork adds basic cursor support (GetFramePointerShape) via grab_cursor function +Its used for https://github.com/scamiv/ActiveClone + # **DXcam** > ***Fastest Python Screenshot for Windows*** ```python From b8c65c1ca3bd3112ae68d66d2f801a13796a4530 Mon Sep 17 00:00:00 2001 From: vimacs <6170744+scamiv@users.noreply.github.com> Date: Mon, 16 Oct 2023 23:07:14 +0200 Subject: [PATCH 14/20] small cleanup modified: dxcam/__init__.py modified: dxcam/core/duplicator.py modified: dxcam/dxcam.py modified: dxcam/processor/numpy_processor.py --- dxcam/__init__.py | 3 +- dxcam/core/duplicator.py | 13 ++----- dxcam/dxcam.py | 1 - dxcam/processor/numpy_processor.py | 57 +++--------------------------- 4 files changed, 8 insertions(+), 66 deletions(-) diff --git a/dxcam/__init__.py b/dxcam/__init__.py index 67dc77e..24ee2a9 100644 --- a/dxcam/__init__.py +++ b/dxcam/__init__.py @@ -85,11 +85,10 @@ def device_info(self) -> str: ret += f"Device[{idx}]:{device}\n" return ret - def output_info(self) -> str: + def output_info(self) -> str: ret = "" for didx, outputs in enumerate(self.outputs): for idx, output in enumerate(outputs): - #print(output) ret += f"Device[{didx}] Output[{idx}]: " ret += f"szDevice[{output.devicename}]: " ret += f"Res:{output.resolution} Rot:{output.rotation_angle}" diff --git a/dxcam/core/duplicator.py b/dxcam/core/duplicator.py index ff6b739..71d4f8a 100644 --- a/dxcam/core/duplicator.py +++ b/dxcam/core/duplicator.py @@ -6,9 +6,6 @@ from dxcam.core.device import Device from dxcam.core.output import Output -#class ac_Cursor: -# _fields_ = [("PointerPositionInfo",DXGI_OUTDUPL_POINTER_POSITION), ("PointerShapeInfo", DXGI_OUTDUPL_POINTER_SHAPE_INFO), ("Shape", bytes)] - @dataclass class Duplicator: @@ -17,7 +14,6 @@ class Duplicator: updated: bool = False output: InitVar[Output] = None device: InitVar[Device] = None - #cursor: ctypes.POINTER(ID3D11Texture2D) = ctypes.POINTER(ID3D11Texture2D)() #use proper type cursor = ac_Cursor() cursor.shape: bytes = None cursor.PointerShapeInfo: DXGI_OUTDUPL_POINTER_SHAPE_INFO = DXGI_OUTDUPL_POINTER_SHAPE_INFO() @@ -31,7 +27,7 @@ def update_frame(self): res = ctypes.POINTER(IDXGIResource)() try: self.duplicator.AcquireNextFrame( - 1000, + 10, ctypes.byref(info), ctypes.byref(res), ) @@ -73,16 +69,11 @@ def get_frame_pointer_shape(self, FrameInfo): PointerShapeInfo = DXGI_OUTDUPL_POINTER_SHAPE_INFO() buffer_size_required = ctypes.c_uint() pPointerShapeBuffer = (ctypes.c_byte*FrameInfo.PointerShapeBufferSize)() - #try: - # Get shape hr = self.duplicator.GetFramePointerShape(FrameInfo.PointerShapeBufferSize, ctypes.byref(pPointerShapeBuffer), ctypes.byref(buffer_size_required), ctypes.byref(PointerShapeInfo)) if FrameInfo.PointerShapeBufferSize > 0: - print("T",PointerShapeInfo.Type,PointerShapeInfo.Width,"x",PointerShapeInfo.Height,"Pitch:",PointerShapeInfo.Pitch,"HS:",PointerShapeInfo.HotSpot.x,PointerShapeInfo.HotSpot.y) + #print("T",PointerShapeInfo.Type,PointerShapeInfo.Width,"x",PointerShapeInfo.Height,"Pitch:",PointerShapeInfo.Pitch,"HS:",PointerShapeInfo.HotSpot.x,PointerShapeInfo.HotSpot.y) if PointerShapeInfo.Type != 2: return False, False # cant handle others rn return PointerShapeInfo, pPointerShapeBuffer - #except: - # return False - #return False return False, False def __repr__(self) -> str: diff --git a/dxcam/dxcam.py b/dxcam/dxcam.py index badf3e7..5c54853 100644 --- a/dxcam/dxcam.py +++ b/dxcam/dxcam.py @@ -89,7 +89,6 @@ def grab(self, region: Tuple[int, int, int, int] = None): return self._grab(region) def grab_cursor(self): - #convert texture(bytearray) to surface return self._duplicator.cursor diff --git a/dxcam/processor/numpy_processor.py b/dxcam/processor/numpy_processor.py index 32776c2..e7dfc7f 100644 --- a/dxcam/processor/numpy_processor.py +++ b/dxcam/processor/numpy_processor.py @@ -39,18 +39,17 @@ def shot(self, image_ptr, rect, width, height): ctypes.memmove(image_ptr, rect.pBits, height*width*4) def process(self, rect, width, height, region, rotation_angle): - #print(self, rect, width, height, region, rotation_angle) width = region[2] - region[0] height = region[3] - region[1] if rotation_angle in (90, 270): width, height = height, width - #buffer = ctypes.cast(rect.pBits, self.PBYTE) - #image = as_array(buffer, (height, width, 4)) + buffer = ctypes.cast(rect.pBits, self.PBYTE) + image = as_array(buffer, (height, width, 4)) # Another approach from https://github.com/Agade09/DXcam - buffer = (ctypes.c_char*height*width*4).from_address(ctypes.addressof(rect.pBits.contents)) - image = ndarray((height, width, 4), dtype=uint8, buffer=buffer) + #buffer = (ctypes.c_char*height*width*4).from_address(ctypes.addressof(rect.pBits.contents)) + #image = ndarray((height, width, 4), dtype=uint8, buffer=buffer) if rotation_angle != 0: image = rot90(image, k=rotation_angle//90, axes=(1, 0)) @@ -58,50 +57,4 @@ def process(self, rect, width, height, region, rotation_angle): if self.color_mode is not None: return self.process_cvtcolor(image) - return image - # def process(self, rect, width, height, region, rotation_angle): - # pitch = int(rect.Pitch) - # ptr = rect.pBits - - # if region[3] - region[1] != height: - # if rotation_angle in (0, 180): - # offset = (region[1] if rotation_angle==0 else height-region[3])*pitch - # height = region[3] - region[1] - # else: - # offset = (region[0] if rotation_angle==270 else width-region[2])*pitch - # width = region[2] - region[0] - # ptr = ctypes.c_void_p(ctypes.addressof(ptr.contents)+offset)#Pointer arithmetic - - # if rotation_angle in (0, 180): - # size = pitch * height - # else: - # size = pitch * width - - # buffer = ctypes.string_at(ptr, size) - # pitch = pitch // 4 - # if rotation_angle in (0, 180): - # image = np.ndarray((height, pitch, 4), dtype=np.uint8, buffer=buffer) - # elif rotation_angle in (90, 270): - # image = np.ndarray((width, pitch, 4), dtype=np.uint8, buffer=buffer) - - # if not self.color_mode is None: - # image = self.process_cvtcolor(image) - - # if rotation_angle == 90: - # image = np.rot90(image, axes=(1, 0)) - # elif rotation_angle == 180: - # image = np.rot90(image, k=2, axes=(0, 1)) - # elif rotation_angle == 270: - # image = np.rot90(image, axes=(0, 1)) - - # if rotation_angle in (0, 180) and pitch != width: - # image = image[:, :width, :] - # elif rotation_angle in (90, 270) and pitch != height: - # image = image[:height, :, :] - - # if region[3] - region[1] != image.shape[0]: - # image = image[region[1] : region[3], :, :] - # if region[2] - region[0] != image.shape[1]: - # image = image[:, region[0] : region[2], :] - - # return image \ No newline at end of file + return image \ No newline at end of file From c8a2883d980144d6f64371e6fe77bbd54ffe2363 Mon Sep 17 00:00:00 2001 From: vimacs <6170744+scamiv@users.noreply.github.com> Date: Tue, 17 Oct 2023 01:18:41 +0200 Subject: [PATCH 15/20] check LastMouseUpdateTime for non-zero value first,allow for all shape formats check LastMouseUpdateTime for non-zero value first allow for all shape formats modified: dxcam/core/duplicator.py modified: dxcam/processor/numpy_processor.py --- dxcam/core/duplicator.py | 14 ++++++-------- dxcam/processor/numpy_processor.py | 6 +++--- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/dxcam/core/duplicator.py b/dxcam/core/duplicator.py index 71d4f8a..4398cc7 100644 --- a/dxcam/core/duplicator.py +++ b/dxcam/core/duplicator.py @@ -14,9 +14,7 @@ class Duplicator: updated: bool = False output: InitVar[Output] = None device: InitVar[Device] = None - cursor = ac_Cursor() - cursor.shape: bytes = None - cursor.PointerShapeInfo: DXGI_OUTDUPL_POINTER_SHAPE_INFO = DXGI_OUTDUPL_POINTER_SHAPE_INFO() + cursor: ac_Cursor = ac_Cursor() def __post_init__(self, output: Output, device: Device) -> None: self.duplicator = ctypes.POINTER(IDXGIOutputDuplication)() @@ -47,10 +45,11 @@ def update_frame(self): except comtypes.COMError as ce: self.duplicator.ReleaseFrame() try: - new_PointerInfo, new_PointerShape = self.get_frame_pointer_shape(info) - if new_PointerShape != False: - self.cursor.Shape = new_PointerShape - self.cursor.PointerShapeInfo = new_PointerInfo + if info.LastMouseUpdateTime > 0: + new_PointerInfo, new_PointerShape = self.get_frame_pointer_shape(info) + if new_PointerShape != False: + self.cursor.Shape = new_PointerShape + self.cursor.PointerShapeInfo = new_PointerInfo self.cursor.PointerPositionInfo = info.PointerPosition except Exception as e: print(e) @@ -72,7 +71,6 @@ def get_frame_pointer_shape(self, FrameInfo): hr = self.duplicator.GetFramePointerShape(FrameInfo.PointerShapeBufferSize, ctypes.byref(pPointerShapeBuffer), ctypes.byref(buffer_size_required), ctypes.byref(PointerShapeInfo)) if FrameInfo.PointerShapeBufferSize > 0: #print("T",PointerShapeInfo.Type,PointerShapeInfo.Width,"x",PointerShapeInfo.Height,"Pitch:",PointerShapeInfo.Pitch,"HS:",PointerShapeInfo.HotSpot.x,PointerShapeInfo.HotSpot.y) - if PointerShapeInfo.Type != 2: return False, False # cant handle others rn return PointerShapeInfo, pPointerShapeBuffer return False, False diff --git a/dxcam/processor/numpy_processor.py b/dxcam/processor/numpy_processor.py index e7dfc7f..dbdbf19 100644 --- a/dxcam/processor/numpy_processor.py +++ b/dxcam/processor/numpy_processor.py @@ -48,8 +48,8 @@ def process(self, rect, width, height, region, rotation_angle): image = as_array(buffer, (height, width, 4)) # Another approach from https://github.com/Agade09/DXcam - #buffer = (ctypes.c_char*height*width*4).from_address(ctypes.addressof(rect.pBits.contents)) - #image = ndarray((height, width, 4), dtype=uint8, buffer=buffer) + # buffer = (ctypes.c_char*height*width*4).from_address(ctypes.addressof(rect.pBits.contents)) + # image = ndarray((height, width, 4), dtype=uint8, buffer=buffer) if rotation_angle != 0: image = rot90(image, k=rotation_angle//90, axes=(1, 0)) @@ -57,4 +57,4 @@ def process(self, rect, width, height, region, rotation_angle): if self.color_mode is not None: return self.process_cvtcolor(image) - return image \ No newline at end of file + return image From 2b4495469313e7b70e52166914f6da8e6d6f15a6 Mon Sep 17 00:00:00 2001 From: scamiv <6170744+scamiv@users.noreply.github.com> Date: Fri, 20 Oct 2023 03:10:52 +0200 Subject: [PATCH 16/20] Moved cursor update logic --- dxcam/core/duplicator.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/dxcam/core/duplicator.py b/dxcam/core/duplicator.py index 4398cc7..cb08a3e 100644 --- a/dxcam/core/duplicator.py +++ b/dxcam/core/duplicator.py @@ -29,6 +29,12 @@ def update_frame(self): ctypes.byref(info), ctypes.byref(res), ) + if info.LastMouseUpdateTime > 0: + new_PointerInfo, new_PointerShape = self.get_frame_pointer_shape(info) + if new_PointerShape != False: + self.cursor.Shape = new_PointerShape + self.cursor.PointerShapeInfo = new_PointerInfo + self.cursor.PointerPositionInfo = info.PointerPosition except comtypes.COMError as ce: if ctypes.c_int32(DXGI_ERROR_ACCESS_LOST).value == ce.args[0] or ctypes.c_int32(ABANDONED_MUTEX_EXCEPTION).value == ce.args[0]: self.release() # Release resources before reinitializing @@ -44,15 +50,7 @@ def update_frame(self): self.texture = res.QueryInterface(ID3D11Texture2D) except comtypes.COMError as ce: self.duplicator.ReleaseFrame() - try: - if info.LastMouseUpdateTime > 0: - new_PointerInfo, new_PointerShape = self.get_frame_pointer_shape(info) - if new_PointerShape != False: - self.cursor.Shape = new_PointerShape - self.cursor.PointerShapeInfo = new_PointerInfo - self.cursor.PointerPositionInfo = info.PointerPosition - except Exception as e: - print(e) + self.updated = True return True From 9dd01d320a3a7bf2982ce98e94acbd8ac2d82525 Mon Sep 17 00:00:00 2001 From: scamiv <6170744+scamiv@users.noreply.github.com> Date: Fri, 20 Oct 2023 19:06:14 +0200 Subject: [PATCH 17/20] update_frame check LastPresentTime check info.LastPresentTime to actually skip repeat frames even if DXGI_ERROR_WAIT_TIMEOUT was not hit. --- dxcam/core/duplicator.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/dxcam/core/duplicator.py b/dxcam/core/duplicator.py index cb08a3e..61db9ea 100644 --- a/dxcam/core/duplicator.py +++ b/dxcam/core/duplicator.py @@ -46,6 +46,12 @@ def update_frame(self): return True else: raise ce + + if info.LastPresentTime == 0: + self.duplicator.ReleaseFrame() + self.updated = False + return True + try: self.texture = res.QueryInterface(ID3D11Texture2D) except comtypes.COMError as ce: From 88d189246f083787d383bd46fe8bc5fe72067485 Mon Sep 17 00:00:00 2001 From: eiyooooo Date: Mon, 29 Jan 2024 22:51:18 +0800 Subject: [PATCH 18/20] fix resolution bug when system dpi is not 96 --- dxcam/core/output.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dxcam/core/output.py b/dxcam/core/output.py index 944885a..1c32dc2 100644 --- a/dxcam/core/output.py +++ b/dxcam/core/output.py @@ -12,6 +12,7 @@ class Output: desc: DXGI_OUTPUT_DESC = None def __post_init__(self): + ctypes.windll.shcore.SetProcessDpiAwareness(2) self.desc = DXGI_OUTPUT_DESC() self.update_desc() From 1263e228c974449262e03786a9a0d86fac26dcee Mon Sep 17 00:00:00 2001 From: eiyooooo Date: Tue, 30 Jan 2024 01:29:29 +0800 Subject: [PATCH 19/20] Fix type hint bug for Python versions under 3.9 --- dxcam/core/stagesurf.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dxcam/core/stagesurf.py b/dxcam/core/stagesurf.py index 9ff9cac..731d5b4 100644 --- a/dxcam/core/stagesurf.py +++ b/dxcam/core/stagesurf.py @@ -4,6 +4,7 @@ from dxcam._libs.dxgi import * from dxcam.core.device import Device from dxcam.core.output import Output +from typing import Tuple @dataclass @@ -26,7 +27,7 @@ def release(self): self.texture.Release() self.texture = None - def rebuild(self, output: Output, device: Device, dim:tuple[int]=None): + def rebuild(self, output: Output, device: Device, dim:Tuple[int]=None): if dim is not None: self.width, self.height = dim else: From dbe9d83785e803903ded124020f754c4c391c4c6 Mon Sep 17 00:00:00 2001 From: WhatEverNames <93147937+AI-M-BOT@users.noreply.github.com> Date: Wed, 26 Jun 2024 21:07:43 -0400 Subject: [PATCH 20/20] Fix DXcam crash when resolution changes in runtime Author: elmoiv (https://github.com/elmoiv) --- dxcam/dxcam.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/dxcam/dxcam.py b/dxcam/dxcam.py index 5c54853..516a163 100644 --- a/dxcam/dxcam.py +++ b/dxcam/dxcam.py @@ -241,6 +241,14 @@ def __capture( frame = self._grab(region) if frame is not None: with self.__lock: + # Reconstruct frame buffer when resolution change, Author is elmoiv (https://github.com/elmoiv) + if frame.shape[0] != self.height or frame.shape[1] != self.width: + self.width, self.height = frame.shape[1], frame.shape[0] + region = (0, 0, frame.shape[1], frame.shape[0]) + frame_shape = (region[3] - region[1], region[2] - region[0], self.channel_size) + self.__frame_buffer = np.ndarray( + (self.max_buffer_len, *frame_shape), dtype=np.uint8 + ) self.__frame_buffer[self.__head] = frame if self.__full: self.__tail = (self.__tail + 1) % self.max_buffer_len