# gpur backlog
Known gaps from the review passes, the multi-vendor detection audit and the
full-codebase review in `docs/code-review.md`. Everything already fixed lives in
`CHANGELOG.md`; only open work is listed here, with closed entries struck
through where the reasoning behind them is still worth keeping.
Roughly ordered by what is worth doing first. Nothing open here is a correctness
bug — item 0 was the only one and it is closed; the remainder is granularity,
cost, coverage and polish.
---
## 0. ~~Per-process GPU memory reads `0MiB` for every NVIDIA process on Windows~~ — CLOSED in 0.11.0
Review finding C4. `GpuProcess::gpu_mem_bytes` and `ProcRow`'s `gpu_mem_bytes` /
`cpu_pct` / `host_mem_bytes` are `Option` now, so a figure the backend could not
read renders as `N/A` (`-` in `--once`) and serializes as `null` rather than a
confident zero. NVML reports no per-process memory at all under WDDM — the
ordinary consumer Windows configuration — so that was every NVIDIA process row
on Windows, not a rare unknown.
The cost was taken deliberately: `--json` and `--log` emit `null` where they
emitted `0`, so a log written by 0.11 or later cannot be read by an older gpur,
which is why it went out as a minor release. The useful direction still works —
0.11 replays pre-0.11 recordings, since `0` remains a valid reading.
Kept here rather than deleted because the asymmetry is worth remembering: the
record is written from `ProcRow` and read back into `GpuProcess`, two types that
line up only by field name, and `#[serde(default)]` covers missing keys but not
explicit nulls. Any future field that gains a `null` has to be widened on both
sides in the same release, or replay rejects whole records — silently, in the
mixed case.
## 1. Windows vendor exclusion is per vendor, not per adapter
**Severity: low.** `src/backend/mod.rs` `compose_with_generic`,
`src/backend/windows.rs` `parse::retain_unclaimed`.
The mixed-rig case is fixed: PDH is now a peer rather than a fallback, told at
probe time which PCI vendors the vendor backends claimed, and drops those
adapters. An NVIDIA + Intel Windows laptop lists both cards, each once.
What remains is the granularity of the match. DXGI gives an adapter LUID (plus
`VendorId`/`DeviceId`); NVML gives a UUID or PCI BDF. `DXGI_ADAPTER_DESC1`
carries no bus/device/function, so nothing links a specific adapter to a
specific NVML device without SetupAPI or WMI to resolve LUID → BDF. The filter
therefore works at vendor granularity, and an adapter DXGI enumerates that its
own vendor's backend does not report is hidden rather than duplicated — the
choice being deliberate, since a card listed twice under two backends is the
worse bug.
**Fix, if it ever matters:** resolve LUID → PCI BDF via SetupAPI
(`SPDRP_LOCATION_INFORMATION`) or `Win32_PnPEntity`, and match that against
`Device::pci_info().bus_id`, falling back to the vendor filter when either side
declines. Not worth the dependency until someone hits the blind spot.
Unverifiable without Windows hardware: only `retain_unclaimed` and the
composition wiring are covered by unit tests. That DXGI's `VendorId` matches
`0x10DE`/`0x1002`/`0x8086` on a real adapter, and that NVML and DXGI agree on
which cards exist, rest on inspection.
## 2. The `/proc` sweep runs on the render thread
**Severity: low.** `src/backend/linux.rs` `sweep_clients`, called from
`App::poll` on the render loop.
Each pid's `fd` directory is now walked once rather than once per driver
(measured 4.2 ms against 7.8–9.9 ms over 588 pids), but the scan is still
synchronous, so `event::poll` cannot run during it and keypresses queue. Fine at
ordinary scale; on a 10k-process node at `--tick-ms 100` the sweep can exceed
the tick.
**And it multiplies per vendor.** Each Linux backend runs its own full `/proc`
walk, so an AMD + Intel + nouveau box stats every fd of every process three
times per tick, discarding the clients that belong to the other backends each
time. Deduplicating means one sweep shared across children, which is a `mod.rs`
design change rather than a per-backend fix.
**Fix:** one shared sweep per poll, on a worker thread handing snapshots to the
UI over a channel. Deliberately deferred — structural, not a bug fix.
## 3. An NVIDIA card is still invisible when NVML is broken rather than absent
**Severity: low.** `src/backend/nvidia.rs`.
The nouveau case is covered: a card on the open driver is picked up from sysfs
behind the NVML probe. What remains is a card bound to the _proprietary_ driver
on a host where NVML cannot initialise — a container without `/dev/nvidiactl`, a
mismatched driver/userspace pair. The card is in `/sys/class/drm` and no backend
claims it.
The reason it is not simply fixed the same way: an `nvidia`-bound card exposes
essentially nothing in sysfs — no busy counter, no VRAM, no hwmon in the usual
place — so listing it would add a name and a PCI address and nothing else.
Whether a row of `n/a` beats an absent card is a product call, not a bug.
## 4. Remaining test gaps
- **The kill path's signal branch.** Unit tests in `app.rs` cover every refusal
and one successful signal against a spawned `sleep`, but no PTY test reaches
it, because mock and replay now correctly refuse to signal. End-to-end
coverage needs a backend stub the test binary can inject — which would also
close the one mouse case left untested, the kill dialog as a modal. The filter
prompt covers the identical `input_mode != Normal` condition meanwhile.
- **Mouse kinds with no behaviour attached** — drag, middle and right button,
and moves all fall to the `_ => None` arm. Untested because untested is what
they are: there is nothing to assert yet.
- ~~**`--graphs block` / `ascii` rendering.**~~ Closed. `tests/tui.rs` renders
under each glyph set and asserts both that the style's own glyphs appear and
that the other two styles' do not — a style that silently fell back to braille
would pass a test looking only for its own output. The block case also pins
the fg/bg complement trick that grows partial cells downward, where the
colour-quantization bug lived.
- ~~**Colour modes.**~~ Closed. `NO_COLOR=1` and `TERM=dumb` must render the
dashboard without emitting a single colour SGR, and a session without
`COLORTERM` must quantize to `38;5`/`48;5` rather than send a truecolor escape
to a 256-colour terminal. Needed `Tui::spawn_with_env` to be able to _unset_ a
variable, since unset and empty differ to `detect_color_mode`.
- ~~`--man` and `--once`'s plain-text stdout are unasserted.~~ Closed in
`tests/smoke.rs`: the man page is asserted on its troff-escaped option entries
(a bare `.TH`/name check would pass on a header with no body), and `--once` on
its populated `--mock` output, including that `--once --log` writes exactly
one record despite the deliberate priming poll.
- **No CI job runs against real GPU hardware.** Inherent to hosted runners; the
mitigation was fixture-testing every backend's pure parsers, which is why
`windows.rs` and `apple.rs` now have unit tests that run on any host. Closing
it properly needs a self-hosted runner.
## 5. NVIDIA temperature threshold unread
**Severity: low.** `Device::temperature_threshold()` is available in
`nvml-wrapper 0.12.1` (`device.rs:4048`) and would give the temperature meter a
per-card scale instead of a fixed one. Needs a new `GpuSnapshot` field and a
`draw_meter` scale change.
Junction temperature is genuinely unavailable rather than merely unimplemented:
the only temperature field ids in `nvml-wrapper-sys 0.9.1` are
`NVML_FI_DEV_MEMORY_TEMP` (wired up) and the four `*_TLIMIT` margins, which are
thresholds, not a hotspot reading.
## 6. Waveform history cannot represent "unknown"
**Severity: low.** `src/app.rs`. `utilization_pct` is `Option`, but an
unreadable sample is recorded as `0` in the history ring because a sparkline has
no glyph for absent. The meter above the graph carries the distinction; the
graph does not. Commented at the site.
## 7. Device naming differs per backend
**Severity: low, cosmetic.** `nvidia.rs` gives the marketing name
(`NVIDIA GeForce RTX 4090`), the Linux backends the pci.ids codename
(`Navi 31 [Radeon RX 7900 XT/...]`), Windows the DXGI description, Apple the SoC
brand. Fallbacks differ too: `NVIDIA GPU 0` (index) against
`AMD GPU 0x744c (card1)` (device id plus card).
**Fix:** settle on a convention — prefer the marketing name, fall back to the
codename, always suffix the card or index — and apply it uniformly.
## 8. ~~`splash::build_path` truncates coordinates to `u8`~~ — CLOSED
`src/splash.rs` now scans `ART` in a `const fn` and asserts both dimensions
against `u8::MAX` at compile time, so art that outgrew the cursor path fails the
build instead of scattering the trail silently. A round-trip test proves every
emitted coordinate, cast back, still names the glyph it came from. The `u8` is
still imposed by `hjkl_splash`'s API and has not changed.
## 9. Residual YAGNI
- ~~`draw_meter` takes 8 arguments behind
`#[allow(clippy::too_many_arguments)]`~~ — closed. The four per-bar arguments
are a named `Meter` struct (`src/ui.rs:647`) and the `allow` is gone, so
clippy enforces the shape. Theme and glyph style stayed positional, matching
the neighbouring drawing functions.
- `src/theme.rs:89` `UiTheme::temp_ok` is `pub` but used only by `temp_style` at
`:173` in the same file, unlike its peers `temp_warn` / `temp_crit`. Narrowing
it alone would be asymmetric.
- `src/keys.rs:7` `enum Mode { Normal }` has one variant threaded through
`Keymap<Action, Mode>`. Required by the `hjkl-keymap` API, so it cannot go,
but no second mode is planned — filter and confirm bypass the keymap entirely.
---
## Decisions taken deliberately
Recorded so they are not re-opened as findings.
- **An Intel GPU with no visible DRM clients reports `n/a`, not `0%`.** The fix
for review finding C1 keys utilization off whether the fdinfo sweep attributed
any client to the device, because "nobody is using it" and "I cannot see who
is using it" are indistinguishable from unprivileged userspace — the sweep
reads nothing for processes another user owns. On a desktop this never shows,
since the compositor holds a client owned by the same user; on a headless box
with a genuinely idle iGPU it does. A device whose clients _were_ read and
summed to zero still reports an honest `0%`, so an idle GPU keeps its empty
meter.
- **A pid that leaves the GPU but stays alive remains in the sysinfo cache**
until it exits. The C5 fix sweeps departed pids with a second no-field
refresh, and `remove_dead` drops the ones that have exited; nothing short of a
`ProcessesToUpdate::All` refresh can evict a live process, and that refresh is
the per-tick cost the narrow update set exists to avoid. The residual set is
bounded by the machine's process table rather than by session length, which
was the finding.
- **`--once` keeps raw MiB** (`vram 40MiB/24560MiB`) while the TUI uses
`human_bytes` (`14G`). The review called this drift, but the TUI is
width-constrained and a one-shot diagnostic is not; precision is worth more
there, and MiB matches what `nvidia-smi` and `nvtop` print.
- **NVIDIA `integrated` is derived from `BusType::Fpci`.** Tegra's on-SoC host
interface is the only NVML signal available, and it could not be verified
without a Jetson. Fail-safe: any error or older driver yields `false`, exactly
the previous behaviour.
- **xe VRAM total comes from `physical_vram_size_bytes`**, which igt asserts is
larger than usable VRAM because it includes reserved and stolen pages. It is
the only figure xe publishes; overstating by the carve-out beats rendering a
16 GB card as memory-less.
- **Intel `gtt_total_bytes` comes from `/proc/meminfo` `MemTotal`.** i915 and xe
size their system-memory region at `totalram_pages()`, so total RAM is the
region size rather than an invented ceiling.
- **`drm-total-*` stays unparsed.** It counts allocated-but-possibly-evicted
pages; the memory column means resident, which is `drm-resident-*`.
- **AMD enc/dec live in a closure-captured map** rather than widening the shared
`ClientSample`. `None` and `Some(0.0)` mean different things for a ring amdgpu
never printed, and only one of three backends reads it.
- **The composite backend reports `name() == "multi"`** rather than a joined
string. Widening the trait to `String` would touch every backend, and leaking
a joined name would leak again on every re-detect. Vendor identity lives in
`driver_info()`, and each card title carries its own device name.
- **`MIN_TICK_MS` is 50, not 100.** The two floors disagreed (CLI 50, `+` key
100); unifying upward would have silently removed `--tick-ms 50`, a capability
users already had.