#include "graphics/host_gpu/renderer/cache/bufferCache.h" #include "common/assert.h" #include "common/logging/log.h" #include "common/profiler.h" #include "graphics/guest_gpu/graphicsRun.h" #include "graphics/host_gpu/graphicContext.h" #include "graphics/host_gpu/renderer/cache/textureCache.h" #include "graphics/host_gpu/renderer/commandScheduler.h" #include "graphics/host_gpu/renderer/render.h" #include "graphics/host_gpu/renderer/renderContext.h" #include "graphics/host_gpu/vulkanCommon.h" #include "kernel/memory.h" #include #include #include #include #include #include #include namespace Libs::Graphics { namespace { constexpr uint64_t MiB = 1024 * 1024; constexpr uint64_t GdsBufferSize = 64 * 1024; } // namespace void BufferCache::WriteDataBuffer(Buffer& buffer, uint64_t address, 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 offset = m_staging_buffer.Copy(bytes, chunk, 4); buffer.CopyFrom(m_scheduler.Current(), m_staging_buffer, offset, buffer.Offset(address), chunk, vk::AccessFlagBits::eHostWrite); bytes += chunk; address += chunk; size -= chunk; } } struct BufferCache::DownloadCopy { Buffer* buffer = nullptr; uint64_t source_offset = 0; uint64_t address = 0; uint64_t size = 0; }; void BufferCache::Register(BufferId id) { const auto& buffer = m_slot_buffers[id]; PageTable::PageRange pages {}; EXIT_IF(!PageTable::TryGetPageRange(buffer.CpuAddress(), buffer.Size(), pages)); for (size_t page = pages.first; page < pages.last_exclusive; ++page) { m_page_table[page] = id; } const auto [it, inserted] = m_buffers.emplace(buffer.CpuAddress(), id); (void)it; EXIT_IF(!inserted); m_total_used_memory += buffer.Size(); } void BufferCache::Unregister(BufferId id) { auto& buffer = m_slot_buffers[id]; if (buffer.is_deleted) { return; } PageTable::PageRange pages {}; EXIT_IF(!PageTable::TryGetPageRange(buffer.CpuAddress(), buffer.Size(), pages)); for (size_t page = pages.first; page < pages.last_exclusive; ++page) { auto* owner = m_page_table.Find(page); if (owner != nullptr && *owner == id) { *owner = {}; } } const auto found = m_buffers.find(buffer.CpuAddress()); EXIT_IF(found == m_buffers.end() || found->second != id); m_buffers.erase(found); if (buffer.Size() > m_total_used_memory) { EXIT("BufferCache: allocation accounting underflow\n"); } m_total_used_memory -= buffer.Size(); buffer.is_deleted = true; } void BufferCache::DeleteBuffer(BufferId id) { auto* buffer = m_slot_buffers.try_get(id); if (buffer == nullptr || buffer->is_deleted) { return; } Unregister(id); if (m_scheduler.Active()) { m_scheduler.DeferOperation([this, id] { m_slot_buffers.erase(id); }); } else { m_slot_buffers.erase(id); } } std::pair BufferCache::DownloadEnvelope(const DownloadCopy& copy) { if (copy.buffer == nullptr || copy.size == 0 || copy.source_offset > copy.buffer->Size() || copy.size > copy.buffer->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.buffer->Size()) { EXIT("BufferCache: aligned download copy exceeds its owner\n"); } return {begin, end - begin}; } void BufferCache::DownloadBufferMemory(std::span copies) { std::vector batch; batch.reserve(copies.size()); uint64_t packed_size = 0; auto& download = m_download_buffer; const auto flush = [&] { const auto [mapped, base_offset] = download.Map(packed_size, DOWNLOAD_ALIGNMENT); EXIT_IF(mapped == nullptr); uint64_t cursor = 0; for (const auto& copy: batch) { const auto [source_begin, envelope_size] = DownloadEnvelope(copy); download.CopyFrom(m_scheduler.Current(), *copy.buffer, source_begin, base_offset + cursor, envelope_size, vk::AccessFlagBits::eMemoryWrite, vk::AccessFlags {}, vk::AccessFlagBits::eMemoryRead | vk::AccessFlagBits::eMemoryWrite, vk::AccessFlagBits::eHostRead); cursor += AlignDownload(envelope_size); } download.Commit(); const auto completion_tick = m_scheduler.CurrentTick(); m_scheduler.FinishCurrent(); m_scheduler.WaitPriorityOperations(completion_tick); cursor = 0; for (const auto& copy: batch) { const auto [source_begin, envelope_size] = DownloadEnvelope(copy); const auto offset = cursor + copy.source_offset - source_begin; download.Invalidate(base_offset + offset, copy.size); Libs::LibKernel::Memory::WriteBacking(copy.address, mapped + offset, copy.size); cursor += AlignDownload(envelope_size); } batch.clear(); packed_size = 0; }; for (auto copy: copies) { while (copy.size != 0) { const auto available = download.Size() - packed_size; const auto prefix = copy.source_offset & 3u; const auto bytes = std::min(copy.size, available - prefix); DownloadCopy part {copy.buffer, copy.source_offset, copy.address, bytes}; const auto [source_begin, envelope_size] = DownloadEnvelope(part); (void)source_begin; packed_size += AlignDownload(envelope_size); batch.push_back(part); copy.source_offset += bytes; copy.address += bytes; copy.size -= bytes; if (packed_size == download.Size()) { flush(); } } } if (!batch.empty()) { flush(); } for (const auto& copy: copies) { m_gpu_modified_ranges.Subtract(copy.address, copy.size); } } BufferCache::BufferCache(GraphicContext& graphics, CommandScheduler& scheduler, PageManager& page_manager, TextureCache& texture_cache) : m_graphics(graphics), m_scheduler(scheduler), m_gds_buffer(graphics, scheduler, MemoryUsage::Stream, 0, AllFlags, GdsBufferSize), 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_texture_cache(texture_cache) { std::memset(m_gds_buffer.Mapped().data(), 0, static_cast(m_gds_buffer.Size())); m_gds_buffer.Flush(0, m_gds_buffer.Size()); const auto null_id = m_slot_buffers.insert(m_graphics, m_scheduler, MemoryUsage::DeviceLocal, 0, AllFlags, 16); EXIT_IF(null_id != NULL_BUFFER_ID); SetVulkanObjectNameF(m_graphics.device, GetBuffer(null_id).Handle(), "Kyty.NullBuffer"); 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_gpu_modified_ranges.Empty()) { EXIT("BufferCache: destroyed with pending GPU-modified ranges\n"); } for (const auto& [vaddr, id]: m_buffers) { (void)vaddr; const auto& buffer = m_slot_buffers[id]; if (m_memory_tracker.IsRegionGpuModified(buffer.CpuAddress(), buffer.Size())) { EXIT("BufferCache: destroyed with GPU-modified buffer\n"); } } m_buffers.clear(); } 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"); } m_memory_tracker.InvalidateRegion(vaddr, size, [this, vaddr, size] { ReadMemory(vaddr, size, true); }); } void BufferCache::ReadMemory(uint64_t vaddr, uint64_t size, bool is_write) { if (!GuestGpu::IsGpuThread() && CommandScheduler::InDeferredOperation()) { EXIT("unsupported buffer readback from an asynchronous GPU completion, " "addr=0x%016" PRIx64 " size=0x%016" PRIx64 "\n", vaddr, size); } m_scheduler.Context().GetGpu().SendCommandSync( [this, vaddr, size, is_write] { ReadMemoryOnGpu(vaddr, size, is_write); }); } void BufferCache::ReadMemoryOnGpu(uint64_t vaddr, uint64_t size, bool is_write) { // CPU invalidation reaches this point only for a GPU-owned tracker page. Resolve the exact // Buffer owner on the GPU thread so the cache index remains single-thread-owned. if (is_write && !IsRegionRegistered(vaddr, size)) { return; } std::vector copies; 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; const auto* owner = m_page_table.Find(copy_address >> PageTable::kPageBits); if (owner == nullptr || !*owner) { EXIT("BufferCache: invalidation readback has no buffer owner\n"); } auto& buffer = m_slot_buffers[*owner]; if (!buffer.IsInBounds(copy_address, 1)) { EXIT("BufferCache: invalidation readback is outside its buffer owner\n"); } const auto copy_size = std::min( range.size - copied, buffer.CpuAddress() + buffer.Size() - copy_address); copies.push_back( {&buffer, buffer.Offset(copy_address), copy_address, copy_size}); copied += copy_size; } } }); if (copies.empty()) { if (!is_write) { return; } // A preceding read fault can consume the last GPU-owned copy after this write invalidation // has already chosen to flush. Complete the CPU ownership handoff even though this callback // no longer has bytes to download. m_memory_tracker.MarkRegionAsCpuModified(vaddr, size); return; } DownloadBufferMemory(copies); // The enumeration above covered whole dirty pages and every exact interval on them. m_memory_tracker.UnmarkRegionAsGpuModified(vaddr, size); if (is_write) { m_memory_tracker.MarkRegionAsCpuModified(vaddr, size); } } void BufferCache::UnmapMemory(uint64_t vaddr, uint64_t size) { if (vaddr == 0 || size == 0 || size > UINT64_MAX - vaddr) { EXIT("BufferCache: invalid unmap range\n"); } std::vector copies; std::vector> modified_buffers; std::vector> retired_buffers; for (const auto& [begin, id]: m_buffers) { const auto& buffer = m_slot_buffers[id]; if (vaddr < begin + buffer.Size() && begin < vaddr + size) { retired_buffers.emplace_back(begin, buffer.Size()); } } for (const auto& [begin, id]: m_buffers) { const auto& buffer = m_slot_buffers[id]; if (vaddr >= begin + buffer.Size() || begin >= vaddr + size || !m_memory_tracker.IsRegionGpuModified(begin, buffer.Size())) { continue; } if (!m_gpu_modified_ranges.Intersects(begin, buffer.Size())) { EXIT("BufferCache: GPU-modified buffer has no dirty ranges\n"); } modified_buffers.emplace_back(begin, buffer.Size()); } for (const auto& [begin, bytes]: modified_buffers) { auto owner = m_buffers.find(begin); if (owner == m_buffers.end() || m_slot_buffers[owner->second].Size() != bytes) { EXIT("BufferCache: unmap owner changed during collection\n"); } auto& buffer = m_slot_buffers[owner->second]; m_memory_tracker.ForEachDownloadRange( begin, buffer.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({&buffer, range.address - begin, range.address, range.size}); } }); } if (!copies.empty()) { DownloadBufferMemory(copies); } for (const auto& [begin, bytes]: modified_buffers) { m_gpu_modified_ranges.Subtract(begin, bytes); m_memory_tracker.UnmarkRegionAsGpuModified(begin, bytes); } for (const auto& [begin, bytes]: retired_buffers) { m_memory_tracker.MarkRegionAsCpuModified(begin, bytes); } if (m_gpu_modified_ranges.Intersects(vaddr, size)) { EXIT("BufferCache: unmap retained dirty byte ranges\n"); } m_memory_tracker.UntrackMemory(vaddr, size); for (const auto& [begin, bytes]: retired_buffers) { (void)bytes; const auto found = m_buffers.find(begin); if (found != m_buffers.end()) { DeleteBuffer(found->second); } } } BufferId BufferCache::FindBuffer(uint64_t vaddr, uint64_t size) { if (vaddr == 0) { return NULL_BUFFER_ID; } if (size == 0 || vaddr >= TRACKER_ADDRESS_SIZE || size > TRACKER_ADDRESS_SIZE - vaddr) { EXIT("BufferCache: invalid buffer discovery request\n"); } const auto* owner = m_page_table.Find(vaddr >> PageTable::kPageBits); if (owner != nullptr && *owner) { auto& buffer = m_slot_buffers[*owner]; if (buffer.IsInBounds(vaddr, size)) { buffer.tick_accessed_last = m_gc_tick; return *owner; } } return CreateBuffer(vaddr, size); } BufferId BufferCache::CreateBuffer(uint64_t vaddr, uint64_t size) { auto& command = m_scheduler.Current(); EXIT_IF(command.IsInvalid() || command.IsExecute()); auto begin = vaddr & ~(CACHING_PAGE_SIZE - 1); auto end = (vaddr + size + CACHING_PAGE_SIZE - 1) & ~(CACHING_PAGE_SIZE - 1); auto first = m_buffers.lower_bound(begin); if (first != m_buffers.begin()) { const auto previous = std::prev(first); if (const auto& buffer = m_slot_buffers[previous->second]; buffer.CpuAddress() + buffer.Size() > begin) { first = previous; } } auto last = first; for (; last != m_buffers.end() && last->first < end; ++last) { const auto& buffer = m_slot_buffers[last->second]; begin = std::min(begin, buffer.CpuAddress()); end = std::max(end, buffer.CpuAddress() + buffer.Size()); } const auto id = m_slot_buffers.insert(m_graphics, m_scheduler, MemoryUsage::DeviceLocal, begin, AllFlags, end - begin); auto& buffer = m_slot_buffers[id]; buffer.tick_accessed_last = m_gc_tick; SetVulkanObjectNameF(m_graphics.device, buffer.Handle(), "Kyty.GameBuffer[guest=0x{:016x} size=0x{:x}]", begin, end - begin); for (auto overlap = first; overlap != last;) { const auto current = overlap++; const auto old_id = current->second; const auto& old = m_slot_buffers[old_id]; buffer.CopyFrom(command, old, 0, old.CpuAddress() - begin, old.Size()); DeleteBuffer(old_id); } Register(id); return id; } bool BufferCache::SynchronizeBuffer(Buffer& buffer, uint64_t vaddr, uint64_t size, bool is_written, bool is_texel_buffer) { std::vector copies; uint64_t total_size = 0; vk::Buffer source; m_memory_tracker.ForEachUploadRange( vaddr, size, is_written, [&](uint64_t address, uint64_t bytes) noexcept { copies.emplace_back(total_size, buffer.Offset(address), bytes); total_size += bytes; }, [&]() noexcept { source = UploadCopies(buffer, copies, total_size); }); if (source) { auto& command = m_scheduler.Current(); command.EndRendering(); const auto native = command.Handle(); vk::BufferMemoryBarrier before {}; before.sType = vk::StructureType::eBufferMemoryBarrier; before.srcAccessMask = vk::AccessFlagBits::eMemoryRead | vk::AccessFlagBits::eMemoryWrite | vk::AccessFlagBits::eTransferRead | vk::AccessFlagBits::eTransferWrite; before.dstAccessMask = vk::AccessFlagBits::eTransferWrite; before.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; before.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; before.buffer = buffer.Handle(); before.offset = 0; before.size = buffer.Size(); native.pipelineBarrier(vk::PipelineStageFlagBits::eAllCommands, vk::PipelineStageFlagBits::eTransfer, vk::DependencyFlagBits::eByRegion, 0, nullptr, 1, &before, 0, nullptr); native.copyBuffer(source, buffer.Handle(), static_cast(copies.size()), copies.data()); auto after = before; after.srcAccessMask = vk::AccessFlagBits::eTransferWrite; after.dstAccessMask = vk::AccessFlagBits::eMemoryRead | vk::AccessFlagBits::eMemoryWrite; native.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer, vk::PipelineStageFlagBits::eAllCommands, vk::DependencyFlagBits::eByRegion, 0, nullptr, 1, &after, 0, nullptr); } if (is_texel_buffer && !is_written) { return SynchronizeBufferFromImage(buffer, vaddr, size); } return false; } vk::Buffer BufferCache::UploadCopies(Buffer& buffer, std::span copies, uint64_t total_size) { if (copies.empty()) { return nullptr; } auto [mapped, base_offset] = m_staging_buffer.Map(total_size, 4); if (mapped != nullptr) { for (auto& copy: copies) { const auto address = buffer.CpuAddress() + copy.dstOffset; std::memcpy(mapped + copy.srcOffset, reinterpret_cast(address), copy.size); copy.srcOffset += base_offset; } m_staging_buffer.Commit(); return m_staging_buffer.Handle(); } auto temporary = std::make_unique(m_graphics, m_scheduler, MemoryUsage::Upload, 0, vk::BufferUsageFlagBits::eTransferSrc, total_size); for (const auto& copy: copies) { const auto address = buffer.CpuAddress() + copy.dstOffset; std::memcpy(temporary->Mapped().data() + copy.srcOffset, reinterpret_cast(address), copy.size); } temporary->Flush(0, total_size); const auto handle = temporary->Handle(); m_scheduler.DeferOperation([owner = std::move(temporary)]() mutable { owner.reset(); }); return handle; } std::pair BufferCache::ObtainBuffer(uint64_t vaddr, uint64_t size, bool is_written, bool is_texel_buffer, BufferId id) { auto& command = m_scheduler.Current(); if (command.IsInvalid() || command.IsExecute() || vaddr == 0 || size == 0 || vaddr >= TRACKER_ADDRESS_SIZE || size > TRACKER_ADDRESS_SIZE - vaddr) { EXIT("BufferCache: buffer request requires a recording command buffer\n"); } if (!is_written && size <= CACHING_PAGE_SIZE && !m_memory_tracker.IsRegionGpuModified(vaddr, size) && m_memory_tracker.IsRegionCpuModified(vaddr, size)) { const auto alignment = std::max( m_graphics.physical_device_properties.limits.minUniformBufferOffsetAlignment, 1); auto [mapped, offset] = m_stream_buffer.Map(size, alignment, false); if (mapped != nullptr && Libs::LibKernel::Memory::TryReadBacking(vaddr, mapped, size)) { m_stream_buffer.Commit(); return {&m_stream_buffer, offset}; } } auto* buffer = m_slot_buffers.try_get(id); if (buffer == nullptr || buffer->is_deleted || !buffer->IsInBounds(vaddr, size)) { id = FindBuffer(vaddr, size); buffer = &m_slot_buffers[id]; } (void)SynchronizeBuffer(*buffer, vaddr, size, is_written, is_texel_buffer); if (is_written) { m_gpu_modified_ranges.Add(vaddr, size); } return {buffer, buffer->Offset(vaddr)}; } std::pair 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"); } auto find_owner = [&]() -> Buffer* { const auto* owner = m_page_table.Find(vaddr >> PageTable::kPageBits); if (owner == nullptr || !*owner) { return nullptr; } auto& buffer = m_slot_buffers[*owner]; return buffer.IsInBounds(vaddr, size) ? &buffer : nullptr; }; { const bool cpu_modified = m_memory_tracker.IsRegionCpuModified(vaddr, size); const bool gpu_modified = m_memory_tracker.IsRegionGpuModified(vaddr, size); const bool has_dirty_buffer_source = m_gpu_modified_ranges.Intersects(vaddr, size); m_memory_tracker.ValidateGpuDirtyOwnership(m_gpu_modified_ranges, vaddr, size, "image source"); auto* owner = find_owner(); if (has_dirty_buffer_source && owner == nullptr) { if (!IsRegionRegistered(vaddr, size)) { EXIT("BufferCache: GPU-dirty image source has no native buffer\n"); } owner = &m_slot_buffers[FindBuffer(vaddr, size)]; } if (owner != nullptr && !cpu_modified && (!gpu_modified || has_dirty_buffer_source)) { owner->tick_accessed_last = m_gc_tick; return {owner, owner->Offset(vaddr)}; } if (has_dirty_buffer_source && owner == nullptr) { EXIT("BufferCache: GPU-dirty image source could not resolve its native owner\n"); } } auto [staging, stage_offset] = m_staging_buffer.Map(size, 16); if (staging == nullptr || (!Libs::LibKernel::Memory::TryReadBacking(vaddr, staging, size) && !Libs::LibKernel::Memory::TryReadPrtBacking(vaddr, staging, size))) { EXIT("BufferCache: failed to read mapped guest image backing\n"); } m_staging_buffer.Commit(); const bool has_dirty_buffer_source = m_gpu_modified_ranges.Intersects(vaddr, size); auto* owner = find_owner(); if (has_dirty_buffer_source && owner == nullptr) { EXIT("BufferCache: GPU-dirty image source lost its native owner\n"); } if (owner == nullptr || (m_memory_tracker.IsRegionGpuModified(vaddr, size) && !has_dirty_buffer_source)) { return {&m_staging_buffer, stage_offset}; } owner->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) { owner->CopyFrom(m_scheduler.Current(), m_staging_buffer, stage_offset + address - vaddr, owner->Offset(address), upload_size, vk::AccessFlagBits::eHostWrite); } }); return {owner, owner->Offset(vaddr)}; } 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"); } Libs::LibKernel::Memory::WriteBacking(vaddr, data.data(), data.size()); const auto end = vaddr + data.size(); for (const auto& [address, id]: m_buffers) { auto& buffer = m_slot_buffers[id]; const auto buffer_end = address + buffer.Size(); const auto begin = std::max(vaddr, address); const auto range_end = std::min(end, buffer_end); if (begin >= range_end) { continue; } WriteDataBuffer(buffer, begin, data.data() + begin - vaddr, range_end - begin); buffer.tick_accessed_last = m_gc_tick; } } 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); { 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; } } m_texture_cache.InvalidateMemoryFromGPU(vaddr, size); const auto id = FindBuffer(vaddr, size); auto [dst, dst_offset] = ObtainBuffer(vaddr, size, true, true, id); EXIT_IF(dst == nullptr); dst->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_gds || src_gds) && ((dst_vaddr | src_vaddr | size) & 3u) != 0) || size > UINT64_MAX - dst_vaddr || size > UINT64_MAX - src_vaddr || (dst_gds && src_gds) || (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 copy range, src=0x%016" PRIx64 " dst=0x%016" PRIx64 " size=0x%016" PRIx64 " src_gds=%d dst_gds=%d\n", src_vaddr, dst_vaddr, size, static_cast(src_gds), static_cast(dst_gds)); } if (src_memory || dst_memory) { 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 && 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(); if (dst_memory) { m_texture_cache.InvalidateMemoryFromGPU(dst_vaddr, size); } const auto src_id = src_memory ? FindBuffer(src_vaddr, size) : BufferId {}; const auto dst_id = dst_memory ? FindBuffer(dst_vaddr, size) : BufferId {}; auto [src, src_offset] = src_memory ? ObtainBuffer(src_vaddr, size, false, true, src_id) : std::pair {&m_gds_buffer, src_vaddr}; auto [dst, dst_offset] = dst_memory ? ObtainBuffer(dst_vaddr, size, true, true, dst_id) : std::pair {&m_gds_buffer, dst_vaddr}; EXIT_IF(src == nullptr || dst == nullptr); if (src == dst && src_offset < dst_offset + size && dst_offset < src_offset + size) { EXIT("BufferCache: resolved Vulkan copy ranges overlap\n"); } dst->CopyFrom(command, *src, src_offset, dst_offset, size); } bool BufferCache::IsRegionRegistered(uint64_t vaddr, uint64_t size) { if (vaddr == 0 || size == 0 || vaddr >= TRACKER_ADDRESS_SIZE || size > TRACKER_ADDRESS_SIZE - vaddr) { EXIT("BufferCache: invalid registered-region query\n"); } // Cached buffers are ordered and non-overlapping. The last buffer beginning before the query // end is therefore the only possible intersection. const auto candidate = m_buffers.lower_bound(vaddr + size); if (candidate == m_buffers.begin()) { return false; } const auto& [address, id] = *std::prev(candidate); return address + m_slot_buffers[id].Size() > vaddr; } 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) { return m_gpu_modified_ranges.Intersects(vaddr, size); } bool BufferCache::IsRegionCpuModified(uint64_t vaddr, uint64_t size) { return m_memory_tracker.IsRegionCpuModified(vaddr, size); } void BufferCache::RunGarbageCollector() { 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) { 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 candidates; for (const auto& [address, id]: m_buffers) { const auto& buffer = m_slot_buffers[id]; if (tick - std::min(tick, buffer.tick_accessed_last) >= age) { candidates.push_back(address); } } std::ranges::sort(candidates, [&](uint64_t left, uint64_t right) { return m_slot_buffers[m_buffers.at(left)].tick_accessed_last < m_slot_buffers[m_buffers.at(right)].tick_accessed_last; }); std::vector dirty_buffers; std::vector copies; size_t retire_count = 0; for (const auto address: candidates) { auto found = m_buffers.find(address); EXIT_IF(found == m_buffers.end()); const auto id = found->second; auto& buffer = m_slot_buffers[id]; m_memory_tracker.ValidateGpuDirtyOwnership(m_gpu_modified_ranges, buffer.CpuAddress(), buffer.Size(), "garbage collection"); const bool dirty = m_memory_tracker.IsRegionGpuModified(buffer.CpuAddress(), buffer.Size()); if (dirty && !aggressive) { continue; } if (dirty) { m_memory_tracker.ForEachDownloadRange( buffer.CpuAddress(), buffer.Size(), [&](uint64_t dirty_address, uint64_t dirty_size) noexcept { m_memory_tracker.ValidateGpuDirtyPages(m_gpu_modified_ranges, dirty_address, dirty_size, "garbage collection"); }, [&](uint64_t dirty_address, uint64_t dirty_size) noexcept { m_gpu_modified_ranges.ForEachIntersection( dirty_address, dirty_size, [&](RangeSet::Range range) { copies.push_back({&buffer, range.address - buffer.CpuAddress(), range.address, range.size}); }); }); dirty_buffers.push_back(id); } else { m_memory_tracker.UntrackMemory(buffer.CpuAddress(), buffer.Size()); DeleteBuffer(id); } if (++retire_count == limit) { break; } } if (dirty_buffers.empty()) { return; } EXIT_IF(copies.empty()); DownloadBufferMemory(copies); for (const auto id: dirty_buffers) { auto& buffer = m_slot_buffers[id]; m_memory_tracker.UnmarkRegionAsGpuModified(buffer.CpuAddress(), buffer.Size()); if (m_memory_tracker.IsRegionGpuModified(buffer.CpuAddress(), buffer.Size()) || m_gpu_modified_ranges.Intersects(buffer.CpuAddress(), buffer.Size())) { EXIT("BufferCache: garbage collection retained GPU ownership\n"); } m_memory_tracker.UntrackMemory(buffer.CpuAddress(), buffer.Size()); Unregister(id); m_slot_buffers.erase(id); } } } // namespace Libs::Graphics