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
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
17 changes: 17 additions & 0 deletions src/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
54 changes: 54 additions & 0 deletions src/core/compat.py
Original file line number Diff line number Diff line change
@@ -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
25 changes: 25 additions & 0 deletions tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
28 changes: 28 additions & 0 deletions tests/test_ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,31 @@ def test_autostart_content_quotes_executable_paths(monkeypatch):

monkeypatch.setattr("src.ui.tray.platform.system", lambda: "Darwin")
assert f"<string>{spaced}</string>" 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 == []
Loading