From d0f6fd8818e6142f21db7cbc962f6dc03e368592 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Wed, 16 Sep 2026 16:57:18 +0200 Subject: [PATCH] Hybrid brightness: native DDC/CI (dxva2) with per-monitor WMI fallback - BrightnessController enumerates monitors, opens each via dxva2 and reads/writes brightness directly (sub-ms); falls back per monitor to screen_brightness_control (WMI) for panels without DDC/CI (laptop internal screens) - native dim sets each monitor to its minimum; restore sets saved value - tagged v1-wmi-only before this change as safe rollback point --- black_screen_dim.py | 207 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 171 insertions(+), 36 deletions(-) diff --git a/black_screen_dim.py b/black_screen_dim.py index e3cc1b2..5151e5b 100644 --- a/black_screen_dim.py +++ b/black_screen_dim.py @@ -45,34 +45,142 @@ def setup_logging(): # -------------------------------------------------------------------------- # Brightness — every monitor # -------------------------------------------------------------------------- -def get_all_brightness(): - try: - vals = sbc.get_brightness() - except Exception as e: - LOG.warning("get_brightness failed: %r", e) - return None - if isinstance(vals, list): - return list(vals) if vals else None - return [vals] +# -------------------------------------------------------------------------- +# Brightness — hybrid: fast native DDC/CI (dxva2) with per-monitor WMI fallback +# -------------------------------------------------------------------------- +class BrightnessController: + """Controls brightness on every monitor. Prefers the native DDC/CI API + (dxva2.dll, sub-ms) and falls back per monitor to screen_brightness_control + (WMI) for panels that don't expose DDC/CI (e.g. laptop internal screens).""" + PHYSICAL_MONITOR = None # ctypes struct, set on Windows -def set_all_brightness(value): - try: - sbc.set_brightness(int(value)) - return True - except Exception as e: - LOG.warning("set_brightness(%s) failed: %r", value, e) - return False + def __init__(self): + self.targets = [] # [("native", handle, (lo,cur,hi)), ("wmi", idx)] + self.saved = None + @staticmethod + def _native(): + return sys.platform == "win32" and hasattr(ctypes.windll, "dxva2") -def restore_all_brightness(original): - if not original: - return - for i, b in enumerate(original): + def _open_physical(self, hmon): + dx = ctypes.windll.dxva2 + arr = (BrightnessController.PHYSICAL_MONITOR * 1)() + if not dx.GetPhysicalMonitorsFromHMONITOR(int(hmon), 1, ctypes.byref(arr)): + return None + return arr[0].hPhysicalMonitor + + def _read_native(self, handle): + dx = ctypes.windll.dxva2 + lo = wt.DWORD(); cur = wt.DWORD(); hi = wt.DWORD() + if not dx.GetMonitorBrightness(int(handle), ctypes.byref(lo), ctypes.byref(cur), ctypes.byref(hi)): + return None + return (lo.value, cur.value, hi.value) + + def _write_native(self, handle, value): try: - sbc.set_brightness(int(b), display=i) + dx = ctypes.windll.dxva2 + return bool(dx.SetMonitorBrightness(int(handle), wt.DWORD(int(value)))) except Exception as e: - LOG.warning("restore brightness monitor %d failed: %r", i, e) + LOG.warning("native write failed: %r", e) + return False + + def _close_native(self, handle): + dx = ctypes.windll.dxva2 + arr = (BrightnessController.PHYSICAL_MONITOR * 1)() + arr[0].hPhysicalMonitor = handle + dx.DestroyPhysicalMonitors(1, ctypes.byref(arr)) + + def _build(self): + self.targets = [] + if self._native(): + for i, hmon in enumerate(enum_monitor_handles()): + handle = None + try: + handle = self._open_physical(hmon) + except Exception as e: + LOG.warning("native open failed: %r", e) + handle = None + if handle is not None: + try: + r = self._read_native(handle) + except Exception as e: + LOG.warning("native read failed: %r", e) + r = None + if r is not None: + self.targets.append(("native", handle, r)) + continue + try: + self._close_native(handle) + except Exception: + pass + self.targets.append(("wmi", i)) + if not self.targets: + LOG.warning("no monitors via native; brightness control disabled") + LOG.info("brightness targets: %s", [t[0] for t in self.targets]) + + def save(self): + self._build() + values = [] + for t in self.targets: + if t[0] == "native": + _, handle, (lo, cur, hi) = t + values.append(cur) + else: + idx = t[1] + try: + v = sbc.get_brightness(display=idx) + except Exception as e: + LOG.warning("wmi get display %d failed: %r", idx, e) + v = None + if isinstance(v, list): + v = v[0] if v else None + values.append(v) + self.saved = values + LOG.info("saved brightness: %s", values) + return values + + def dim(self): + for t in self.targets: + if t[0] == "native": + _, handle, (lo, cur, hi) = t + ok = self._write_native(handle, lo) + LOG.info("native dim -> %s (%s)", lo, "ok" if ok else "FAIL") + else: + idx = t[1] + try: + sbc.set_brightness(0, display=idx) + LOG.info("wmi dim display %d", idx) + except Exception as e: + LOG.warning("wmi dim display %d failed: %r", idx, e) + + def restore(self): + if self.saved is None: + self.close() + return + for t, val in zip(self.targets, self.saved): + if val is None: + continue + if t[0] == "native": + _, handle, _ = t + ok = self._write_native(handle, val) + LOG.info("native restore %s (%s)", val, "ok" if ok else "FAIL") + else: + idx = t[1] + try: + sbc.set_brightness(int(val), display=idx) + LOG.info("wmi restore display %d -> %s", idx, val) + except Exception as e: + LOG.warning("wmi restore display %d failed: %r", idx, e) + self.close() + + def close(self): + for t in self.targets: + if t[0] == "native": + try: + self._close_native(t[1]) + except Exception: + pass # -------------------------------------------------------------------------- @@ -222,6 +330,31 @@ if sys.platform == "win32": kernel32.GetLastError.argtypes = [] kernel32.GetLastError.restype = wt.DWORD + # --- native DDC/CI brightness (dxva2) --- + class PHYSICAL_MONITOR(ctypes.Structure): + _fields_ = [ + ("hPhysicalMonitor", wt.HANDLE), + ("szPhysicalMonitorDescription", ctypes.c_wchar * 128), + ] + + try: + dx = ctypes.windll.dxva2 + dx.GetPhysicalMonitorsFromHMONITOR.argtypes = [ + HMONITOR, wt.DWORD, ctypes.POINTER(PHYSICAL_MONITOR)] + dx.GetPhysicalMonitorsFromHMONITOR.restype = wt.BOOL + dx.GetMonitorBrightness.argtypes = [ + wt.HANDLE, ctypes.POINTER(wt.DWORD), + ctypes.POINTER(wt.DWORD), ctypes.POINTER(wt.DWORD)] + dx.GetMonitorBrightness.restype = wt.BOOL + dx.SetMonitorBrightness.argtypes = [wt.HANDLE, wt.DWORD] + dx.SetMonitorBrightness.restype = wt.BOOL + dx.DestroyPhysicalMonitors.argtypes = [ + wt.DWORD, ctypes.POINTER(PHYSICAL_MONITOR)] + dx.DestroyPhysicalMonitors.restype = wt.BOOL + BrightnessController.PHYSICAL_MONITOR = PHYSICAL_MONITOR + except Exception as e: + LOG.warning("dxva2 init failed: %r", e) + _hwnds = [] _hint_active = True _hook_id = None @@ -238,6 +371,16 @@ if sys.platform == "win32": user32.EnumDisplayMonitors(None, None, MONITORENUMPROC(cb), 0) return rects + def enum_monitor_handles(): + handles = [] + + def cb(hmon, hdc, lprect, lparam): + handles.append(hmon) + return True + + user32.EnumDisplayMonitors(None, None, MONITORENUMPROC(cb), 0) + return handles + def _wndproc(hwnd, msg, wparam, lparam): global _hint_active if msg == WM_PAINT: @@ -357,15 +500,9 @@ if sys.platform == "win32": break -def _dim_and_save(): - """Save current brightness and dim all monitors in the background so the - black overlay can appear instantly; the dimming itself is invisible.""" - global _original_brightness - orig = get_all_brightness() - _original_brightness = orig - LOG.info("saved brightness: %s", orig) - if orig is not None: - set_all_brightness(0) +def _dim_and_save(bc): + bc.save() + bc.dim() def main(): @@ -373,7 +510,8 @@ def main(): LOG.info("starting Black Screen Dim") # Show the black overlay FIRST so the screen goes black immediately. # Brightness saving/dimming runs in the background, invisible behind it. - dim_thread = threading.Thread(target=_dim_and_save, daemon=True) + bc = BrightnessController() + dim_thread = threading.Thread(target=_dim_and_save, args=(bc,), daemon=True) dim_thread.start() try: if sys.platform == "win32": @@ -384,10 +522,7 @@ def main(): LOG.exception("overlay error: %r", e) finally: dim_thread.join(timeout=10) - orig = _original_brightness - if orig is not None: - restore_all_brightness(orig) - LOG.info("restored brightness: %s", orig) + bc.restore() LOG.info("exiting")