From 187372cbde56a8892e4810d8c99d6e2debd96ad5 Mon Sep 17 00:00:00 2001 From: riperiperi Date: Fri, 18 Nov 2022 14:58:24 +0000 Subject: [PATCH] Prune ForceDirty and CheckModified caches on unmap (#3862) * Prune ForceDirty and CheckModified caches on unmap Since we're now using this for modified checks on the HLE indirect draw method, I'm worried that leaving these to forever gather cache entries isn't the best idea for performance in the long term, and it could keep old buffer objects alive for longer than they should be. This PR adds the ability to prune invalid entries before checking these caches, and queues it whenever gpu memory is unmapped. It also aligns modified checks to the page size, as I figured it would be possible for a huge number of overlapping over a game's runtime. This prevents Super Mario Odyssey from having 10s of thousands of entries in the modified cache in Metro Kingdom, and them duplicating when entering and leaving a building (should be cleared, as they were unmapped). * Address Feedback --- Ryujinx.Graphics.Gpu/GpuChannel.cs | 2 + Ryujinx.Graphics.Gpu/Memory/BufferCache.cs | 82 +++++++++++++++++++--- 2 files changed, 76 insertions(+), 8 deletions(-) diff --git a/Ryujinx.Graphics.Gpu/GpuChannel.cs b/Ryujinx.Graphics.Gpu/GpuChannel.cs index f3bdd57632..b73756c64d 100644 --- a/Ryujinx.Graphics.Gpu/GpuChannel.cs +++ b/Ryujinx.Graphics.Gpu/GpuChannel.cs @@ -67,6 +67,7 @@ namespace Ryujinx.Graphics.Gpu // Since the memory manager changed, make sure we will get pools from addresses of the new memory manager. TextureManager.ReloadPools(); + MemoryManager.Physical.BufferCache.QueuePrune(); } /// @@ -77,6 +78,7 @@ namespace Ryujinx.Graphics.Gpu private void MemoryUnmappedHandler(object sender, UnmapEventArgs e) { TextureManager.ReloadPools(); + MemoryManager.Physical.BufferCache.QueuePrune(); } /// diff --git a/Ryujinx.Graphics.Gpu/Memory/BufferCache.cs b/Ryujinx.Graphics.Gpu/Memory/BufferCache.cs index 894d009c0e..85ed49d59b 100644 --- a/Ryujinx.Graphics.Gpu/Memory/BufferCache.cs +++ b/Ryujinx.Graphics.Gpu/Memory/BufferCache.cs @@ -28,6 +28,7 @@ namespace Ryujinx.Graphics.Gpu.Memory private readonly Dictionary _dirtyCache; private readonly Dictionary _modifiedCache; + private bool _pruneCaches; public event Action NotifyBuffersModified; @@ -136,6 +137,11 @@ namespace Ryujinx.Graphics.Gpu.Memory /// Size in bytes of the buffer public void ForceDirty(MemoryManager memoryManager, ulong gpuVa, ulong size) { + if (_pruneCaches) + { + Prune(); + } + if (!_dirtyCache.TryGetValue(gpuVa, out BufferCacheEntry result) || result.EndGpuAddress < gpuVa + size || result.UnmappedSequence != result.Buffer.UnmappedSequence) @@ -158,17 +164,29 @@ namespace Ryujinx.Graphics.Gpu.Memory /// True if modified, false otherwise public bool CheckModified(MemoryManager memoryManager, ulong gpuVa, ulong size, out ulong outAddr) { - if (!_modifiedCache.TryGetValue(gpuVa, out BufferCacheEntry result) || - result.EndGpuAddress < gpuVa + size || - result.UnmappedSequence != result.Buffer.UnmappedSequence) + if (_pruneCaches) { - ulong address = TranslateAndCreateBuffer(memoryManager, gpuVa, size); - result = new BufferCacheEntry(address, gpuVa, GetBuffer(address, size)); - - _modifiedCache[gpuVa] = result; + Prune(); } - outAddr = result.Address; + // Align the address to avoid creating too many entries on the quick lookup dictionary. + ulong mask = BufferAlignmentMask; + ulong alignedGpuVa = gpuVa & (~mask); + ulong alignedEndGpuVa = (gpuVa + size + mask) & (~mask); + + size = alignedEndGpuVa - alignedGpuVa; + + if (!_modifiedCache.TryGetValue(alignedGpuVa, out BufferCacheEntry result) || + result.EndGpuAddress < alignedEndGpuVa || + result.UnmappedSequence != result.Buffer.UnmappedSequence) + { + ulong address = TranslateAndCreateBuffer(memoryManager, alignedGpuVa, size); + result = new BufferCacheEntry(address, alignedGpuVa, GetBuffer(address, size)); + + _modifiedCache[alignedGpuVa] = result; + } + + outAddr = result.Address | (gpuVa & mask); return result.Buffer.IsModified(result.Address, size); } @@ -435,6 +453,54 @@ namespace Ryujinx.Graphics.Gpu.Memory } } + /// + /// Prune any invalid entries from a quick access dictionary. + /// + /// Dictionary to prune + /// List used to track entries to delete + private void Prune(Dictionary dictionary, ref List toDelete) + { + foreach (var entry in dictionary) + { + if (entry.Value.UnmappedSequence != entry.Value.Buffer.UnmappedSequence) + { + (toDelete ??= new()).Add(entry.Key); + } + } + + if (toDelete != null) + { + foreach (ulong entry in toDelete) + { + dictionary.Remove(entry); + } + } + } + + /// + /// Prune any invalid entries from the quick access dictionaries. + /// + private void Prune() + { + List toDelete = null; + + Prune(_dirtyCache, ref toDelete); + + toDelete?.Clear(); + + Prune(_modifiedCache, ref toDelete); + + _pruneCaches = false; + } + + /// + /// Queues a prune of invalid entries the next time a dictionary cache is accessed. + /// + public void QueuePrune() + { + _pruneCaches = true; + } + /// /// Disposes all buffers in the cache. /// It's an error to use the buffer manager after disposal.