Unravel Engine C++ Reference
Loading...
Searching...
No Matches
eviction.cpp
Go to the documentation of this file.
1#include "eviction.h"
2
3#include "graphics.h"
4
5#include <algorithm>
6#include <atomic>
7#include <chrono>
8#include <cmath>
9#include <mutex>
10#include <type_traits>
11#include <vector>
12
13namespace gfx
14{
15
16namespace detail
17{
18std::uint64_t g_eviction_frame{0};
19} // namespace detail
20
21namespace
22{
23using clock = std::chrono::steady_clock;
24
25auto to_ms(clock::duration d) -> double
26{
27 return std::chrono::duration<double, std::milli>(d).count();
28}
29} // namespace
30
34{
35public:
36 static auto instance() -> eviction_registry&
37 {
38 static eviction_registry s_instance;
39 return s_instance;
40 }
41
42 auto get_init_status() const -> eviction::init_status
43 {
44 // Atomic so the hot-path is_supported() check is lock-free; the registry mutex still
45 // guards the actual transitions in init/shutdown.
46 return init_status_.load(std::memory_order_acquire);
47 }
48
50 {
51 std::lock_guard<std::mutex> lk(mutex_);
52 default_config_ = cfg;
53 const auto* gpu_stats = gfx::get_stats();
54 if(gpu_stats == nullptr)
55 {
56 set_init_status(eviction::init_status::failed);
58 }
59 auto status = gpu_stats->gpuMemoryMax > 0 ? eviction::init_status::ok : eviction::init_status::unsupported;
60
61 // D3D11 and OpenGL drivers do their own resource paging behind the API, so a second
62 // layer of eviction on top would just thrash. Vulkan/Metal/D3D12 surface explicit memory
63 // budgets through bgfx and need the help.
64 if(gfx::get_renderer_type() == gfx::renderer_type::Direct3D11 ||
65 gfx::get_renderer_type() == gfx::renderer_type::OpenGL)
66 {
68 }
69 set_init_status(status);
70 if(status == eviction::init_status::ok)
71 {
72 seed_startup_budget_locked(gpu_stats);
73 }
74 return status;
75 }
76 void shutdown()
77 {
78 std::lock_guard<std::mutex> lk(mutex_);
79 for(auto* r : resident_)
80 {
81 r->evict_slot_ = UINT32_MAX;
82 }
83 for(auto* r : evicted_)
84 {
85 r->evict_slot_ = UINT32_MAX;
86 }
87 resident_.clear();
88 evicted_.clear();
89 resident_bytes_ = 0;
90 evicted_bytes_ = 0;
91 queued_allocation_bytes_.store(0, std::memory_order_relaxed);
92 external_queued_bytes_.store(0, std::memory_order_relaxed);
93 pending_release_bytes_.store(0, std::memory_order_relaxed);
94 budget_ = {};
95 budget_used_bytes_ = 0;
97 }
98
100 {
101 if(r == nullptr)
102 {
103 return;
104 }
105 std::lock_guard<std::mutex> lk(mutex_);
107 {
108 return;
109 }
110 // Stamp the lifetime markers so the very first sweep treats the resource as freshly used
111 // (rather than appearing infinitely idle because last_use_frame_ == 0). evict_frame_ gets
112 // the same treatment so the thrash-detection window is honest from frame zero.
113 const std::uint64_t frame = detail::g_eviction_frame;
114 r->last_use_frame_ = frame;
115 r->evict_frame_ = frame;
116 add_to(resident_, r);
117 const std::uint64_t sz = r->gpu_size();
118 resident_bytes_ += sz;
119 queued_allocation_bytes_.fetch_add(sz, std::memory_order_relaxed);
120 }
121
123 {
124 if(r == nullptr)
125 {
126 return;
127 }
128 std::lock_guard<std::mutex> lk(mutex_);
130 {
131 return;
132 }
133 const std::uint64_t frame = detail::g_eviction_frame;
134 r->last_use_frame_ = frame;
135 r->evict_frame_ = frame;
136 add_to(evicted_, r);
137 evicted_bytes_ += r->gpu_size();
138 }
139
141 {
142 if(r == nullptr || r->evict_slot_ == UINT32_MAX)
143 {
144 return;
145 }
146 std::lock_guard<std::mutex> lk(mutex_);
147 if(r->get_evict_state() == evict_state::resident)
148 {
149 const std::uint64_t sz = r->gpu_size();
150 resident_bytes_ -= sz;
151 note_pending_release_locked(sz);
152 remove_from(resident_, r);
153 }
154 else
155 {
156 evicted_bytes_ -= r->gpu_size();
157 remove_from(evicted_, r);
158 }
159 r->evict_slot_ = UINT32_MAX;
160 }
161
163 {
164 if(r == nullptr)
165 {
166 return;
167 }
168 std::unique_lock<std::mutex> lk(mutex_);
169 if(get_init_status() != eviction::init_status::ok || r->evict_slot_ == UINT32_MAX ||
170 r->get_evict_state() == evict_state::resident)
171 {
172 return;
173 }
174 restore_locked(lk, r);
175 }
176
178 {
179 std::unique_lock<std::mutex> lk(mutex_);
181 {
182 return snapshot();
183 }
184 // Copy the evicted set so a concurrent unregister (rare; the registry mutex serializes
185 // it) cannot invalidate our iterator. restore_locked rebalances evicted_/resident_ in
186 // place so iterating the original would be UB.
187 const std::vector<ievictable*> pending = evicted_;
188 for(auto* r : pending)
189 {
190 if(r->evict_slot_ != UINT32_MAX && r->get_evict_state() == evict_state::evicted)
191 {
192 restore_locked(lk, r);
193 }
194 }
195 return snapshot();
196 }
197
199 {
200 std::lock_guard<std::mutex> lk(mutex_);
202 {
203 return snapshot();
204 }
205
206 sweep_request req;
207 req.strat = cfg.strat;
208 req.min_age_frames = cfg.min_age_frames;
209 req.max_idle_frames = cfg.max_idle_frames;
210 req.max_evictions = cfg.max_evictions;
211
212 if(cfg.strat == eviction::strategy::age_ttl)
213 {
214 req.target_resident = 0;
215 req.free_limit = UINT64_MAX;
216 }
217 else
218 {
219 const std::uint64_t target = (cfg.target_bytes != 0) ? cfg.target_bytes : cfg.budget_bytes;
220 if(target == 0)
221 {
222 return snapshot();
223 }
224 req.target_resident = target;
225 req.free_limit = UINT64_MAX;
226 }
227 return do_sweep(req);
228 }
229
230 auto evict_bytes(std::uint64_t free_bytes,
231 eviction::strategy strat,
232 std::uint32_t min_age_frames,
233 std::uint32_t max_evictions) -> eviction::stats
234 {
235 std::lock_guard<std::mutex> lk(mutex_);
237 {
238 return snapshot();
239 }
240 sweep_request req;
241 req.strat = strat;
242 req.min_age_frames = min_age_frames;
243 req.max_idle_frames = 0;
244 req.max_evictions = max_evictions;
245 req.target_resident = 0;
246 req.free_limit = free_bytes;
247 return do_sweep(req);
248 }
249
251 {
252 std::lock_guard<std::mutex> lk(mutex_);
254 {
255 return snapshot();
256 }
257 sweep_request req;
259 req.min_age_frames = 0;
260 req.max_idle_frames = 0;
261 req.max_evictions = 0;
262 req.target_resident = 0;
263 req.free_limit = UINT64_MAX;
264 return do_sweep(req);
265 }
266
268 {
270 {
271 std::lock_guard<std::mutex> lk(mutex_);
272 cfg = default_config_;
273 }
274 return evict(cfg);
275 }
276
277 void set_budget(const eviction::budget_state& budget, std::uint64_t used_bytes)
278 {
279 std::lock_guard<std::mutex> lk(mutex_);
281 {
282 return;
283 }
284 budget_ = budget;
285 budget_used_bytes_ = used_bytes;
286 publish_budget_locked();
287 }
288
289 auto current_budget() const -> eviction::budget_state
290 {
291 // Seqlock publish: lock-free readers without a 32-byte std::atomic (needs libatomic on some Linux builds).
292 for(;;)
293 {
294 const auto seq = published_budget_seq_.load(std::memory_order_acquire);
295 if(seq & 1u)
296 {
297 continue;
298 }
299 const auto snapshot = published_budget_;
300 if(published_budget_seq_.load(std::memory_order_acquire) == seq)
301 {
302 return snapshot;
303 }
304 }
305 }
306
307 auto snapshot_default_config() const -> eviction::config
308 {
309 std::lock_guard<std::mutex> lk(mutex_);
310 return default_config_;
311 }
312
314 {
315 std::lock_guard<std::mutex> lk(mutex_);
316 return snapshot();
317 }
318
319 void note_pending_allocation(std::uint64_t bytes)
320 {
322 {
323 return;
324 }
325 // The hot path: a worker thread is registering a render target / compute-write texture.
326 // Atomic and lock-free so we never block resource creation on the registry mutex.
327 queued_allocation_bytes_.fetch_add(bytes, std::memory_order_relaxed);
328 external_queued_bytes_.fetch_add(bytes, std::memory_order_relaxed);
329 }
330
331 auto peek_external_queued_bytes() const -> std::uint64_t
332 {
333 return external_queued_bytes_.load(std::memory_order_relaxed);
334 }
335
336 auto peek_pending_release_bytes() const -> std::uint64_t
337 {
338 return pending_release_bytes_.load(std::memory_order_relaxed);
339 }
340
341 auto peek_queued_bytes() const -> std::uint64_t
342 {
343 return queued_allocation_bytes_.load(std::memory_order_relaxed);
344 }
345
347 {
348 // Called whenever bgfx has processed the command queue (bgfx::frame OR a mid-frame
349 // flush). Both clear the in-flight counter; only frame() rolls over the peak diagnostics
350 // (see on_frame_advanced) so a coalescing flush mid-frame does not erase the worst-case
351 // reading the profiler is about to surface.
352 queued_allocation_bytes_.store(0, std::memory_order_relaxed);
353 external_queued_bytes_.store(0, std::memory_order_relaxed);
354 pending_release_bytes_.store(0, std::memory_order_relaxed);
355 }
356
358 {
359 // Per-frame peak timers used by the profiler. Called from set_frame / advance_frame so
360 // diagnostics reflect the entire frame up to the profiler read.
361 last_pass_ms_ = 0.0;
362 last_restore_ms_ = 0.0;
363 last_pass_scanned_ = 0;
364 last_pass_evicted_ = 0;
365 last_pass_freed_bytes_ = 0;
366 }
367
368private:
369 eviction_registry() = default;
370
371 void set_init_status(eviction::init_status s)
372 {
373 init_status_.store(s, std::memory_order_release);
374 }
375
376 static auto startup_safety_margin(std::uint64_t hard_limit_bytes) -> std::uint64_t
377 {
378 constexpr std::uint64_t k_floor = std::uint64_t(64) * 1024 * 1024;
379 return std::max(k_floor, hard_limit_bytes / 50);
380 }
381
382 void seed_startup_budget_locked(const gfx::stats* gpu_stats)
383 {
384 if(gpu_stats == nullptr || gpu_stats->gpuMemoryMax <= 0)
385 {
386 return;
387 }
388 const std::uint64_t gpu_max = static_cast<std::uint64_t>(gpu_stats->gpuMemoryMax);
389 eviction::budget_state b;
390 b.hard_limit_bytes = gpu_max;
391 // Match default @ref unravel::eviction_settings fractions so reclaim_for works before the
392 // first frame_begin publish.
393 b.soft_budget_bytes = static_cast<std::uint64_t>(static_cast<double>(gpu_max) * 0.85);
394 b.target_bytes = static_cast<std::uint64_t>(static_cast<double>(gpu_max) * 0.75);
395 b.safety_margin_bytes = startup_safety_margin(gpu_max);
396 budget_ = b;
397 budget_used_bytes_ = static_cast<std::uint64_t>(std::max<std::int64_t>(0, gpu_stats->gpuMemoryUsed));
398 publish_budget_locked();
399 }
400
401 void publish_budget_locked()
402 {
403 published_budget_seq_.fetch_add(1, std::memory_order_release);
404 published_budget_ = budget_;
405 published_budget_seq_.fetch_add(1, std::memory_order_release);
406 }
407
408 void note_pending_release_locked(std::uint64_t bytes)
409 {
410 if(bytes != 0)
411 {
412 pending_release_bytes_.fetch_add(bytes, std::memory_order_relaxed);
413 }
414 }
415
416 static void add_to(std::vector<ievictable*>& bucket, ievictable* r)
417 {
418 r->evict_slot_ = static_cast<std::uint32_t>(bucket.size());
419 bucket.push_back(r);
420 }
421
422 static void remove_from(std::vector<ievictable*>& bucket, ievictable* r)
423 {
424 const std::uint32_t idx = r->evict_slot_;
425 ievictable* last = bucket.back();
426 bucket[idx] = last;
427 last->evict_slot_ = idx;
428 bucket.pop_back();
429 }
430
433 struct sweep_request
434 {
436 std::uint32_t min_age_frames = 0;
437 std::uint32_t max_idle_frames = 0;
438 std::uint32_t max_evictions = 0;
439 std::uint64_t target_resident = 0;
440 std::uint64_t free_limit = UINT64_MAX;
441 };
442
443 void restore_locked(std::unique_lock<std::mutex>& lk, ievictable* r)
444 {
445 const auto t0 = clock::now();
446 const std::uint64_t sz = r->gpu_size();
447 // on_restore may load GPU resources and call @ref reclaim_for — never hold the registry
448 // mutex across that callback (reclaim re-locks for sweeps).
449 lk.unlock();
450 const bool ok = r->on_restore();
451 lk.lock();
452 last_restore_ms_ = std::max(last_restore_ms_, to_ms(clock::now() - t0));
453 if(!ok)
454 {
455 ++failed_restores_;
456 return;
457 }
458 if(r->evict_slot_ == UINT32_MAX || r->get_evict_state() != evict_state::resident)
459 {
460 return;
461 }
462 const std::uint32_t slot = r->evict_slot_;
463 if(slot >= evicted_.size() || evicted_[slot] != r)
464 {
465 return;
466 }
467 remove_from(evicted_, r);
468 add_to(resident_, r);
469 evicted_bytes_ -= sz;
470 resident_bytes_ += sz;
471 queued_allocation_bytes_.fetch_add(sz, std::memory_order_relaxed);
472 ++total_restores_;
473 total_bytes_restored_ += sz;
474 const std::uint64_t frame = detail::g_eviction_frame;
475 if(frame - r->evict_frame_ < default_config_.min_age_frames)
476 {
477 ++thrash_events_;
478 }
479 }
480
481 auto do_sweep(const sweep_request& req) -> eviction::stats
482 {
483 const auto t0 = clock::now();
484 const std::uint64_t frame = detail::g_eviction_frame;
485
486 candidates_.clear();
487 collect_candidates(req, frame);
488 order_candidates(req.strat);
489
490 std::uint64_t pass_evicted = 0;
491 std::uint64_t freed = 0;
492 for(auto* r : candidates_)
493 {
494 if(req.max_evictions != 0 && pass_evicted >= req.max_evictions)
495 {
496 break;
497 }
498 if(resident_bytes_ <= req.target_resident || freed >= req.free_limit)
499 {
500 break;
501 }
502 const std::uint64_t sz = r->gpu_size();
503 r->on_evict();
504 note_pending_release_locked(sz);
505 remove_from(resident_, r);
506 add_to(evicted_, r);
507 resident_bytes_ -= sz;
508 evicted_bytes_ += sz;
509 r->evict_frame_ = frame;
510 ++pass_evicted;
511 ++total_evictions_;
512 total_bytes_evicted_ += sz;
513 freed += sz;
514 }
515
516 // Per-frame peaks: max-of, not last-of, so a small fast sweep right before the profiler
517 // reads the stats cannot mask a slow expensive sweep that happened earlier in the same
518 // frame. Reset on bgfx::frame via on_frame_advanced.
519 last_pass_scanned_ = std::max<std::uint64_t>(last_pass_scanned_, candidates_.size());
520 last_pass_evicted_ = std::max<std::uint64_t>(last_pass_evicted_, pass_evicted);
521 last_pass_freed_bytes_ = std::max<std::uint64_t>(last_pass_freed_bytes_, freed);
522 last_pass_ms_ = std::max(last_pass_ms_, to_ms(clock::now() - t0));
523 return snapshot();
524 }
525
526 void collect_candidates(const sweep_request& req, std::uint64_t frame)
527 {
528 for(auto* r : resident_)
529 {
530 if(r->get_evict_class() != evict_class::evictable)
531 {
532 continue;
533 }
534 const std::uint64_t idle = frame - r->get_last_use_frame();
535 if(req.min_age_frames != 0 && idle < req.min_age_frames)
536 {
537 continue;
538 }
539 if(req.strat == eviction::strategy::age_ttl &&
540 (req.max_idle_frames == 0 || idle <= req.max_idle_frames))
541 {
542 continue;
543 }
544 candidates_.push_back(r);
545 }
546 }
547
548 void order_candidates(eviction::strategy strat)
549 {
550 switch(strat)
551 {
554 std::sort(candidates_.begin(),
555 candidates_.end(),
556 [](const ievictable* a, const ievictable* b) -> bool
557 {
558 return a->get_last_use_frame() < b->get_last_use_frame();
559 });
560 break;
562 std::sort(candidates_.begin(),
563 candidates_.end(),
564 [](const ievictable* a, const ievictable* b) -> bool
565 {
566 return a->get_use_count() < b->get_use_count();
567 });
568 break;
570 std::sort(candidates_.begin(),
571 candidates_.end(),
572 [](const ievictable* a, const ievictable* b) -> bool
573 {
574 return a->gpu_size() > b->gpu_size();
575 });
576 break;
577 }
578 }
579
580 auto snapshot() const -> eviction::stats
581 {
582 eviction::stats s;
583 s.resident_count = resident_.size();
584 s.resident_bytes = resident_bytes_;
585 s.evicted_count = evicted_.size();
586 s.evicted_bytes = evicted_bytes_;
587 s.registered_count = resident_.size() + evicted_.size();
588 s.total_evictions = total_evictions_;
589 s.total_restores = total_restores_;
590 s.total_bytes_evicted = total_bytes_evicted_;
591 s.total_bytes_restored = total_bytes_restored_;
592 s.failed_restores = failed_restores_;
593 s.thrash_events = thrash_events_;
594 // Expose the soft budget as the "budget" for tooling — it is the line above which the
595 // driver starts working. Hard limit and safety margin are advisory and shown only via
596 // current_budget() if needed by callers.
597 s.budget_bytes = budget_.soft_budget_bytes;
598 s.target_bytes = budget_.target_bytes;
599 s.budget_used_bytes = budget_used_bytes_;
600 s.last_pass_scanned = last_pass_scanned_;
601 s.last_pass_evicted = last_pass_evicted_;
602 s.last_pass_freed_bytes = last_pass_freed_bytes_;
603 s.pending_release_bytes = pending_release_bytes_.load(std::memory_order_relaxed);
604 s.last_pass_ms = last_pass_ms_;
605 s.last_restore_ms = last_restore_ms_;
606 return s;
607 }
608
609 mutable std::mutex mutex_;
610 std::atomic<eviction::init_status> init_status_{eviction::init_status::unsupported};
611 eviction::config default_config_{};
612
613 std::vector<ievictable*> resident_;
614 std::vector<ievictable*> evicted_;
615 std::vector<ievictable*> candidates_;
616
617 std::uint64_t resident_bytes_ = 0;
618 std::uint64_t evicted_bytes_ = 0;
622 std::atomic<std::uint64_t> queued_allocation_bytes_{0};
623 std::atomic<std::uint64_t> external_queued_bytes_{0};
624 std::atomic<std::uint64_t> pending_release_bytes_{0};
625 std::uint64_t total_evictions_ = 0;
626 std::uint64_t total_restores_ = 0;
627 std::uint64_t total_bytes_evicted_ = 0;
628 std::uint64_t total_bytes_restored_ = 0;
629 std::uint64_t failed_restores_ = 0;
630 std::uint64_t thrash_events_ = 0;
631 eviction::budget_state budget_{};
632 std::atomic<std::uint32_t> published_budget_seq_{0};
633 eviction::budget_state published_budget_{};
634 std::uint64_t budget_used_bytes_ = 0;
635 std::uint64_t last_pass_scanned_ = 0;
636 std::uint64_t last_pass_evicted_ = 0;
637 std::uint64_t last_pass_freed_bytes_ = 0;
638 double last_pass_ms_ = 0.0;
639 double last_restore_ms_ = 0.0;
640};
641
642namespace eviction
643{
644
645auto init(const config& cfg) -> init_status
646{
647 return eviction_registry::instance().init(cfg);
648}
649
650auto is_supported() -> bool
651{
652 return eviction_registry::instance().get_init_status() == eviction::init_status::ok;
653}
654
656{
657 if(debug_consumed_bytes() > 0)
658 {
660 }
661 eviction_registry::instance().shutdown();
662}
663
664auto evict(const config& cfg) -> stats
665{
666 return eviction_registry::instance().evict(cfg);
667}
668
669auto evict() -> stats
670{
671 return eviction_registry::instance().evict_default();
672}
673
674auto evict_bytes(std::uint64_t free_bytes, strategy strat, std::uint32_t min_age_frames, std::uint32_t max_evictions)
675 -> stats
676{
677 return eviction_registry::instance().evict_bytes(free_bytes, strat, min_age_frames, max_evictions);
678}
679
681{
682 return eviction_registry::instance().evict_all();
683}
684
685namespace
686{
687auto live_gpu_used() -> std::uint64_t
688{
689 const auto* gpu_stats = gfx::get_stats();
690 if(gpu_stats == nullptr)
691 {
692 return 0;
693 }
694 return static_cast<std::uint64_t>(std::max<std::int64_t>(0, gpu_stats->gpuMemoryUsed));
695}
696
697auto credit_pending_release(std::uint64_t gross) -> std::uint64_t
698{
699 const std::uint64_t pending = eviction_registry::instance().peek_pending_release_bytes();
700 return gross > pending ? gross - pending : 0;
701}
702
703auto project_occupancy(std::uint64_t used, std::uint64_t queued, std::uint64_t request, std::uint64_t margin)
704 -> std::uint64_t
705{
706 return credit_pending_release(used + queued + request + margin);
707}
708
709auto projected_allocation_bytes(std::uint64_t bytes) -> std::uint64_t
710{
711 const budget_state budget = eviction_registry::instance().current_budget();
712 if(budget.hard_limit_bytes == 0)
713 {
714 return 0;
715 }
716 return project_occupancy(live_gpu_used(),
718 bytes,
719 budget.safety_margin_bytes);
720}
721} // namespace
722
723auto would_allocation_fit(std::uint64_t bytes) -> bool
724{
725 if(bytes == 0 || !is_supported())
726 {
727 return true;
728 }
729 const budget_state budget = eviction_registry::instance().current_budget();
730 if(budget.hard_limit_bytes == 0)
731 {
732 return true;
733 }
734 return projected_allocation_bytes(bytes) <= budget.hard_limit_bytes;
735}
736
737auto reclaim_for(std::uint64_t bytes, reclaim_kind kind) -> reclaim_result
738{
739 if(bytes == 0 || !is_supported())
740 {
742 }
743 const budget_state budget = eviction_registry::instance().current_budget();
744 if(budget.hard_limit_bytes == 0)
745 {
747 }
748
749 if(kind == reclaim_kind::evictable)
750 {
752 }
753
754 const std::uint64_t projected = projected_allocation_bytes(bytes);
755 if(projected <= budget.soft_budget_bytes)
756 {
758 }
759
760 const eviction::config cfg = eviction_registry::instance().snapshot_default_config();
761 const std::uint64_t deficit = projected > budget.target_bytes ? projected - budget.target_bytes : 0;
762 const stats sweep =
763 evict_bytes(deficit, cfg.strat, cfg.min_age_frames, cfg.max_evictions);
764 const std::uint64_t freed = sweep.last_pass_freed_bytes;
765
766 auto after_evict = [&]() -> std::uint64_t
767 {
768 return projected_allocation_bytes(bytes);
769 };
770
771 std::uint64_t occupancy = after_evict();
772
773 // immediate: evicted destroys must land on the GPU before the imminent allocation.
774 if(bytes > 0 && freed > 0)
775 {
776 gfx::frames(1, BGFX_FRAME_FLUSH);
777 occupancy = after_evict();
779 }
780
781 if(occupancy <= budget.hard_limit_bytes)
782 {
784 }
785
786 gfx::frames(1, BGFX_FRAME_FLUSH);
787 occupancy = after_evict();
789}
790
791auto peek_queued_bytes() -> std::uint64_t
792{
793 return eviction_registry::instance().peek_queued_bytes();
794}
795
796auto peek_pending_release_bytes() -> std::uint64_t
797{
798 return eviction_registry::instance().peek_pending_release_bytes();
799}
800
801auto peek_external_queued_bytes() -> std::uint64_t
802{
803 return eviction_registry::instance().peek_external_queued_bytes();
804}
805
807{
808 eviction_registry::instance().note_pending_allocation(bytes);
809}
810
812{
813 eviction_registry::instance().clear_queued_allocations();
814}
815
816namespace
817{
820struct debug_reserve
821{
822 std::vector<gfx::texture_handle> chunks;
823 std::uint64_t bytes = 0;
824};
825
826auto reserved() -> debug_reserve&
827{
828 static debug_reserve s_reserved;
829 return s_reserved;
830}
831
832constexpr std::uint16_t k_reserve_dim = 4096; // 4096x4096 RGBA8
833constexpr std::uint64_t k_reserve_chunk = std::uint64_t(k_reserve_dim) * k_reserve_dim * 4; // == 64 MiB
834
837auto alloc_reserve_chunk(std::uint16_t dim) -> std::uint64_t
838{
840 gfx::create_texture_2d(dim, dim, false, 1, gfx::texture_format::RGBA8, BGFX_TEXTURE_NONE, nullptr);
841 if(!bgfx::isValid(handle))
842 {
843 return 0;
844 }
845 reserved().chunks.push_back(handle);
846 const std::uint64_t sz = std::uint64_t(dim) * dim * 4;
847 reserved().bytes += sz;
848 return sz;
849}
850} // namespace
851
852auto debug_consume_memory(std::uint64_t bytes) -> std::uint64_t
853{
854 std::uint64_t remaining = bytes;
855 while(remaining >= k_reserve_chunk)
856 {
857 const std::uint64_t got = alloc_reserve_chunk(k_reserve_dim);
858 if(got == 0)
859 {
860 return reserved().bytes; // allocation failed - stop reserving
861 }
862 remaining -= got;
863 }
864 if(remaining > 0)
865 {
866 // Smallest square RGBA8 texture that covers the remainder.
867 const double texels = static_cast<double>(remaining) / 4.0;
868 auto side = static_cast<std::uint32_t>(std::ceil(std::sqrt(texels)));
869 side = std::clamp<std::uint32_t>(side, 1, k_reserve_dim);
870 alloc_reserve_chunk(static_cast<std::uint16_t>(side));
871 }
872 return reserved().bytes;
873}
874
875auto debug_simulate_budget(std::uint64_t target_free_bytes) -> std::uint64_t
876{
877 const auto* gpu_stats = gfx::get_stats();
878 if(gpu_stats == nullptr || gpu_stats->gpuMemoryMax <= 0)
879 {
880 return debug_consumed_bytes();
881 }
882 // Absolute target: start from a clean slate, then let the released VRAM settle so the usage we
883 // read back reflects only the real (non-reserved) consumers.
884 if(debug_consumed_bytes() > 0)
885 {
887 }
888 gpu_stats = gfx::get_stats();
889 const auto budget = static_cast<std::uint64_t>(gpu_stats->gpuMemoryMax);
890 const auto used = static_cast<std::uint64_t>(std::max<std::int64_t>(0, gpu_stats->gpuMemoryUsed));
891 if(target_free_bytes >= budget)
892 {
893 return debug_consumed_bytes();
894 }
895 const std::uint64_t desired_used = budget - target_free_bytes;
896 if(desired_used <= used)
897 {
898 return debug_consumed_bytes(); // less is already free than the requested target
899 }
900 return debug_consume_memory(desired_used - used);
901}
902
904{
905 for(const auto handle : reserved().chunks)
906 {
907 if(bgfx::isValid(handle))
908 {
910 }
911 }
912 reserved().chunks.clear();
913 reserved().bytes = 0;
914 // Pump the command buffer so the destroys are serviced and their VRAM is reclaimed before return.
915 gfx::frames(1, BGFX_FRAME_FLUSH);
916}
917
918auto debug_consumed_bytes() -> std::uint64_t
919{
920 return reserved().bytes;
921}
922
924{
925 auto& reg = eviction_registry::instance();
926 if(!is_supported())
927 {
928 return reg.get_stats();
929 }
930 // Pre-flight reclaim: ensure there is room for the full evicted pool before we start
931 // recreating handles. Doing it once up-front (rather than once per restore) avoids holding
932 // the registry mutex across reclaim_for, which would deadlock since reclaim_for re-locks via
933 // evict_bytes. We accept the slight over-estimate (queued allocations are double-counted by
934 // gpu_used as bgfx catches up); reclaim_for treats that as headroom unless we are truly tight.
935 const auto pre = reg.get_stats();
936 if(pre.evicted_bytes != 0)
937 {
938 (void)reclaim_for(pre.evicted_bytes, reclaim_kind::immediate);
939 }
940 return reg.restore_all();
941}
942
943void set_budget(const budget_state& budget, std::uint64_t used_bytes)
944{
945 eviction_registry::instance().set_budget(budget, used_bytes);
946}
947
949{
950 return eviction_registry::instance().current_budget();
951}
952
954{
955 return eviction_registry::instance().get_stats();
956}
957
958void set_frame(std::uint64_t frame)
959{
961 // New frame: reset the per-frame peak diagnostics so the profiler sees this frame's worst
962 // case, not a value carried over from previous frames.
963 eviction_registry::instance().on_frame_advanced();
964}
965
967{
969 eviction_registry::instance().on_frame_advanced();
970}
971
973{
974 eviction_registry::instance().register_resource(resource);
975}
976
978{
979 eviction_registry::instance().register_evicted_resource(resource);
980}
981
983{
984 eviction_registry::instance().unregister_resource(resource);
985}
986
988{
989 eviction_registry::instance().restore_resource(resource);
990}
991
992} // namespace eviction
993} // namespace gfx
entt::handle b
entt::handle a
static auto instance() -> eviction_registry &
Definition eviction.cpp:36
auto current_budget() const -> eviction::budget_state
Definition eviction.cpp:289
auto evict_default() -> eviction::stats
Definition eviction.cpp:267
void unregister_resource(ievictable *r)
Definition eviction.cpp:140
auto evict_bytes(std::uint64_t free_bytes, eviction::strategy strat, std::uint32_t min_age_frames, std::uint32_t max_evictions) -> eviction::stats
Definition eviction.cpp:230
void note_pending_allocation(std::uint64_t bytes)
Definition eviction.cpp:319
auto init(const eviction::config &cfg) -> eviction::init_status
Definition eviction.cpp:49
void set_budget(const eviction::budget_state &budget, std::uint64_t used_bytes)
Definition eviction.cpp:277
auto peek_queued_bytes() const -> std::uint64_t
Definition eviction.cpp:341
auto evict(const eviction::config &cfg) -> eviction::stats
Definition eviction.cpp:198
auto peek_external_queued_bytes() const -> std::uint64_t
Definition eviction.cpp:331
void register_resource(ievictable *r)
Definition eviction.cpp:99
auto get_stats() -> eviction::stats
Definition eviction.cpp:313
auto peek_pending_release_bytes() const -> std::uint64_t
Definition eviction.cpp:336
void restore_resource(ievictable *r)
Definition eviction.cpp:162
auto snapshot_default_config() const -> eviction::config
Definition eviction.cpp:307
auto restore_all() -> eviction::stats
Definition eviction.cpp:177
auto get_init_status() const -> eviction::init_status
Definition eviction.cpp:42
auto evict_all() -> eviction::stats
Definition eviction.cpp:250
void register_evicted_resource(ievictable *r)
Definition eviction.cpp:122
std::vector< gfx::texture_handle > chunks
Definition eviction.cpp:822
std::uint64_t bytes
Definition eviction.cpp:823
uint32_t frame
Definition graphics.cpp:23
std::uint64_t g_eviction_frame
Definition eviction.cpp:18
void note_pending_allocation(std::uint64_t bytes)
Definition eviction.cpp:806
auto is_supported() -> bool
Definition eviction.cpp:650
void restore_resource(ievictable *resource)
Definition eviction.cpp:987
auto current_budget() -> budget_state
Definition eviction.cpp:948
auto evict() -> stats
Run a single eviction pass using the config supplied to init.
Definition eviction.cpp:669
auto get_stats() -> stats
Snapshot the current statistics.
Definition eviction.cpp:953
auto restore_all() -> stats
Definition eviction.cpp:923
auto debug_simulate_budget(std::uint64_t target_free_bytes) -> std::uint64_t
Definition eviction.cpp:875
auto debug_consume_memory(std::uint64_t bytes) -> std::uint64_t
Definition eviction.cpp:852
auto evict_bytes(std::uint64_t free_bytes, strategy strat, std::uint32_t min_age_frames, std::uint32_t max_evictions) -> stats
Definition eviction.cpp:674
auto evict_all() -> stats
Definition eviction.cpp:680
void set_budget(const budget_state &budget, std::uint64_t used_bytes)
Definition eviction.cpp:943
void register_resource(ievictable *resource)
Begin tracking a resource (assumed resident). Called from handle_impl::make_evictable.
Definition eviction.cpp:972
auto would_allocation_fit(std::uint64_t bytes) -> bool
Definition eviction.cpp:723
void register_evicted_resource(ievictable *resource)
Definition eviction.cpp:977
auto peek_pending_release_bytes() -> std::uint64_t
Definition eviction.cpp:796
void clear_queued_allocations()
Definition eviction.cpp:811
auto debug_consumed_bytes() -> std::uint64_t
Total bytes currently reserved by debug_consume_memory.
Definition eviction.cpp:918
auto init(const config &cfg) -> init_status
Definition eviction.cpp:645
auto peek_external_queued_bytes() -> std::uint64_t
Definition eviction.cpp:801
void shutdown()
Definition eviction.cpp:655
strategy
Selection policy used by a sweep to choose eviction victims.
Definition eviction.h:51
@ lfu
Least frequently used first (smallest use count).
@ lru
Least recently used first (smallest last-use frame).
@ age_ttl
Every resource idle for longer than config::max_idle_frames.
@ largest_first
Largest resources first (fastest headroom recovery).
reclaim_kind
Controls whether reclaim_for pumps the GPU command buffer after evicting.
Definition eviction.h:83
void debug_release_memory()
Definition eviction.cpp:903
init_status
Result of init. Determines whether the system tracks resources at all.
Definition eviction.h:42
@ failed
Initialization failed (graphics not ready).
@ ok
Initialized and active.
@ unsupported
Backend does not report a GPU memory budget; eviction is disabled.
@ unnecessary
Eviction is not needed for this backend (driver manages residency).
auto peek_queued_bytes() -> std::uint64_t
Definition eviction.cpp:791
void set_frame(std::uint64_t frame)
Set the global frame counter (call once per presented frame).
Definition eviction.cpp:958
void advance_frame()
Advance the global frame counter by one.
Definition eviction.cpp:966
auto reclaim_for(std::uint64_t bytes, reclaim_kind kind) -> reclaim_result
Definition eviction.cpp:737
void unregister_resource(ievictable *resource)
Stop tracking a resource. Called from handle_impl's destructor.
Definition eviction.cpp:982
@ insufficient
Eviction could not free enough room; the allocation may exhaust device memory.
@ headroom
Already enough headroom; nothing was done.
@ reclaimed
Eviction (and possibly a command-buffer pump) freed enough room.
@ evictable
May be evicted by a sweep.
bgfx::Stats stats
Definition graphics.h:31
void frames(int _count, int32_t _flags)
renderer_type get_renderer_type()
Definition graphics.cpp:440
@ evicted
GPU handle was destroyed; a CPU-side backing is retained for restore.
@ resident
GPU handle is live and usable.
const stats * get_stats()
Definition graphics.cpp:450
bgfx::TextureHandle texture_handle
Definition graphics.h:51
texture_handle create_texture_2d(uint16_t _width, uint16_t _height, bool _hasMips, uint16_t _numLayers, texture_format _format, uint64_t _flags, const memory_view *_mem)
Definition graphics.cpp:678
void destroy(index_buffer_handle _handle)
Definition graphics.cpp:505
Provides a sequence-based action management system for controlling and scheduling actions.
Hash specialization for batch_key to enable use in std::unordered_map.
std::uint64_t soft_budget_bytes
Definition eviction.h:107
std::uint64_t hard_limit_bytes
Definition eviction.h:106
std::uint64_t target_bytes
Definition eviction.h:108
Configuration for a single eviction pass.
Definition eviction.h:114
strategy strat
Victim selection policy.
Definition eviction.h:116
std::uint32_t min_age_frames
Never evict a resource used within this many frames (anti-thrash protection).
Definition eviction.h:123
std::uint32_t max_evictions
Maximum number of resources to evict in a single pass (bounds latency). 0 means unlimited.
Definition eviction.h:127
Cumulative and last-pass statistics reported by the eviction system.
Definition eviction.h:132
std::uint64_t last_pass_freed_bytes
GPU bytes reclaimed by the most recent sweep.
Definition eviction.h:149
gfx::uniform_handle handle
Definition uniform.cpp:9