Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
18 changes: 17 additions & 1 deletion docs/source/release-history/v10.2.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,11 @@ Actual performance improvements will vary depending on factors such as:
- hardware
- whether the shared-memory capture path is available

## Windows Capture Improvements
## Windows Improvements

Windows has received both capture-reliability improvements and a package restructure that prepares the codebase for additional backends.

### Capture with `CreateDIBSection`

The Windows screenshot implementation now uses **`CreateDIBSection`** instead of `GetDIBits`.

Expand All @@ -166,6 +170,18 @@ Additional improvements include:
- improved Win32 error handling and diagnostics
- fixes for capture failures during extended recordings

### Package Restructure

`mss.windows` is now a **package** rather than a single module, with a `choose_impl()` dispatcher that mirrors the `mss.linux` architecture. This prepares the project for future Windows backends (such as a DXGI-based implementation) that will sit alongside the existing GDI code.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need to mention the internal aspects: choose_impl, MSSImplWindows, BACKENDS, or mss.windows.gdi. Discussing internal implementation details in user-facing documentation risks confusion about what users are supposed to access.

It should be sufficient to say that the Windows stuff was restructured, the existing GDI backend is still the default - and currently only - implementation, and that this was done to prepare for a DXGI implementation.


The existing GDI implementation has moved to `mss.windows.gdi`, but the previously-public names remain importable from `mss.windows` unchanged:

- `mss.windows.MSS`
- `mss.windows.MSSImplWindows`
- `mss.windows.BACKENDS`

Existing code does not need to be updated.

## macOS Stability Fix

A memory leak in the macOS backend has been fixed.
Expand Down
5 changes: 3 additions & 2 deletions src/mss/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,9 +158,10 @@ def _choose_impl(**kwargs: Any) -> MSSImplementation:
return choose_impl_linux(**kwargs)

if os_ == "windows":
from mss.windows import MSSImplWindows # noqa: PLC0415
from mss.windows import choose_impl as choose_impl_windows # noqa: PLC0415

return MSSImplWindows(**kwargs)
# Windows has its own factory to choose the backend.
return choose_impl_windows(**kwargs)

msg = f"System {os_!r} not (yet?) implemented."
raise ScreenShotError(msg)
Expand Down
63 changes: 63 additions & 0 deletions src/mss/windows/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""Microsoft Windows backend dispatcher for MSS screenshot implementations."""

from __future__ import annotations

import warnings
from typing import Any

from mss.base import MSS as _MSS
from mss.base import MSSImplementation
from mss.exception import ScreenShotError

# Re-export the GDI implementation so existing code that references
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unnecessary; MSSImplWindows was never publicly released.

# ``mss.windows.MSSImplWindows`` keeps working.
from mss.windows.gdi import MSSImplWindows

__all__ = ["MSS"]

BACKENDS = ["default"]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would suggest adding an explicit "gdi", similar to how the Linux implementation has all the backends available by explicit request. Currently, of course, this would be the same as "default". The intention, though, is that "default" will eventually be able to look at things like the Windows version to choose the backend to use.



class MSS(_MSS):
"""Deprecated Microsoft Windows compatibility constructor.

Use :class:`mss.MSS` instead.
"""

def __init__(self, /, **kwargs: Any) -> None:
# TODO(jholveck): #493 Remove compatibility constructor after 10.x transition period.
warnings.warn(
"mss.windows.MSS is deprecated and will be removed in 11.0; use mss.MSS instead",
DeprecationWarning,
stacklevel=2,
)
super().__init__(**kwargs)


def choose_impl(backend: str = "default", **kwargs: Any) -> MSSImplementation:
"""Return a backend-specific MSS implementation for Microsoft Windows.

Selects and instantiates the appropriate Windows backend based on the
``backend`` parameter.

:param backend: Backend selector. Valid values:

- ``"default"`` (default): GDI-based backend using ``BitBlt`` and
``CreateDIBSection`` for direct memory access to pixel data.

.. versionadded:: 10.3.0 Prior to this version, Windows had a single
implementation selected through :class:`mss.windows.MSS`.

:param kwargs: Additional keyword arguments passed to the backend class.
:returns: An MSS backend implementation.

.. versionadded:: 10.3.0 Prior to this version, this didn't exist:
Windows had a single implementation selected through
:class:`mss.windows.MSS`.
"""
backend = backend.lower()
if backend == "default":
return MSSImplWindows(**kwargs)
assert backend not in BACKENDS # noqa: S101
msg = f"Backend {backend!r} not (yet?) implemented."
raise ScreenShotError(msg)
37 changes: 7 additions & 30 deletions src/mss/windows.py → src/mss/windows/gdi.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Windows GDI-based backend for MSS.
"""GDI-based backend for MSS on Microsoft Windows.

