Coverage for core / dpi_awareness.py: 47.4%
19 statements
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-12 04:49 +0000
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-12 04:49 +0000
1"""
2core.dpi_awareness — single source of truth for Windows per-monitor DPI awareness.
4Why this module exists:
5 Without explicit DPI awareness on Windows, the OS returns LOGICAL coords
6 from the high-level APIs (pyautogui.size, EnumDisplayMonitors,
7 GetWindowRect) but PHYSICAL pixels from low-level capture
8 (pyautogui.screenshot uses BitBlt of the desktop DC at physical res).
10 On a 150%-scaled 2560x1440 display:
11 - pyautogui.size() → (1707, 960) (logical)
12 - pyautogui.screenshot() → 2560x1440 (physical)
13 - EnumDisplayMonitors → (0,0,1707,960) (logical)
14 - VLM grounds coordinates in IMAGE space (physical) → caller scales
15 to (logical) for pyautogui.click → click misses by ~1.5x.
17 Setting DPI awareness once per process makes every API return PHYSICAL
18 coords consistently. Idempotent: calling twice with the same value is
19 a no-op; calling with a different value silently fails (so existing
20 DPI-aware processes aren't disturbed).
22Why a dedicated module:
23 Two call sites needed it (integrations/vlm/local_computer_tool.py for
24 screenshot/click DPI, integrations/remote_desktop/window_capture.py for
25 EnumDisplayMonitors / GetWindowRect). A second copy was added in the
26 Phase 1 VLM commit (693ccad7) and immediately flagged as a DRY
27 violation. Promoting here so there is exactly ONE place that knows
28 about SetProcessDpiAwareness and the (Win 8.1+ shcore vs Win 7 user32)
29 branch, and exactly ONE place to update if Microsoft adds a new tier
30 (e.g. PER_MONITOR_DPI_AWARE_V2 already exists at value 4).
32Use:
33 from core.dpi_awareness import ensure_dpi_aware
34 ensure_dpi_aware() # safe at module-load OR lazy first-call
36 # Optional: query whether we successfully set it
37 from core.dpi_awareness import is_dpi_aware
38 if is_dpi_aware():
39 ...
40"""
41import logging
42import sys
44logger = logging.getLogger('hevolve.dpi_awareness')
46# PROCESS_PER_MONITOR_DPI_AWARE — value passed to SetProcessDpiAwareness.
47# Win 8.1+ accepts 0 (UNAWARE), 1 (SYSTEM_DPI_AWARE), 2 (PER_MONITOR_DPI_AWARE).
48# Win 10 1607+ also has SetProcessDpiAwarenessContext for V2 (DPI scale changes
49# without process restart) but value 2 is the most portable + sufficient for
50# VLM screenshot/click coordinate consistency.
51_PER_MONITOR_DPI_AWARE = 2
53_dpi_aware_set: bool = False
56def ensure_dpi_aware() -> None:
57 """Make this process DPI-aware on Windows. No-op on macOS / Linux.
59 Idempotent and crash-proof: any failure is logged at debug level and
60 swallowed. Safe to call from module-load AND from lazy-first-call
61 paths (the second call is a Win32 no-op when awareness is already set).
62 """
63 global _dpi_aware_set
64 if sys.platform != 'win32':
65 return
66 if _dpi_aware_set:
67 return
68 try:
69 import ctypes
70 try:
71 ctypes.windll.shcore.SetProcessDpiAwareness(_PER_MONITOR_DPI_AWARE)
72 except (AttributeError, OSError):
73 # Pre-Win-8.1: shcore.dll missing or function absent. Fall back
74 # to the older system-wide DPI awareness API. Better than
75 # nothing; coords stay consistent within a single monitor at
76 # the cost of multi-monitor coord weirdness on mixed-DPI setups.
77 ctypes.windll.user32.SetProcessDPIAware()
78 except Exception as e:
79 # Don't blow up the importer of any module that calls this — log
80 # and continue. Worst case: VLM clicks land slightly off, which
81 # the loop's verify-and-retry path will detect.
82 logger.debug(f"DPI awareness setup skipped: {e}")
83 return
84 _dpi_aware_set = True
87def is_dpi_aware() -> bool:
88 """True if :func:`ensure_dpi_aware` has successfully run.
90 Note: returns False on non-Windows (where the concept doesn't apply)
91 AND when the call ran but failed (logged at debug). Callers that
92 need to know "are coordinates physical?" should use this OR explicit
93 platform checks.
94 """
95 return _dpi_aware_set