diff --git a/src/ifcviewer/GpuBudget.cpp b/src/ifcviewer/GpuBudget.cpp index 4b683743eb..38cffb6a76 100644 --- a/src/ifcviewer/GpuBudget.cpp +++ b/src/ifcviewer/GpuBudget.cpp @@ -21,27 +21,36 @@ #include -void GpuBudget::configure(std::uint64_t device_free_bytes, - std::uint64_t reserve_bytes, - std::uint64_t hard_cap_bytes) { - bounded_ = false; - budget_ = 0; - if (device_free_bytes > 0) { - bounded_ = true; - budget_ = device_free_bytes > reserve_bytes - ? device_free_bytes - reserve_bytes - : 0; - } - if (hard_cap_bytes > 0) { - budget_ = bounded_ ? std::min(budget_, hard_cap_bytes) : hard_cap_bytes; - bounded_ = true; - } - if (bounded_) budget_ = std::max(budget_, kMinCacheBudgetBytes); +void GpuBudget::bound(std::uint64_t budget) { + if (hard_cap_ > 0) budget = std::min(budget, hard_cap_); + bounded_ = true; + budget_ = std::max(budget, kMinCacheBudgetBytes); +} + +void GpuBudget::setHardCap(std::uint64_t hard_cap_bytes) { + hard_cap_ = hard_cap_bytes; + if (hard_cap_ > 0) bound(bounded_ ? budget_ : hard_cap_); +} + +void GpuBudget::update(std::uint64_t device_free_bytes, + std::uint64_t cache_capacity_bytes) { + if (device_free_bytes == 0) return; + const std::uint64_t available = cache_capacity_bytes + device_free_bytes; + const std::uint64_t margin = margin_bytes(); + bound(available > margin ? available - margin : 0); } bool GpuBudget::onPressure(std::uint64_t cache_capacity_bytes, - std::uint64_t bytes_needed) { + std::uint64_t bytes_needed, + std::uint64_t device_free_bytes) { ++pressure_events_; + // The driver refused bytes_needed while reporting device_free_bytes + // free, so at least (free - needed) of what it reports is not really + // available. Remember that so update() stops short of it next time. + if (device_free_bytes > bytes_needed) { + learned_margin_ = std::max(learned_margin_, + device_free_bytes - bytes_needed + kPressureSlackBytes); + } // What the cache may keep once the failed allocation and its slack // have been carved out of what it holds right now. The pool's actual // capacity, not the previous budget, is the honest baseline: the @@ -54,7 +63,6 @@ bool GpuBudget::onPressure(std::uint64_t cache_capacity_bytes, : 0; const std::uint64_t lowered = std::max(target, kMinCacheBudgetBytes); if (bounded_ && lowered >= budget_) return false; - bounded_ = true; - budget_ = lowered; + bound(lowered); return true; } diff --git a/src/ifcviewer/GpuBudget.h b/src/ifcviewer/GpuBudget.h index db1303df45..653f9a276d 100644 --- a/src/ifcviewer/GpuBudget.h +++ b/src/ifcviewer/GpuBudget.h @@ -33,15 +33,29 @@ // never takes the last byte: it grows only up to this budget, and yields // whenever a required allocation fails. // -// The budget's *source* differs per platform, the mechanism does not: -// - desktop: derived from the driver's free-memory report minus a reserve -// for the attachments (GpuMemory.h); -// - web: no memory query exists, so a fixed ceiling (the wasm heap) and -// pressure feedback alone; -// - either: when a required allocation still fails, onPressure() lowers -// the budget so the pool releases enough for that allocation to succeed -// on retry. Over a session the budget converges on what the device -// actually allows, which is the only information WebGPU ever gives. +// The budget is *live*, the way D3D12's QueryVideoMemoryInfo and Vulkan's +// memory_budget are meant to be used: on desktop the driver's free-memory +// report (GpuMemory.h) is polled and +// +// budget = cache capacity + device free - margin +// +// is recomputed each time, so the cache tracks what the device can give as +// other processes come and go. The attachments are eager, so at any poll +// they are already inside "used" at the *actual* surface size; nothing is +// idled for a hypothetical bigger window -- a resize that no longer fits is +// answered by the pressure path instead. +// +// The margin has a fixed part for the required-tier allocations that come +// later (the next model's metadata, staging) and a *learned* part: drivers +// refuse allocations while still reporting memory free (measured here: a +// refusal with 221 MB "free"), and a budget that trusts the report would +// grow straight back into the same refusal after every shrink. A pressure +// event therefore records how much reported-free memory turned out to be +// unusable, and the margin keeps that from then on. +// +// Web has no memory query, so it keeps a fixed ceiling (the wasm heap) and +// pressure feedback alone. The budget's source differs per platform, the +// mechanism does not. // // Pure policy, no wgpu: the pool applies the number via // BufferPool::setMaxTotalCapacity / shrinkToCapacity. @@ -52,40 +66,53 @@ public: // allocation that still fails at the floor is a genuinely exhausted // device, and the caller degrades instead. static constexpr std::uint64_t kMinCacheBudgetBytes = 64ull * 1024 * 1024; - // Headroom added on top of the failed allocation when lowering the + // Held back for required allocations made after the cache has grown + // (a later model's metadata buffers, readback staging, driver + // bookkeeping). + static constexpr std::uint64_t kFixedMarginBytes = 256ull * 1024 * 1024; + // Headroom added on top of a failed allocation when lowering the // budget, so the very next small required allocation does not fail // again and trigger another shrink cycle. - static constexpr std::uint64_t kPressureSlackBytes = 32ull * 1024 * 1024; + static constexpr std::uint64_t kPressureSlackBytes = 32ull * 1024 * 1024; - // `device_free_bytes` 0 = unknown (web, or an unsupported driver). - // `reserve_bytes` is what the required tier is expected to need at its - // largest (attachments at the biggest plausible surface, plus margin). - // `hard_cap_bytes` 0 = none; otherwise an absolute ceiling regardless - // of device memory (the wasm heap on web). - void configure(std::uint64_t device_free_bytes, - std::uint64_t reserve_bytes, - std::uint64_t hard_cap_bytes); + // Absolute ceiling regardless of device memory (the wasm heap on web). + // 0 = none. + void setHardCap(std::uint64_t hard_cap_bytes); - // False when nothing bounds the cache yet (no device information, no - // cap, no pressure so far). The pool then grows until the driver - // refuses, exactly as before; the first pressure event bounds it. + // Desktop: a fresh driver report. `device_free_bytes` 0 = the query + // could not answer -- ignored, the budget keeps its last value. + void update(std::uint64_t device_free_bytes, + std::uint64_t cache_capacity_bytes); + + // A required allocation of `bytes_needed` failed while the cache held + // `cache_capacity_bytes` and the driver reported `device_free_bytes` + // free (0 = unknown). Lowers the budget so that shrinking the cache to + // it frees bytes_needed + slack, and learns the unusable headroom for + // future update() calls. Returns false when the budget could not be + // lowered any further (already at the floor): the device is exhausted + // and the caller must degrade rather than retry. + bool onPressure(std::uint64_t cache_capacity_bytes, + std::uint64_t bytes_needed, + std::uint64_t device_free_bytes); + + // False until something bounds the cache (a device report, a cap, or + // a pressure event). The pool then grows until the driver refuses, + // exactly as before; the first of those bounds it. bool bounded() const { return bounded_; } // Meaningful only when bounded(). std::uint64_t cache_budget_bytes() const { return budget_; } - - // A required allocation of `bytes_needed` failed while the cache held - // `cache_capacity_bytes`. Lowers the budget so that shrinking the cache - // to it frees bytes_needed + slack. Returns false when the budget could - // not be lowered any further (already at the floor): the device is - // exhausted and the caller must degrade rather than retry. - bool onPressure(std::uint64_t cache_capacity_bytes, - std::uint64_t bytes_needed); - - std::uint32_t pressure_events() const { return pressure_events_; } + // Fixed + learned margin applied by update(). + std::uint64_t margin_bytes() const { return kFixedMarginBytes + learned_margin_; } + std::uint32_t pressure_events() const { return pressure_events_; } private: + void bound(std::uint64_t budget); + bool bounded_ = false; std::uint64_t budget_ = 0; + std::uint64_t hard_cap_ = 0; + // Reported-free memory that a refusal proved unusable, plus slack. + std::uint64_t learned_margin_ = 0; std::uint32_t pressure_events_ = 0; }; diff --git a/src/ifcviewer/ViewportCore.cpp b/src/ifcviewer/ViewportCore.cpp index cb93187da4..4f62718892 100644 --- a/src/ifcviewer/ViewportCore.cpp +++ b/src/ifcviewer/ViewportCore.cpp @@ -29,7 +29,6 @@ #include "CameraMath.h" #include "GpuAllocScope.h" -#include "GpuMemory.h" #include "InstanceCompose.h" #include "Log.h" @@ -1606,8 +1605,6 @@ bool ViewportCore::createPool() { // required tier (attachments, model metadata, staging) will need, // because those are allocated later and the frame cannot be drawn // without them; see GpuBudget.h for the model. - std::uint64_t device_free = 0; - std::uint64_t hard_cap = 0; #if defined(__EMSCRIPTEN__) // No memory query on web. Cap total pool capacity below the wasm heap // ceiling: a growth that would push the heap past MAXIMUM_MEMORY is a @@ -1616,18 +1613,17 @@ bool ViewportCore::createPool() { // transient decompression buffers, and wgpu overhead. Big federations // then keep a bounded, highest-priority resident set instead of // aborting. Device exhaustion below that is learnt through pressure. - hard_cap = 3072ull * 1024 * 1024; // 3 GB (heap ceiling 4 GB) + budget_.setHardCap(3072ull * 1024 * 1024); // 3 GB (heap ceiling 4 GB) #else { WGPUAdapterInfo adapter_info = WGPU_ADAPTER_INFO_INIT; wgpuAdapterGetInfo(adapter_, &adapter_info); - const ifcviewer::GpuMemoryInfo mem = - ifcviewer::queryGpuMemory(adapter_info.vendorID, adapter_info.deviceID); + adapter_vendor_id_ = adapter_info.vendorID; + adapter_device_id_ = adapter_info.deviceID; wgpuAdapterInfoFreeMembers(adapter_info); - if (mem.valid) device_free = mem.free_bytes(); } + pollDeviceMemory(); #endif - budget_.configure(device_free, requiredTierReserveBytes(), hard_cap); pool_.setMaxTotalCapacity(budget_.bounded() ? budget_.cache_budget_bytes() : 0); Log::info() << "wgpu: pool per-sub-buffer capacity = " @@ -1637,11 +1633,13 @@ bool ViewportCore::createPool() { if (budget_.bounded()) { Log::info() << "wgpu: geometry cache budget = " << (budget_.cache_budget_bytes() / (1024 * 1024)) << " MB" - << (device_free > 0 - ? " (device free " + std::to_string(device_free / (1024 * 1024)) - + " MB - reserve " - + std::to_string(requiredTierReserveBytes() / (1024 * 1024)) - + " MB)" + << (device_vram_total_bytes_ > 0 + ? " (device free " + + std::to_string((device_vram_total_bytes_ - device_vram_used_bytes_) + / (1024 * 1024)) + + " MB - margin " + + std::to_string(budget_.margin_bytes() / (1024 * 1024)) + + " MB; tracks the driver's report)" : " (fixed cap)"); } else { Log::info() << "wgpu: geometry cache budget unknown (no device memory " @@ -1650,6 +1648,44 @@ bool ViewportCore::createPool() { return true; } +ifcviewer::GpuMemoryInfo ViewportCore::queryDeviceMemory() const { +#if defined(__EMSCRIPTEN__) + return {}; +#else + return ifcviewer::queryGpuMemory(adapter_vendor_id_, adapter_device_id_); +#endif +} + +void ViewportCore::pollDeviceMemory() { + if (device_vram_poll_timer_.isValid() + && device_vram_poll_timer_.elapsed() < 1000) return; + device_vram_poll_timer_.start(); + const ifcviewer::GpuMemoryInfo mem = queryDeviceMemory(); + if (!mem.valid) return; + device_vram_used_bytes_ = mem.used_bytes; + device_vram_total_bytes_ = mem.total_bytes; + budget_.update(mem.free_bytes(), pool_.total_capacity_bytes()); + applyBudgetToPool(); +} + +void ViewportCore::applyBudgetToPool() { + if (!budget_.bounded()) return; + const std::uint64_t budget = budget_.cache_budget_bytes(); + pool_.setMaxTotalCapacity(budget); + // Whole sub-buffers are the release granularity, so only act once the + // excess is worth one; below that the fixed margin covers it. + const std::uint64_t capacity = pool_.total_capacity_bytes(); + if (capacity < budget + BufferPool::MIN_SUB_BUFFER_BYTES) return; + const std::uint64_t released = pool_.shrinkToCapacity( + budget, [this](int sub_idx) { evictChunksInSubBuffer(sub_idx); }); + if (released > 0) { + const double mb = 1.0 / (1024.0 * 1024.0); + Log::info() << "[wgpu] device has less to give: geometry cache budget now " + << double(budget) * mb << " MB, released " + << double(released) * mb << " MB"; + } +} + std::uint64_t ViewportCore::attachmentBytesPerPixel() { // Sizes follow the formats in ensureDepthTexture / ensureMsaaColorTexture / // ensureSelectionOutlineTextures / createPickAttachments. @@ -1662,19 +1698,11 @@ std::uint64_t ViewportCore::attachmentBytesPerPixel() { + 4 + 8 + 16 + 4; // pick: R32Uint id, RGBA16F normal, RGBA32F position, depth32 } -std::uint64_t ViewportCore::requiredTierReserveBytes() { - // Attachments at a 4K surface — a user can maximise onto a larger - // monitor after load, and the cache must already have left room — - // plus a fixed margin for per-model metadata buffers, readback - // staging, and the driver's own bookkeeping. - constexpr std::uint64_t kMaxPlausiblePixels = 3840ull * 2160; - constexpr std::uint64_t kFixedMargin = 256ull * 1024 * 1024; - return attachmentBytesPerPixel() * kMaxPlausiblePixels + kFixedMargin; -} - bool ViewportCore::onRequiredAllocationFailed(const char* what, std::uint64_t bytes) { const std::uint64_t capacity_before = pool_.total_capacity_bytes(); - const bool lowered = budget_.onPressure(capacity_before, bytes); + const ifcviewer::GpuMemoryInfo mem = queryDeviceMemory(); + const bool lowered = budget_.onPressure(capacity_before, bytes, + mem.valid ? mem.free_bytes() : 0); const double mb = 1.0 / (1024.0 * 1024.0); if (!lowered) { Log::warn() << "[wgpu] out of memory allocating " << what << " (" @@ -7951,21 +7979,7 @@ void ViewportCore::render() { stats.vram_used_bytes = pool_.total_used_bytes(); stats.vram_capacity_bytes = pool_.total_capacity_bytes(); stats.vram_budget_bytes = budget_.bounded() ? budget_.cache_budget_bytes() : 0; -#if !defined(__EMSCRIPTEN__) - if (!device_vram_poll_timer_.isValid() - || device_vram_poll_timer_.elapsed() >= 1000) { - device_vram_poll_timer_.start(); - WGPUAdapterInfo adapter_info = WGPU_ADAPTER_INFO_INIT; - wgpuAdapterGetInfo(adapter_, &adapter_info); - const ifcviewer::GpuMemoryInfo mem = - ifcviewer::queryGpuMemory(adapter_info.vendorID, adapter_info.deviceID); - wgpuAdapterInfoFreeMembers(adapter_info); - if (mem.valid) { - device_vram_used_bytes_ = mem.used_bytes; - device_vram_total_bytes_ = mem.total_bytes; - } - } -#endif + pollDeviceMemory(); stats.device_vram_used_bytes = device_vram_used_bytes_; stats.device_vram_total_bytes = device_vram_total_bytes_; host_->onFrameStats(stats); diff --git a/src/ifcviewer/ViewportCore.h b/src/ifcviewer/ViewportCore.h index 5f5c22c932..5690c45382 100644 --- a/src/ifcviewer/ViewportCore.h +++ b/src/ifcviewer/ViewportCore.h @@ -50,6 +50,7 @@ #include "AxisIndicatorRenderer.h" #include "BufferPool.h" #include "GpuBudget.h" +#include "GpuMemory.h" #include "InstanceCompose.h" #include "InstancedGeometry.h" #include "ModelGpuData.h" @@ -1075,14 +1076,23 @@ private: // observed and answered by shrinking the geometry cache, instead of // surfacing as an invalid resource that aborts in wgpuQueueSubmit. - // Reserve to hold back from the cache for the required tier: the - // per-pixel attachments at the largest plausible surface plus margin. - static std::uint64_t requiredTierReserveBytes(); // Bytes every per-pixel attachment set costs (MSAA colour + depth, - // selection mask trio, pick MRT + depth) — used to size both the - // reserve and the pressure carve-out when an attachment set fails. + // selection mask trio, pick MRT + depth) — sizes the pressure + // carve-out when an attachment set fails. static std::uint64_t attachmentBytesPerPixel(); + // Desktop: the driver's view of the adapter wgpu picked (GpuMemory.h); + // `valid` false on web or an unsupported driver. + ifcviewer::GpuMemoryInfo queryDeviceMemory() const; + // Desktop, at most once a second from render(): refresh the device + // figures for FrameStats and re-derive the live cache budget from + // them, shrinking the pool when the device has less to give than the + // pool holds (another process took memory). + void pollDeviceMemory(); + // Push budget_ to the pool: the growth ceiling, and a shrink when the + // pool is over it by at least a sub-buffer. + void applyBudgetToPool(); + // A required allocation of `bytes` (`what` names it for the log) // failed. Lowers the budget, evicts and releases cache sub-buffers // down to it, and on desktop waits for the device to actually reclaim @@ -1120,6 +1130,10 @@ private: void releaseRenderAttachments(); GpuBudget budget_; + // Adapter ids, read once at init, for matching the driver's memory + // report to the card wgpu is actually using. + std::uint32_t adapter_vendor_id_ = 0; + std::uint32_t adapter_device_id_ = 0; // Latched false by ensureRenderAttachments when the device could not // fit the attachments; re-evaluated on the next configureSurface. bool render_attachments_ok_ = true; @@ -1557,9 +1571,10 @@ private: std::uint32_t last_visible_objects_ = 0; std::uint32_t last_visible_triangles_ = 0; std::uint32_t last_sub_draws_ = 0; - // Device-wide VRAM readout for FrameStats. The driver query is too - // slow for per-frame use, so it is re-polled at most once a second - // and the last answer is repeated in between. + // Device-wide VRAM readout for FrameStats and the live cache budget + // (pollDeviceMemory). The driver query is too slow for per-frame use, + // so it is re-polled at most once a second and the last answer is + // repeated in between. std::uint64_t device_vram_used_bytes_ = 0; std::uint64_t device_vram_total_bytes_ = 0; Stopwatch device_vram_poll_timer_; diff --git a/src/ifcviewer/tests/test_gpu_budget.cpp b/src/ifcviewer/tests/test_gpu_budget.cpp index d1964972cd..73472d9b8f 100644 --- a/src/ifcviewer/tests/test_gpu_budget.cpp +++ b/src/ifcviewer/tests/test_gpu_budget.cpp @@ -18,10 +18,12 @@ ********************************************************************************/ // GpuBudget decides how much device memory the streamed-geometry cache may -// hold. It is pure policy: a number derived from what the platform can -// tell us (desktop: driver free-memory report; web: nothing but a heap -// ceiling) and lowered by pressure events when a required allocation fails -// anyway. These pin down the arithmetic and the floor behaviour. +// hold. It is pure policy: a live number derived from what the platform +// can tell us (desktop: the driver's free-memory report; web: nothing but +// a heap ceiling), lowered by pressure events when a required allocation +// fails anyway, and learning from those how much reported-free memory the +// driver will not actually grant. These pin down the arithmetic, the floor +// and the learning. #include "GpuBudget.h" @@ -31,48 +33,62 @@ namespace { constexpr std::uint64_t MB = 1024ull * 1024; } -TEST_CASE("unknown device memory and no cap leaves the cache unbounded", "[gpu_budget]") { +TEST_CASE("nothing known leaves the cache unbounded", "[gpu_budget]") { GpuBudget b; - b.configure(0, 512 * MB, 0); + REQUIRE_FALSE(b.bounded()); + b.update(0, 512 * MB); // query could not answer: still unbounded REQUIRE_FALSE(b.bounded()); } -TEST_CASE("desktop budget is free memory minus the required-tier reserve", "[gpu_budget]") { +TEST_CASE("a device report bounds the cache at held + free - margin", "[gpu_budget]") { GpuBudget b; - b.configure(2800 * MB, 800 * MB, 0); + b.update(2800 * MB, 0); REQUIRE(b.bounded()); - REQUIRE(b.cache_budget_bytes() == 2000 * MB); + REQUIRE(b.cache_budget_bytes() == 2800 * MB - GpuBudget::kFixedMarginBytes); + + // The pool now holds 1000 MB and the driver reports 1800 MB free: the + // cache's own bytes count as available to it. + b.update(1800 * MB, 1000 * MB); + REQUIRE(b.cache_budget_bytes() == 2800 * MB - GpuBudget::kFixedMarginBytes); + + // Another process took 1000 MB: the budget follows the device down. + b.update(800 * MB, 1000 * MB); + REQUIRE(b.cache_budget_bytes() == 1800 * MB - GpuBudget::kFixedMarginBytes); + // ...and back up when it is released. + b.update(1800 * MB, 1000 * MB); + REQUIRE(b.cache_budget_bytes() == 2800 * MB - GpuBudget::kFixedMarginBytes); } -TEST_CASE("a reserve larger than free memory floors the budget, not zero", "[gpu_budget]") { +TEST_CASE("less than the margin available floors the budget, not zero", "[gpu_budget]") { GpuBudget b; - b.configure(300 * MB, 800 * MB, 0); + b.update(100 * MB, 0); REQUIRE(b.bounded()); REQUIRE(b.cache_budget_bytes() == GpuBudget::kMinCacheBudgetBytes); } TEST_CASE("a hard cap bounds the cache on its own (web) and clamps a device-derived budget", "[gpu_budget]") { GpuBudget web; - web.configure(0, 0, 3072 * MB); + web.setHardCap(3072 * MB); REQUIRE(web.bounded()); REQUIRE(web.cache_budget_bytes() == 3072 * MB); GpuBudget both; - both.configure(8000 * MB, 800 * MB, 3072 * MB); + both.setHardCap(3072 * MB); + both.update(8000 * MB, 0); REQUIRE(both.cache_budget_bytes() == 3072 * MB); GpuBudget small_device; - small_device.configure(2800 * MB, 800 * MB, 3072 * MB); - REQUIRE(small_device.cache_budget_bytes() == 2000 * MB); + small_device.setHardCap(3072 * MB); + small_device.update(2800 * MB, 0); + REQUIRE(small_device.cache_budget_bytes() == 2800 * MB - GpuBudget::kFixedMarginBytes); } TEST_CASE("pressure lowers the budget below what the cache currently holds", "[gpu_budget]") { GpuBudget b; - b.configure(0, 0, 0); REQUIRE_FALSE(b.bounded()); // The pool grew to 2048 MB unbounded; a 120 MB attachment set then failed. - REQUIRE(b.onPressure(2048 * MB, 120 * MB)); + REQUIRE(b.onPressure(2048 * MB, 120 * MB, 0)); REQUIRE(b.bounded()); REQUIRE(b.cache_budget_bytes() == 2048 * MB - 120 * MB - GpuBudget::kPressureSlackBytes); @@ -80,33 +96,57 @@ TEST_CASE("pressure lowers the budget below what the cache currently holds", "[g } TEST_CASE("pressure is measured against actual capacity, not the previous budget", "[gpu_budget]") { - // Budget said 2000 MB but the driver only ever granted 1024 MB; a + // Budget said ~2500 MB but the driver only ever granted 1024 MB; a // failure must carve out of the 1024, else nothing would be released. GpuBudget b; - b.configure(2800 * MB, 800 * MB, 0); - REQUIRE(b.onPressure(1024 * MB, 100 * MB)); + b.update(2800 * MB, 0); + REQUIRE(b.onPressure(1024 * MB, 100 * MB, 0)); REQUIRE(b.cache_budget_bytes() == 1024 * MB - 100 * MB - GpuBudget::kPressureSlackBytes); } TEST_CASE("pressure never raises the budget", "[gpu_budget]") { GpuBudget b; - b.configure(0, 0, 500 * MB); - // Pool is at 256 MB (below budget) and a tiny allocation fails: - // capacity - carve is 224 MB-ish, which IS lower, so it lowers. - REQUIRE(b.onPressure(256 * MB, 0)); + b.setHardCap(500 * MB); + REQUIRE(b.onPressure(256 * MB, 0, 0)); REQUIRE(b.cache_budget_bytes() == 256 * MB - GpuBudget::kPressureSlackBytes); // A later event whose arithmetic lands above the current budget is a no-op. - REQUIRE_FALSE(b.onPressure(4096 * MB, 0)); + REQUIRE_FALSE(b.onPressure(4096 * MB, 0, 0)); REQUIRE(b.cache_budget_bytes() == 256 * MB - GpuBudget::kPressureSlackBytes); } TEST_CASE("pressure bottoms out at the floor and then reports exhaustion", "[gpu_budget]") { GpuBudget b; - b.configure(0, 0, 0); - REQUIRE(b.onPressure(100 * MB, 90 * MB)); + REQUIRE(b.onPressure(100 * MB, 90 * MB, 0)); REQUIRE(b.cache_budget_bytes() == GpuBudget::kMinCacheBudgetBytes); // Already at the floor: nothing more to give. - REQUIRE_FALSE(b.onPressure(64 * MB, 90 * MB)); + REQUIRE_FALSE(b.onPressure(64 * MB, 90 * MB, 0)); REQUIRE(b.pressure_events() == 2); } + +TEST_CASE("a refusal with memory still reported free teaches the margin", "[gpu_budget]") { + GpuBudget b; + b.update(2800 * MB, 0); + REQUIRE(b.margin_bytes() == GpuBudget::kFixedMarginBytes); + + // 59 MB refused with 221 MB "free" (the measured crash): at least + // 162 MB of what the driver reports is not usable. + REQUIRE(b.onPressure(2048 * MB, 59 * MB, 221 * MB)); + REQUIRE(b.margin_bytes() + == GpuBudget::kFixedMarginBytes + 162 * MB + GpuBudget::kPressureSlackBytes); + + // The next live report stops short by the learned amount, so the pool + // does not grow straight back into the same refusal. + b.update(221 * MB, 2048 * MB); + REQUIRE(b.cache_budget_bytes() == 2048 * MB + 221 * MB - b.margin_bytes()); + + // Learning only ever grows; a later refusal with less phantom free + // memory does not shrink it. + b.onPressure(1500 * MB, 59 * MB, 100 * MB); + REQUIRE(b.margin_bytes() + == GpuBudget::kFixedMarginBytes + 162 * MB + GpuBudget::kPressureSlackBytes); + // A refusal that needed more than was reported free teaches nothing. + b.onPressure(1500 * MB, 500 * MB, 100 * MB); + REQUIRE(b.margin_bytes() + == GpuBudget::kFixedMarginBytes + 162 * MB + GpuBudget::kPressureSlackBytes); +}