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
This commit is contained in:
1 parent
dc2f754f86
commit
d0f6fd8818
1 file changed
+171
-36
+171
-36
@@ -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")
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user