gpur 0.13.0

btop-style GPU monitor TUI — NVIDIA, AMD, Intel, Apple Silicon; Linux, macOS, Windows
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
# gpur backlog

Open work from the review passes, the multi-vendor detection audit and the
full-codebase review of v0.10.1, whose separate document was folded into this
one once every finding it raised had shipped. The 0.12.0 code and performance
reviews were folded in the same way: their still-open items are the entries
below, and their verdicts and decisions live in the settled-by-review and
decisions sections at the bottom. Closed items are removed rather than struck
through — what was fixed, and why, lives in `CHANGELOG.md`. Anything from a
closed item that is still a live constraint on future work is kept below, either
as a deliberate decision or in the settled-by-review list, rather than as a
task.

Roughly ordered by what is worth doing first. Nothing open here is a correctness
bug; the remainder is granularity, cost, coverage and polish. Every item that is
still open is open because it turns on a product or architecture call rather
than on effort.

No fabricated readings remain anywhere in the UI: the device gauges stopped
inventing a `0` in 0.10.2, the process table in 0.11.0, and the graph history —
the last holdout, because a waveform has no glyph for absent — in 0.11.1.

---

## Pending: the v0.12.0 AUR publish

**Everything else in the 0.12.0 release landed.** The GitHub release carries all
25 assets, crates.io reports 0.12.0 as its max version, and the Homebrew, Scoop
and Alpine jobs succeeded. Only `Publish AUR (gpur-bin)` failed, on
`git clone ssh://aur@aur.archlinux.org/gpur-bin.git`:

> The AUR is down due to maintenance. We will be back soon.

Not a configuration fault. The same `git ls-remote` fails identically from a
developer machine with a working AUR account, while `ssh aur@aur.archlinux.org`
answers its greeter normally — the interactive shell is up and git-upload-pack
is not. Re-run attempts at the time all hit the same banner, and re-checks on
2026-08-03 and 2026-08-04 got the same one — so this is an outage measured in
days, not minutes.

**To finish:** once `git ls-remote ssh://aur@aur.archlinux.org/gpur-bin.git`
succeeds, `gh run rerun 30735988517 --failed`. The job is idempotent — it exits
early with "nothing to push" if the PKGBUILD is already current — so re-running
it is safe whatever state the AUR repo is in. Delete this section once it is
green.

## 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. Remaining test gaps

- **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.
- **The threaded sweep reaches a real card on both Linux backends.** AMD has
  `the_worker_thread_feeds_the_backend_on_real_hardware` in `amd.rs` and Intel
  gained its twin (same name, `intel.rs`) — each gives the backend a scanner
  wired the production way and asserts its own render node reaches a row. The
  other hardware tests pin the shared scanner to synchronous through `amd()` /
  `intel()`, deliberately — a test that opens a client and polls is asserting on
  that client, so the walk has to happen after the open. The Intel twin was
  written against the machine this backlog is written on but only runs where
  `GPUR_REQUIRE_INTEL` is set; it has not yet been run on real silicon.
- **One walk serving every vendor: the count half is pinned, the live half is
  not.** Both backends take `SweepCursor::default()` (`amd.rs`, `intel.rs`), the
  shared `ProcScanner`, so structurally an AMD + Intel machine takes one walk a
  tick rather than one per backend. `a_cursor_attributes_each_walk_once` shows
  two cursors over one scanner each receive the same walk, and
  `two_cursors_over_one_worker_share_each_walk` now pins the count: two cursors
  polling one worker-backed scanner cost one paced walk, not two. What still has
  no observation is the thing itself — two LIVE backends walking the shared
  scanner on an actual mixed-vendor box (this one has AMD cards only). Next time
  such a machine runs the suite with `GPUR_REQUIRE_AMD` and `GPUR_REQUIRE_INTEL`
  set, a poll of both backends asserting their process rows land together would
  close it.
- **`kill_dialog_opens_for_a_real_process_and_cancels` (tests/tui.rs) is flaky
  under parallel load.** It waits for the stub backend's `sleep 60` row and has
  timed out twice (2026-08-04) while the suite ran other tests in the same
  binary — the screen showed two rows whose COMMAND column did not name `sleep`,
  as if sysinfo enrichment of the stub's child row raced. It passes in isolation
  and in every other full run, and no change under test has been implicated.
  Uninvestigated; run the tui binary serially if it bites again.
- **No before/after measurement of the responsiveness this bought.** The 4.2 ms
  over 588 pids in the old entry measured the walk itself, and the walk's code
  is unchanged — what moved is which thread runs it. That keystrokes no longer
  queue behind it follows from `App::poll` no longer walking, not from anything
  timed, and the pathological case that motivated the work (a 10k-process node
  at `--tick-ms 100`) has never been run.
- **The hjkl 0.40 path re-home was read, not run, off Linux.** `hjkl-config`
  0.40 delegates config/cache resolution to the new `hjkl-xdg` crate instead of
  its own inline `xdg_base`. The two resolvers were compared side by side and
  implement the same policy — XDG vars honoured on every platform when absolute
  and non-empty, otherwise `~/.config` / `~/.cache` via `dirs::home_dir()`,
  deliberately not `%APPDATA%` or `~/Library/Application Support` — so no path
  should move anywhere. What was actually exercised is Linux only:
  `tests/tui.rs` points `XDG_CONFIG_HOME` / `XDG_CACHE_HOME` at a sandbox,
  drives the real binary through a PTY and reads `state.json` back out of it.
  The Windows and macOS halves rest on reading the resolver.
- **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` have unit tests that run on any host. Closing it
  properly needs a self-hosted runner.

  This is the gap behind every "unverifiable without hardware" note in this
  file, and it is load-bearing: the WDDM per-process-memory bug fixed in 0.11.0
  was diagnosed from NVIDIA's documentation, not from an observation, and
  nothing in CI could have caught it or can confirm the fix.

  **The Linux half is verified, by hand rather than by CI.** The `hardware`
  modules in `intel.rs` and `amd.rs` run against whatever card the developer's
  machine has and skip otherwise. Both have been run green on real silicon —
  `amd.rs` on the amdgpu APU-plus-discrete machine this backlog is written on,
  with `GPUR_REQUIRE_AMD`, `GPUR_REQUIRE_AMD_APU` and `GPUR_REQUIRE_AMD_DGPU`
  all set; `intel.rs` on a separate Intel machine with `GPUR_REQUIRE_INTEL`. The
  gates are not vacuous: setting `GPUR_REQUIRE_INTEL` on the AMD machine fails
  the Intel suite outright rather than skipping it.

  So what remains here is not that the Linux backends are unverified but that
  **nothing enforces the verification** — the skip is still the default on any
  machine, and NVIDIA, Windows and Apple have no equivalent live suite at all. A
  self-hosted runner setting those variables is what makes the pass a standing
  guarantee rather than a thing someone remembered to do.

- **What the AMD hardware tests still do not reach**, on the machine they were
  written against (an amdgpu APU plus an amdgpu discrete card):
  - **`radeon`.** No pre-GCN card to run it, so `is_amd_driver`'s second half is
    fixture-only. The class tests would file such a card as discrete (`is_apu`
    sees no `gpu_metrics`), which is right, but nothing confirms it.
  - **`pcie_bw`.** Neither card implements the counter — the APU because the
    kernel marks it unsupported, Navi 31 because RDNA3 never implemented
    `get_pcie_usage` — so `pcie_kbs`'s live path is fixture-only.
    `pcie_bandwidth_is_reported_only_where_this_asic_counts_it` asserts the
    absence, which is the half that is checkable here. Needs a Vega or Navi
    1x/2x card.
  - **Engine utilization and the enc/dec split.** The tests open a render node
    to have a client to attribute, but a bare open submits no work: every
    `drm-engine-*` counter stays absent, so the ns-delta path is exercised only
    for its zero case and `enc`/`dec` never appear. Closing it means submitting
    real GPU work from a test, which is a dependency (a compute runtime) rather
    than an afternoon.
  - **Throttling.** `the_throttle_label_only_names_reasons_this_card_measures`
    checks that a label can only name a reason whose inputs exist; it cannot
    make a card hit its cap, so the branch that produces a label is never taken
    on an idle box.

- **The perf review's cost figures were never profiled.** The walk figure (4.2
  ms over 588 pids) is the code's own comment, and NVML round-trip latency and
  per-frame render time were never measured. The 0.12.0 perf pass paced the walk
  structurally (a 200 ms floor, `fe37c82`), which bounds the cost, but the
  numbers the review quoted are not measurements taken on this machine.

## 5. Intel memory totals could come from the DRM query ioctl

**Severity: low, accuracy.** The Intel backend reads its system-pool total from
`/proc/meminfo` `MemTotal`, which is exactly what i915 reports for its system
region — verified against `/sys/kernel/debug/dri/*/i915_gem_objects` on a Tiger
Lake part, where `system: total:0x3d5cc9000` equals `MemTotal` to the byte. So
the total is right; what sysfs cannot give is anything more.

`DRM_I915_QUERY_MEMORY_REGIONS` and xe's `DRM_XE_DEVICE_QUERY_MEM_REGIONS` on
`/dev/dri/renderD*` (mode `0666`, so no privileges needed) publish per-region
`probed_size` and `unallocated_size`. That would add the stolen region as its
own figure instead of folding it into the system bucket, and — the real prize —
give an Arc's local memory a _free_ figure rather than a sum over the fdinfo of
processes this user happens to be able to read.

**Cost:** a raw ioctl and per-driver structs in a backend that is otherwise pure
sysfs reads, plus opening a device node. **Fix:** worth it only if the free-VRAM
figure on discrete Arc is wanted; the iGPU totals would not change.

## 6. Residual YAGNI

- `UiTheme::temp_ok` (`src/theme.rs`) is `pub` but used only by `temp_style` in
  the same file, unlike its peers `temp_warn` / `temp_crit`. Narrowing it alone
  would be asymmetric.
- `enum Mode { Normal }` (`src/keys.rs`) 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.

## 7. Noted upstream, not gpur's to fix

- **`hjkl-splash`'s changelog files a removal against the wrong release.** Its
  0.40.0 entry says the `ratatui` feature and `start_screen::render` were
  dropped there, but neither exists in any 0.33.x published to crates.io either
  — no `pub fn render` in the source and no `ratatui` in the manifest, in 0.33.3
  through 0.33.6 — and that crate's `src` is byte-identical between 0.33.6 and
  0.40.0. So the removal shipped at or before 0.33.3 and the entry followed
  late, which the crate's changelog jumping straight from 0.2.0 to 0.40.0
  explains: the whole 0.33 series was published without entries. Harmless for
  gpur, which renders the splash itself and never called `start_screen::render`,
  but it misleads the next consumer reading that changelog to decide whether an
  upgrade is safe — and it means the 0.33 series has no changelog at all. Fix
  belongs in the hjkl repo.

## 9. Headless sync mode walks `/proc` once per backend cursor

**Severity: negligible.** `--once`/`--json` on a mixed AMD+Intel box walks twice
per poll (one per cursor — the shared-walk dedup is lost in synchronous mode),
and the scanner worker thread stays alive through the one-shot run. A one-shot
pays a few extra ms. Fix if it ever matters: share one synchronous walk per
poll.

## 11. Micro-optimisations declined as not worth the churn

From the 0.12.0 perf review's hardening list. All correct today; each is pure
waste at the margin, and none was worth the churn when the finding was closed.
Revisit individually if the relevant path shows up in a profile.

- **History front-drain is an O(cap) memmove per poll** once the ring is full
  (`src/app.rs` `poll_inner`, `drain(..overflow)` on four vecs). A `VecDeque`
  makes it O(1); single-digit MB/s at real widths, so only worth it if the cap
  grows with terminal width.
- **`draw_meter` pushes one `Span` per meter column per frame** (`src/ui.rs`) —
  ~2 × meter width × cards spans/frame on a wide terminal. Idiomatic ratatui;
  revisit only if frame rate becomes a goal.

---

## Settled by review

The v0.10.1 full-codebase review raised sixteen findings across correctness,
security, duplication and dead code. All of them shipped between 0.10.2 and
0.11.1 except one that was declined; each is described in `CHANGELOG.md` under
the release that carried it.

Re-verified against the tree at 0.11.1 before this list replaced the review
document — every fix is still in place, and the properties below still hold.
They are recorded so a later pass does not spend time re-deriving them.

- **Device identity is sound.** `device_keys` degrades a duplicate or absent
  `device_id` to a positional key, positional keys are never persisted, and
  `CompositeBackend` namespaces every child's ids by backend name — the index
  only where two children share a name, which `detect()` cannot produce — so two
  backends cannot mint the same key, and the namespace survives a re-detect
  whose child set changed. The vacated-slot de-duplication is correct.
- **Process index rebasing is sound.** `CompositeBackend::processes` accumulates
  by `slots`, the high-water mark, rather than by the current poll's device
  count, and drops rows outside their own child's span. The three-child and
  middle-child-resize tests cover the off-by-one variants.
- **Mouse hit-testing cannot select an off-screen row.** `proc_visible` is
  bounded by both the drawn row count and `procs.len()`, and `proc_scroll` is
  clamped to `total - visible` before the table slice, so that slice is provably
  in range.
- **The numeric guards hold.** `gradient` handles empty and single-stop ramps,
  `parse_size` and the Apple VRAM total saturate, `le_int` rejects non-scalars,
  `windowed` handles short data, and `proc_pane_height` computes in `u32`. All
  carry tests.
- **Modal input is contained.** Control chords cannot reach the filter buffer,
  mouse events are ignored while a modal is up, and `Ctrl-C` still quits from
  filter mode.
- **The kill path holds up.** Pid identity is pinned by `(pid, start_time)`
  across the confirmation gap; pid 1 and gpur itself are refused; an absent
  executable refuses kernel threads and processes this user cannot read;
  `can_signal()` is re-checked inside `confirm_kill` rather than only when the
  dialog opens; mock and replay refuse outright, so a recording from a stranger
  is not a one-keystroke signal primitive; and `CompositeBackend::can_signal` is
  the AND of its children.
- **The PDH buffer handling is correct.** It allocates with the item type's
  alignment and sizes from the `PDH_MORE_DATA` byte count, and the `Vec`
  outlives the raw pointer derived from it.

### The 0.12.0 reviews

Both review documents from 2026-08-04 were folded into this file — their open
items became the entries above, and the documents were deleted; the full texts
are recoverable from git (`a285467` code review, `ad28ab8` perf review). Their
verdicts, so a later pass does not re-derive them:

- **The 0.12.0 code review found no open correctness findings.** Fourteen
  suspicious paths were traced and disproved (kill/pidfd identity,
  `parse_start_ticks`, braille dot orientation, `windowed` bounds, PDH sizing,
  `Instant` deltas, counter resets, layout arithmetic, the Intel
  attributed/video invariant, composite slot bookkeeping, sweep-state pruning,
  state-file atomicity, unknown-vs-zero rendering, LUID matching, mock safety,
  `--once`/replay framing), and the eight commits between the 0.12.0 review and
  its predecessor close the predecessor's eight findings one-to-one, each with a
  regression test.
- **The 0.12.0 perf review's three findings were fixed the same day**: `fe37c82`
  (paced sweep), `f5d76f0` (NVML static cache), `2d63c5e` (container-id cache).
  Its hardening items are the micro-optimisation entry above; its coverage gaps
  map onto the hardware and profiling entries above.
- **Out of scope of both reviews:** `pkg/` templates, `assets/`, prose docs, and
  dependency internals beyond the nvml-wrapper/sysinfo spot-checks.

## Decisions taken deliberately

Recorded so they are not re-opened as findings.

- **Per-process figures may be one poll old, and that is the trade the worker
  thread buys.** `ProcScanner` publishes walks from another thread and the poll
  reads the newest finished one, so a walk that lands mid-tick is drawn on the
  following tick. Stale is not the same as wrong: a utilization is a cumulative
  counter's delta divided by the interval between the two readings, and both
  backends divide by `ProcSnapshot::at` — when the walk actually ran — rather
  than by the clock at attribution. Measuring against attribution time is the
  tempting simplification and it inflates: a walk delayed by a slow tick would
  report a card that idled through it as pegged, which
  `utilization_spans_the_two_walks_and_survives_a_poll_without_one` (in both
  `amd.rs` and `intel.rs`) pins at 100% against the correct 50%/75%.
- **The first poll of a session waits for the first walk.** There is no earlier
  reading to fall back on, and starting with an empty process table that fills
  in a tick later reads as "no processes" rather than as "not yet measured". The
  wait is bounded by `FIRST_SCAN_WAIT` so a worker that never publishes costs
  one late frame instead of a hang. Every later poll takes whatever is ready.
- **Where a walk outlasts the tick, process figures update at the walk's pace,
  not the tick's.** `SweepCursor` returns `None` on a poll with no new walk and
  the last figures are redrawn, so a `--tick-ms 50` session on a machine whose
  walk takes 70 ms refreshes the device gauges every 50 ms and the process rows
  every 70. That is the intended degradation rather than a bug to file: the
  alternative is the tick waiting on the walk, which is what this change
  removed. The utilizations stay honest across it because they are measured
  between the walks' own timestamps.
- **A poll that finds no new walk redraws the last figures rather than
  re-deriving them.** `SweepCursor` hands each walk to a backend once. Feeding
  the same walk twice would divide identical counters by a zero interval and
  report every process idle, so a walk running late would render as every card
  flickering to 0% — worst exactly when the machine is busy enough to delay it.
  The cached figures each backend keeps (`AmdBackend::media`, `IntelBuckets`)
  exist for that, not as an optimisation.
- **`--once` and `--json` keep the walk on the polling thread.** They poll twice
  a known sleep apart and report the delta, so both walks have to bracket that
  sleep; the newest-finished-walk rule would leave the sleep outside the
  interval being measured and answer about a few milliseconds of the wrong
  moment. `backend::sweep_on_poll_thread`, called from `main` when headless, is
  the switch. There is no UI to keep responsive in that mode, so the thing the
  worker exists to protect is not at stake.
- **A record field cannot gain a `null` on one side only.** `--json` and `--log`
  are written from `ProcRow` and read back into `GpuProcess`, two types that
  line up only by field name, and `#[serde(default)]` covers a missing key but
  not an explicit null. So widening a field to `Option` on the writing side
  without widening it on the reading side makes `--replay` reject whole records
  — silently skipping frames when only some carry the null, and refusing the
  file outright when all of them do. Both sides must move in the same release.
  This is what made the 0.11.0 process-metric change a minor rather than a
  patch.
- **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.
- **The Linux fdinfo sweeps report `Some(0)` per process, never unknown.**
  fdinfo names a memory region only when the client holds something in it, so a
  client with no `vram` or `gtt` region genuinely holds nothing there. That is a
  measurement; degrading it to `n/a` alongside the 0.11.0 change would have
  hidden real idle clients.
- **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.
- **Block graphs lose resolution rather than direction under Mono.** Unicode has
  upper partial blocks only at ⅛ and ½, so the down-growing half of a block
  waveform is normally drawn with a complement trick — bar in the background,
  hole in the foreground — which needs two distinct colours. `ColorMode::Mono`
  has none, so that half now falls back to `▔`/`▀`/`█`, rounded to the nearest
  and rounding ties down. Three levels instead of eight is the price of a bar
  that points the way it grows; before, the complement was drawn in the default
  foreground and the whole half rendered inverted.
- **Sub-cell quantization stays hand-rolled.** Five sites across three functions
  turn a value into filled sub-units — `mini_spark`'s three glyph branches,
  `draw_waveform`'s `dots_for`, and `draw_waveform_cells` — and they quantize
  against different unit counts (4, 8, and a per-half row count) with different
  clamp floors (0 or 1, depending on whether the widget draws a baseline). A
  shared helper would take both as parameters and save only the arithmetic, so
  the review declined it. Worth revisiting only if a fourth caller appears,
  since this is the kind of arithmetic that drifts.
- **An unreadable graph sample is a glyph, not a colour.** The waveform marks it
  `·` in braille and block — a character in neither value ramp, and already this
  UI's mark for nothing-here, since `draw_meter` paints an empty non-ascii track
  with it — and the dim styling only reinforces that. Colour and `DIM` are both
  lost under `NO_COLOR`, under `TERM=dumb`, on a terminal that ignores `DIM`,
  and in any screenshot or copy-paste, so neither can carry the distinction on
  its own.
- **Ascii spells the same thing `_`, and that collides with `mini_spark`.**
  `--graphs ascii` is chosen by people whose font may not have `·` at all, which
  is the same reason they are not being handed braille. That leaves the baseline
  `_` meaning opposite things in two widgets: `mini_spark` is styled whole by
  its caller, so it cannot dim anything and spells unknown as a gap, leaving `_`
  for a measured 0; the waveform must not leave gaps — the trace has to stay
  continuous — so there `_` is unknown and a measured 0 takes the level-1 `.`.
  Each widget is unambiguous within itself, and the alternative was giving up
  the distinction in whichever widget lost the glyph, on exactly the terminals
  that can least afford to lose it.
- **`--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. For the same
  reason `--once` spells an unreadable per-process figure `-` while the TUI
  spells it `N/A`: the columns on one whitespace-splittable line agree with each
  other, which matters more there than agreeing with the table.
- **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.
- **The process command line is not cached** even though the container id is. A
  pid's cgroup path is ~static, so caching the container on (pid, start time) is
  safe; the command line re-derives each poll from sysinfo's already-cached
  cmdline — an in-memory join with no I/O — so an in-place `exec`, which keeps
  the pid and its start time, never leaves a stale COMMAND column. Cache it only
  if the join ever shows up in a profile.
- **`enforced_power_limit` is not cached at probe** with the other
  session-static NVML values. It changes when the user moves the power cap;
  caching it would show a stale limit until restart. One query per poll keeps
  the displayed limit live.
- **The kill-dialog PTY tests use a separate stub backend, never a signalable
  mock.** `GPUR_STUB_BACKEND=1` (`src/backend/stub.rs`) reports one real local
  process so the dialog is reachable from the harness; making the mock
  signalable instead would weaken the guard that exists to stop exactly that —
  fabricated pids must never be signalable.