#include "graphics/host_gpu/renderer/cache/bufferCache.h" #include "common/assert.h" #include "common/logging/log.h" #include "common/profiler.h" #include "graphics/host_gpu/graphicContext.h" #include "graphics/host_gpu/renderer/cache/resourceMutex.h" #include "graphics/host_gpu/renderer/cache/textureCache.h" #include "graphics/host_gpu/renderer/commandScheduler.h" #include "graphics/host_gpu/renderer/render.h" #include "kernel/memory.h" #include #include #include #include #include namespace Libs::Graphics { namespace { thread_local const void* g_cache_lock_owner = nullptr; constexpr uint64_t MiB = 1024 * 1024; constexpr uint64_t GdsBufferSize = 64 * 1024; class FaultSafeCacheLock final { public: FaultSafeCacheLock(const void* owner, Common::Mutex& mutex): m_mutex(mutex) { if (g_cache_lock_owner != nullptr) { EXIT("BufferCache: recursive cache lock acquisition\n"); } g_cache_lock_owner = owner; m_mutex.Lock(); } ~FaultSafeCacheLock() { m_mutex.Unlock(); g_cache_lock_owner = nullptr; } private: Common::Mutex& m_mutex; }; } // namespace uint64_t BufferCache::AlignDown(uint64_t value) noexcept { return value & ~(CACHING_PAGE_SIZE - 1); } uint64_t BufferCache::AlignUp(uint64_t value) { if (value > UINT64_MAX - (CACHING_PAGE_SIZE - 1)) { EXIT("BufferCache: address alignment overflow\n"); } return (value + CACHING_PAGE_SIZE - 1) & ~(CACHING_PAGE_SIZE - 1); } bool BufferCache::PageOverlaps(uint64_t left, uint64_t left_size, uint64_t right, uint64_t right_size) noexcept { const auto left_begin = left & ~(TRACKER_PAGE_SIZE - 1); const auto left_end = (left + left_size + TRACKER_PAGE_SIZE - 1) & ~(TRACKER_PAGE_SIZE - 1); const auto right_begin = right & ~(TRACKER_PAGE_SIZE - 1); const auto right_end = (right + right_size + TRACKER_PAGE_SIZE - 1) & ~(TRACKER_PAGE_SIZE - 1); return left_begin < right_end && right_begin < left_end; } void BufferCache::Upload(CommandBuffer& command, Buffer& destination, uint64_t destination_offset, const void* source, uint64_t size) { auto* bytes = static_cast(source); while (size != 0) { const auto chunk = std::min(size, m_staging_buffer.Size()); const auto stage_offset = m_staging_buffer.Copy(bytes, chunk, 4); destination.CopyFrom(command, m_staging_buffer, stage_offset, destination_offset, chunk, vk::AccessFlagBits::eHostWrite); bytes += chunk; destination_offset += chunk; size -= chunk; } } bool BufferCache::ResolveOverlap(CacheRange& merged, CacheRange candidate) noexcept { if (merged.address == 0 || merged.size == 0 || candidate.address == 0 || candidate.size == 0 || merged.size > UINT64_MAX - merged.address || candidate.size > UINT64_MAX - candidate.address) { EXIT("BufferCache: invalid overlap-merge range\n"); } const auto merged_end = merged.address + merged.size; const auto candidate_end = candidate.address + candidate.size; if (merged.address >= candidate_end || candidate.address >= merged_end) { return false; } const auto address = std::min(merged.address, candidate.address); const auto end = std::max(merged_end, candidate_end); merged = {.address = address, .size = end - address}; return true; } struct BufferCache::CachedBuffer { uint64_t vaddr = 0; uint64_t size = 0; std::shared_ptr buffer; uint64_t tick_accessed_last = 0; }; struct BufferCache::DownloadCopy { std::shared_ptr owner; uint64_t source_offset = 0; uint64_t address = 0; uint64_t size = 0; }; struct BufferCache::DownloadRange { uint64_t address = 0; uint64_t size = 0; uint64_t offset = 0; }; struct BufferCache::RetiredBuffer { uint64_t address = 0; uint64_t size = 0; std::shared_ptr owner; }; struct BufferCache::FaultReadback { PageFaultAccess access = PageFaultAccess::Unknown; uint64_t vaddr = 0; uint64_t size = 0; std::vector ranges; bool installed = false; [[nodiscard]] bool Active() const noexcept { return !ranges.empty(); } void Reset() { access = PageFaultAccess::Unknown; vaddr = 0; size = 0; installed = false; ranges.clear(); } }; struct BufferCache::PendingBackingPublication { uint64_t address = 0; uint64_t size = 0; uint64_t tick = 0; }; std::pair BufferCache::DownloadEnvelope(const DownloadCopy& copy) { if (copy.owner == nullptr || copy.size == 0 || copy.source_offset > copy.owner->Size() || copy.size > copy.owner->Size() - copy.source_offset) { EXIT("BufferCache: invalid download copy\n"); } const auto begin = copy.source_offset & ~uint64_t {3}; if (copy.source_offset > UINT64_MAX - copy.size || copy.source_offset + copy.size > UINT64_MAX - 3) { EXIT("BufferCache: download copy alignment overflow\n"); } const auto end = (copy.source_offset + copy.size + 3) & ~uint64_t {3}; if (end > copy.owner->Size()) { EXIT("BufferCache: aligned download copy exceeds its owner\n"); } return {begin, end - begin}; } std::vector BufferCache::RecordDownloads(std::span copies) { uint64_t reservation_size = 0; for (const auto& copy: copies) { const auto [source_begin, envelope_size] = DownloadEnvelope(copy); (void)source_begin; if (envelope_size > UINT64_MAX - (DOWNLOAD_ALIGNMENT - 1)) { EXIT("BufferCache: download batch alignment overflow\n"); } const auto aligned_size = AlignDownload(envelope_size); if (aligned_size > UINT64_MAX - reservation_size) { EXIT("BufferCache: download batch overflow\n"); } reservation_size += aligned_size; } if (reservation_size == 0) { return {}; } auto& download = m_download_buffer; const auto [mapped, base_offset] = download.Map(reservation_size, DOWNLOAD_ALIGNMENT); if (mapped == nullptr) { EXIT("BufferCache: download batch could not reserve the shared stream\n"); } std::vector downloads; downloads.reserve(copies.size()); uint64_t cursor = 0; for (const auto& copy: copies) { const auto [source_begin, envelope_size] = DownloadEnvelope(copy); const auto prefix = copy.source_offset - source_begin; download.CopyFrom(m_scheduler.Current(), *copy.owner, source_begin, base_offset + cursor, envelope_size, vk::AccessFlagBits::eMemoryWrite, vk::AccessFlags {}, vk::AccessFlagBits::eMemoryRead | vk::AccessFlagBits::eMemoryWrite, vk::AccessFlagBits::eHostRead); downloads.push_back({copy.address, copy.size, base_offset + cursor + prefix}); cursor += AlignDownload(envelope_size); } download.Commit(); return downloads; } void BufferCache::PublishDownloads(std::span downloads) { for (const auto& range: downloads) { m_download_buffer.Invalidate(range.offset, range.size); Libs::LibKernel::Memory::WriteBacking( range.address, m_download_buffer.Mapped().data() + range.offset, range.size); } } void BufferCache::QueueGarbageDownload(std::span copies, RetiredBuffer retire) { if (copies.empty()) { return; } auto downloads = RecordDownloads(copies); const auto tick = m_scheduler.CurrentTick(); BeginBackingPublication(retire.address, retire.size, tick); m_scheduler.DeferOperation([this, downloads = std::move(downloads), retire = std::move(retire), tick]() mutable { PublishDownloads(downloads); { FaultSafeCacheLock lock(this, m_mutex); if (m_memory_tracker.IsRegionGpuModified(retire.address, retire.size)) { m_memory_tracker.ForEachDownloadRange( retire.address, retire.size, [&](uint64_t address, uint64_t size) noexcept { m_memory_tracker.ValidateGpuDirtyPages(m_gpu_modified_ranges, address, size, "asynchronous garbage retirement"); }, [](uint64_t, uint64_t) noexcept {}); } for (const auto& range: downloads) { m_gpu_modified_ranges.Subtract(range.address, range.size); } if (m_memory_tracker.IsRegionGpuModified(retire.address, retire.size) || !m_gpu_modified_ranges.Intersections(retire.address, retire.size).empty()) { EXIT("BufferCache: asynchronous garbage collection retained GPU ownership\n"); } m_memory_tracker.UntrackMemory(retire.address, retire.size); } CompleteBackingPublication(retire.address, retire.size, tick); }); } BufferCache::BufferCache(GraphicContext& graphics, CommandScheduler& scheduler, PageManager& page_manager, TextureCache& texture_cache, ResourceMutex& resource_mutex) : m_graphics(graphics), m_scheduler(scheduler), m_gds_buffer(graphics, scheduler, MemoryUsage::Stream, 0, AllFlags, GdsBufferSize), m_fault_readback(std::make_unique()), m_memory_tracker(page_manager), m_staging_buffer(graphics, scheduler, MemoryUsage::Upload, 512 * MiB), m_stream_buffer(graphics, scheduler, MemoryUsage::Stream, 64 * MiB), m_download_buffer(graphics, scheduler, MemoryUsage::Download, 32 * MiB), m_device_buffer(graphics, scheduler, MemoryUsage::DeviceLocal, 128 * MiB), m_page_manager(page_manager), m_texture_cache(texture_cache), m_resource_mutex(resource_mutex) { std::memset(m_gds_buffer.Mapped().data(), 0, static_cast(m_gds_buffer.Size())); m_gds_buffer.Flush(0, m_gds_buffer.Size()); if (!m_graphics.CanReportMemoryUsage()) { return; } constexpr int64_t GiB = 1024ll * 1024 * 1024; constexpr int64_t target_threshold = 8 * GiB; const auto budget = static_cast(std::min(m_graphics.GetTotalMemoryBudget(), INT64_MAX)); const auto threshold = std::min(budget, target_threshold); const auto expected = std::min(budget - 6 * threshold / 10, budget - GiB); const auto critical = std::min(budget - 2 * threshold / 10, budget - GiB / 2); m_trigger_gc_memory = static_cast(std::max(expected, GiB)); m_critical_gc_memory = static_cast(std::max(critical, 2 * GiB)); } BufferCache::~BufferCache() { if (m_fault_readback->Active()) { EXIT("BufferCache: destroyed with an active fault readback\n"); } if (!m_gpu_modified_ranges.Empty()) { EXIT("BufferCache: destroyed with pending GPU-modified ranges\n"); } if (!m_pending_backing_publications.empty()) { EXIT("BufferCache: destroyed with pending backing publications\n"); } for (const auto& [vaddr, cached]: m_buffers) { (void)vaddr; if (m_memory_tracker.IsRegionGpuModified(cached->vaddr, cached->size)) { EXIT("BufferCache: destroyed with GPU-modified buffer\n"); } } m_buffers.clear(); } bool BufferCache::SynchronizeBacking(uint64_t vaddr, uint64_t size) { bool waited = false; for (;;) { uint64_t tick = 0; const auto page_begin = vaddr & ~(TRACKER_PAGE_SIZE - 1); const auto page_end = (vaddr + size + TRACKER_PAGE_SIZE - 1) & ~(TRACKER_PAGE_SIZE - 1); CacheRange affected {.address = page_begin, .size = page_end - page_begin}; { FaultSafeCacheLock lock(this, m_mutex); bool changed = true; while (changed) { changed = false; for (const auto& [address, cached]: m_buffers) { const CacheRange previous = affected; if (ResolveOverlap(affected, {address, cached->size}) && (previous.address != affected.address || previous.size != affected.size)) { changed = true; } } } } { std::lock_guard lock(m_publication_mutex); for (const auto& publication: m_pending_backing_publications) { if (publication.address < affected.address + affected.size && affected.address < publication.address + publication.size) { tick = std::max(tick, publication.tick); } } } if (tick == 0) { return waited; } waited = true; m_scheduler.Wait(tick); m_scheduler.WaitPriorityOperations(tick); } } void BufferCache::RefreshInvalidatedRanges(CommandBuffer& command, CachedBuffer& cached, uint64_t vaddr, uint64_t size, bool upload) { const auto invalidated = m_image_invalidated_ranges.Intersections(vaddr, size); if (upload) { std::array bytes; for (const auto& range: invalidated) { for (uint64_t copied = 0; copied < range.size;) { const auto chunk = std::min(range.size - copied, bytes.size()); if (!Libs::LibKernel::Memory::TryReadBacking(range.address + copied, bytes.data(), chunk)) { EXIT("BufferCache: failed to refresh an invalidated image alias\n"); } Upload(command, *cached.buffer, cached.buffer->Offset(range.address + copied), bytes.data(), chunk); copied += chunk; } } } if (!invalidated.empty()) { m_image_invalidated_ranges.Subtract(vaddr, size); } } StreamBuffer& BufferCache::GetUtilityBuffer(MemoryUsage usage) noexcept { switch (usage) { case MemoryUsage::Upload: return m_staging_buffer; case MemoryUsage::Stream: return m_stream_buffer; case MemoryUsage::Download: return m_download_buffer; case MemoryUsage::DeviceLocal: return m_device_buffer; } EXIT("BufferCache: invalid utility-buffer usage\n"); } BufferBinding BufferCache::UploadTransient(const void* data, uint64_t size, uint64_t alignment) { EXIT_IF(data == nullptr || size == 0); if (auto [mapped, offset] = m_stream_buffer.Map(size, alignment, false); mapped != nullptr) { std::memcpy(mapped, data, static_cast(size)); m_stream_buffer.Commit(); return {{}, m_stream_buffer.Handle(), offset}; } auto owner = std::make_shared(m_graphics, m_scheduler, MemoryUsage::Upload, 0, AllFlags, size); owner->Write(0, data, size); return {owner, owner->Handle(), 0}; } void BufferCache::InvalidateMemory(uint64_t vaddr, uint64_t size) { if (vaddr == 0 || size == 0 || vaddr >= TRACKER_ADDRESS_SIZE || size > TRACKER_ADDRESS_SIZE - vaddr) { EXIT("BufferCache: invalid memory-invalidation range\n"); } (void)SynchronizeBacking(vaddr, size); if (!HasPageOverlap(vaddr, size)) { return; } m_memory_tracker.InvalidateRegion(vaddr, size, [this, vaddr, size] { ReadMemory(vaddr, size); }); } void BufferCache::ReadMemory(uint64_t vaddr, uint64_t size) { (void)SynchronizeBacking(vaddr, size); std::vector copies; { FaultSafeCacheLock lock(this, m_mutex); m_memory_tracker.ForEachDownloadRange( vaddr, size, [&](uint64_t address, uint64_t bytes) noexcept { m_memory_tracker.ValidateGpuDirtyPages(m_gpu_modified_ranges, address, bytes, "memory invalidation"); }, [&](uint64_t address, uint64_t bytes) noexcept { for (const auto range: m_gpu_modified_ranges.Intersections(address, bytes)) { for (uint64_t copied = 0; copied < range.size;) { const auto copy_address = range.address + copied; auto owner = m_buffers.upper_bound(copy_address); if (owner == m_buffers.begin()) { EXIT("BufferCache: invalidation readback has no buffer owner\n"); } auto& cached = *std::prev(owner)->second; if (!cached.buffer->IsInBounds(copy_address, 1)) { EXIT( "BufferCache: invalidation readback is outside its buffer owner\n"); } const auto copy_size = std::min(range.size - copied, cached.vaddr + cached.size - copy_address); copies.push_back({cached.buffer, cached.buffer->Offset(copy_address), copy_address, copy_size}); copied += copy_size; } } }); } if (copies.empty()) { return; } auto downloads = RecordDownloads(copies); m_scheduler.FinishCurrent(); PublishDownloads(downloads); { FaultSafeCacheLock lock(this, m_mutex); m_memory_tracker.ForEachDownloadRange( vaddr, size, [&](uint64_t address, uint64_t bytes) noexcept { m_memory_tracker.ValidateGpuDirtyPages(m_gpu_modified_ranges, address, bytes, "memory invalidation completion"); }, [](uint64_t, uint64_t) noexcept {}); for (const auto& range: downloads) { m_gpu_modified_ranges.Subtract(range.address, range.size); } } } bool BufferCache::InvalidateMemory(PageFaultAccess access, uint64_t vaddr, uint64_t size, PageFaultPhase phase) noexcept { const auto page = vaddr & ~(TRACKER_PAGE_SIZE - 1); if (size == 0 || size > page + TRACKER_PAGE_SIZE - vaddr) { EXIT("BufferCache: invalid page-fault range\n"); } if (phase == PageFaultPhase::Complete) { FaultSafeCacheLock lock(this, m_mutex); auto& fault = *m_fault_readback; if (!fault.Active()) { return m_memory_tracker.CompleteCpuFault(vaddr, size, access, false); } if (fault.access != access || fault.vaddr != vaddr || fault.size != size || fault.installed) { EXIT("BufferCache: mismatched fault readback completion\n"); } PublishDownloads(fault.ranges); if (!m_memory_tracker.CompleteCpuFault(vaddr, size, access, true)) { EXIT("BufferCache: failed to complete downloaded CPU fault\n"); } fault.installed = true; return true; } if (phase == PageFaultPhase::Release) { FaultSafeCacheLock lock(this, m_mutex); auto& fault = *m_fault_readback; if (fault.Active()) { if (fault.access != access || fault.vaddr != vaddr || fault.size != size || !fault.installed) { EXIT("BufferCache: mismatched fault readback release\n"); } for (const auto& range: fault.ranges) { m_gpu_modified_ranges.Subtract(range.address, range.size); } fault.Reset(); } return true; } if (phase != PageFaultPhase::Invalidate) { EXIT("BufferCache: unsupported page-fault phase\n"); } const auto action = m_memory_tracker.BeginCpuFault(vaddr, size, access); if (action != CpuFaultAction::Download) { return action == CpuFaultAction::Continue; } auto& fault = *m_fault_readback; std::vector copies; { FaultSafeCacheLock lock(this, m_mutex); if (fault.Active()) { EXIT("BufferCache: nested fault readback\n"); } fault.access = access; fault.vaddr = vaddr; fault.size = size; m_gpu_modified_ranges.ForEachIntersection( page, TRACKER_PAGE_SIZE, [&](RangeSet::Range range) { auto owner = m_buffers.upper_bound(range.address); if (owner == m_buffers.begin()) { EXIT("BufferCache: fault readback has no buffer owner\n"); } --owner; auto& cached = *owner->second; if (!cached.buffer->IsInBounds(range.address, range.size)) { EXIT("BufferCache: fault readback is outside its buffer owner\n"); } copies.push_back({cached.buffer, cached.buffer->Offset(range.address), range.address, range.size}); }); if (copies.empty()) { EXIT("BufferCache: GPU-dirty fault page has no dirty byte ranges\n"); } } fault.ranges = RecordDownloads(copies); if (!fault.Active()) { EXIT("BufferCache: GPU-dirty fault page has no dirty byte ranges\n"); } m_scheduler.FinishCurrent(); return true; } void BufferCache::UnmapMemory(uint64_t vaddr, uint64_t size) { if (vaddr == 0 || size == 0 || size > UINT64_MAX - vaddr) { EXIT("BufferCache: invalid unmap range\n"); } (void)SynchronizeBacking(vaddr, size); std::vector copies; std::vector dirty_ranges; std::vector> modified_buffers; std::vector> backing_writes; std::vector> retired_buffers; { FaultSafeCacheLock lock(this, m_mutex); for (const auto& [begin, cached]: m_buffers) { if (vaddr < begin + cached->size && begin < vaddr + size) { retired_buffers.emplace_back(begin, cached->size); } } for (const auto& [begin, cached]: m_buffers) { if (vaddr >= begin + cached->size || begin >= vaddr + size || !m_memory_tracker.IsRegionGpuModified(begin, cached->size)) { continue; } const auto dirty = m_gpu_modified_ranges.Intersections(begin, cached->size); if (dirty.empty()) { EXIT("BufferCache: GPU-modified buffer has no dirty ranges\n"); } dirty_ranges.insert(dirty_ranges.end(), dirty.begin(), dirty.end()); modified_buffers.emplace_back(begin, cached->size); } if (!dirty_ranges.empty()) { backing_writes = m_page_manager.ReserveBackingWrites(dirty_ranges); } for (const auto& [begin, bytes]: modified_buffers) { auto owner = m_buffers.find(begin); if (owner == m_buffers.end() || owner->second->size != bytes) { EXIT("BufferCache: unmap owner changed during collection\n"); } auto& cached = *owner->second; m_memory_tracker.ForEachDownloadRange( begin, cached.size, [&](uint64_t address, uint64_t bytes) noexcept { m_memory_tracker.ValidateGpuDirtyPages(m_gpu_modified_ranges, address, bytes, "unmap"); }, [&](uint64_t address, uint64_t bytes) noexcept { for (const auto& range: m_gpu_modified_ranges.Intersections(address, bytes)) { copies.push_back( {cached.buffer, range.address - begin, range.address, range.size}); } }); } } if (!copies.empty()) { auto downloads = RecordDownloads(copies); m_scheduler.FinishCurrent(); PublishDownloads(downloads); } else if (!retired_buffers.empty()) { // Image uploads can reference a clean cached buffer without owning it. Submit the active // command stream before removing such backing. m_scheduler.FinishCurrent(); } backing_writes.clear(); { FaultSafeCacheLock lock(this, m_mutex); for (const auto& [begin, bytes]: modified_buffers) { if (!m_memory_tracker.IsRegionGpuModified(begin, bytes)) { continue; } m_memory_tracker.ForEachDownloadRange( begin, bytes, [&](uint64_t address, uint64_t download_size) noexcept { m_memory_tracker.ValidateGpuDirtyPages(m_gpu_modified_ranges, address, download_size, "unmap retirement"); }, [](uint64_t, uint64_t) noexcept {}); } for (const auto& [begin, bytes]: modified_buffers) { m_gpu_modified_ranges.Subtract(begin, bytes); } for (const auto& [begin, bytes]: retired_buffers) { m_memory_tracker.MarkRegionAsCpuModified(begin, bytes); } if (!m_gpu_modified_ranges.Intersections(vaddr, size).empty()) { EXIT("BufferCache: unmap retained dirty byte ranges\n"); } m_image_invalidated_ranges.Subtract(vaddr, size); m_memory_tracker.UntrackMemory(vaddr, size); for (auto it = m_buffers.begin(); it != m_buffers.end();) { if (vaddr < it->first + it->second->size && it->first < vaddr + size) { if (it->second->size > m_total_used_memory) { EXIT("BufferCache: allocation accounting underflow\n"); } m_total_used_memory -= it->second->size; it = m_buffers.erase(it); } else { ++it; } } } } BufferCache::CachedBuffer& BufferCache::GetOrCreateBuffer(CommandBuffer& command, uint64_t vaddr, uint64_t size) { const auto begin = AlignDown(vaddr); const auto end = AlignUp(vaddr + size); auto it = m_buffers.upper_bound(vaddr); if (it != m_buffers.begin()) { auto previous = std::prev(it); if (previous->second->buffer->IsInBounds(vaddr, size)) { it = previous; } } if (it != m_buffers.end() && it->second->buffer->IsInBounds(vaddr, size)) { it->second->tick_accessed_last = m_gc_tick; return *it->second; } CacheRange merged {.address = begin, .size = end - begin}; using Iterator = decltype(m_buffers.begin()); std::vector overlaps; auto first = m_buffers.lower_bound(begin); if (first != m_buffers.begin()) { auto previous = std::prev(first); if (ResolveOverlap(merged, {previous->second->vaddr, previous->second->size})) { first = previous; } } for (auto candidate = first; candidate != m_buffers.end(); ++candidate) { if (candidate->first >= merged.address + merged.size) { break; } if (ResolveOverlap(merged, {candidate->second->vaddr, candidate->second->size})) { overlaps.push_back(candidate); } } for (const auto overlap: overlaps) { auto& old = *overlap->second; std::vector> uploads; m_memory_tracker.ForEachUploadRange( old.vaddr, old.size, false, [&](uint64_t address, uint64_t bytes) noexcept { uploads.emplace_back(address, bytes); }, [&]() noexcept { for (const auto& [address, bytes]: uploads) { Upload(command, *old.buffer, old.buffer->Offset(address), reinterpret_cast(address), bytes); } }); } auto cached = std::make_unique(); cached->vaddr = merged.address; cached->size = merged.size; cached->tick_accessed_last = m_gc_tick; cached->buffer = std::make_shared(m_graphics, m_scheduler, MemoryUsage::DeviceLocal, merged.address, AllFlags, merged.size); for (const auto overlap: overlaps) { const auto& old = *overlap->second; cached->buffer->CopyFrom(command, *old.buffer, 0, old.vaddr - cached->vaddr, old.size); command.RetainResourceUntilFence(old.buffer); } for (const auto overlap: overlaps) { if (overlap->second->size > m_total_used_memory) { EXIT("BufferCache: allocation accounting underflow\n"); } m_total_used_memory -= overlap->second->size; m_buffers.erase(overlap); } m_total_used_memory += cached->size; return *m_buffers.emplace(cached->vaddr, std::move(cached)).first->second; } BufferBinding BufferCache::ObtainBuffer(CommandBuffer& command, uint64_t vaddr, uint64_t size, bool is_written, bool is_read, bool is_formatted) { if (command.IsInvalid() || command.IsExecute()) { EXIT("BufferCache: buffer request requires a recording command buffer\n"); } std::lock_guard transaction(m_resource_mutex); (void)SynchronizeBacking(vaddr, size); if (is_read && !is_written && size <= CACHING_PAGE_SIZE && !m_memory_tracker.IsRegionGpuModified(vaddr, size) && m_memory_tracker.IsRegionCpuModified(vaddr, size)) { std::vector data(size); if (Libs::LibKernel::Memory::TryReadBacking(vaddr, data.data(), size)) { return UploadTransient(data.data(), size, 16); } } if (is_formatted && is_read && !is_written) { (void)m_texture_cache.SynchronizeImageToBuffer(vaddr, size); } else if (is_formatted && is_written) { (void)m_texture_cache.InvalidateMemoryFromGPU(vaddr, size, true); } FaultSafeCacheLock lock(this, m_mutex); auto& cached = GetOrCreateBuffer(command, vaddr, size); std::vector> uploads; m_memory_tracker.ForEachUploadRange( vaddr, size, is_written, [&](uint64_t address, uint64_t bytes) noexcept { uploads.emplace_back(address, bytes); }, [&]() noexcept { for (const auto& [address, bytes]: uploads) { Upload(command, *cached.buffer, cached.buffer->Offset(address), reinterpret_cast(address), bytes); } }); RefreshInvalidatedRanges(command, cached, vaddr, size, is_read); if (is_written) { m_gpu_modified_ranges.Add(vaddr, size); } return {cached.buffer, cached.buffer->Handle(), cached.buffer->Offset(vaddr)}; } std::shared_ptr BufferCache::ObtainNullBuffer() { std::shared_ptr buffer; { FaultSafeCacheLock lock(this, m_mutex); if (m_null_buffer != nullptr) { return m_null_buffer; } m_null_buffer = std::make_shared(m_graphics, m_scheduler, MemoryUsage::DeviceLocal, 0, AllFlags, 16); buffer = m_null_buffer; } const std::array zeros {}; Upload(m_scheduler.Current(), *buffer, 0, zeros.data(), zeros.size()); return buffer; } ImageBufferSource BufferCache::ObtainBufferForImage(uint64_t vaddr, uint64_t size) { if (vaddr == 0 || size == 0 || vaddr >= TRACKER_ADDRESS_SIZE || size > TRACKER_ADDRESS_SIZE - vaddr) { EXIT("BufferCache: invalid image source\n"); } (void)SynchronizeBacking(vaddr, size); auto find_owner = [&]() { auto owner = m_buffers.upper_bound(vaddr); if (owner == m_buffers.begin()) { return m_buffers.end(); } --owner; return owner->second->buffer->IsInBounds(vaddr, size) ? owner : m_buffers.end(); }; { FaultSafeCacheLock lock(this, m_mutex); const bool cpu_modified = m_memory_tracker.IsRegionCpuModified(vaddr, size); const bool gpu_modified = m_memory_tracker.IsRegionGpuModified(vaddr, size); const auto dirty = m_gpu_modified_ranges.Intersections(vaddr, size); const bool invalidated = !m_image_invalidated_ranges.Intersections(vaddr, size).empty(); const bool requested_gpu_owned = !dirty.empty(); m_memory_tracker.ValidateGpuDirtyOwnership(m_gpu_modified_ranges, vaddr, size, "image source"); auto owner = find_owner(); if (requested_gpu_owned && owner == m_buffers.end()) { CacheRange merged {.address = AlignDown(vaddr), .size = AlignUp(vaddr + size) - AlignDown(vaddr)}; using Iterator = decltype(m_buffers.begin()); std::vector overlaps; auto first = m_buffers.lower_bound(merged.address); if (first != m_buffers.begin()) { auto previous = std::prev(first); if (ResolveOverlap(merged, {previous->second->vaddr, previous->second->size})) { first = previous; } } for (auto candidate = first; candidate != m_buffers.end(); ++candidate) { if (candidate->first >= merged.address + merged.size) { break; } if (ResolveOverlap(merged, {candidate->second->vaddr, candidate->second->size})) { overlaps.push_back(candidate); } } if (overlaps.empty()) { EXIT("BufferCache: GPU-dirty image source has no native buffer\n"); } auto cached = std::make_unique(); cached->vaddr = merged.address; cached->size = merged.size; cached->tick_accessed_last = m_gc_tick; cached->buffer = std::make_shared(m_graphics, m_scheduler, MemoryUsage::DeviceLocal, merged.address, AllFlags, merged.size); for (const auto overlap: overlaps) { const auto& old = *overlap->second; cached->buffer->CopyFrom(m_scheduler.Current(), *old.buffer, 0, old.vaddr - cached->vaddr, old.size); m_scheduler.Current().RetainResourceUntilFence(old.buffer); } for (const auto overlap: overlaps) { if (overlap->second->size > m_total_used_memory) { EXIT("BufferCache: allocation accounting underflow\n"); } m_total_used_memory -= overlap->second->size; m_buffers.erase(overlap); } m_total_used_memory += cached->size; owner = m_buffers.emplace(cached->vaddr, std::move(cached)).first; if (!owner->second->buffer->IsInBounds(vaddr, size)) { EXIT("BufferCache: merged image source does not contain the requested range\n"); } } if (owner != m_buffers.end() && !cpu_modified && !invalidated && (!gpu_modified || requested_gpu_owned)) { DiscardGpuDirtyBytesLocked(vaddr, size, "image source transfer"); owner->second->tick_accessed_last = m_gc_tick; return {owner->second->buffer.get(), owner->second->buffer->Offset(vaddr), requested_gpu_owned}; } if (requested_gpu_owned && owner == m_buffers.end()) { EXIT("BufferCache: GPU-dirty image source could not resolve its native owner\n"); } } // Direct-memory backing remains readable while PageManager protects the guest mapping. The // fallback exists for plain host mappings used by standalone renderer tests and is deliberately // performed outside the cache lock so a page fault cannot recurse into BufferCache. const auto stage_address = vaddr & ~(TRACKER_PAGE_SIZE - 1); const auto stage_end = (vaddr + size + TRACKER_PAGE_SIZE - 1) & ~(TRACKER_PAGE_SIZE - 1); const auto stage_size = stage_end - stage_address; (void)SynchronizeBacking(stage_address, stage_size); std::vector bytes(stage_size); if (!Libs::LibKernel::Memory::TryReadBacking(stage_address, bytes.data(), stage_size)) { EXIT("BufferCache: failed to read mapped guest image backing\n"); } FaultSafeCacheLock lock(this, m_mutex); const auto dirty = m_gpu_modified_ranges.Intersections(vaddr, size); const bool invalidated = !m_image_invalidated_ranges.Intersections(vaddr, size).empty(); const bool requested_gpu_owned = !dirty.empty(); auto owner = find_owner(); if (requested_gpu_owned && owner == m_buffers.end()) { EXIT("BufferCache: GPU-dirty image source lost its native owner\n"); } const auto stage_offset = m_staging_buffer.Copy(bytes.data(), stage_size, 16); if (owner == m_buffers.end() || invalidated || (m_memory_tracker.IsRegionGpuModified(vaddr, size) && !requested_gpu_owned)) { return {&m_staging_buffer, stage_offset + vaddr - stage_address, false}; } auto& cached = *owner->second; cached.tick_accessed_last = m_gc_tick; std::vector> uploads; m_memory_tracker.ForEachUploadRange( vaddr, size, false, [&](uint64_t address, uint64_t upload_size) noexcept { uploads.emplace_back(address, upload_size); }, [&]() noexcept { for (const auto& [address, upload_size]: uploads) { cached.buffer->CopyFrom( m_scheduler.Current(), m_staging_buffer, stage_offset + address - stage_address, cached.buffer->Offset(address), upload_size, vk::AccessFlagBits::eHostWrite); } }); DiscardGpuDirtyBytesLocked(vaddr, size, "staged image source transfer"); return {cached.buffer.get(), cached.buffer->Offset(vaddr), requested_gpu_owned}; } void BufferCache::DiscardGpuDirtyBytesLocked(uint64_t vaddr, uint64_t size, const char* operation) { m_memory_tracker.ValidateGpuDirtyOwnership(m_gpu_modified_ranges, vaddr, size, operation); m_gpu_modified_ranges.Subtract(vaddr, size); const auto page_begin = vaddr & ~(TRACKER_PAGE_SIZE - 1); const auto page_end = (vaddr + size + TRACKER_PAGE_SIZE - 1) & ~(TRACKER_PAGE_SIZE - 1); for (auto page = page_begin; page < page_end; page += TRACKER_PAGE_SIZE) { if (m_gpu_modified_ranges.Intersections(page, TRACKER_PAGE_SIZE).empty() && m_memory_tracker.IsRegionGpuModified(page, TRACKER_PAGE_SIZE)) { m_memory_tracker.UnmarkRegionAsGpuModified(page, TRACKER_PAGE_SIZE); } } m_memory_tracker.ValidateGpuDirtyOwnership(m_gpu_modified_ranges, vaddr, size, operation); } void BufferCache::DiscardGpuDirtyBytes(uint64_t vaddr, uint64_t size) { if (vaddr == 0 || size == 0 || vaddr >= TRACKER_ADDRESS_SIZE || size > TRACKER_ADDRESS_SIZE - vaddr) { EXIT("BufferCache: invalid dirty-byte discard range\n"); } FaultSafeCacheLock lock(this, m_mutex); DiscardGpuDirtyBytesLocked(vaddr, size, "image output supersession"); } void BufferCache::WriteHostMemory(uint64_t vaddr, std::span data) { if (vaddr == 0 || data.empty() || data.size() > UINT64_MAX - vaddr) { EXIT("BufferCache: invalid host DMA write\n"); } (void)SynchronizeBacking(vaddr, data.size()); Libs::LibKernel::Memory::WriteBacking(vaddr, data.data(), data.size()); FaultSafeCacheLock lock(this, m_mutex); const auto end = vaddr + data.size(); for (auto& [address, cached]: m_buffers) { const auto cached_end = address + cached->size; const auto begin = std::max(vaddr, address); const auto range_end = std::min(end, cached_end); if (begin >= range_end) { continue; } Upload(m_scheduler.Current(), *cached->buffer, cached->buffer->Offset(begin), data.data() + begin - vaddr, range_end - begin); cached->tick_accessed_last = m_gc_tick; } m_image_invalidated_ranges.Subtract(vaddr, data.size()); } std::pair, uint64_t> BufferCache::ObtainBufferForImageWrite(uint64_t vaddr, uint64_t size) { if (vaddr == 0 || size == 0 || vaddr >= TRACKER_ADDRESS_SIZE || size > TRACKER_ADDRESS_SIZE - vaddr) { EXIT("BufferCache: invalid image destination\n"); } const auto stage_address = vaddr & ~(TRACKER_PAGE_SIZE - 1); const auto stage_end = (vaddr + size + TRACKER_PAGE_SIZE - 1) & ~(TRACKER_PAGE_SIZE - 1); const auto stage_size = stage_end - stage_address; (void)SynchronizeBacking(stage_address, stage_size); std::vector bytes(stage_size); if (!Libs::LibKernel::Memory::TryReadBacking(stage_address, bytes.data(), stage_size)) { EXIT("BufferCache: failed to preserve guest bytes around an image mirror\n"); } FaultSafeCacheLock lock(this, m_mutex); auto& cached = GetOrCreateBuffer(m_scheduler.Current(), vaddr, size); m_memory_tracker.ValidateGpuDirtyOwnership(m_gpu_modified_ranges, vaddr, size, "image destination"); if (!m_gpu_modified_ranges.Intersections(vaddr, size).empty()) { EXIT("BufferCache: image destination aliases GPU-owned buffer bytes\n"); } const auto stage_offset = m_staging_buffer.Copy(bytes.data(), stage_size, 16); std::vector> uploads; m_memory_tracker.ForEachUploadRange( vaddr, size, false, [&](uint64_t address, uint64_t upload_size) noexcept { uploads.emplace_back(address, upload_size); }, [&]() noexcept { for (const auto& [address, upload_size]: uploads) { cached.buffer->CopyFrom( m_scheduler.Current(), m_staging_buffer, stage_offset + address - stage_address, cached.buffer->Offset(address), upload_size, vk::AccessFlagBits::eHostWrite); } }); return {cached.buffer, cached.buffer->Offset(vaddr)}; } void BufferCache::FillBuffer(uint64_t vaddr, uint64_t size, uint32_t value, bool is_gds) { if ((vaddr & 3u) != 0 || size == 0 || (size & 3u) != 0 || size > UINT64_MAX - vaddr) { EXIT("BufferCache: fill range must be dword aligned\n"); } if (is_gds) { if (vaddr > m_gds_buffer.Size() || size > m_gds_buffer.Size() - vaddr) { EXIT("BufferCache: GDS fill range is out of bounds\n"); } m_gds_buffer.Fill(vaddr, size, value); return; } if (vaddr == 0) { EXIT("BufferCache: invalid fill memory address\n"); } (void)m_texture_cache.ClearMeta(vaddr); { std::lock_guard transaction(m_resource_mutex); const auto region = m_texture_cache.QueryRegion(vaddr, size); if (!HasGpuDirtyBytes(vaddr, size) && !region.gpu_image_bytes) { if (region.image_bytes) { m_texture_cache.InvalidateMemory(vaddr, size); } std::array values; values.fill(value); const std::span bytes {reinterpret_cast(values.data()), sizeof(values)}; for (uint64_t offset = 0; offset < size;) { const auto chunk = std::min(size - offset, bytes.size()); WriteHostMemory(vaddr + offset, bytes.first(chunk)); offset += chunk; } return; } } auto& command = m_scheduler.Current(); auto dst = ObtainBuffer(command, vaddr, size, true, false, true); EXIT_IF(dst.buffer == nullptr || dst.owner == nullptr); command.RetainResourceUntilFence(dst.owner); auto owner = std::static_pointer_cast(dst.owner); owner->Fill(dst.offset, size, value); } void BufferCache::CopyBuffer(uint64_t dst_vaddr, uint64_t src_vaddr, uint64_t size, bool dst_gds, bool src_gds) { const bool dst_memory = !dst_gds; const bool src_memory = !src_gds; if ((dst_memory && dst_vaddr == 0) || (src_memory && src_vaddr == 0) || size == 0 || ((dst_vaddr | src_vaddr | size) & 3u) != 0 || size > UINT64_MAX - dst_vaddr || size > UINT64_MAX - src_vaddr || (dst_gds && src_gds) || (dst_gds == src_gds && src_vaddr < dst_vaddr + size && dst_vaddr < src_vaddr + size) || (dst_gds && (dst_vaddr > m_gds_buffer.Size() || size > m_gds_buffer.Size() - dst_vaddr)) || (src_gds && (src_vaddr > m_gds_buffer.Size() || size > m_gds_buffer.Size() - src_vaddr))) { EXIT("BufferCache: invalid or overlapping copy range\n"); } if (src_memory || dst_memory) { std::lock_guard transaction(m_resource_mutex); if (src_memory) { (void)SynchronizeBacking(src_vaddr, size); } const auto src_region = src_memory ? m_texture_cache.QueryRegion(src_vaddr, size) : TextureCache::RegionInfo {}; const auto dst_region = dst_memory ? m_texture_cache.QueryRegion(dst_vaddr, size) : TextureCache::RegionInfo {}; if (src_memory && src_region.gpu_image_bytes && !m_texture_cache.SynchronizeImageToBuffer(src_vaddr, size)) { EXIT("BufferCache: GPU copy source image could not be synchronized\n"); } if (src_memory && dst_memory && !HasGpuDirtyBytes(src_vaddr, size) && !HasGpuDirtyBytes(dst_vaddr, size) && !src_region.gpu_image_bytes && !dst_region.gpu_image_bytes) { if (dst_region.image_bytes) { m_texture_cache.InvalidateMemory(dst_vaddr, size); } std::array bytes; for (uint64_t offset = 0; offset < size;) { const auto chunk = std::min(size - offset, bytes.size()); if (!Libs::LibKernel::Memory::TryReadBacking(src_vaddr + offset, bytes.data(), chunk)) { EXIT("BufferCache: host DMA source has no direct backing\n"); } WriteHostMemory(dst_vaddr + offset, std::span {bytes}.first(chunk)); offset += chunk; } return; } } auto& command = m_scheduler.Current(); auto src = src_memory ? ObtainBuffer(command, src_vaddr, size, false, true) : BufferBinding {.buffer = m_gds_buffer.Handle(), .offset = src_vaddr}; auto dst = dst_memory ? ObtainBuffer(command, dst_vaddr, size, true, false, true) : BufferBinding {.buffer = m_gds_buffer.Handle(), .offset = dst_vaddr}; EXIT_IF(src.buffer == nullptr || dst.buffer == nullptr || (dst_memory && dst.owner == nullptr)); if (src.owner != nullptr) { command.RetainResourceUntilFence(src.owner); } if (dst.owner != nullptr) { command.RetainResourceUntilFence(dst.owner); } if (src.buffer == dst.buffer && src.offset < dst.offset + size && dst.offset < src.offset + size) { EXIT("BufferCache: resolved Vulkan copy ranges overlap\n"); } auto& source = src.owner != nullptr ? *std::static_pointer_cast(src.owner) : src_gds ? m_gds_buffer : m_stream_buffer; auto& destination = dst.owner != nullptr ? *std::static_pointer_cast(dst.owner) : m_gds_buffer; if (source.Handle() != src.buffer || destination.Handle() != dst.buffer) { EXIT("BufferCache: resolved copy owner does not match its Vulkan handle\n"); } destination.CopyFrom(command, source, src.offset, dst.offset, size); } bool BufferCache::HasPageOverlap(uint64_t vaddr, uint64_t size) { if (vaddr == 0 || size == 0 || vaddr >= TRACKER_ADDRESS_SIZE || size > TRACKER_ADDRESS_SIZE - vaddr) { EXIT("BufferCache: invalid page-overlap query\n"); } FaultSafeCacheLock lock(this, m_mutex); for (const auto& [address, cached]: m_buffers) { if (PageOverlaps(vaddr, size, address, cached->size)) { return true; } } return false; } bool BufferCache::IsRegionGpuModified(uint64_t vaddr, uint64_t size) { return m_memory_tracker.IsRegionGpuModified(vaddr, size); } bool BufferCache::HasGpuDirtyBytes(uint64_t vaddr, uint64_t size) { FaultSafeCacheLock lock(this, m_mutex); return !m_gpu_modified_ranges.Intersections(vaddr, size).empty(); } bool BufferCache::IsRegionCpuModified(uint64_t vaddr, uint64_t size) { return m_memory_tracker.IsRegionCpuModified(vaddr, size); } void BufferCache::InvalidateImageAliases(uint64_t vaddr, uint64_t size) { if (vaddr == 0 || size == 0 || vaddr >= TRACKER_ADDRESS_SIZE || size > TRACKER_ADDRESS_SIZE - vaddr) { EXIT("BufferCache: invalid image-alias invalidation\n"); } FaultSafeCacheLock lock(this, m_mutex); const auto end = vaddr + size; for (const auto& [address, cached]: m_buffers) { const auto cached_end = address + cached->size; const auto begin = std::max(vaddr, address); const auto range_end = std::min(end, cached_end); if (begin >= range_end) { continue; } const auto bytes = range_end - begin; if (!m_gpu_modified_ranges.Intersections(begin, bytes).empty()) { EXIT("BufferCache: image ownership overlaps exact dirty buffer bytes\n"); } m_image_invalidated_ranges.Add(begin, bytes); } } void BufferCache::BeginBackingPublication(uint64_t vaddr, uint64_t size, uint64_t tick) { if (vaddr == 0 || size == 0 || tick == 0 || vaddr >= TRACKER_ADDRESS_SIZE || size > TRACKER_ADDRESS_SIZE - vaddr) { EXIT("BufferCache: invalid pending backing publication\n"); } std::lock_guard lock(m_publication_mutex); m_pending_backing_publications.push_back({vaddr, size, tick}); } void BufferCache::CompleteBackingPublication(uint64_t vaddr, uint64_t size, uint64_t tick) { std::lock_guard lock(m_publication_mutex); const auto publication = std::ranges::find_if(m_pending_backing_publications, [&](const auto& pending) { return pending.address == vaddr && pending.size == size && pending.tick == tick; }); if (publication == m_pending_backing_publications.end()) { EXIT("BufferCache: completed an unknown backing publication\n"); } m_pending_backing_publications.erase(publication); } void BufferCache::PublishImageBuffer(uint64_t vaddr, uint64_t size) { FaultSafeCacheLock lock(this, m_mutex); auto owner = m_buffers.end(); for (auto it = m_buffers.begin(); it != m_buffers.end(); ++it) { if (!PageOverlaps(vaddr, size, it->second->vaddr, it->second->size)) { continue; } if (owner != m_buffers.end() || !it->second->buffer->IsInBounds(vaddr, size)) { EXIT("BufferCache: image destination aliases a non-containing cached buffer\n"); } owner = it; } m_memory_tracker.ValidateGpuDirtyOwnership(m_gpu_modified_ranges, vaddr, size, "image destination publication"); if (owner == m_buffers.end() || m_memory_tracker.IsRegionCpuModified(vaddr, size) || !m_gpu_modified_ranges.Intersections(vaddr, size).empty()) { EXIT("BufferCache: image destination requires clean buffer ownership\n"); } m_memory_tracker.MarkRegionAsGpuModified(vaddr, size); m_gpu_modified_ranges.Add(vaddr, size); m_image_invalidated_ranges.Subtract(vaddr, size); m_memory_tracker.ValidateGpuDirtyOwnership(m_gpu_modified_ranges, vaddr, size, "published image destination"); owner->second->tick_accessed_last = m_gc_tick; } void BufferCache::RunGarbageCollector() { std::lock_guard transaction(m_resource_mutex); const auto tick = m_gc_tick++; if (m_graphics.CanReportMemoryUsage()) { m_total_used_memory = m_graphics.GetDeviceMemoryUsage(); } if (m_total_used_memory < m_trigger_gc_memory || m_fault_readback->Active()) { return; } const bool aggressive = m_total_used_memory >= m_critical_gc_memory; const uint64_t age = std::min(aggressive ? 80 : 160, tick); const size_t limit = aggressive ? 64 : 32; std::vector retires; std::vector>> dirty_retires; { FaultSafeCacheLock lock(this, m_mutex); std::vector candidates; for (const auto& [address, owner]: m_buffers) { const auto& cached = *owner; if (tick - std::min(tick, cached.tick_accessed_last) < age) { continue; } candidates.push_back(address); } std::ranges::sort(candidates, [&](uint64_t left, uint64_t right) { return m_buffers.at(left)->tick_accessed_last < m_buffers.at(right)->tick_accessed_last; }); if (candidates.size() > limit) { candidates.resize(limit); } for (const auto address: candidates) { auto& cached = *m_buffers.at(address); m_memory_tracker.ValidateGpuDirtyOwnership(m_gpu_modified_ranges, cached.vaddr, cached.size, "garbage collection"); retires.push_back({address, cached.size, cached.buffer}); // GC runs immediately before submission. Preserve every source referenced by commands // already recorded in the active batch. m_scheduler.Current().RetainResourceUntilFence(cached.buffer); } for (const auto& retire: retires) { if (!m_memory_tracker.IsRegionGpuModified(retire.address, retire.size)) { continue; } auto& copies = dirty_retires.emplace_back(retire, std::vector {}).second; m_memory_tracker.ForEachDownloadRange( retire.address, retire.size, [&](uint64_t address, uint64_t size) noexcept { m_memory_tracker.ValidateGpuDirtyPages(m_gpu_modified_ranges, address, size, "garbage collection"); }, [&](uint64_t address, uint64_t size) noexcept { for (const auto range: m_gpu_modified_ranges.Intersections(address, size)) { copies.push_back({retire.owner, range.address - retire.address, range.address, range.size}); } }); } } for (auto& [retire, copies]: dirty_retires) { QueueGarbageDownload(copies, std::move(retire)); } FaultSafeCacheLock lock(this, m_mutex); for (const auto& retire: retires) { auto found = m_buffers.find(retire.address); if (found == m_buffers.end() || found->second->size != retire.size || found->second->buffer != retire.owner) { EXIT("BufferCache: garbage-collection owner changed during download\n"); } if (!m_memory_tracker.IsRegionGpuModified(retire.address, retire.size)) { m_memory_tracker.UntrackMemory(retire.address, retire.size); } m_image_invalidated_ranges.Subtract(retire.address, retire.size); if (retire.size > m_total_used_memory) { EXIT("BufferCache: allocation accounting underflow\n"); } m_total_used_memory -= retire.size; m_buffers.erase(found); } } } // namespace Libs::Graphics