Skip to content

Commit 3a0c78c

Browse files
committed
Implement an XShmGetImage-based backend.
This is close to complete, but there's a few things that need to be chased down: notably, the test_thread_safety test is failing, for some reason. It also currently doesn't work correctly if the root window size increases. That said, this is quite promising: on my computer, the new backend can take 4k screenshots at 30-34 fps, while the XGetImage backend could only run at 11-14 fps.
1 parent 4a4ce34 commit 3a0c78c

File tree

10 files changed

+260
-56
lines changed

10 files changed

+260
-56
lines changed

src/mss/linux/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ def mss(backend: str = "default", **kwargs: Any) -> MSSBase:
1414
from . import xgetimage # noqa: PLC0415
1515

1616
return xgetimage.MSS(**kwargs)
17+
if backend == "xshmgetimage":
18+
from . import xshmgetimage # noqa: PLC0415
19+
20+
return xshmgetimage.MSS(**kwargs)
1721
msg = f"Backend {backend!r} not (yet?) implemented."
1822
raise ScreenShotError(msg)
1923

src/mss/linux/base.py

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from .xcb import LIB
1010

1111
if TYPE_CHECKING:
12+
from mss.models import Monitor
1213
from mss.screenshot import ScreenShot
1314

1415
SUPPORTED_DEPTHS = {24, 32}
@@ -48,8 +49,8 @@ def __init__(self, /, **kwargs: Any) -> None: # noqa: PLR0912
4849
# Get the connection setup information that was included when we connected.
4950
xcb_setup = xcb.get_setup(self.conn)
5051
screens = xcb.setup_roots(xcb_setup)
51-
pref_screen = screens[pref_screen_num]
52-
self.root = self.drawable = pref_screen.root
52+
self.pref_screen = screens[pref_screen_num]
53+
self.root = self.drawable = self.pref_screen.root
5354

5455
# We don't probe the XFixes presence or version until we need it.
5556
self._xfixes_ready: bool | None = None
@@ -61,8 +62,8 @@ def __init__(self, /, **kwargs: Any) -> None: # noqa: PLR0912
6162
# Currently, we assume that the drawable we're capturing is the root; when we add single-window capture,
6263
# we'll have to ask the server for its depth and visual.
6364
assert self.root == self.drawable # noqa: S101
64-
self.drawable_depth = pref_screen.root_depth
65-
self.drawable_visual_id = pref_screen.root_visual.value
65+
self.drawable_depth = self.pref_screen.root_depth
66+
self.drawable_visual_id = self.pref_screen.root_visual.value
6667
# Server image byte order
6768
if xcb_setup.image_byte_order != xcb.ImageOrder.LSBFirst:
6869
msg = "Only X11 servers using LSB-First images are supported."
@@ -92,7 +93,7 @@ def __init__(self, /, **kwargs: Any) -> None: # noqa: PLR0912
9293
raise ScreenShotError(msg)
9394
# Visual, the interpretation of pixels (like indexed, grayscale, etc). (Visuals are arranged by depth, so
9495
# we iterate over the depths first.)
95-
for xcb_depth in xcb.screen_allowed_depths(pref_screen):
96+
for xcb_depth in xcb.screen_allowed_depths(self.pref_screen):
9697
if xcb_depth.depth == self.drawable_depth:
9798
break
9899
else:
@@ -226,3 +227,41 @@ def _cursor_impl(self) -> ScreenShot:
226227
# unfortunate historical accident that makes it have to return the cursor image in a different format.
227228