Uses user32/gdi32 APIs to capture the desktop and enumerate monitors.
This implementation uses CreateDIBSection for direct memory access to pixel data.
Expand All @@ -8,7 +8,6 @@

import ctypes
import sys
import warnings
from ctypes import POINTER, WINFUNCTYPE, Structure, WinError, _Pointer
from ctypes.wintypes import (
BOOL,
Expand All @@ -31,7 +30,6 @@
)
from typing import TYPE_CHECKING

from mss.base import MSS as _MSS
from mss.base import MSSImplementation
from mss.exception import ScreenShotError

Expand All @@ -40,25 +38,7 @@

from mss.models import CFunctionsErrChecked, Monitor, Monitors

__all__ = ("MSS",)

BACKENDS = ["default"]


class MSS(_MSS):
"""Deprecated Windows compatibility constructor.

Use :class:`mss.MSS` instead.
"""

def __init__(self, /, **kwargs: Any) -> None:
# TODO(jholveck): #493 Remove compatibility constructor after 10.x transition period.
warnings.warn(
"mss.windows.MSS is deprecated and will be removed in 11.0; use mss.MSS instead",
DeprecationWarning,
stacklevel=2,
)
super().__init__(**kwargs)
__all__ = ("MSSImplWindows",)


LPCRECT = POINTER(RECT) # Actually a const pointer, but ctypes has no const.
Expand Down Expand Up @@ -128,7 +108,7 @@ class DISPLAY_DEVICEW(Structure): # noqa: N801
MONITORNUMPROC = WINFUNCTYPE(BOOL, HMONITOR, HDC, POINTER(RECT), LPARAM)


def _errcheck(result: BOOL | _Pointer, func: Callable, arguments: tuple) -> tuple:
def _errcheck(result: int | _Pointer, func: Callable, arguments: tuple) -> tuple:
"""If the result is zero, raise an exception."""
if not result:
# Notably, the errno that is in winerror may not be relevant. Use the winerror and strerror attributes
Expand Down Expand Up @@ -183,13 +163,14 @@ def _errcheck(result: BOOL | _Pointer, func: Callable, arguments: tuple) -> tupl


class MSSImplWindows(MSSImplementation):
"""Multiple ScreenShots implementation for Microsoft Windows.
"""Multiple ScreenShots implementation for Microsoft Windows (GDI backend).

This implementation uses CreateDIBSection for direct memory access to pixel data,
which eliminates the need for GetDIBits. The DIB pixel data is written directly
to system-managed memory that we can read from.

This has no Windows-specific constructor parameters.
This backend is selected by ``backend="default"`` and has no Windows-specific
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As above, I suggest making "gdi" the name for this backend, and "default" essentially an alias.

constructor parameters.

.. seealso::

Expand All @@ -209,13 +190,9 @@ class MSSImplWindows(MSSImplementation):
"user32",
}

def __init__(self, *, backend: str = "default") -> None:
def __init__(self) -> None:
super().__init__()

if backend != "default":
msg = 'The only valid backend on this platform is "default".'
raise ScreenShotError(msg)

# user32 and gdi32 should not be changed after initialization.
self.user32 = ctypes.WinDLL("user32", use_last_error=True)
self.gdi32 = ctypes.WinDLL("gdi32", use_last_error=True)
Expand Down
6 changes: 4 additions & 2 deletions src/tests/test_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ def test_sdist() -> None:
f"mss-{__version__}/src/mss/py.typed",
f"mss-{__version__}/src/mss/screenshot.py",
f"mss-{__version__}/src/mss/tools.py",
f"mss-{__version__}/src/mss/windows.py",
f"mss-{__version__}/src/mss/windows/__init__.py",
f"mss-{__version__}/src/mss/windows/gdi.py",
f"mss-{__version__}/src/tests/__init__.py",
f"mss-{__version__}/src/tests/bench_bgra2rgb.py",
f"mss-{__version__}/src/tests/bench_general.py",
Expand Down Expand Up @@ -157,5 +158,6 @@ def test_wheel() -> None:
"mss/py.typed",
"mss/screenshot.py",
"mss/tools.py",
"mss/windows.py",
"mss/windows/__init__.py",
"mss/windows/gdi.py",
]
9 changes: 9 additions & 0 deletions src/tests/test_windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,22 @@
import pytest

import mss
from mss.exception import ScreenShotError

try:
import mss.windows
except ImportError:
pytestmark = pytest.mark.skip


def test_choose_impl_unknown_backend_raises() -> None:
"""``choose_impl()`` must reject backends that are not registered in ``BACKENDS``."""
bogus = "definitely-not-a-real-backend"

with pytest.raises(ScreenShotError, match=bogus):
mss.windows.choose_impl(backend=bogus)


def test_region_caching() -> None:
"""The region to grab is cached, ensure this is well-done."""
with mss.MSS() as sct:
Expand Down
Loading