// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later // SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later #pragma once #include #include #include #include #include #include #include #include #include #include #include #include #include "common/container/unordered_map.h" #include #include #include "common/common_types.h" #include "common/spin_recursive_mutex.h" #include "common/div_ceil.h" #include "common/literals.h" #include "common/lru_cache.h" #include "common/range_sets.h" #include "common/scope_exit.h" #include "common/settings.h" #include "common/slot_vector.h" #include "video_core/buffer_cache/buffer_base.h" #include "video_core/buffer_cache/virtual_range_cache.h" #include "video_core/control/channel_state_cache.h" #include "video_core/delayed_destruction_ring.h" #include "video_core/dirty_flags.h" #include "video_core/engines/maxwell_3d.h" #include "video_core/engines/kepler_compute.h" #include "video_core/engines/maxwell_3d.h" #include "video_core/memory_manager.h" #include "video_core/surface.h" #include "video_core/texture_cache/types.h" namespace VideoCommon { using BufferId = Common::SlotId; using VideoCore::Surface::PixelFormat; using namespace Common::Literals; #ifdef __APPLE__ constexpr u32 NUM_VERTEX_BUFFERS = 16; #else constexpr u32 NUM_VERTEX_BUFFERS = 32; #endif constexpr u32 NUM_TRANSFORM_FEEDBACK_BUFFERS = 4; constexpr u32 NUM_GRAPHICS_UNIFORM_BUFFERS = 18; constexpr u32 NUM_COMPUTE_UNIFORM_BUFFERS = 8; // Native compute shaders bind up to 18 global memory ranges; the enable masks are 32 bits wide. constexpr u32 NUM_STORAGE_BUFFERS = 32; constexpr u32 NUM_TEXTURE_BUFFERS = 32; constexpr u32 NUM_STAGES = 5; static_assert(NUM_GRAPHICS_UNIFORM_BUFFERS <= 32, "fast bitmask must fit u32"); using UniformBufferSizes = std::array, NUM_STAGES>; using ComputeUniformBufferSizes = std::array; enum class ObtainBufferSynchronize : u32 { NoSynchronize = 0, FullSynchronize = 1, SynchronizeNoDirty = 2, }; enum class ObtainBufferOperation : u32 { DoNothing = 0, MarkAsWritten = 1, DiscardWrite = 2, MarkQuery = 3, }; static constexpr BufferId NULL_BUFFER_ID{0}; static constexpr u32 DEFAULT_SKIP_CACHE_SIZE = static_cast(4_KiB); struct Binding { DAddr device_addr{}; GPUVAddr gpu_addr{}; u32 size{}; BufferId buffer_id; u32 segment_first{}; u32 segment_count{}; }; struct MultiRangeSegment { BufferId buffer_id; DAddr device_addr{}; u32 size{}; }; struct TextureBufferBinding : Binding { PixelFormat format; }; static constexpr Binding NULL_BINDING{ .device_addr = 0, .size = 0, .buffer_id = NULL_BUFFER_ID, }; template struct HostBindings { boost::container::static_vector buffers; boost::container::static_vector offsets; boost::container::static_vector sizes; boost::container::static_vector strides; u32 min_index{NUM_VERTEX_BUFFERS}; u32 max_index{0}; }; class BufferCacheChannelInfo : public ChannelInfo { public: BufferCacheChannelInfo() = delete; BufferCacheChannelInfo(Tegra::Control::ChannelState& state) noexcept : ChannelInfo(state) {} BufferCacheChannelInfo(const BufferCacheChannelInfo& state) = delete; BufferCacheChannelInfo& operator=(const BufferCacheChannelInfo&) = delete; Binding index_buffer; std::array vertex_buffers; std::array, NUM_STAGES> uniform_buffers; std::array, NUM_STAGES> storage_buffers; std::array, NUM_STAGES> texture_buffers; std::array transform_feedback_buffers; Binding count_buffer_binding; Binding indirect_buffer_binding; std::array compute_uniform_buffers; std::array compute_storage_buffers; std::array compute_texture_buffers; std::array enabled_uniform_buffer_masks{}; u32 enabled_compute_uniform_buffer_mask = 0; const UniformBufferSizes* uniform_buffer_sizes{}; const ComputeUniformBufferSizes* compute_uniform_buffer_sizes{}; std::array enabled_storage_buffers{}; std::array written_storage_buffers{}; u32 enabled_compute_storage_buffers = 0; u32 written_compute_storage_buffers = 0; u32 total_graphics_storage_buffers = 0; u32 total_compute_storage_buffers = 0; std::array enabled_texture_buffers{}; std::array written_texture_buffers{}; std::array image_texture_buffers{}; u32 enabled_compute_texture_buffers = 0; u32 written_compute_texture_buffers = 0; u32 image_compute_texture_buffers = 0; std::array uniform_cache_hits{}; std::array uniform_cache_shots{}; u32 uniform_buffer_skip_cache_size = DEFAULT_SKIP_CACHE_SIZE; bool has_deleted_buffers = false; std::array dirty_uniform_buffers{}; std::array fast_bound_uniform_buffers{}; std::array, NUM_STAGES> uniform_buffer_binding_sizes{}; }; template class BufferCache : public VideoCommon::ChannelSetupCaches { // Page size for caching purposes. // This is unrelated to the CPU page size and it can be changed as it seems optimal. #ifdef YUZU_LEGACY static constexpr u32 CACHING_PAGEBITS = 12; #else static constexpr u32 CACHING_PAGEBITS = 16; #endif static constexpr u64 CACHING_PAGESIZE = u64{1} << CACHING_PAGEBITS; static constexpr bool IS_OPENGL = P::IS_OPENGL; static constexpr bool HAS_PERSISTENT_UNIFORM_BUFFER_BINDINGS = P::HAS_PERSISTENT_UNIFORM_BUFFER_BINDINGS; static constexpr bool HAS_FULL_INDEX_AND_PRIMITIVE_SUPPORT = P::HAS_FULL_INDEX_AND_PRIMITIVE_SUPPORT; static constexpr bool NEEDS_BIND_UNIFORM_INDEX = P::NEEDS_BIND_UNIFORM_INDEX; static constexpr bool NEEDS_BIND_STORAGE_INDEX = P::NEEDS_BIND_STORAGE_INDEX; static constexpr bool USE_MEMORY_MAPS = P::USE_MEMORY_MAPS; static constexpr bool SEPARATE_IMAGE_BUFFERS_BINDINGS = P::SEPARATE_IMAGE_BUFFER_BINDINGS; static constexpr bool USE_MEMORY_MAPS_FOR_UPLOADS = P::USE_MEMORY_MAPS_FOR_UPLOADS; #ifdef YUZU_LEGACY static constexpr s64 TARGET_THRESHOLD = 3_GiB; #else static constexpr s64 TARGET_THRESHOLD = 4_GiB; #endif static constexpr s64 DEFAULT_EXPECTED_MEMORY = 512_MiB; static constexpr s64 DEFAULT_CRITICAL_MEMORY = 1_GiB; // Debug Flags. static constexpr bool DISABLE_DOWNLOADS = true; using Maxwell = Tegra::Engines::Maxwell3D::Regs; using Runtime = typename P::Runtime; using Buffer = typename P::Buffer; using Async_Buffer = typename P::Async_Buffer; using MemoryTracker = typename P::MemoryTracker; struct OverlapResult { boost::container::small_vector ids; DAddr begin; DAddr end; bool has_stream_leap = false; }; public: explicit BufferCache(Tegra::MaxwellDeviceMemoryManager& device_memory_, Runtime& runtime_); ~BufferCache(); void TickFrame(); bool BindMultiRangeStorage(const Binding& binding, bool is_written, std::span pool); void ResolveMultiRangeStorage(Binding& binding, bool is_written, std::vector& pool); void UnmapGPUMemory(size_t as_id, GPUVAddr gpu_addr, size_t size); void WriteMemory(DAddr device_addr, u64 size); void CachedWriteMemory(DAddr device_addr, u64 size); bool OnCPUWrite(DAddr device_addr, u64 size); void DownloadMemory(DAddr device_addr, u64 size); std::optional GetFlushArea(DAddr device_addr, u64 size); /// Without downloads at every fence, a guest CPU read is the only way GPU-written buffer /// data reaches guest memory, so reads must ask the buffer cache (GetFlushArea). bool FlushesOnCpuRead() const { return !AsyncBufferDownloads(); } bool InlineMemory(DAddr dest_address, size_t copy_size, std::span inlined_buffer); void BindGraphicsUniformBuffer(size_t stage, u32 index, GPUVAddr gpu_addr, u32 size); void DisableGraphicsUniformBuffer(size_t stage, u32 index); void UpdateGraphicsBuffers(bool is_indexed); void UpdateComputeBuffers(); void BindHostGeometryBuffers(bool is_indexed); void BindHostStageBuffers(size_t stage); void BindHostComputeBuffers(); void SetUniformBuffersState(const std::array& mask, const UniformBufferSizes* sizes); void SetComputeUniformBufferState(u32 mask, const ComputeUniformBufferSizes* sizes); void UnbindGraphicsStorageBuffers(size_t stage); bool BindGraphicsStorageBuffer(size_t stage, size_t ssbo_index, u32 cbuf_index, u32 cbuf_offset, bool is_written); void UnbindGraphicsTextureBuffers(size_t stage); void BindGraphicsTextureBuffer(size_t stage, size_t tbo_index, GPUVAddr gpu_addr, u32 size, PixelFormat format, bool is_written, bool is_image); void UnbindComputeStorageBuffers(); void BindComputeStorageBuffer(size_t ssbo_index, u32 cbuf_index, u32 cbuf_offset, bool is_written); void UnbindComputeTextureBuffers(); void BindComputeTextureBuffer(size_t tbo_index, GPUVAddr gpu_addr, u32 size, PixelFormat format, bool is_written, bool is_image); [[nodiscard]] std::pair ObtainBuffer(GPUVAddr gpu_addr, u32 size, ObtainBufferSynchronize sync_info, ObtainBufferOperation post_op); [[nodiscard]] std::pair ObtainCPUBuffer(DAddr gpu_addr, u32 size, ObtainBufferSynchronize sync_info, ObtainBufferOperation post_op); void FlushCachedWrites(); /// Return true when there are uncommitted buffers to be downloaded [[nodiscard]] bool HasUncommittedFlushes() const noexcept; void AccumulateFlushes(); /// Return true when the caller should wait for async downloads [[nodiscard]] bool ShouldWaitAsyncFlushes() const noexcept; /// Commit asynchronous downloads void CommitAsyncFlushes(); void CommitAsyncFlushesHigh(); /// Pop asynchronous downloads void PopAsyncFlushes(); void PopAsyncBuffers(); bool DMACopy(GPUVAddr src_address, GPUVAddr dest_address, u64 amount); bool DMAClear(GPUVAddr src_address, u64 amount, u32 value); /// Switch 2 GPU-written command lists (nx2.h, g_nx2_command_list_pages). While collecting (a /// compute dispatch or a DMA copy being recorded), writes to command list pages are noted; /// Nx2CopyListPages then records copies of those pages into host memory after the work. void Nx2CollectListCopies(bool collect) { nx2_collect_list_copies = collect; if (!collect) { nx2_list_copy_requests.clear(); } } /// True when it recorded copies, which the GPU completes at `tick` (the caller submits). bool Nx2CopyListPages(u64 tick); enum class Nx2ListRead { Unknown, Copied, Synced }; /// Front thread: Synced when guest memory holds the page `device_addr` lies in (flushed and /// written by no GPU work since), else Copied with `out` filled from the copy of its last /// write once the GPU is done with it. Unknown otherwise: the caller drains and flushes. Nx2ListRead Nx2ReadListCopy(DAddr device_addr, std::span out); /// Front thread, the split drained: the command list page was flushed to guest memory. void Nx2NoteListSynced(DAddr page); /// CPU and inline writes: they reach guest memory, so a synced page stays synced, but its copy /// no longer holds the page. void Nx2NoteListHostWrite(DAddr device_addr, u64 size); /// Return true when a CPU region is modified from the GPU [[nodiscard]] bool IsRegionGpuModified(DAddr addr, size_t size); /// Return true when a region is registered on the cache [[nodiscard]] bool IsRegionRegistered(DAddr addr, size_t size); /// Return true when a CPU region is modified from the CPU [[nodiscard]] bool IsRegionCpuModified(DAddr addr, size_t size); void SetDrawIndirect( const Tegra::Engines::Maxwell3D::DrawManager::IndirectParams* current_draw_indirect_) { current_draw_indirect = current_draw_indirect_; } [[nodiscard]] std::pair GetDrawIndirectCount(); [[nodiscard]] std::pair GetDrawIndirectBuffer(); template void BufferOperations(Func&& func) { do { channel_state->has_deleted_buffers = false; func(); } while (channel_state->has_deleted_buffers); } Common::SpinRecursiveMutex mutex; Runtime& runtime; bool any_buffer_uploaded = false; private: template static void ForEachEnabledBit(u32 enabled_mask, Func&& func) { for (u32 index = 0; enabled_mask != 0; ++index, enabled_mask >>= 1) { const int disabled_bits = std::countr_zero(enabled_mask); index += disabled_bits; enabled_mask >>= disabled_bits; func(index); } } template void ForEachBufferInRange(DAddr device_addr, u64 size, Func&& func) { const u64 page_end = Common::DivCeil(device_addr + size, CACHING_PAGESIZE); for (u64 page = device_addr >> CACHING_PAGEBITS; page < page_end;) { const BufferId buffer_id = page_table[page]; if (!buffer_id) { ++page; continue; } Buffer& buffer = slot_buffers[buffer_id]; func(buffer_id, buffer); const DAddr end_addr = buffer.CpuAddr() + buffer.SizeBytes(); page = Common::DivCeil(end_addr, CACHING_PAGESIZE); } } static bool IsRangeGranular(DAddr device_addr, size_t size) { return (device_addr & ~Core::DEVICE_PAGEMASK) == ((device_addr + size) & ~Core::DEVICE_PAGEMASK); } void RunGarbageCollector(bool aggressive_gc); void BindHostIndexBuffer(); void BindHostVertexBuffers(); void BindHostDrawIndirectBuffers(); void BindHostGraphicsUniformBuffers(size_t stage); void BindHostGraphicsUniformBuffer(size_t stage, u32 index, u32 binding_index, bool needs_bind); void BindHostGraphicsStorageBuffers(size_t stage); void BindHostGraphicsTextureBuffers(size_t stage); void BindHostTransformFeedbackBuffers(); void BindHostVertexBuffer(u32 index, Buffer& buffer, u32 offset, u32 size, u32 stride); void BindHostComputeUniformBuffers(); void BindHostComputeStorageBuffers(); void BindHostComputeTextureBuffers(); void DoUpdateGraphicsBuffers(bool is_indexed); void DoUpdateComputeBuffers(); void UpdateIndexBuffer(); void UpdateVertexBuffers(); void UpdateVertexBuffer(u32 index); void UpdateDrawIndirect(); void UpdateUniformBuffers(size_t stage); void UpdateStorageBuffers(size_t stage); void UpdateTextureBuffers(size_t stage); void UpdateTransformFeedbackBuffers(); void UpdateTransformFeedbackBuffer(u32 index); void UpdateComputeUniformBuffers(); void UpdateComputeStorageBuffers(); void UpdateComputeTextureBuffers(); void MarkWrittenBuffer(BufferId buffer_id, DAddr device_addr, u32 size, bool needs_sync = false); /// Counts a GPU write to the command list pages in the range (invalidating their copies) and, /// while collecting, asks for new copies. void Nx2NoteListWrite(BufferId buffer_id, DAddr device_addr, u64 size); [[nodiscard]] BufferId FindBuffer(DAddr device_addr, u32 size, bool sparse_compatible); void SynchronizeBufferWrites(Buffer& buffer); [[nodiscard]] OverlapResult ResolveOverlaps(DAddr device_addr, u32 wanted_size); void JoinOverlap(BufferId new_buffer_id, BufferId overlap_id, bool accumulate_stream_score); [[nodiscard]] BufferId CreateBuffer(DAddr device_addr, u32 wanted_size, bool sparse_compatible); void Register(BufferId buffer_id); void Unregister(BufferId buffer_id); template void ChangeRegister(BufferId buffer_id); void TouchBuffer(Buffer& buffer, BufferId buffer_id) noexcept; bool SynchronizeBuffer(Buffer& buffer, DAddr device_addr, u32 size); void UploadMemory(Buffer& buffer, u64 total_size_bytes, u64 largest_copy, std::span copies); void ImmediateUploadMemory(Buffer& buffer, u64 largest_copy, std::span copies); void MappedUploadMemory(Buffer& buffer, u64 total_size_bytes, std::span copies); void DownloadBufferMemory(Buffer& buffer_id); void DownloadBufferMemory(Buffer& buffer_id, DAddr device_addr, u64 size); /// Downloads the GPU-written data of several buffers with one wait for the GPU. void DownloadBuffersMemory(std::span buffer_ids); void DeleteBuffer(BufferId buffer_id, bool do_not_mark = false); [[nodiscard]] Binding StorageBufferBinding(GPUVAddr ssbo_addr, u32 cbuf_index, bool is_written) const; [[nodiscard]] TextureBufferBinding GetTextureBufferBinding(GPUVAddr gpu_addr, u32 size, PixelFormat format); [[nodiscard]] std::span ImmediateBufferWithData(DAddr device_addr, size_t size); [[nodiscard]] std::span ImmediateBuffer(size_t wanted_capacity); [[nodiscard]] bool HasFastUniformBufferBound(size_t stage, u32 binding_index) const noexcept; [[nodiscard]] Binding& VertexBufferSlot(u32 index); [[nodiscard]] const Binding& VertexBufferSlot(u32 index) const; void UpdateVertexBufferSlot(u32 index, const Binding& binding); /// Flags the vertex buffers overlapping a written range so host copies derived from them are /// rebuilt. void MarkVertexBuffersStale(DAddr device_addr, u64 size); /// Re-dirties repacked vertex buffers whose source data was written since they were bound. void RefreshRepackedVertexBuffers(); void ClearDownload(DAddr base_addr, u64 size); void InlineMemoryImplementation(DAddr dest_address, size_t copy_size, std::span inlined_buffer); Tegra::MaxwellDeviceMemoryManager& device_memory; Common::SlotVector slot_buffers; #ifdef YUZU_LEGACY static constexpr size_t TICKS_TO_DESTROY = 6; #else static constexpr size_t TICKS_TO_DESTROY = 8; #endif DelayedDestructionRing delayed_destruction_ring; const Tegra::Engines::Maxwell3D::DrawManager::IndirectParams* current_draw_indirect{}; u32 last_index_count = 0; u32 enabled_vertex_buffers_mask = 0; u32 stale_vertex_buffers_mask = 0; u64 vertex_buffers_serial = 0; std::array v_buffer{}; boost::container::small_vector upload_copies; MemoryTracker memory_tracker; Common::RangeSet uncommitted_gpu_modified_ranges; /// Whether GPU-written buffer ranges are copied back to guest memory at the next fence; /// otherwise only when something flushes them (as upstream yuzu below High GPU accuracy). /// Not for Switch 2 titles: title 1 had 250-330 MB/s copied back (a fence per compute /// dispatch, the fencing thread holding the buffer cache while it wrote them), while its CPU /// read GPU-written memory at most 64 times in 200 s (counted with reactive flushing) and /// the native MME's DMA reads never touched those pages. Without them a fence costs ~1 us /// instead of ~5 and draw setup ~7% less. EDEN_NX2_BUFFER_DOWNLOADS=0/1 forces it. bool AsyncBufferDownloads() const { static const int forced = [] { const char* value = std::getenv("EDEN_NX2_BUFFER_DOWNLOADS"); return value == nullptr ? -1 : (value[0] == '0' ? 0 : 1); }(); if (forced >= 0) { return forced == 1; } return gpu_memory == nullptr || !gpu_memory->UsesNativeTiling(); } /// Whether a GPU write to this range is copied back at the next fence. Ranges the guest CPU /// has read (Preflushable, set by the first reactive flush) always are: later reads skip the /// flush and rely on these downloads, e.g. collision meshes skinned by compute shaders. bool DownloadsAtFence(DAddr device_addr, u64 size) noexcept { return AsyncBufferDownloads() || memory_tracker.IsRegionPreflushable(device_addr, size); } /// GPU writes that are not downloaded at fences: trap the guest CPU's next read of their /// pages, which flushes them once and marks them Preflushable (then DownloadsAtFence). void ArmReadTraps(DAddr device_addr, u64 size) { const DAddr begin = Common::AlignDown(device_addr, Core::DEVICE_PAGESIZE); const DAddr end = Common::AlignUp(device_addr + size, Core::DEVICE_PAGESIZE); u64 armed_bytes = 0; read_trapped_ranges.ForEachInRange(begin, end - begin, [&](DAddr lower, DAddr upper) { armed_bytes += upper - lower; }); if (armed_bytes == end - begin) { return; } read_trapped_ranges.Add(begin, end - begin); device_memory.ArmReadTraps(begin, end - begin); } Common::RangeSet read_trapped_ranges; Common::RangeSet gpu_modified_ranges; std::deque> committed_gpu_modified_ranges; // Async Buffers Common::OverlapRangeSet async_downloads; std::deque> async_buffers; std::deque> pending_downloads; std::optional current_buffer; std::deque async_buffers_death_ring; size_t immediate_buffer_capacity = 0; Common::ScratchBuffer immediate_buffer_alloc; /// Guest memory large uploads came from since the last cold hint (device address, size). std::vector> uploaded_buffer_ranges; struct LRUItemParams { using ObjectType = BufferId; using TickType = u64; }; Common::LeastRecentlyUsedCache lru_cache; VirtualRangeCache virtual_ranges; std::vector graphics_segments; std::vector compute_segments; u64 frame_tick = 0; u64 total_used_memory = 0; u64 buffer_bytes = 0; ///< The buffers' own size (total_used_memory may be the device's) u64 minimum_memory = 0; u64 critical_memory = 0; BufferId inline_buffer_id; #ifdef YUZU_LEGACY bool immediately_free = false; #endif // Covers the device address space (MaxwellDeviceTraits::device_virtual_bits). std::array> CACHING_PAGEBITS)> page_table; Common::ScratchBuffer tmp_buffer; /// A command list page (Nx2CopyListPages): the GPU writes noted to it and the last copy. using Nx2Staging = decltype(std::declval().DownloadStagingBuffer(size_t{}, true)); struct Nx2ListPage { u64 writes{}; ///< GPU writes noted to the page u64 copy_writes{}; ///< `writes` when the copy was made: the copy is current while equal u64 copy_tick{}; ///< GPU tick that completes the copy /// `writes` when the front last flushed the page: guest memory holds it while equal u64 synced_writes{~u64{0}}; u32 copy_begin{}; ///< Bytes of the page the copy holds (a buffer can end inside it) u32 copy_end{}; /// Written outside a compute dispatch or a DMA copy (a draw's storage writes) at least /// once: the front can't tell from what it sent whether such a write is still queued, so /// the page's lists always drain the split instead of reading a copy. bool draw_written{}; std::optional copy; }; std::mutex nx2_list_mutex; ///< nx2_list_pages: the split's front reads the copies std::unordered_map nx2_list_pages; /// Copies replaced while the GPU may still write them: freed once it is past their tick. std::vector> nx2_retired_list_copies; boost::container::small_vector, 8> nx2_list_copy_requests; bool nx2_collect_list_copies{}; }; } // namespace VideoCommon