228229
return self.cls_image(data, region)
230+
231+
def _grab_impl_xgetimage(self, monitor: Monitor, /) -> ScreenShot:
232+
"""Retrieve all pixels from a monitor using GetImage.
233+
234+
This is used by the XGetImage backend, and also the XShmGetImage
235+
backend in fallback mode.
236+
"""
237+
238+
if self.conn is None:
239+
msg = "Cannot take screenshot while the connection is closed"
240+
raise ScreenShotError(msg)
241+
242+
img_reply = xcb.get_image(
243+
self.conn,
244+
xcb.ImageFormat.ZPixmap,
245+
self.drawable,
246+
monitor["left"],
247+
monitor["top"],
248+
monitor["width"],
249+
monitor["height"],
250+
ALL_PLANES,
251+
)
252+
253+
# Now, save the image. This is a reference into the img_reply structure.
254+
img_data_arr = xcb.get_image_data(img_reply)
255+
# Copy this into a new bytearray, so that it will persist after we clear the image structure.
256+
img_data = bytearray(img_data_arr)
257+
258+
if img_reply.depth != self.drawable_depth or img_reply.visual.value != self.drawable_visual_id:
259+
# This should never happen; a window can't change its visual.
260+
msg = (
261+
"Server returned an image with a depth or visual different than it initially reported: "
262+
f"expected {self.drawable_depth},{hex(self.drawable_visual_id)}, "
263+
f"got {img_reply.depth},{hex(img_reply.visual.value)}"
264+
)
265+
raise ScreenShotError(msg)
266+
267+
return self.cls_image(img_data, monitor)

src/mss/linux/xcb.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
RANDR_MINOR_VERSION,
1212
RENDER_MAJOR_VERSION,
1313
RENDER_MINOR_VERSION,
14+
SHM_MAJOR_VERSION,
15+
SHM_MINOR_VERSION,
1416
XFIXES_MAJOR_VERSION,
1517
XFIXES_MINOR_VERSION,
1618
Atom,
@@ -52,6 +54,10 @@
5254
ScreenIterator,
5355
Setup,
5456
SetupIterator,
57+
ShmCreateSegmentReply,
58+
ShmGetImageReply,
59+
ShmQueryVersionReply,
60+
ShmSeg,
5561
Timestamp,
5662
VisualClass,
5763
Visualid,
@@ -91,6 +97,12 @@
9197
setup_pixmap_formats,
9298
setup_roots,
9399
setup_vendor,
100+
shm_attach_fd,
101+
shm_create_segment,
102+
shm_create_segment_reply_fds,
103+
shm_detach,
104+
shm_get_image,
105+
shm_query_version,
94106
xfixes_get_cursor_image,
95107
xfixes_get_cursor_image_cursor_image,
96108
xfixes_query_version,

src/mss/linux/xcbgen.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -631,7 +631,7 @@ def xfixes_get_cursor_image_cursor_image(r: XfixesGetCursorImageReply) -> Array[
631631
)
632632

633633

634-
def shm_create_segment_reply_fds(c: Connection, r: ShmCreateSegmentReply) -> _Pointer[c_int]:
634+
def shm_create_segment_reply_fds(c: Connection | _Pointer[Connection], r: ShmCreateSegmentReply) -> _Pointer[c_int]:
635635
return LIB.shm.xcb_shm_create_segment_reply_fds(c, r)
636636

637637

