Skip to content

Commit 712503a

Browse files
authored
feat(linux): Implement an XShmGetImage-based backend (#431)
* Add XCB MIT-SHM support, and factor out the XCB setup This only adds the support for the XCB MIT-SHM extension to mss's internal xcb libraries. The actual usage of shared memory for screenshots will be done in a future commit. * 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. * Expand the xcb-util-errors test to include xshmgetimage * Track down the closed memfd. Previously, I had been trying to close the memfd in _shutdown_shm, and ignoring EBADF. It turns out that XCB will close the memfd when you send it to the X server. I think this was one potential cause of the issues I saw in test_thread_safety: the two threads would be reallocated each others' fds, leading to thread A closing an fd that thread B was using, thinking that it was thread A's memfd. Fix so that the memfd is only explicitly closed in an error situation. * Ensure that an X11 connection is open during the test session. Under X11, when the last client disconnects, the server resets. If a new client tries to connect before the reset is complete, it may fail. Since we often run the tests under Xvfb, they're frequently the only clients. Since our tests run in rapid succession, this combination can lead to intermittent failures. To avoid this, we open a connection at the start of the test session and keep it open until the end. * Update the docs Also, before marking XShmGetImage as unavailable if the first grab() fails, also test to see if the fallback XGetImage also fails (such as if the user gave an out-of-bounds rect). In that case, just reraise the exception and try XShmGetImage again with the next grab(). * Changes that should have been in the previous commit * Change the default backend on Linux to xshmgetimage Also, fix a minor error in the docs formatting. * Add a --backend argument to the CLI
1 parent 1fc97aa commit 712503a

28 files changed

+1542
-355
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ See Git checking messages for full history.
55
## 10.1.1.dev0 (2025-xx-xx)
66
- Linux: check the server for Xrandr support version (#417)
77
- Linux: improve typing and error messages for X libraries (#418)
8-
- Linux: add a new XCB backend for better thread safety, error-checking, and future development (#425)
8+
- Linux: introduce an XCB-powered backend stack with a factory in ``mss.linux`` while keeping the Xlib code as a fallback (#425)
9+
- Linux: add the XShmGetImage backend with automatic XGetImage fallback and explicit status reporting (#431)
910
- :heart: contributors: @jholveck
1011

1112
## 10.1.0 (2025-08-16)

CHANGES.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
11
# Technical Changes
22

3+
## 10.1.1 (2025-xx-xx)
4+
5+
### linux/__init__.py
6+
- Added an ``mss()`` factory to select between the different GNU/Linux backends.
7+
8+
### linux/xlib.py
9+
- Moved the legacy Xlib backend into the ``mss.linux.xlib`` module to be used as a fallback implementation.
10+
11+
### linux/xgetimage.py
12+
- Added an XCB-based backend that mirrors XGetImage semantics.
13+
14+
### linux/xshmgetimage.py
15+
- Added an XCB backend powered by XShmGetImage with ``shm_status`` and ``shm_fallback_reason`` attributes for diagnostics.
16+
317
## 10.1.0 (2025-08-16)
418

519
### darwin.py

docs/source/api.rst

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,36 @@ GNU/Linux
3333

3434
.. module:: mss.linux
3535

36+
Factory function to return the appropriate backend implementation.
37+
38+
.. function:: mss(backend="default", **kwargs)
39+
40+
:keyword str backend: Backend name ("default", "xlib", "xgetimage", or "xshmgetimage").
41+
:keyword display: Display name (e.g., ":0.0") for the X server. Default is taken from the :envvar:`DISPLAY` environment variable.
42+
:type display: str or None
43+
:param kwargs: Additional arguments passed to the backend MSS class.
44+
:rtype: :class:`mss.base.MSSBase`
45+
:return: Backend-specific MSS instance.
46+
47+
Factory returning a proper MSS class instance for GNU/Linux.
48+
The backend parameter selects the implementation:
49+
50+
- "default" or "xshmgetimage": XCB-based backend using XShmGetImage (default, with automatic fallback to XGetImage)
51+
- "xgetimage": XCB-based backend using XGetImage
52+
- "xlib": Traditional Xlib-based backend retained for environments without working XCB libraries
53+
54+
.. function:: MSS(*args, **kwargs)
55+
56+
Alias for :func:`mss` for backward compatibility.
57+
58+
59+
Xlib Backend
60+
^^^^^^^^^^^^
61+
62+
.. module:: mss.linux.xlib
63+
64+
Legacy Xlib-based backend, kept as a fallback when XCB is unavailable.
65+
3666
.. attribute:: CFUNCTIONS
3767

3868
.. versionadded:: 6.1.0
@@ -79,6 +109,55 @@ GNU/Linux
79109

80110
.. versionadded:: 8.0.0
81111

112+
113+
XGetImage Backend
114+
^^^^^^^^^^^^^^^^^
115+
116+
.. module:: mss.linux.xgetimage
117+
118+
XCB-based backend using XGetImage protocol.
119+
120+
.. class:: MSS
121+
122+
XCB implementation using XGetImage for screenshot capture.
123+
124+
125+
XShmGetImage Backend
126+
^^^^^^^^^^^^^^^^^^^^
127+
128+
.. module:: mss.linux.xshmgetimage
129+
130+
XCB-based backend using XShmGetImage protocol with shared memory.
131+
132+
.. class:: ShmStatus
133+
134+
Enum describing the availability of the X11 MIT-SHM extension used by the backend.
135+
136+
.. attribute:: UNKNOWN
137+
138+
Initial state before any capture confirms availability or failure.
139+
140+
.. attribute:: AVAILABLE
141+
142+
Shared-memory capture works and will continue to be used.
143+
144+
.. attribute:: UNAVAILABLE
145+
146+
Shared-memory capture failed; MSS will use XGetImage.
147+
148+
.. class:: MSS
149+
150+
XCB implementation using XShmGetImage for screenshot capture.
151+
Falls back to XGetImage if shared memory extension is unavailable.
152+
153+
.. attribute:: shm_status
154+
155+
Current shared-memory availability, using :class:`mss.linux.xshmgetimage.ShmStatus`.
156+
157+
.. attribute:: shm_fallback_reason
158+
159+
Optional string describing why the backend fell back to XGetImage when MIT-SHM is unavailable.
160+
82161
Windows
83162
-------
84163

docs/source/developers.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,12 @@ To build the documentation, simply type::
5050

5151
$ python -m pip install -e '.[docs]'
5252
$ sphinx-build -d docs docs/source docs_out --color -W -bhtml
53+
54+
55+
XCB Code Generator
56+
==================
57+
58+
The GNU/Linux XCB backends rely on generated ctypes bindings. If you need to
59+
add new XCB requests or types, do **not** edit ``src/mss/linux/xcbgen.py`` by
60+
hand. Instead, follow the workflow described in ``src/xcbproto/README.md``,
61+
which explains how to update ``gen_xcb_to_py.py`` and regenerate the bindings.

docs/source/examples.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,15 @@ You can handle data using a custom class:
103103

104104
.. versionadded:: 3.1.0
105105

106+
GNU/Linux XShm backend
107+
----------------------
108+
109+
Select the XShmGetImage backend explicitly and inspect whether it is active or
110+
falling back to XGetImage:
111+
112+
.. literalinclude:: examples/linux_xshm_backend.py
113+
:lines: 7-
114+
106115
PIL
107116
===
108117

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
"""This is part of the MSS Python's module.
2+
Source: https://github.com/BoboTiG/python-mss.
3+
4+
Select the XShmGetImage backend explicitly and inspect its status.
5+
"""
6+
7+
from mss.linux.xshmgetimage import MSS as mss
8+
9+
with mss() as sct:
10+
screenshot = sct.grab(sct.monitors[1])
11+
print(f"Captured screenshot dimensions: {screenshot.size.width}x{screenshot.size.height}")
12+
13+
print(f"shm_status: {sct.shm_status.name}")
14+
if sct.shm_fallback_reason:
15+
print(f"Falling back to XGetImage because: {sct.shm_fallback_reason}")
16+
else:
17+
print("MIT-SHM capture active.")

docs/source/usage.rst

Lines changed: 55 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ Usage
55
Import
66
======
77

8-
So MSS can be used as simply as::
8+
MSS can be used as simply as::
99

1010
from mss import mss
1111

@@ -20,6 +20,11 @@ Or import the good one based on your operating system::
2020
# Microsoft Windows
2121
from mss.windows import MSS as mss
2222

23+
On GNU/Linux you can also import a specific backend (see :ref:`backends`)
24+
directly when you need a particular implementation, for example::
25+
26+
from mss.linux.xshmgetimage import MSS as mss
27+
2328

2429
Instance
2530
========
@@ -49,18 +54,56 @@ This is a much better usage, memory efficient::
4954
Also, it is a good thing to save the MSS instance inside an attribute of your class and calling it when needed.
5055

5156

57+
.. _backends:
58+
59+
Backends
60+
--------
61+
62+
Some platforms have multiple ways to take screenshots. In MSS, these are known as *backends*. The :py:func:`mss` functions will normally autodetect which one is appropriate for your situation, but you can override this if you want. For instance, you may know that your specific situation requires a particular backend.
63+
64+
If you want to choose a particular backend, you can use the :py::`backend` keyword to :py:func:`mss`::
65+
66+
with mss(backend="xgetimage") as sct:
67+
...
68+
69+
Alternatively, you can also directly import the backend you want to use::
70+
71+
from mss.linux.xgetimage import MSS as mss
72+
73+
Currently, only the GNU/Linux implementation has multiple backends. These are described in their own section below.
74+
75+
5276
GNU/Linux
5377
---------
5478

55-
On GNU/Linux, you can specify which display to use (useful for distant screenshots via SSH)::
56-
57-
with mss(display=":0.0") as sct:
58-
# ...
79+
Display
80+
^^^^^^^
5981

60-
A more specific example (only valid on GNU/Linux):
82+
On GNU/Linux, the default display is taken from the :envvar:`DISPLAY` environment variable. You can instead specify which display to use (useful for distant screenshots via SSH) using the ``display`` keyword:
6183

6284
.. literalinclude:: examples/linux_display_keyword.py
63-
:lines: 9-
85+
:lines: 7-
86+
87+
88+
Backends
89+
^^^^^^^^
90+
91+
The GNU/Linux implementation has multiple backends (see :ref:`backends`), or ways it can take screenshots. The :py:func:`mss.mss` and :py:func:`mss.linux.mss` functions will normally autodetect which one is appropriate, but you can override this if you want.
92+
93+
There are three available backends.
94+
95+
:py:mod:`xshmgetimage` (default)
96+
The fastest backend, based on :c:func:`xcb_shm_get_image`. It is roughly three times faster than :py:mod:`xgetimage`
97+
and is used automatically. When the MIT-SHM extension is unavailable (for example on remote SSH displays), it
98+
transparently falls back to :py:mod:`xgetimage` so you can always request it safely.
99+
100+
:py:mod:`xgetimage`
101+
A highly-compatible, but slower, backend based on :c:func:`xcb_get_image`. Use this explicitly only when you know
102+
that :py:mod:`xshmgetimage` cannot operate in your environment.
103+
104+
:py:mod:`xlib`
105+
The legacy backend powered by :c:func:`XGetImage`. It is kept solely for systems where XCB libraries are
106+
unavailable and no new features are being added to it.
64107

65108

66109
Command Line
@@ -73,8 +116,8 @@ You can use ``mss`` via the CLI::
73116
Or via direct call from Python::
74117

75118
$ python -m mss --help
76-
usage: __main__.py [-h] [-c COORDINATES] [-l {0,1,2,3,4,5,6,7,8,9}]
77-
[-m MONITOR] [-o OUTPUT] [-q] [-v] [--with-cursor]
119+
usage: mss [-h] [-c COORDINATES] [-l {0,1,2,3,4,5,6,7,8,9}] [-m MONITOR]
120+
[-o OUTPUT] [--with-cursor] [-q] [-b BACKEND] [-v]
78121

79122
options:
80123
-h, --help show this help message and exit
@@ -86,6 +129,9 @@ Or via direct call from Python::
86129
the monitor to screenshot
87130
-o OUTPUT, --output OUTPUT
88131
the output file name
132+
-b, --backend BACKEND
133+
platform-specific backend to use
134+
(Linux: default/xlib/xgetimage/xshmgetimage; macOS/Windows: default)
89135
--with-cursor include the cursor
90136
-q, --quiet do not print created files
91137
-v, --version show program's version number and exit

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ ignore = [
179179
"docs/source/*" = [
180180
"ERA001", # commented code
181181
"INP001", # file `xxx` is part of an implicit namespace package
182+
"N811", # importing constant (MSS) as non-constant (mss)
182183
]
183184
"src/tests/*" = [
184185
"FBT001", # boolean-typed positional argument in function definition

src/mss/__main__.py

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,38 @@
33
"""
44

55
import os.path
6+
import platform
67
import sys
7-
from argparse import ArgumentParser
8+
from argparse import ArgumentError, ArgumentParser
89

910
from mss import __version__
1011
from mss.exception import ScreenShotError
1112
from mss.factory import mss
1213
from mss.tools import to_png
1314

1415

16+
def _backend_cli_choices() -> list[str]:
17+
os_name = platform.system().lower()
18+
if os_name == "darwin":
19+
from mss import darwin # noqa: PLC0415
20+
21+
return list(darwin.BACKENDS)
22+
if os_name == "linux":
23+
from mss import linux # noqa: PLC0415
24+
25+
return list(linux.BACKENDS)
26+
if os_name == "windows":
27+
from mss import windows # noqa: PLC0415
28+
29+
return list(windows.BACKENDS)
30+
return ["default"]
31+
32+
1533
def main(*args: str) -> int:
1634
"""Main logic."""
17-
cli_args = ArgumentParser(prog="mss")
35+
backend_choices = _backend_cli_choices()
36+
37+
cli_args = ArgumentParser(prog="mss", exit_on_error=False)
1838
cli_args.add_argument(
1939
"-c",
2040
"--coordinates",
@@ -40,9 +60,19 @@ def main(*args: str) -> int:
4060
action="store_true",
4161
help="do not print created files",
4262
)
63+
cli_args.add_argument(
64+
"-b", "--backend", default="default", choices=backend_choices, help="platform-specific backend to use"
65+
)
4366
cli_args.add_argument("-v", "--version", action="version", version=__version__)
4467

45-
options = cli_args.parse_args(args or None)
68+
try:
69+
options = cli_args.parse_args(args or None)
70+
except ArgumentError as e:
71+
# By default, parse_args will print and the error and exit. We
72+
# return instead of exiting, to make unit testing easier.
73+
cli_args.print_usage(sys.stderr)
74+
print(f"{cli_args.prog}: error: {e}", file=sys.stderr)
75+
return 2
4676
kwargs = {"mon": options.monitor, "output": options.output}
4777
if options.coordinates:
4878
try:
@@ -61,7 +91,7 @@ def main(*args: str) -> int:
6191
kwargs["output"] = "sct-{top}x{left}_{width}x{height}.png"
6292

6393
try:
64-
with mss(with_cursor=options.with_cursor) as sct:
94+
with mss(with_cursor=options.with_cursor, backend=options.backend) as sct:
6595
if options.coordinates:
6696
output = kwargs["output"].format(**kwargs["mon"])
6797
sct_img = sct.grab(kwargs["mon"])

src/mss/base.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,15 @@
1818

1919
from mss.models import Monitor, Monitors
2020

21+
# Prior to 3.11, Python didn't have the Self type. typing_extensions does, but we don't want to depend on it.
22+
try:
23+
from typing import Self
24+
except ImportError: # pragma: nocover
25+
try:
26+
from typing_extensions import Self
27+
except ImportError: # pragma: nocover
28+
Self = Any # type: ignore[assignment]
29+
2130
try:
2231
from datetime import UTC
2332
except ImportError: # pragma: nocover
@@ -58,7 +67,7 @@ def __init__(
5867
msg = 'The only valid backend on this platform is "default".'
5968
raise ScreenShotError(msg)
6069

61-
def __enter__(self) -> MSSBase: # noqa:PYI034
70+
def __enter__(self) -> Self:
6271
"""For the cool call `with MSS() as mss:`."""
6372
return self
6473

0 commit comments

Comments
 (0)