GPU usage monitoring with selectable animation source (CPU/GPU/Both) - #41
Conversation
…Both)
- New 'Usage Source' tray menu (CPU / GPU / CPU + GPU), persisted per
platform (Windows registry / macOS defaults / Linux settings.conf),
default CPU so existing installs are unaffected. Tooltip shows the
selected source's usage; sleep/idle logic follows effective usage.
- New GpuMonitor trait (fn get_gpu_usage() -> io::Result<f64>) with
per-platform implementations:
- Windows: PDH \GPU Engine(*)\Utilization Percentage (Win10 1803+,
all vendors); max across engine instances (Task Manager semantics).
- macOS: IORegistry PerformanceStatistics 'Device Utilization %'
(powermetrics data source, no root). IOKit owns the
IOServiceMatching dictionary (CFReleasing it segfaults - bisected
on real hardware), so the looked-up service handle is cached for
the process lifetime with a one-shot re-lookup on failed reads.
- Linux: /sys/class/drm/card*/device/gpu_busy_percent + nvidia-smi,
max of available sources.
- build.rs: winres import host-gated, resource embedding target-gated
(winres is a target.'cfg(windows)' build-dep resolved against host).
- Verified: Windows (RTX 4090, all 3 sources), Linux (nix build +
smoke test on NixOS), macOS (real hardware, real IORegistry reads).
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c0ad6cdc24
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
- Windows: parse PDH GPU Engine instance names (pid/luid/phys/eng), sum per-process utilization per physical engine (capped at 100%), max across engines; fixes underreporting when multiple processes share an engine and a latent 318-byte buffer overflow (allocated size/24 items instead of size bytes / count items). - macOS: enumerate ALL IOAccelerator/AGXAccelerator services via IOServiceGetServices instead of only the first match; per-service model name for the menu. - Linux: per-card entries from nvidia-smi (index+name) and sysfs (amdgpu); i915 no longer claimed (needs perf/PMU, not viable for a tray app). - New tray menu group 'GPU Device' (All GPUs + per-device, shown only with >=2 GPUs); persisted GpuScope setting on all three platforms; auto-fallback to All GPUs when the selected device disappears. Addresses the multi-GPU aggregation review comments on PR #41.
…rvices does not exist)
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 863af91509
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
The PDH GPU Engine instance name's luid token is a per-GPU-context id — a single physical GPU (this 4090) exposes many luids (3 observed, 727 instances). Using luid as the device id split one card into several 'GPU 0' entries. The physical GPU is the phys field, and a physical engine is (phys, eng); per-engine utilization is the sum of all its (process, context) instances, max across engines. Verified on a live RTX 4090: 33 physical engines, per-engine sums stay <= 100 under load, matching Task Manager's GPU busy. Also add a Windows GPU smoke test (mirroring macOS/Linux).
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c7fbbeca42
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
- macOS: re-enumerate accelerator services every ~30s (eGPU hot-plug) and never cache an empty result, so a later-attached GPU is discoverable. - Scope selection is now session-only (device ids are only stable for the current boot); drop the persisted GpuScope setting on all platforms. - app.rs: only run the stale-device recovery when a GPU sample was actually requested (source != CPU), so CPU mode no longer re-enumerates (and spawns nvidia-smi) every second.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a014cd0fbd
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6fc9717ead
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Windows: - Enumerate installed adapters via DXGI (IDXGIFactory1::EnumAdapters1) so idle (no process-context) secondary GPUs appear in the device menu; software fallback adapters (e.g. Microsoft Basic Render Driver) are filtered out. - Identify physical GPUs by their adapter LUID, which appears in both the PDH instance names and DXGI's AdapterLuid, so menu ids and sampling ids match exactly. The phys field is not a reliable per-adapter index (it is phys_0 for every adapter on some multi-GPU systems). - Allocate the PDH array buffer with an explicit alignment (AlignedBuf) to avoid misaligned-access UB when PDH fills it with counter-value items. macOS: - Reference-count IOKit service handles (SERVICE_REFS) and release them via an RAII EntriesGuard, so superseded or detached device handles are IOObjectRelease'd exactly once when their last reference drops. Cargo.toml: add Win32_Graphics_Dxgi to the windows feature set.
The GPU device submenu was only rebuilt on unrelated setting events, so an eGPU attached (or detached) after startup stayed absent from the menu until the user changed another setting. Re-enumerate and install a fresh menu in the ShowMenu handler before show_menu(), synchronously (not via ui_update) so the fresh menu is installed before it is displayed.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 47ca72a6de
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| Some(cache) => { | ||
| cache.samples_since_refresh += 1; | ||
| cache.samples_since_refresh >= GPU_REFRESH_INTERVAL | ||
| } |
There was a problem hiding this comment.
Refresh macOS GPUs when rebuilding the menu
With the default AnimationSource::Cpu, sample_usage never calls the GPU monitor, so after the initial cache fill this counter advances only when a menu rebuild calls enumerate_gpus. Consequently, attaching an eGPU while CPU mode is selected leaves it absent until the menu has been opened roughly 30 times, rather than refreshing on open. The fresh evidence beyond the earlier cache finding is that the new ShowMenu rebuild still reaches this counter-gated cached path; explicit enumeration should force a refresh, or the refresh deadline should advance independently of GPU sampling.
Useful? React with 👍 / 👎.
The 'GPU Device' menu was shown whenever the machine has more than one GPU, even when the animation is driven by CPU where the device selection is irrelevant. Gate it on current_source != AnimationSource::Cpu so it only appears for the Gpu and Both sources. The menu is already rebuilt on each source change (SetAnimationSource) and on ShowMenu, so it appears/disappears correctly.
What
RustCat can now animate off GPU usage, not just CPU. A new "Usage Source" tray menu item (CPU / GPU / CPU + GPU) selects the source; the choice is persisted per platform (Windows registry / macOS
defaults/ Linux settings.conf) and defaults to CPU, so existing installs are unaffected. The tooltip shows the selected source's usage, and the sleep/idle logic now follows the effective usage of the selected source.How
New
GpuMonitortrait (fn get_gpu_usage() -> io::Result<f64>) with per-platform implementations:\GPU Engine(*)\Utilization Percentage(Win10 1803+, all vendors). Max across engine instances, matching Task Manager's "GPU busy" semantics (summing would double-count parallel engines).PerformanceStatistics→"Device Utilization %"(the same datapowermetricsreads, no root). IOKit takes ownership of theIOServiceMatchingdictionary (CFReleasing it afterwards segfaults — bisected on real hardware), so the looked-up service handle is cached for the process lifetime, with a one-shot re-lookup if a property read fails (stale service, e.g. GPU hot-plug)./sys/class/drm/card*/device/gpu_busy_percent(amdgpu/i915) andnvidia-smi, max of available sources.Build fixes along the way:
build.rs: winres import gated on the build host and resource embedding additionally gated on the target (winres is atarget.'cfg(windows)'build-dependency, which cargo resolves against the host — the runtime-only gate brokenix buildon Linux, and the original host-cfg gate ran winres when cross-compiling to macOS).Verified
nix build .#rustcatpasses; GPU smoke test passes, readingnvidia-smiwithout an X server.cargo buildclean (no new warnings); GPU smoke test passes with real IORegistry readings.Smoke tests are deliberately lenient (pass on machines without a GPU), so
cargo teststays green everywhere.