diff --git a/CHANGELOG.md b/CHANGELOG.md index ebc44a4..d7c717f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,12 +6,15 @@ > **Release Cadence Update:** Planned releases are shifting from weekly to fortnightly (targeting Wednesdays) as we transition toward a monthly release cycle. Critical fixes will still be released immediately as emergency patches. ### Plan -- Aggressive window refresh loop and focus-stealing on Windows [#19] ### Added ### Fixed +- Aggressive window refresh loop and focus-stealing on Windows [#19] +- Legacy hardware (≤4 cores and ≤8 GiB RAM) falls back to software rendering on every platform, sidestepping the GPU/DWM recomposition loop behind the flicker +- Software-rendering fallback uses Qt's `AA_UseSoftwareOpenGL` (Qt 6 removed `AA_DisableHardwareAcceleration`) + ### Docs ## v1.0.1 (2026-08-22) diff --git a/pyproject.toml b/pyproject.toml index abc582b..65f545d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "glint-monitor" -version = "1.0.1" +version = "1.0.2" description = "A Modern Painter-Rendered Desktop System Monitor" readme = "README.md" requires-python = ">=3.10" diff --git a/src/app.py b/src/app.py index 2ebe91c..67baa79 100644 --- a/src/app.py +++ b/src/app.py @@ -2,13 +2,30 @@ import sys +from PyQt6.QtCore import Qt from PyQt6.QtWidgets import QApplication +from src.core.compat import is_low_spec from src.ui.hud import GlassHUD from src.ui.tray import TrayManager +def _maybe_disable_hardware_acceleration() -> None: + """Degrade the renderer on low-spec hosts before QApplication exists. + + Must be called before ``QApplication`` is constructed, because on legacy + hardware the GPU/DWM recomposition of the translucent HUD causes + refresh/focus loops. Applied on every platform so behavior is consistent + regardless of the compositor or graphics stack in use. + """ + if is_low_spec(): + # Qt 6 removed AA_DisableHardwareAcceleration; AA_UseSoftwareOpenGL is + # its replacement and routes rendering through the software rasterizer. + QApplication.setAttribute(Qt.ApplicationAttribute.AA_UseSoftwareOpenGL) + + def main() -> int: + _maybe_disable_hardware_acceleration() app = QApplication(sys.argv) app.setApplicationName("Glint") app.setOrganizationName("ZFordDev") diff --git a/src/core/compat.py b/src/core/compat.py new file mode 100644 index 0000000..03a0bf2 --- /dev/null +++ b/src/core/compat.py @@ -0,0 +1,54 @@ +"""Hardware capability detection for rendering fallbacks. + +Certain subsets of legacy Windows hardware trigger a DWM recomposition +loop when the HUD's translucent, painter-rendered surface is refreshed on +the GUI thread. Rather than forcing hardware acceleration off everywhere, +we only degrade the renderer when the machine clearly falls below a +generation threshold. +""" + +from __future__ import annotations + +import logging + +import psutil + +logger = logging.getLogger(__name__) + +# A legacy profile: few logical cores plus modest physical memory. These +# thresholds are intentionally conservative so modern machines keep +# hardware-accelerated rendering and only clearly dated hardware falls back. +LOW_CORE_THRESHOLD = 4 +LOW_RAM_THRESHOLD_BYTES = 8 * 1024**3 # 8 GiB + + +def _logical_cores() -> int | None: + try: + count = psutil.cpu_count(logical=True) + return count if count else None + except (OSError, RuntimeError): + return None + + +def _total_memory() -> int | None: + try: + return int(psutil.virtual_memory().total) + except (OSError, RuntimeError): + return None + + +def is_low_spec() -> bool: + """Return True when the host is likely GPU-limited legacy hardware.""" + cores = _logical_cores() + memory = _total_memory() + low_cores = cores is not None and cores <= LOW_CORE_THRESHOLD + low_memory = memory is not None and memory <= LOW_RAM_THRESHOLD_BYTES + if low_cores and low_memory: + logger.debug( + "Low-spec profile detected (cores=%s, ram=%s GiB); disabling hardware acceleration", + cores, + None if memory is None else round(memory / 1024**3, 1), + ) + return True + logger.debug("Rendering profile: cores=%s, ram=%s", cores, memory) + return False diff --git a/tests/test_core.py b/tests/test_core.py index 899e33f..c37ed39 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -165,3 +165,28 @@ def test_get_all_skips_heavy_probes_after_stop(): def test_bundled_default_theme_loads(): assert load_theme()["colors"]["text"] + + +def test_low_spec_falls_back_when_cores_and_ram_are_legacy(monkeypatch): + from src.core import compat + + monkeypatch.setattr(compat, "_logical_cores", lambda: 4) + monkeypatch.setattr(compat, "_total_memory", lambda: 6 * 1024**3) + assert compat.is_low_spec() is True + + +def test_low_spec_requires_both_undersized_metrics(monkeypatch): + from src.core import compat + + # Plenty of RAM but few cores -> not legacy enough to degrade rendering. + monkeypatch.setattr(compat, "_logical_cores", lambda: 2) + monkeypatch.setattr(compat, "_total_memory", lambda: 32 * 1024**3) + assert compat.is_low_spec() is False + + +def test_low_spec_tolerates_missing_probes(monkeypatch): + from src.core import compat + + monkeypatch.setattr(compat, "_logical_cores", lambda: None) + monkeypatch.setattr(compat, "_total_memory", lambda: None) + assert compat.is_low_spec() is False diff --git a/tests/test_ui.py b/tests/test_ui.py index cb18a4e..8b17783 100644 --- a/tests/test_ui.py +++ b/tests/test_ui.py @@ -77,3 +77,31 @@ def test_autostart_content_quotes_executable_paths(monkeypatch): monkeypatch.setattr("src.ui.tray.platform.system", lambda: "Darwin") assert f"{spaced}" in TrayManager._startup_content() + + +def test_hardware_acceleration_disabled_when_low_spec(monkeypatch): + import src.app as app_module + + calls = [] + monkeypatch.setattr(app_module, "is_low_spec", lambda: True) + monkeypatch.setattr( + app_module.QApplication, + "setAttribute", + staticmethod(lambda attribute: calls.append(attribute)), + ) + app_module._maybe_disable_hardware_acceleration() + assert calls == [app_module.Qt.ApplicationAttribute.AA_UseSoftwareOpenGL] + + +def test_hardware_acceleration_kept_when_not_low_spec(monkeypatch): + import src.app as app_module + + calls = [] + monkeypatch.setattr(app_module, "is_low_spec", lambda: False) + monkeypatch.setattr( + app_module.QApplication, + "setAttribute", + staticmethod(lambda attribute: calls.append(attribute)), + ) + app_module._maybe_disable_hardware_acceleration() + assert calls == []