diff --git a/CHANGELOG.md b/CHANGELOG.md index 0366594..6e2276a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ All notable user-facing changes are documented here. This project follows [Seman ## [Unreleased] +## [1.3.2] - 2026-07-13 + +### Fixed + +- Preserve valid per-process dedicated-memory values when another WDDM row is corrupt instead of replacing the entire dedicated column with `N/A`. +- Reject individual stale rows against NVIDIA's current board allocation with bounded sampling slack, catching both the impossible Zen value and the inflated DWM value from the reported Windows counter failure. + ## [1.3.1] - 2026-07-13 ### Fixed diff --git a/README.md b/README.md index 53f99c2..e971f86 100644 --- a/README.md +++ b/README.md @@ -213,7 +213,7 @@ Options: 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 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. +Windows also has a documented GPU Process Memory counter issue that accumulates stale allocations and can produce implausibly large per-process values. VGPU-Mon marks only an individually impossible row as `N/A !`, writes a blank CSV field or JSON `null` for that row, and raises `wddm_process_memory_warning`. Valid process rows remain visible. On NVIDIA cards, a row is also rejected when it exceeds current board allocation by more than bounded sampling slack, which catches stale values such as a 9 GiB process on a board using about 4 GiB. 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. diff --git a/docs/architecture.md b/docs/architecture.md index 919e63b..47df70a 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -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. 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. +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. VGPU-Mon rejects only an individually impossible dedicated row and preserves other process values. The upper bound is physical VRAM, tightened on NVIDIA hardware to current NVML board allocation plus bounded sampling slack. 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. diff --git a/src/main.c b/src/main.c index a8e9b20..07be847 100644 --- a/src/main.c +++ b/src/main.c @@ -683,13 +683,13 @@ static bool sample_app(App *app) { } else { app->process_count = 0; } - /* 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); + /* Reject only rows that cannot fit inside physical VRAM or, when NVML is + available, the board's current allocation plus sampling slack. A broken + WDDM row must not erase otherwise useful process-memory data. */ + app->wddm_process_memory_suspect = + invalidate_implausible_dedicated_gpu_memory( + app->processes, app->process_count, app->telemetry.memory_total, + app->telemetry.memory_used, app->telemetry.nvml_available) > 0; update_visible_processes(app); add_history_sample(app); log_sample(app); @@ -835,9 +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. 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" + " model. Dedicated/shared values come from WDDM counters. Only an individual row\n" + " that cannot fit the physical board reading is shown as N/A; other process rows\n" + " remain visible. A trailing ! marks the documented Windows 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); } diff --git a/src/util.c b/src/util.c index 3d0f17c..f5fef54 100644 --- a/src/util.c +++ b/src/util.c @@ -92,23 +92,30 @@ bool dedicated_gpu_memory_plausible(uint64_t bytes, uint64_t physical_total) { return physical_total == 0 || bytes <= physical_total; } -bool quarantine_invalid_dedicated_gpu_memory(GpuProcess *processes, size_t count, - uint64_t physical_total) { - if (!processes || physical_total == 0) return false; - bool invalid = false; +size_t invalidate_implausible_dedicated_gpu_memory( + GpuProcess *processes, size_t count, uint64_t physical_total, + uint64_t board_memory_used, bool board_memory_used_available) { + if (!processes) return 0; + uint64_t ceiling = physical_total; + if (board_memory_used_available && board_memory_used > 0) { + const uint64_t minimum_slack = 256ULL * 1024ULL * 1024ULL; + uint64_t slack = board_memory_used / 8ULL; + if (slack < minimum_slack) slack = minimum_slack; + uint64_t used_ceiling = UINT64_MAX - board_memory_used < slack + ? UINT64_MAX : board_memory_used + slack; + if (ceiling == 0 || used_ceiling < ceiling) ceiling = used_ceiling; + } + if (ceiling == 0) return 0; + + size_t invalid_count = 0; for (size_t i = 0; i < count; ++i) { - if (!dedicated_gpu_memory_plausible(processes[i].dedicated_bytes, - physical_total)) { - invalid = true; - break; + if (processes[i].dedicated_bytes > ceiling) { + processes[i].dedicated_memory_invalid = true; + processes[i].dedicated_bytes = 0; + invalid_count++; } } - if (!invalid) return false; - for (size_t i = 0; i < count; ++i) { - processes[i].dedicated_memory_invalid = true; - processes[i].dedicated_bytes = 0; - } - return true; + return invalid_count; } void iso_timestamp(char *buffer, size_t buffer_size) { diff --git a/src/version.h b/src/version.h index 7e57ec0..b91541e 100644 --- a/src/version.h +++ b/src/version.h @@ -1,7 +1,7 @@ #ifndef VGPU_VERSION_H #define VGPU_VERSION_H -#define VGPU_VERSION "1.3.1" -#define VGPU_VERSION_NUM 1,3,1,0 +#define VGPU_VERSION "1.3.2" +#define VGPU_VERSION_NUM 1,3,2,0 #endif diff --git a/src/vgpu.h b/src/vgpu.h index 1855a61..ab9626b 100644 --- a/src/vgpu.h +++ b/src/vgpu.h @@ -92,8 +92,9 @@ void wide_to_utf8(const wchar_t *source, char *target, size_t target_size); void sanitize_csv_field(const char *source, char *target, size_t target_size); bool contains_case_insensitive(const char *haystack, const char *needle); bool dedicated_gpu_memory_plausible(uint64_t bytes, uint64_t physical_total); -bool quarantine_invalid_dedicated_gpu_memory(GpuProcess *processes, size_t count, - uint64_t physical_total); +size_t invalidate_implausible_dedicated_gpu_memory( + GpuProcess *processes, size_t count, uint64_t physical_total, + uint64_t board_memory_used, bool board_memory_used_available); void iso_timestamp(char *buffer, size_t buffer_size); bool query_process_details(DWORD pid, ProcessDetails *details); bool terminate_process_safely(DWORD pid, char *message, size_t message_size); diff --git a/tests/test_core.c b/tests/test_core.c index 84ef9d6..b66704b 100644 --- a/tests/test_core.c +++ b/tests/test_core.c @@ -30,23 +30,24 @@ static void test_format_bytes(void) { 16ULL * 1024ULL * 1024ULL * 1024ULL)); EXPECT(dedicated_gpu_memory_plausible(UINT64_MAX, 0)); - GpuProcess valid[2] = {{0}}; - valid[0].dedicated_bytes = 9ULL * 1024ULL * 1024ULL * 1024ULL; - valid[1].dedicated_bytes = 512ULL * 1024ULL * 1024ULL; - EXPECT(!quarantine_invalid_dedicated_gpu_memory( - valid, _countof(valid), 16ULL * 1024ULL * 1024ULL * 1024ULL)); - EXPECT(!valid[0].dedicated_memory_invalid && valid[0].dedicated_bytes != 0); - - GpuProcess corrupt[3] = {{0}}; - corrupt[0].dedicated_bytes = 9ULL * 1024ULL * 1024ULL * 1024ULL; - corrupt[1].dedicated_bytes = 75ULL * 1024ULL * 1024ULL * 1024ULL; - corrupt[2].dedicated_bytes = 512ULL * 1024ULL * 1024ULL; - EXPECT(quarantine_invalid_dedicated_gpu_memory( - corrupt, _countof(corrupt), 16ULL * 1024ULL * 1024ULL * 1024ULL)); - for (size_t i = 0; i < _countof(corrupt); ++i) { - EXPECT(corrupt[i].dedicated_memory_invalid); - EXPECT(corrupt[i].dedicated_bytes == 0); - } + GpuProcess rows[4] = {{0}}; + rows[0].dedicated_bytes = 9ULL * 1024ULL * 1024ULL * 1024ULL; + rows[1].dedicated_bytes = 75ULL * 1024ULL * 1024ULL * 1024ULL; + rows[2].dedicated_bytes = 512ULL * 1024ULL * 1024ULL; + rows[3].dedicated_bytes = 2ULL * 1024ULL * 1024ULL * 1024ULL; + EXPECT(invalidate_implausible_dedicated_gpu_memory( + rows, _countof(rows), 16ULL * 1024ULL * 1024ULL * 1024ULL, + 4ULL * 1024ULL * 1024ULL * 1024ULL, true) == 2); + EXPECT(rows[0].dedicated_memory_invalid && rows[0].dedicated_bytes == 0); + EXPECT(rows[1].dedicated_memory_invalid && rows[1].dedicated_bytes == 0); + EXPECT(!rows[2].dedicated_memory_invalid && rows[2].dedicated_bytes != 0); + EXPECT(!rows[3].dedicated_memory_invalid && rows[3].dedicated_bytes != 0); + + GpuProcess total_only = {0}; + total_only.dedicated_bytes = 9ULL * 1024ULL * 1024ULL * 1024ULL; + EXPECT(invalidate_implausible_dedicated_gpu_memory( + &total_only, 1, 16ULL * 1024ULL * 1024ULL * 1024ULL, 0, false) == 0); + EXPECT(!total_only.dedicated_memory_invalid); } static void test_parse_gpu_instance(void) {