From c0c24bb4ca7fbafcb5958c8a99f984067a69b135 Mon Sep 17 00:00:00 2001 From: Daydatao <52039764+Daydatao@users.noreply.github.com> Date: Thu, 12 Nov 2026 17:01:34 +0100 Subject: [PATCH 1/2] perf(vk): cap the system-heap budget for idle live imports on Linux --- docs/tested-configurations.md | 2 ++ gpu/vk/retired_give_back.h | 8 ++++++ gpu/vk/vk_memory.cpp | 43 ++++++++++++++++++-------------- tests/retired_give_back_test.cpp | 7 ++++++ tests/test_retired_give_back.py | 2 ++ 5 files changed, 43 insertions(+), 19 deletions(-) diff --git a/docs/tested-configurations.md b/docs/tested-configurations.md index d1636605f..58b260d98 100644 --- a/docs/tested-configurations.md +++ b/docs/tested-configurations.md @@ -14,6 +14,8 @@ otherwise. | Date | Build | OS / desktop | GPU / driver | CPU / RAM | Settings | Where | Result | |---|---|---|---|---|---|---|---| +| 2026-11-12 | v2.1.0 (source e561602e) | Arch Linux, KDE Plasma Wayland, 2560x1440 window (mailbox, udmabuf on) | RX 9070 XT 16 GB, Mesa RADV 26.2.6 | Core i5-13600KF, 32 GB | 2560x1440 output, V-Sync off, parallel recorder on (`RX_VK_RECORDER=thread`, `RX_VK_BATCH=0`), `RX_VK_MIRROR_MB=4096`, idle live-import cap on (`RX_VK_IMPORT_HIGH=25`, `RX_VK_IMPORT_LOW=20`: ~3,858 MB high / ~3,086 MB low on the 15,434 MB system heap budget) | Long session across Hyrule (176,640 presents, ~1.5 h) | 63.5-65.6 fps at 641-708 draws, 67.2-68.3 fps at 186 draws throughout the session. 257 idle live imports (7,872 MB in all) given back; live imports stayed at 50-64 pools (2,256-2,720 MB), capture submit 0.18-0.43 ms (scan 0.08 ms over 3,923-4,251 MB), queue submit 0.32-0.73 ms, readbacks of GPU results waited 0.64-1.14 ms each. No device lost | +| 2026-11-12 | v2.1.0 (source e561602e) | as above | as above | as above | Default import watermarks (`imports_give_back` off on the 15,434 MB system heap budget since `budget >= 12 GB`) | Long session across Hyrule (684,360 presents, ~2 h) | At 10 min (40,920 presents): 60+ fps, 55 live imports (2,000 MB), capture submit 0.27 ms (scan 0.05 ms over 734 ranges, 2,417 MB), queue submit 0.55 ms, readback wait 2.19 ms each (270 of 3,000). After ~2 h (684,120 presents): faded to 44-48.4 fps at 303-408 draws (`vi: real-time step on: ratio 1.352, game's frame rate 44`); 221 live imports (7,392 MB; 67 retired given back, 2,992 MB), working set 15,351 MB (peak 16,107 MB, 1,511 MB swap), capture submit 0.50 ms (scan 0.09 ms over 334 ranges, 7,218 MB), queue submit 1.11-1.14 ms, readback wait 6.29-7.03 ms each (312-340 of 3,000). No device lost | | 2026-11-11 | v2.1.0-rc.3-246-g87991bc8 (source 6d41d308), main built from source (llvm-mingw, no DLSS) | Windows 11, 1280x720 window (mailbox, V-Sync Auto) | RTX 3080 10 GB, 621.14 | Core i9-12900K, 64 GB | Default options, uncapped (`HOS_FPS_MAX=1000`); a warm-up run first, no pipeline compiled while measuring | Hyrule Field (adult), by the Lost Woods bridge where the Cheats teleport puts Link, standing; 120 s from 45 s after the teleport | Two runs: 78.4 and 79.3 fps, median frame 12.18 and 12.23 ms, 30 and 37 frames over 20 ms, 1,335-1,453 draws. Readbacks of GPU results: about 1,550 of 3,000 waited, 0.04-0.07 ms each (medians). Against the nightly below for [#2602](https://github.com/NotRexed/Hookshot/pull/2602) | | 2026-11-11 | v2.1.0-rc.3-193-g55b976c0 (source ae9e7c1b), the last nightly before #2602, built from source as above | as above | as above | as above | as above | as above | Two runs, interleaved with the two above: 59.7 and 60.8 fps, median frame 16.39 and 16.32 ms, 264 and 22 frames over 20 ms, 1,355-1,445 draws. Readbacks of GPU results: 172 of 3,000 waited, 5.80 and 5.56 ms each (medians) | | 2026-11-11 | v2.1.0-rc.3-46-gddafc1f (source c872b22b): the nightly with #2314's `gpu/vk/vk_memory.cpp` (97c1a202) swapped in | Ubuntu 26.04.1, kernel 7.0.0, GNOME 50 Wayland | RTX 4070 12 GB, 615.71.09 | Ryzen 7 5800X3D, 64 GB | 2560x1440 output, DLSS DLAA (model K), frame generation off, Balanced preset (texture budget 6144 MB), Reflex on, `RX_VK_PARALLEL=4` | Kokiri Forest, the stream by the village: fresh launch, then again after about 10 minutes of exploring | Fresh 59.0 fps at about 2,505 draws (memory sync 3.9 ms); after exploring 49.5 fps at about 2,760 draws (memory sync 6.3 ms). 99.96% of the texture-shadow copies came from staging. No device lost | diff --git a/gpu/vk/retired_give_back.h b/gpu/vk/retired_give_back.h index ccc1875d8..d318dbaa0 100644 --- a/gpu/vk/retired_give_back.h +++ b/gpu/vk/retired_give_back.h @@ -45,4 +45,12 @@ constexpr uint64_t want(uint64_t used, uint64_t budget, int high, int low) { return used > target ? used - target : 0; } +/// The system-heap cap for idle live imports (imports_give_back) when RX_VK_IMPORT_HIGH is not set: on 32 GB+ Linux PCs +/// the heap's budget is 15+ GB, and leaving imports_give_back off above 12 GB (or waiting for 85% of 15 GB) let 200+ idle +/// unmirrored pools stay in the driver's buffer list and the submit-time capture ranges over a 2 h session (7.4 GB live). +constexpr uint64_t kLiveBudgetCap = 4500ull << 20; +constexpr uint64_t live_budget(uint64_t budget, bool env_high_set) { + return env_high_set || budget <= kLiveBudgetCap ? budget : kLiveBudgetCap; +} + } // namespace retired_give_back diff --git a/gpu/vk/vk_memory.cpp b/gpu/vk/vk_memory.cpp index a6072f9e2..cba4c3365 100644 --- a/gpu/vk/vk_memory.cpp +++ b/gpu/vk/vk_memory.cpp @@ -3261,14 +3261,16 @@ void mirrors_give_back(uint64_t want_bytes) { // (pools_mu held) } } -// ---- imports given back under pressure (16 GB PCs). The driver pins every imported pool and counts it in the system heap -// the GPU reaches, whose budget is about half the PC's RAM (7.5 GB of 16): imports were kept for the session, the heap -// went past its budget (7758 of 7503 MB on an RTX 3060 with 16 GB) and new imports failed with VK_ERROR_OUT_OF_DEVICE_MEMORY: -// terrain left out, load zones stuck. Past RX_VK_IMPORT_HIGH percent of the budget (85) the pools bound least lately give -// their imports back down to RX_VK_IMPORT_LOW percent (75): only pools without a mirror, not bound for kImportIdle -// submissions and done on the GPU. They leave the table as a retired pool does (the page table's entries 0, the caches -// emptied) and are imported again at their next binding; their memory is freed once everything recorded until now is -// done. On Linux, on by default only where that heap's budget is under 12 GB (where it ran out); off by default on Windows. +// ---- imports given back under pressure. The driver pins every imported pool and counts it in the system heap +// the GPU reaches, whose budget is about half the PC's RAM (7.5 GB of 16, 15.4 GB of 32): imports were kept for the session, +// the heap went past its budget (7758 of 7503 MB on an RTX 3060 with 16 GB) and new imports failed with +// VK_ERROR_OUT_OF_DEVICE_MEMORY: terrain left out, load zones stuck; on 32 GB+ Linux PCs (where the budget is over 12 GB and +// this valve was off) 200+ idle unmirrored pools piled up over a two-hour session (7.4 GB live), bloating every amdgpu submit's +// buffer list and the submit-time capture scan. Past RX_VK_IMPORT_HIGH percent (85) of the budget (capped at 4500 MB by default; +// RX_VK_IMPORT_HIGH= uses the full budget) the pools bound least lately give their imports back down to RX_VK_IMPORT_LOW +// percent (75): only pools without a mirror, not bound for kImportIdle submissions and done on the GPU. They leave the table as +// a retired pool does (the page table's entries 0, the caches emptied) and are imported again at their next binding; their +// memory is freed once everything recorded until now is done. On by default on Linux; off by default on Windows. // RX_VK_IMPORT_HIGH=0: off, =85: on anywhere. (Caveat: last_value counts bindings, not shaders reaching a pool through the // page table; a pool only reached that way would read zeros until it is bound again. Not seen in play: the pools given back // were long unbound.) ---- @@ -3292,28 +3294,31 @@ void imports_give_back() { // (pools_mu held) // frees pools nothing uses any more, this one pools still live that have been idle static const char* env_high = getenv("RX_VK_IMPORT_HIGH"); #ifdef _WIN32 - static const int high = env_high ? atoi(env_high) : 0; + static const int high = std::clamp(env_int("RX_VK_IMPORT_HIGH", 0), 0, 100); #else - static const int high = env_high ? atoi(env_high) : 85; + static const int high = std::clamp(env_int("RX_VK_IMPORT_HIGH", 85), 0, 100); #endif - static const int low = getenv("RX_VK_IMPORT_LOW") ? std::clamp(atoi(getenv("RX_VK_IMPORT_LOW")), 10, 95) : 75; + static const int low = std::clamp(env_int("RX_VK_IMPORT_LOW", 75), 10, 95); static uint64_t calls = 0; if (high <= 0 || ++calls % 16) return; // (the driver asked every 16 submissions) uint64_t used = 0, budget = 0; if (!system_heap_usage(&used, &budget)) return; - static const bool small = [&] { // (on by default where the budget is small: 16 GB of RAM and less) - const bool on = env_high || budget < (12ull << 30); - logf("vk: imports given back past %d%% of a %llu MB system heap budget: %s", high, (unsigned long long)(budget >> 20), - on ? "on" : "off (RX_VK_IMPORT_HIGH=85 turns it on)"); - return on; + const uint64_t cap = retired_give_back::live_budget(budget, env_high && *env_high); + static const bool logged_cfg = [&] { + if (cap < budget) + logf("vk: imports given back past %d%% of a %llu MB cap (%llu MB system heap budget): on", high, + (unsigned long long)(cap >> 20), (unsigned long long)(budget >> 20)); + else + logf("vk: imports given back past %d%% of a %llu MB system heap budget: on", high, (unsigned long long)(budget >> 20)); + return true; }(); + (void)logged_cfg; // (with both valves on, the other ran just before, on this same submission (the two count alike): what it queued is // still in the driver's figure until the GPU is past it, and live pools would go for the same bytes. Only where // retired imports wait; elsewhere they go back at every submission and this valve reads what it always read) if (retired_mode() == retired_give_back::Mode::GRACE) used = retired_give_back::settled(used, dead_import_bytes); - if (!small || used <= budget / 100 * (uint64_t)high) return; - const uint64_t target = budget / 100 * (uint64_t)low; - const uint64_t want = used > target ? used - target : 0; + const uint64_t want = retired_give_back::want(used, cap, high, low); + if (!want) return; std::vector::iterator> idle; for (auto it = imported.begin(); it != imported.end(); ++it) { const Pool& p = it->second; diff --git a/tests/retired_give_back_test.cpp b/tests/retired_give_back_test.cpp index 2dd9a987f..fc9a2e77a 100644 --- a/tests/retired_give_back_test.cpp +++ b/tests/retired_give_back_test.cpp @@ -11,6 +11,8 @@ namespace { using retired_give_back::Mode; using retired_give_back::due; +using retired_give_back::kLiveBudgetCap; +using retired_give_back::live_budget; using retired_give_back::mode; using retired_give_back::settled; using retired_give_back::want; @@ -51,6 +53,11 @@ static_assert(want(8 * kGB / 100 * 85, 8 * kGB, kHigh, kLow) == 0, "at the high static_assert(want(7 * kGB, 8 * kGB, kHigh, kLow) == 7 * kGB - 8 * kGB / 100 * 75, "past it: down to the low watermark"); static_assert(want(8 * kGB, 8 * kGB, 0, kLow) == 0, "RX_VK_IMPORT_HIGH=0: off"); static_assert(settled(7 * kGB, kGB) == 6 * kGB && settled(kGB, 2 * kGB) == 0, "what is queued is as good as freed"); +static_assert(live_budget(3800 * kMB, false) == 3800 * kMB, "under the cap: the heap's own budget"); +static_assert(live_budget(15434 * kMB, false) == kLiveBudgetCap, "32 GB PC, default: capped at 4500 MB (high ~3825 MB, low ~3375 MB)"); +static_assert(live_budget(15434 * kMB, true) == 15434 * kMB, "RX_VK_IMPORT_HIGH set: the heap's full budget"); +static_assert(want(3878 * kMB, live_budget(15434 * kMB, false), kHigh, kLow) == 3878 * kMB - 3375 * kMB, + "3878 MB used on a 15434 MB heap: past 85% of the 4500 MB cap, gives back down to 75% (3375 MB)"); struct Retired { uint64_t bytes; int64_t at_ms; bool imported = true; }; diff --git a/tests/test_retired_give_back.py b/tests/test_retired_give_back.py index fd85de429..0b16a9474 100644 --- a/tests/test_retired_give_back.py +++ b/tests/test_retired_give_back.py @@ -28,6 +28,8 @@ def check_source(): # (what is queued is not asked for twice, by this valve or by imports_give_back's) "retired_give_back::settled(used, dead_import_bytes), budget, high, low)", "if (retired_mode() == retired_give_back::Mode::GRACE) used = retired_give_back::settled(used, dead_import_bytes);", + "const uint64_t cap = retired_give_back::live_budget(budget, env_high && *env_high);", + "const uint64_t want = retired_give_back::want(used, cap, high, low);", "dead_import_bytes += p.hi - p.lo;", "dead_import_bytes -= std::min(dead_import_bytes, dead_imports.front().bytes);"): if needle not in src: From 6c9c7f492ecba75acfab8a1ab0702dacc3c0fe02 Mon Sep 17 00:00:00 2001 From: Daydatao <52039764+Daydatao@users.noreply.github.com> Date: Thu, 12 Nov 2026 17:51:52 +0100 Subject: [PATCH 2/2] perf(vk): scope default import cap to >= 12 GB budgets and add RX_VK_IMPORT_CAP_MB --- docs/tested-configurations.md | 2 +- gpu/vk/retired_give_back.h | 22 ++++++++++++++++------ gpu/vk/vk_memory.cpp | 32 ++++++++++++++++++++------------ tests/retired_give_back_test.cpp | 8 ++++++-- tests/test_retired_give_back.py | 3 ++- 5 files changed, 45 insertions(+), 22 deletions(-) diff --git a/docs/tested-configurations.md b/docs/tested-configurations.md index 58b260d98..5f58c7170 100644 --- a/docs/tested-configurations.md +++ b/docs/tested-configurations.md @@ -15,7 +15,7 @@ otherwise. | Date | Build | OS / desktop | GPU / driver | CPU / RAM | Settings | Where | Result | |---|---|---|---|---|---|---|---| | 2026-11-12 | v2.1.0 (source e561602e) | Arch Linux, KDE Plasma Wayland, 2560x1440 window (mailbox, udmabuf on) | RX 9070 XT 16 GB, Mesa RADV 26.2.6 | Core i5-13600KF, 32 GB | 2560x1440 output, V-Sync off, parallel recorder on (`RX_VK_RECORDER=thread`, `RX_VK_BATCH=0`), `RX_VK_MIRROR_MB=4096`, idle live-import cap on (`RX_VK_IMPORT_HIGH=25`, `RX_VK_IMPORT_LOW=20`: ~3,858 MB high / ~3,086 MB low on the 15,434 MB system heap budget) | Long session across Hyrule (176,640 presents, ~1.5 h) | 63.5-65.6 fps at 641-708 draws, 67.2-68.3 fps at 186 draws throughout the session. 257 idle live imports (7,872 MB in all) given back; live imports stayed at 50-64 pools (2,256-2,720 MB), capture submit 0.18-0.43 ms (scan 0.08 ms over 3,923-4,251 MB), queue submit 0.32-0.73 ms, readbacks of GPU results waited 0.64-1.14 ms each. No device lost | -| 2026-11-12 | v2.1.0 (source e561602e) | as above | as above | as above | Default import watermarks (`imports_give_back` off on the 15,434 MB system heap budget since `budget >= 12 GB`) | Long session across Hyrule (684,360 presents, ~2 h) | At 10 min (40,920 presents): 60+ fps, 55 live imports (2,000 MB), capture submit 0.27 ms (scan 0.05 ms over 734 ranges, 2,417 MB), queue submit 0.55 ms, readback wait 2.19 ms each (270 of 3,000). After ~2 h (684,120 presents): faded to 44-48.4 fps at 303-408 draws (`vi: real-time step on: ratio 1.352, game's frame rate 44`); 221 live imports (7,392 MB; 67 retired given back, 2,992 MB), working set 15,351 MB (peak 16,107 MB, 1,511 MB swap), capture submit 0.50 ms (scan 0.09 ms over 334 ranges, 7,218 MB), queue submit 1.11-1.14 ms, readback wait 6.29-7.03 ms each (312-340 of 3,000). No device lost | +| 2026-11-12 | v2.1.0 (source e561602e) | as above | as above | as above | 2560x1440 output, V-Sync off, default `hookshot_env.txt` (default 2,048 MB mirror budget, default recorder, `imports_give_back` off on the 15,434 MB system heap budget since `budget >= 12 GB`) | Long session across Hyrule (684,360 presents, ~2 h) | At 10 min (40,920 presents): 60+ fps, 55 live imports (2,000 MB), capture submit 0.27 ms (scan 0.05 ms over 734 ranges, 2,417 MB), queue submit 0.55 ms, readback wait 2.19 ms each (270 of 3,000). After ~2 h (684,120 presents): faded to 44-48.4 fps at 303-408 draws (`vi: real-time step on: ratio 1.352, game's frame rate 44`); 221 live imports (7,392 MB; 67 retired given back, 2,992 MB), working set 15,351 MB (peak 16,107 MB, 1,511 MB swap), capture submit 0.50 ms (scan 0.09 ms over 334 ranges, 7,218 MB), queue submit 1.11-1.14 ms, readback wait 6.29-7.03 ms each (312-340 of 3,000). No device lost | | 2026-11-11 | v2.1.0-rc.3-246-g87991bc8 (source 6d41d308), main built from source (llvm-mingw, no DLSS) | Windows 11, 1280x720 window (mailbox, V-Sync Auto) | RTX 3080 10 GB, 621.14 | Core i9-12900K, 64 GB | Default options, uncapped (`HOS_FPS_MAX=1000`); a warm-up run first, no pipeline compiled while measuring | Hyrule Field (adult), by the Lost Woods bridge where the Cheats teleport puts Link, standing; 120 s from 45 s after the teleport | Two runs: 78.4 and 79.3 fps, median frame 12.18 and 12.23 ms, 30 and 37 frames over 20 ms, 1,335-1,453 draws. Readbacks of GPU results: about 1,550 of 3,000 waited, 0.04-0.07 ms each (medians). Against the nightly below for [#2602](https://github.com/NotRexed/Hookshot/pull/2602) | | 2026-11-11 | v2.1.0-rc.3-193-g55b976c0 (source ae9e7c1b), the last nightly before #2602, built from source as above | as above | as above | as above | as above | as above | Two runs, interleaved with the two above: 59.7 and 60.8 fps, median frame 16.39 and 16.32 ms, 264 and 22 frames over 20 ms, 1,355-1,445 draws. Readbacks of GPU results: 172 of 3,000 waited, 5.80 and 5.56 ms each (medians) | | 2026-11-11 | v2.1.0-rc.3-46-gddafc1f (source c872b22b): the nightly with #2314's `gpu/vk/vk_memory.cpp` (97c1a202) swapped in | Ubuntu 26.04.1, kernel 7.0.0, GNOME 50 Wayland | RTX 4070 12 GB, 615.71.09 | Ryzen 7 5800X3D, 64 GB | 2560x1440 output, DLSS DLAA (model K), frame generation off, Balanced preset (texture budget 6144 MB), Reflex on, `RX_VK_PARALLEL=4` | Kokiri Forest, the stream by the village: fresh launch, then again after about 10 minutes of exploring | Fresh 59.0 fps at about 2,505 draws (memory sync 3.9 ms); after exploring 49.5 fps at about 2,760 draws (memory sync 6.3 ms). 99.96% of the texture-shadow copies came from staging. No device lost | diff --git a/gpu/vk/retired_give_back.h b/gpu/vk/retired_give_back.h index d318dbaa0..f2a63b817 100644 --- a/gpu/vk/retired_give_back.h +++ b/gpu/vk/retired_give_back.h @@ -40,17 +40,27 @@ constexpr uint64_t settled(uint64_t used, uint64_t pending) { return used > pend /// The bytes to give back: none up to high percent of the budget (0: off), past it down to low percent. constexpr uint64_t want(uint64_t used, uint64_t budget, int high, int low) { - if (high <= 0 || used <= budget / 100 * (uint64_t)high) return 0; + if (high <= 0 || !budget || used <= budget / 100 * (uint64_t)high) return 0; const uint64_t target = budget / 100 * (uint64_t)low; return used > target ? used - target : 0; } -/// The system-heap cap for idle live imports (imports_give_back) when RX_VK_IMPORT_HIGH is not set: on 32 GB+ Linux PCs -/// the heap's budget is 15+ GB, and leaving imports_give_back off above 12 GB (or waiting for 85% of 15 GB) let 200+ idle -/// unmirrored pools stay in the driver's buffer list and the submit-time capture ranges over a 2 h session (7.4 GB live). +/// The system-heap budget imports_give_back measures against: +/// - Under 12 GB (16 GB PCs and the Steam Deck), the heap's own budget as before (#479), unless RX_VK_IMPORT_CAP_MB > 0. +/// - At 12 GB and above (32 GB+ PCs, where imports_give_back was off), capped at kLiveBudgetCap (4500 MB) by default so +/// idle unmirrored pools do not pile up in amdgpu's per-submit BO list and capture ranges over a multi-hour session. +/// - RX_VK_IMPORT_CAP_MB= sets the cap in MB (> 0: capped on any budget; 0: off at >= 12 GB unless a watermark switch +/// is set). Setting RX_VK_IMPORT_HIGH or RX_VK_IMPORT_LOW without RX_VK_IMPORT_CAP_MB uses the full system-heap budget. +constexpr uint64_t kSmallBudget = 12ull << 30; constexpr uint64_t kLiveBudgetCap = 4500ull << 20; -constexpr uint64_t live_budget(uint64_t budget, bool env_high_set) { - return env_high_set || budget <= kLiveBudgetCap ? budget : kLiveBudgetCap; +constexpr uint64_t live_budget(uint64_t budget, bool env_pct_set, int env_cap_mb = -1) { + if (env_cap_mb > 0) { + const uint64_t cap = (uint64_t)env_cap_mb << 20; + return budget < cap ? budget : cap; + } + if (env_pct_set || budget < kSmallBudget) return budget; + if (env_cap_mb == 0) return 0; + return budget < kLiveBudgetCap ? budget : kLiveBudgetCap; } } // namespace retired_give_back diff --git a/gpu/vk/vk_memory.cpp b/gpu/vk/vk_memory.cpp index cba4c3365..88fa39f0a 100644 --- a/gpu/vk/vk_memory.cpp +++ b/gpu/vk/vk_memory.cpp @@ -3264,16 +3264,19 @@ void mirrors_give_back(uint64_t want_bytes) { // (pools_mu held) // ---- imports given back under pressure. The driver pins every imported pool and counts it in the system heap // the GPU reaches, whose budget is about half the PC's RAM (7.5 GB of 16, 15.4 GB of 32): imports were kept for the session, // the heap went past its budget (7758 of 7503 MB on an RTX 3060 with 16 GB) and new imports failed with -// VK_ERROR_OUT_OF_DEVICE_MEMORY: terrain left out, load zones stuck; on 32 GB+ Linux PCs (where the budget is over 12 GB and -// this valve was off) 200+ idle unmirrored pools piled up over a two-hour session (7.4 GB live), bloating every amdgpu submit's -// buffer list and the submit-time capture scan. Past RX_VK_IMPORT_HIGH percent (85) of the budget (capped at 4500 MB by default; -// RX_VK_IMPORT_HIGH= uses the full budget) the pools bound least lately give their imports back down to RX_VK_IMPORT_LOW -// percent (75): only pools without a mirror, not bound for kImportIdle submissions and done on the GPU. They leave the table as -// a retired pool does (the page table's entries 0, the caches emptied) and are imported again at their next binding; their -// memory is freed once everything recorded until now is done. On by default on Linux; off by default on Windows. -// RX_VK_IMPORT_HIGH=0: off, =85: on anywhere. (Caveat: last_value counts bindings, not shaders reaching a pool through the -// page table; a pool only reached that way would read zeros until it is bound again. Not seen in play: the pools given back -// were long unbound.) ---- +// VK_ERROR_OUT_OF_DEVICE_MEMORY: terrain left out, load zones stuck; on 32 GB+ Linux PCs (where the budget is 12 GB or more +// and this valve was off) 200+ idle unmirrored pools piled up over a two-hour session (7.4 GB live), bloating every amdgpu +// submit's buffer list and the submit-time capture scan. Past RX_VK_IMPORT_HIGH percent (85) of the effective budget the pools +// bound least lately give their imports back down to RX_VK_IMPORT_LOW percent (75): only pools without a mirror, not bound for +// kImportIdle submissions and done on the GPU. Under a 12 GB heap budget (16 GB PCs and the Steam Deck) the effective budget is +// the heap's own, as before; at 12 GB and above it is capped at 4500 MB by default (RX_VK_IMPORT_CAP_MB= sets another cap, +// and RX_VK_IMPORT_CAP_MB=0 turns the >= 12 GB cap off; setting RX_VK_IMPORT_HIGH or RX_VK_IMPORT_LOW explicitly without +// RX_VK_IMPORT_CAP_MB uses the full heap budget instead of the 4500 MB cap). They leave the table as a retired pool does (the +// page table's entries 0, the caches emptied) and are imported again at their next binding; their memory is freed once +// everything recorded until now is done. On by default on Linux; off by default on Windows. RX_VK_IMPORT_HIGH=0: off, =85: on +// anywhere against the full budget. (Caveat: last_value counts bindings, not shaders reaching a pool through the page table; a +// pool only reached that way would read zeros until it is bound again. Not seen in play: the pools given back were long +// unbound.) ---- bool system_heap_usage(uint64_t* used, uint64_t* budget); // (vk_vma.cpp) std::deque::node_type> evicted_pools; int64_t st_imports_evicted = 0; @@ -3293,6 +3296,8 @@ void imports_give_back() { // (pools_mu held) // watermarks (retired_imports_give_back), on by default on Windows AMD and Intel whatever the budget: that one // frees pools nothing uses any more, this one pools still live that have been idle static const char* env_high = getenv("RX_VK_IMPORT_HIGH"); + static const char* env_low = getenv("RX_VK_IMPORT_LOW"); + static const int cap_mb = env_int("RX_VK_IMPORT_CAP_MB", -1); #ifdef _WIN32 static const int high = std::clamp(env_int("RX_VK_IMPORT_HIGH", 0), 0, 100); #else @@ -3303,9 +3308,12 @@ void imports_give_back() { // (pools_mu held) if (high <= 0 || ++calls % 16) return; // (the driver asked every 16 submissions) uint64_t used = 0, budget = 0; if (!system_heap_usage(&used, &budget)) return; - const uint64_t cap = retired_give_back::live_budget(budget, env_high && *env_high); + const uint64_t cap = retired_give_back::live_budget(budget, (env_high && *env_high) || (env_low && *env_low), cap_mb); static const bool logged_cfg = [&] { - if (cap < budget) + if (!cap) + logf("vk: imports given back past %d%% of a %llu MB system heap budget: off (RX_VK_IMPORT_CAP_MB=0)", high, + (unsigned long long)(budget >> 20)); + else if (cap < budget) logf("vk: imports given back past %d%% of a %llu MB cap (%llu MB system heap budget): on", high, (unsigned long long)(cap >> 20), (unsigned long long)(budget >> 20)); else diff --git a/tests/retired_give_back_test.cpp b/tests/retired_give_back_test.cpp index fc9a2e77a..12235c605 100644 --- a/tests/retired_give_back_test.cpp +++ b/tests/retired_give_back_test.cpp @@ -52,10 +52,14 @@ static_assert(want(6 * kGB, 8 * kGB, kHigh, kLow) == 0, "75%: under the high wat static_assert(want(8 * kGB / 100 * 85, 8 * kGB, kHigh, kLow) == 0, "at the high watermark, not past it"); static_assert(want(7 * kGB, 8 * kGB, kHigh, kLow) == 7 * kGB - 8 * kGB / 100 * 75, "past it: down to the low watermark"); static_assert(want(8 * kGB, 8 * kGB, 0, kLow) == 0, "RX_VK_IMPORT_HIGH=0: off"); +static_assert(want(8 * kGB, 0, kHigh, kLow) == 0, "budget 0 (RX_VK_IMPORT_CAP_MB=0): off"); static_assert(settled(7 * kGB, kGB) == 6 * kGB && settled(kGB, 2 * kGB) == 0, "what is queued is as good as freed"); -static_assert(live_budget(3800 * kMB, false) == 3800 * kMB, "under the cap: the heap's own budget"); +static_assert(live_budget(7503 * kMB, false) == 7503 * kMB, "under 12 GB (16 GB PC / Deck): the heap's own budget, unchanged"); static_assert(live_budget(15434 * kMB, false) == kLiveBudgetCap, "32 GB PC, default: capped at 4500 MB (high ~3825 MB, low ~3375 MB)"); -static_assert(live_budget(15434 * kMB, true) == 15434 * kMB, "RX_VK_IMPORT_HIGH set: the heap's full budget"); +static_assert(live_budget(15434 * kMB, true) == 15434 * kMB, "RX_VK_IMPORT_HIGH or RX_VK_IMPORT_LOW set: the heap's full budget"); +static_assert(live_budget(15434 * kMB, false, 0) == 0, "RX_VK_IMPORT_CAP_MB=0 on >= 12 GB: off, as on main"); +static_assert(live_budget(7503 * kMB, false, 0) == 7503 * kMB, "RX_VK_IMPORT_CAP_MB=0 under 12 GB: keeps the < 12 GB behaviour"); +static_assert(live_budget(15434 * kMB, false, 6000) == 6000 * kMB, "RX_VK_IMPORT_CAP_MB=6000: tunable cap in MB"); static_assert(want(3878 * kMB, live_budget(15434 * kMB, false), kHigh, kLow) == 3878 * kMB - 3375 * kMB, "3878 MB used on a 15434 MB heap: past 85% of the 4500 MB cap, gives back down to 75% (3375 MB)"); diff --git a/tests/test_retired_give_back.py b/tests/test_retired_give_back.py index 0b16a9474..8b9e98332 100644 --- a/tests/test_retired_give_back.py +++ b/tests/test_retired_give_back.py @@ -28,7 +28,8 @@ def check_source(): # (what is queued is not asked for twice, by this valve or by imports_give_back's) "retired_give_back::settled(used, dead_import_bytes), budget, high, low)", "if (retired_mode() == retired_give_back::Mode::GRACE) used = retired_give_back::settled(used, dead_import_bytes);", - "const uint64_t cap = retired_give_back::live_budget(budget, env_high && *env_high);", + 'static const int cap_mb = env_int("RX_VK_IMPORT_CAP_MB", -1);', + "const uint64_t cap = retired_give_back::live_budget(budget, (env_high && *env_high) || (env_low && *env_low), cap_mb);", "const uint64_t want = retired_give_back::want(used, cap, high, low);", "dead_import_bytes += p.hi - p.lo;", "dead_import_bytes -= std::min(dead_import_bytes, dead_imports.front().bytes);"):