@@ -665,7 +665,7 @@ def get_property(
665665

666666

667667
def no_operation(c: Connection) -> None:
668-
return LIB.xcb.xcb_no_operation(c).check(c)
668+
return LIB.xcb.xcb_no_operation_checked(c).check(c)
669669

670670

671671
def randr_query_version(
@@ -716,7 +716,7 @@ def shm_get_image(
716716

717717

718718
def shm_attach_fd(c: Connection, shmseg: ShmSeg, shm_fd: c_int | int, read_only: c_uint8 | int) -> None:
719-
return LIB.shm.xcb_shm_attach_fd(c, shmseg, shm_fd, read_only).check(c)
719+
return LIB.shm.xcb_shm_attach_fd_checked(c, shmseg, shm_fd, read_only).check(c)
720720

721721

722722
def shm_create_segment(
@@ -726,7 +726,7 @@ def shm_create_segment(
726726

727727

728728
def shm_detach(c: Connection, shmseg: ShmSeg) -> None:
729-
return LIB.shm.xcb_shm_detach(c, shmseg).check(c)
729+
return LIB.shm.xcb_shm_detach_checked(c, shmseg).check(c)
730730

731731

732732
def xfixes_query_version(
@@ -860,8 +860,8 @@ def initialize() -> None: # noqa: PLR0915
860860
[POINTER(Connection), c_uint8, Window, Atom, Atom, c_uint32, c_uint32],
861861
GetPropertyReply,
862862
)
863-
LIB.xcb.xcb_no_operation.argtypes = (Connection,)
864-
LIB.xcb.xcb_no_operation.restype = VoidCookie
863+
LIB.xcb.xcb_no_operation_checked.argtypes = (POINTER(Connection),)
864+
LIB.xcb.xcb_no_operation_checked.restype = VoidCookie
865865
initialize_xcb_typed_func(
866866
LIB.randr, "xcb_randr_query_version", [POINTER(Connection), c_uint32, c_uint32], RandrQueryVersionReply
867867
)
@@ -890,21 +890,21 @@ def initialize() -> None: # noqa: PLR0915
890890
[POINTER(Connection), Drawable, c_int16, c_int16, c_uint16, c_uint16, c_uint32, c_uint8, ShmSeg, c_uint32],
891891
ShmGetImageReply,
892892
)
893-
LIB.shm.xcb_shm_attach_fd.argtypes = (
894-
Connection,
893+
LIB.shm.xcb_shm_attach_fd_checked.argtypes = (
894+
POINTER(Connection),
895895
ShmSeg,
896896
c_int,
897897
c_uint8,
898898
)
899-
LIB.shm.xcb_shm_attach_fd.restype = VoidCookie
899+
LIB.shm.xcb_shm_attach_fd_checked.restype = VoidCookie
900900
initialize_xcb_typed_func(
901901
LIB.shm, "xcb_shm_create_segment", [POINTER(Connection), ShmSeg, c_uint32, c_uint8], ShmCreateSegmentReply
902902
)
903-
LIB.shm.xcb_shm_detach.argtypes = (
904-
Connection,
903+
LIB.shm.xcb_shm_detach_checked.argtypes = (
904+
POINTER(Connection),
905905
ShmSeg,
906906
)
907-
LIB.shm.xcb_shm_detach.restype = VoidCookie
907+
LIB.shm.xcb_shm_detach_checked.restype = VoidCookie
908908
initialize_xcb_typed_func(
909909
LIB.xfixes, "xcb_xfixes_query_version", [POINTER(Connection), c_uint32, c_uint32], XfixesQueryVersionReply
910910
)

src/mss/linux/xgetimage.py

Lines changed: 3 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
1-
from mss.exception import ScreenShotError
21
from mss.models import Monitor
32
from mss.screenshot import ScreenShot
43

5-
from . import xcb
6-
from .base import ALL_PLANES, MSSXCBBase
4+
from .base import MSSXCBBase
75

86

97
class MSS(MSSXCBBase):
@@ -15,36 +13,6 @@ class MSS(MSSXCBBase):
1513
* XFixes: Including the cursor.
1614
"""
1715

18-
def _grab_impl(self, monitor: Monitor, /) -> ScreenShot:
16+
def _grab_impl(self, monitor: Monitor) -> ScreenShot:
1917
"""Retrieve all pixels from a monitor. Pixels have to be RGBX."""
20-
21-
if self.conn is None:
22-
msg = "Cannot take screenshot while the connection is closed"
23-
raise ScreenShotError(msg)
24-
25-
img_reply = xcb.get_image(
26-
self.conn,
27-
xcb.ImageFormat.ZPixmap,
28-
self.drawable,
29-
monitor["left"],
30-
monitor["top"],
31-
monitor["width"],
32-
monitor["height"],
33-
ALL_PLANES,
34-
)
35-
36-
# Now, save the image. This is a reference into the img_reply structure.
37-
img_data_arr = xcb.get_image_data(img_reply)
38-
# Copy this into a new bytearray, so that it will persist after we clear the image structure.
39-
img_data = bytearray(img_data_arr)
40-
41-
if img_reply.depth != self.drawable_depth or img_reply.visual.value != self.drawable_visual_id:
42-
# This should never happen; a window can't change its visual.
43-
msg = (
44-
"Server returned an image with a depth or visual different than it initially reported: "
45-
f"expected {self.drawable_depth},{hex(self.drawable_visual_id)}, "
46-
f"got {img_reply.depth},{hex(img_reply.visual.value)}"
47-
)
48-
raise ScreenShotError(msg)
49-
50-
return self.cls_image(img_data, monitor)
18+
return super()._grab_impl_xgetimage(monitor)

0 commit comments

Comments
 (0)