Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Go for speed, but need many patches #70

Open
wants to merge 24 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a70ab3a
Use numpy directly for BGRA->RGB and BGRA->BGR conversion.
Agade09 Apr 15, 2023
b7ac4c2
Reduce overhead in ctypes.string_at() when grabbing from a region sma…
Agade09 Apr 15, 2023
2833caf
Fixed regression where numpy_processor was no longer correct for rota…
Agade09 Apr 16, 2023
94bec3f
Fixed not applying performance optimization if a region is defined wh…
Agade09 Apr 16, 2023
c9859c2
Almost completely removed overhead of ctypes.string_at by using the f…
Agade09 Apr 17, 2023
87a8117
Don't call _validate_region in grab if the region is None because in …
Agade09 Apr 17, 2023
bf02b69
Translate 'BGRA' color_mode to None in NumpyProcessor so the existing…
Agade09 Apr 17, 2023
204a558
Revert using numpy for conversion of BGRA->BGR/RGB. Numpy was faster …
Agade09 Apr 17, 2023
a0964dc
Merge pull request #1 from Agade09/main
AI-M-BOT Apr 26, 2023
592acb7
Go for speed, but need many patches
AI-M-BOT Jul 29, 2023
a32a43b
Integrate updates from https://github.com/Agade09/DXcam
AI-M-BOT Aug 9, 2023
a78fc1f
Add basic pointer support:
scamiv Oct 16, 2023
53c2501
Doh
scamiv Oct 16, 2023
547a830
Update README.md
scamiv Oct 16, 2023
b8c65c1
small cleanup
scamiv Oct 16, 2023
d6370e1
Merge branch 'main' of https://github.com/scamiv/DXcam
scamiv Oct 16, 2023
c8a2883
check LastMouseUpdateTime for non-zero value first,allow for all shap…
scamiv Oct 16, 2023
2b44954
Moved cursor update logic
scamiv Oct 20, 2023
9dd01d3
update_frame check LastPresentTime
scamiv Oct 20, 2023
004f9fd
Merge pull request #2 from scamiv/main
AI-M-BOT Nov 9, 2023
88d1892
fix resolution bug when system dpi is not 96
eiyooooo Jan 29, 2024
1263e22
Fix type hint bug for Python versions under 3.9
eiyooooo Jan 29, 2024
137c674
Merge pull request #3 from eiyooooo/main
AI-M-BOT Feb 1, 2024
dbe9d83
Fix DXcam crash when resolution changes in runtime
AI-M-BOT Jun 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
3 changes: 2 additions & 1 deletion dxcam/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,12 @@ 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):
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
Expand Down
22 changes: 21 additions & 1 deletion dxcam/_libs/dxgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -41,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_ = [
Expand Down Expand Up @@ -112,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"),
Expand Down
32 changes: 30 additions & 2 deletions dxcam/core/duplicator.py
Original file line number Diff line number Diff line change
@@ -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 *
Expand All @@ -13,6 +14,7 @@ class Duplicator:
updated: bool = False
output: InitVar[Output] = None
device: InitVar[Device] = None
cursor: ac_Cursor = ac_Cursor()

def __post_init__(self, output: Output, device: Device) -> None:
self.duplicator = ctypes.POINTER(IDXGIOutputDuplication)()
Expand All @@ -23,22 +25,38 @@ def update_frame(self):
res = ctypes.POINTER(IDXGIResource)()
try:
self.duplicator.AcquireNextFrame(
0,
10,
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]:
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
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:
self.duplicator.ReleaseFrame()

self.updated = True
return True

Expand All @@ -50,6 +68,16 @@ 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)()
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)
return PointerShapeInfo, pPointerShapeBuffer
return False, False

def __repr__(self) -> str:
return "<{} Initalized:{}>".format(
self.__class__.__name__,
Expand Down
1 change: 1 addition & 0 deletions dxcam/core/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
15 changes: 11 additions & 4 deletions dxcam/core/stagesurf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -26,8 +27,12 @@ def release(self):
self.texture.Release()
self.texture = None

def rebuild(self, output: Output, device: Device):
self.width, self.height = output.surface_size
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
Expand All @@ -47,13 +52,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}"
Expand Down
91 changes: 85 additions & 6 deletions dxcam/dxcam.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -63,24 +68,84 @@ 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
self._validate_region(region)
frame = self._grab(region)
return frame

if not self.region==region:
self._validate_region(region)

return self._grab(region)

def grab_cursor(self):
return self._duplicator.cursor


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)

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

_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.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)
)
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

_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.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)
)
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
Expand Down Expand Up @@ -176,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
Expand Down Expand Up @@ -233,6 +306,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()
Expand Down
3 changes: 3 additions & 0 deletions dxcam/processor/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading