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
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,19 @@ All notable user-facing changes are documented here. This project follows [Seman

## [Unreleased]

## [1.3.1] - 2026-07-13

### Fixed

- Quarantine the complete WDDM dedicated-memory snapshot when Windows returns an impossible per-process value, preventing smaller stale values from being presented as trustworthy VRAM usage.
- Detach the installer helper from the terminal and stop relaunching a competing foreground monitor after the invoking shell regains keyboard input.
- Remove the persistent `Click headers to sort` hint from the interactive footer.

### Security and quality

- Return `null`/blank dedicated-memory values in JSON and CSV whenever the affected WDDM snapshot is quarantined.
- Exercise snapshot-wide WDDM quarantine and verify that live update handoff never relaunches an input-competing monitor.

## [1.3.0] - 2026-07-13

### Added
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ It is a real full-screen terminal application, not a wrapper around `nvidia-smi`
- Pause/resume and adjustable 250-5000 ms refresh interval
- CSV logging
- One-shot human-readable and JSON output for scripts
- Automatic, checksum-verified updates for installed interactive launches
- Automatic, checksum-verified background updates for installed interactive launches
- Statically linked MSVC runtime; no NVML SDK or third-party runtime required

## Runtime requirements and compatibility
Expand Down Expand Up @@ -63,7 +63,7 @@ vgpu --json

Existing terminal processes keep their old environment. Uninstalling VGPU-Mon from Windows Settings removes the PATH entry only when the installer originally added it.

Starting with VGPU-Mon 1.2.0, an installed interactive launch checks the latest stable release's small `version.txt` manifest. If a newer version exists, VGPU-Mon downloads that exact versioned installer, verifies its SHA-256 with Windows cryptography, installs it without elevation, and reopens with the same interactive options. Network or GitHub failures do not prevent the monitor from opening. Redirected output and script commands (`--json`, `--once`, `--version`, and `--demo`) never perform an automatic update check.
Starting with VGPU-Mon 1.2.0, an installed interactive launch checks the latest stable release's small `version.txt` manifest. If a newer version exists, VGPU-Mon downloads that exact versioned installer, verifies its SHA-256 with Windows cryptography, and installs it without elevation. The command then returns cleanly; run `vgpu` again after a few seconds to start the updated monitor. The updater deliberately does not relaunch inside the old terminal because the shell would already own that terminal's input. Network or GitHub failures do not prevent the monitor from opening. Redirected output and script commands (`--json`, `--once`, `--version`, and `--demo`) never perform an automatic update check.

Use `vgpu --update` to check immediately. Use `--no-update` for one launch or set `VGPU_MON_NO_UPDATE=1` to disable automatic checks. Portable ZIP copies do not update silently; `vgpu --update` converts a portable copy into the normal per-user installation.

Expand Down Expand Up @@ -211,9 +211,9 @@ Options:
- **DXGI 1.4** enumerates hardware adapters and reports local video-memory usage and the current OS memory budget.
- **NVML** supplies NVIDIA board telemetry. VGPU-Mon uses the versioned memory API to separate driver/firmware-reserved VRAM from application allocation. `nvml.dll` is discovered and loaded at runtime; the project does not ship NVIDIA libraries.

VRAM figures from NVML, DXGI, and WDDM can differ because they describe different layers: physical board usage, the operating-system budget, and per-process commitments. A WDDM process commitment can exceed physical VRAM because it is not a resident-byte measurement. VGPU-Mon marks process columns with `*`, labels JSON/CSV fields as commitments, and keeps the physical total and OS budget separate.
VRAM figures from NVML, DXGI, and WDDM can differ because they describe different accounting layers. Per-process WDDM values include memory shared across processes, so adding every row can exceed the physical total even though a valid individual dedicated-memory reading cannot. VGPU-Mon marks process columns with `*`, labels JSON/CSV fields as commitments, and keeps the physical total and OS budget separate.

Windows also has a documented GPU Process Memory counter issue that can produce implausibly large per-process values. VGPU-Mon appends `!`, raises `wddm_process_memory_warning` in JSON, and preserves the raw value for diagnosis instead of silently clamping it. Physical board usage at the top remains sourced from NVML/DXGI.
Windows also has a documented GPU Process Memory counter issue that accumulates stale allocations and can produce implausibly large per-process values. If one process's dedicated value exceeds the GPU's physical total, VGPU-Mon quarantines that snapshot's entire dedicated column: every row displays `N/A !`, CSV fields are blank, JSON values are `null`, and `wddm_process_memory_warning` is raised. The complete snapshot is quarantined because smaller values from the same affected counter source can look plausible while still being stale. Physical board usage at the top remains sourced from NVML/DXGI.

Terminating a process is not the same as resetting a GPU. VGPU-Mon deliberately does not expose driver resets or unsafe attempts to cancel individual command queues.

Expand Down
6 changes: 3 additions & 3 deletions docs/architecture.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Architecture

VGPU-Mon is a native Windows application. It has no service, kernel component, injected DLL, persistent background updater, or vendor SDK dependency. An installed interactive launch performs a bounded HTTPS update check before provider initialization; a verified update uses a short-lived PowerShell handoff only after the monitor exits.
VGPU-Mon is a native Windows application. It has no service, kernel component, injected DLL, persistent background updater, or vendor SDK dependency. An installed interactive launch performs a bounded HTTPS update check before provider initialization; a verified update uses a short-lived, console-detached PowerShell handoff only after the monitor exits.

## Data flow

Expand All @@ -10,7 +10,7 @@ VGPU-Mon is a native Windows application. It has no service, kernel component, i
4. The sampler normalizes those sources into one `GpuTelemetry` snapshot plus bounded process rows and chart histories.
5. The renderer builds one bounded frame in memory and repaints the visible terminal region in a single write.

The updater reads `version.txt` from the latest stable GitHub Release, validates its strict version, installer, and hash fields, and compares semantic versions. It downloads an exact tag-specific installer to the user temporary directory and hashes it with Windows CNG before starting a transient installer helper. Offline, malformed, oversized, downgraded, or hash-mismatched responses fail closed for the update and fail open for monitoring. Non-interactive output paths do not access the network.
The updater reads `version.txt` from the latest stable GitHub Release, validates its strict version, installer, and hash fields, and compares semantic versions. It downloads an exact tag-specific installer to the user temporary directory and hashes it with Windows CNG before starting a transient installer helper. The helper has no inherited console handles and never relaunches a second foreground process after the invoking shell regains input ownership. Offline, malformed, oversized, downgraded, or hash-mismatched responses fail closed for the update and fail open for monitoring. Non-interactive output paths do not access the network.

`--demo` replaces steps 1–3 with deterministic values while keeping the real sampling, layout, JSON, input, and rendering paths. It exists for testing and UI previews; it is never presented as hardware telemetry.

Expand All @@ -28,7 +28,7 @@ The interactive UI uses Windows console input records and VT output. Every updat

## Accounting caveats

NVML physical allocation, DXGI OS budget/usage, and WDDM per-process commitments describe different layers and do not necessarily sum. The UI labels commitments explicitly and preserves implausible WDDM values with a warning instead of disguising a Windows counter anomaly.
NVML physical allocation, DXGI OS budget/usage, and WDDM per-process commitments describe different layers and do not necessarily sum. Microsoft documents that GPU Process Memory can accumulate stale allocations. If one dedicated row exceeds the physical GPU total, the entire dedicated snapshot is reported as unavailable: once the counter source is demonstrably corrupt, smaller rows cannot be distinguished safely from stale values.

The displayed per-process GPU percentage is the busiest engine for that process, matching the accounting style used by Windows Task Manager. It is not additive across dissimilar engines.

Expand Down
55 changes: 30 additions & 25 deletions src/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -554,9 +554,11 @@ static void log_sample(App *app) {
}
sanitize_csv_field(process->name, process_name, sizeof(process_name));
sanitize_csv_field(process->engine, engine, sizeof(engine));
fprintf(app->log_file, "%lu,\"%s\",%.2f,%llu,%llu,\"%s\"\n",
(unsigned long)process->pid, process_name, process->gpu_percent,
(unsigned long long)process->dedicated_bytes,
fprintf(app->log_file, "%lu,\"%s\",%.2f,",
(unsigned long)process->pid, process_name, process->gpu_percent);
if (!process->dedicated_memory_invalid)
fprintf(app->log_file, "%llu", (unsigned long long)process->dedicated_bytes);
fprintf(app->log_file, ",%llu,\"%s\"\n",
(unsigned long long)process->shared_bytes, engine);
}
if (fflush(app->log_file) != 0 || ferror(app->log_file)) {
Expand Down Expand Up @@ -681,15 +683,13 @@ static bool sample_app(App *app) {
} else {
app->process_count = 0;
}
app->wddm_process_memory_suspect = false;
if (app->telemetry.memory_total) {
for (size_t i = 0; i < app->process_count; ++i) {
if (app->processes[i].dedicated_bytes > app->telemetry.memory_total) {
app->wddm_process_memory_suspect = true;
break;
}
}
}
/* Microsoft documents that one bad GPU Process Memory value means the
affected counter source is accumulating stale allocations. There is no
trustworthy way to identify the other poisoned rows, so quarantine the
dedicated column for the whole snapshot instead of allowing a smaller
but equally stale value (for example DWM) to look real. */
app->wddm_process_memory_suspect = quarantine_invalid_dedicated_gpu_memory(
app->processes, app->process_count, app->telemetry.memory_total);
update_visible_processes(app);
add_history_sample(app);
log_sample(app);
Expand Down Expand Up @@ -835,8 +835,9 @@ static void render_help(TextBuffer *buffer) {
" Home/End Oldest retained/live Hover Exact point value\n\n"
"%sAccounting%s\n"
" Process GPU %% is the busiest Windows GPU engine for that PID, matching Task Manager's\n"
" model. Dedicated/shared values come from WDDM counters. A trailing ! marks a value\n"
" larger than physical VRAM, a known Windows counter anomaly rather than residency.\n"
" model. Dedicated/shared values come from WDDM counters. If Windows returns one\n"
" impossible dedicated value, the full dedicated snapshot is shown as N/A because\n"
" other rows can contain the same documented stale-allocation counter error.\n"
" Board telemetry comes from NVML; DXGI supplies a vendor-neutral memory fallback.\n",
ANSI_BOLD, ANSI_RESET, ANSI_BOLD, ANSI_RESET, ANSI_BOLD, ANSI_RESET);
}
Expand Down Expand Up @@ -955,10 +956,11 @@ static void render_process_table(App *app, TextBuffer *buffer, int columns, int
char pid[32], gpu[32], dedicated[32], shared[32];
snprintf(pid, sizeof(pid), "%lu", (unsigned long)process->pid);
snprintf(gpu, sizeof(gpu), "%.1f%%", process->gpu_percent);
format_bytes(process->dedicated_bytes, dedicated, sizeof(dedicated));
if (process->dedicated_memory_invalid)
snprintf(dedicated, sizeof(dedicated), "N/A !");
else
format_bytes(process->dedicated_bytes, dedicated, sizeof(dedicated));
format_bytes(process->shared_bytes, shared, sizeof(shared));
if (app->telemetry.memory_total && process->dedicated_bytes > app->telemetry.memory_total)
strncat_s(dedicated, sizeof(dedicated), " !", _TRUNCATE);

if (row == app->selection) text_buffer_append(buffer, "%s", ANSI_REVERSE);
for (int i = 0; i < layout.indent; ++i) text_buffer_append(buffer, " ");
Expand Down Expand Up @@ -1416,7 +1418,7 @@ static void render_app(App *app, TextBuffer *buffer) {
char footer[512], clipped[512];
const char *health = !app->pdh_ready && !app->demo_mode ? "! WDDM counters unavailable | " :
app->wddm_process_memory_suspect ? "! WDDM process-memory anomaly | " : "";
snprintf(footer, sizeof(footer), "%sClick headers to sort | %s %s | %zu processes | filter: %s | c chart | h help | q quit",
snprintf(footer, sizeof(footer), "%s%s %s | %zu processes | filter: %s | c chart | h help | q quit",
health,
sort_name(app->sort_mode), app->sort_descending ? "high-low" : "low-high",
app->visible_count, app->filter[0] ? app->filter : "none");
Expand Down Expand Up @@ -1780,8 +1782,10 @@ static void print_once_json(const App *app) {
const GpuProcess *process = app->visible[i];
if (i) putchar(',');
printf("{\"pid\":%lu,\"name\":", (unsigned long)process->pid); write_json_string(process->name);
printf(",\"gpu_percent\":%.2f,\"dedicated_commit_bytes\":%llu,\"shared_commit_bytes\":%llu,\"engine\":",
process->gpu_percent, (unsigned long long)process->dedicated_bytes,
printf(",\"gpu_percent\":%.2f,\"dedicated_commit_bytes\":", process->gpu_percent);
if (process->dedicated_memory_invalid) fputs("null", stdout);
else printf("%llu", (unsigned long long)process->dedicated_bytes);
printf(",\"shared_commit_bytes\":%llu,\"engine\":",
(unsigned long long)process->shared_bytes);
write_json_string(process->engine);
putchar('}');
Expand All @@ -1805,9 +1809,10 @@ static void print_once_table(const App *app) {
const GpuProcess *process = app->visible[i];
char name[64], dedicated[32], shared[32];
truncate_text(process->name, name, sizeof(name), 32);
if (process->dedicated_memory_invalid) snprintf(dedicated, sizeof(dedicated), "N/A !");
else format_bytes(process->dedicated_bytes, dedicated, sizeof(dedicated));
printf("%-8lu %-32s %7.1f%% %12s %12s %-14s\n", (unsigned long)process->pid, name,
process->gpu_percent,
format_bytes(process->dedicated_bytes, dedicated, sizeof(dedicated)),
process->gpu_percent, dedicated,
format_bytes(process->shared_bytes, shared, sizeof(shared)), process->engine);
}
}
Expand All @@ -1830,8 +1835,8 @@ static bool parse_chart_metric(const char *name, ChartMetric *metric) {
}

static void print_help(void) {
printf(
"VGPU-Mon %s - live Windows GPU process monitor\n\n"
fputs(
"VGPU-Mon " VGPU_VERSION " - live Windows GPU process monitor\n\n"
"Usage: vgpu [options]\n\n"
" --once Print one snapshot and exit\n"
" --json Print one snapshot as JSON\n"
Expand All @@ -1854,7 +1859,7 @@ static void print_help(void) {
"Run without options in Windows Terminal for the interactive UI.\n"
"Headers are mouse-clickable; click again to reverse the sort.\n"
"Charts support wheel zoom, Shift+wheel or arrow-key panning, and exact hover values.\n",
VGPU_VERSION);
stdout);
}

static bool initialize_app(App *app) {
Expand Down
Loading
Loading