Add short duration texture cache (#3754)

* Add short duration texture cache

This texture cache takes textures that lose their last pool reference and keeps them alive until the next frame, or until an incompatible overlap removes it. This is done since under certain circumstances, a texture's reference can be wiped from a pool despite it still being in use - though typically the reference will return when rendering the next frame.

While this may slightly increase texture memory usage when quickly going through a bunch of temporary textures, it's still bounded due to the overlap removal rule.

This greatly increases performance in Hyrule Warriors: Age of Calamity. It may positively affect some UE4 games which dip framerate severely under certain circumstances.

* Small optimization

* Don't forget this.

* Add short cache dictionary

* Address feedback

* Address some feedback
This commit is contained in:
riperiperi 2023-01-17 03:39:46 +00:00 committed by GitHub
parent e68650237d
commit f0e27a23a5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 228 additions and 11 deletions

View file

@ -4,6 +4,28 @@ using System.Collections.Generic;
namespace Ryujinx.Graphics.Gpu.Image namespace Ryujinx.Graphics.Gpu.Image
{ {
/// <summary>
/// An entry on the short duration texture cache.
/// </summary>
class ShortTextureCacheEntry
{
public readonly TextureDescriptor Descriptor;
public readonly int InvalidatedSequence;
public readonly Texture Texture;
/// <summary>
/// Create a new entry on the short duration texture cache.
/// </summary>
/// <param name="descriptor">Last descriptor that referenced the texture</param>
/// <param name="texture">The texture</param>
public ShortTextureCacheEntry(TextureDescriptor descriptor, Texture texture)
{
Descriptor = descriptor;
InvalidatedSequence = texture.InvalidatedSequence;
Texture = texture;
}
}
/// <summary> /// <summary>
/// A texture cache that automatically removes older textures that are not used for some time. /// A texture cache that automatically removes older textures that are not used for some time.
/// The cache works with a rotated list with a fixed size. When new textures are added, the /// The cache works with a rotated list with a fixed size. When new textures are added, the
@ -16,6 +38,11 @@ namespace Ryujinx.Graphics.Gpu.Image
private readonly LinkedList<Texture> _textures; private readonly LinkedList<Texture> _textures;
private readonly ConcurrentQueue<Texture> _deferredRemovals; private readonly ConcurrentQueue<Texture> _deferredRemovals;
private HashSet<ShortTextureCacheEntry> _shortCacheBuilder;
private HashSet<ShortTextureCacheEntry> _shortCache;
private Dictionary<TextureDescriptor, ShortTextureCacheEntry> _shortCacheLookup;
/// <summary> /// <summary>
/// Creates a new instance of the automatic deletion cache. /// Creates a new instance of the automatic deletion cache.
/// </summary> /// </summary>
@ -23,6 +50,11 @@ namespace Ryujinx.Graphics.Gpu.Image
{ {
_textures = new LinkedList<Texture>(); _textures = new LinkedList<Texture>();
_deferredRemovals = new ConcurrentQueue<Texture>(); _deferredRemovals = new ConcurrentQueue<Texture>();
_shortCacheBuilder = new HashSet<ShortTextureCacheEntry>();
_shortCache = new HashSet<ShortTextureCacheEntry>();
_shortCacheLookup = new Dictionary<TextureDescriptor, ShortTextureCacheEntry>();
} }
/// <summary> /// <summary>
@ -130,6 +162,85 @@ namespace Ryujinx.Graphics.Gpu.Image
_deferredRemovals.Enqueue(texture); _deferredRemovals.Enqueue(texture);
} }
/// <summary>
/// Attempt to find a texture on the short duration cache.
/// </summary>
/// <param name="descriptor">The texture descriptor</param>
/// <returns>The texture if found, null otherwise</returns>
public Texture FindShortCache(in TextureDescriptor descriptor)
{
if (_shortCacheLookup.Count > 0 && _shortCacheLookup.TryGetValue(descriptor, out var entry))
{
if (entry.InvalidatedSequence == entry.Texture.InvalidatedSequence)
{
return entry.Texture;
}
else
{
_shortCacheLookup.Remove(descriptor);
}
}
return null;
}
/// <summary>
/// Removes a texture from the short duration cache.
/// </summary>
/// <param name="texture">Texture to remove from the short cache</param>
public void RemoveShortCache(Texture texture)
{
bool removed = _shortCache.Remove(texture.ShortCacheEntry);
removed |= _shortCacheBuilder.Remove(texture.ShortCacheEntry);
if (removed)
{
texture.DecrementReferenceCount();
_shortCacheLookup.Remove(texture.ShortCacheEntry.Descriptor);
texture.ShortCacheEntry = null;
}
}
/// <summary>
/// Adds a texture to the short duration cache.
/// It starts in the builder set, and it is moved into the deletion set on next process.
/// </summary>
/// <param name="texture">Texture to add to the short cache</param>
/// <param name="descriptor">Last used texture descriptor</param>
public void AddShortCache(Texture texture, ref TextureDescriptor descriptor)
{
var entry = new ShortTextureCacheEntry(descriptor, texture);
_shortCacheBuilder.Add(entry);
_shortCacheLookup.Add(entry.Descriptor, entry);
texture.ShortCacheEntry = entry;
texture.IncrementReferenceCount();
}
/// <summary>
/// Delete textures from the short duration cache.
/// Moves the builder set to be deleted on next process.
/// </summary>
public void ProcessShortCache()
{
HashSet<ShortTextureCacheEntry> toRemove = _shortCache;
foreach (var entry in toRemove)
{
entry.Texture.DecrementReferenceCount();
_shortCacheLookup.Remove(entry.Descriptor);
entry.Texture.ShortCacheEntry = null;
}
toRemove.Clear();
_shortCache = _shortCacheBuilder;
_shortCacheBuilder = toRemove;
}
public IEnumerator<Texture> GetEnumerator() public IEnumerator<Texture> GetEnumerator()
{ {
return _textures.GetEnumerator(); return _textures.GetEnumerator();

View file

@ -91,7 +91,17 @@ namespace Ryujinx.Graphics.Gpu.Image
/// <returns>A reference to the descriptor</returns> /// <returns>A reference to the descriptor</returns>
public ref readonly T2 GetDescriptorRef(int id) public ref readonly T2 GetDescriptorRef(int id)
{ {
return ref MemoryMarshal.Cast<byte, T2>(PhysicalMemory.GetSpan(Address + (ulong)id * DescriptorSize, DescriptorSize))[0]; return ref GetDescriptorRefAddress(Address + (ulong)id * DescriptorSize);
}
/// <summary>
/// Gets a reference to the descriptor for a given address.
/// </summary>
/// <param name="address">Address of the descriptor</param>
/// <returns>A reference to the descriptor</returns>
public ref readonly T2 GetDescriptorRefAddress(ulong address)
{
return ref MemoryMarshal.Cast<byte, T2>(PhysicalMemory.GetSpan(address, DescriptorSize))[0];
} }
/// <summary> /// <summary>

View file

@ -138,6 +138,10 @@ namespace Ryujinx.Graphics.Gpu.Image
public LinkedListNode<Texture> CacheNode { get; set; } public LinkedListNode<Texture> CacheNode { get; set; }
/// <summary> /// <summary>
/// Entry for this texture in the short duration cache, if present.
/// </summary>
public ShortTextureCacheEntry ShortCacheEntry { get; set; }
/// Physical memory ranges where the texture data is located. /// Physical memory ranges where the texture data is located.
/// </summary> /// </summary>
public MultiRange Range { get; private set; } public MultiRange Range { get; private set; }
@ -1555,6 +1559,20 @@ namespace Ryujinx.Graphics.Gpu.Image
_poolOwners.Add(new TexturePoolOwner { Pool = pool, ID = id }); _poolOwners.Add(new TexturePoolOwner { Pool = pool, ID = id });
} }
_referenceCount++; _referenceCount++;
if (ShortCacheEntry != null)
{
_physicalMemory.TextureCache.RemoveShortCache(this);
}
}
/// <summary>
/// Indicates that the texture has one reference left, and will delete on reference decrement.
/// </summary>
/// <returns>True if there is one reference remaining, false otherwise</returns>
public bool HasOneReference()
{
return _referenceCount == 1;
} }
/// <summary> /// <summary>
@ -1624,6 +1642,14 @@ namespace Ryujinx.Graphics.Gpu.Image
_poolOwners.Clear(); _poolOwners.Clear();
} }
if (ShortCacheEntry != null && _context.IsGpuThread())
{
// If this is called from another thread (unmapped), the short cache will
// have to remove this texture on a future tick.
_physicalMemory.TextureCache.RemoveShortCache(this);
}
InvalidatedSequence++; InvalidatedSequence++;
} }

View file

@ -894,6 +894,16 @@ namespace Ryujinx.Graphics.Gpu.Image
} }
} }
/// <summary>
/// Attempt to find a texture on the short duration cache.
/// </summary>
/// <param name="descriptor">The texture descriptor</param>
/// <returns>The texture if found, null otherwise</returns>
public Texture FindShortCache(in TextureDescriptor descriptor)
{
return _cache.FindShortCache(descriptor);
}
/// <summary> /// <summary>
/// Tries to find an existing texture matching the given buffer copy destination. If none is found, returns null. /// Tries to find an existing texture matching the given buffer copy destination. If none is found, returns null.
/// </summary> /// </summary>
@ -1178,6 +1188,33 @@ namespace Ryujinx.Graphics.Gpu.Image
_cache.RemoveDeferred(texture); _cache.RemoveDeferred(texture);
} }
/// <summary>
/// Adds a texture to the short duration cache. This typically keeps it alive for two ticks.
/// </summary>
/// <param name="texture">Texture to add to the short cache</param>
/// <param name="descriptor">Last used texture descriptor</param>
public void AddShortCache(Texture texture, ref TextureDescriptor descriptor)
{
_cache.AddShortCache(texture, ref descriptor);
}
/// <summary>
/// Removes a texture from the short duration cache.
/// </summary>
/// <param name="texture">Texture to remove from the short cache</param>
public void RemoveShortCache(Texture texture)
{
_cache.RemoveShortCache(texture);
}
/// <summary>
/// Ticks periodic elements of the texture cache.
/// </summary>
public void Tick()
{
_cache.ProcessShortCache();
}
/// <summary> /// <summary>
/// Disposes all textures and samplers in the cache. /// Disposes all textures and samplers in the cache.
/// It's an error to use the texture cache after disposal. /// It's an error to use the texture cache after disposal.

View file

@ -1,3 +1,4 @@
using System;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics; using System.Runtime.Intrinsics;
@ -6,7 +7,7 @@ namespace Ryujinx.Graphics.Gpu.Image
/// <summary> /// <summary>
/// Maxwell texture descriptor, as stored on the GPU texture pool memory region. /// Maxwell texture descriptor, as stored on the GPU texture pool memory region.
/// </summary> /// </summary>
struct TextureDescriptor : ITextureDescriptor struct TextureDescriptor : ITextureDescriptor, IEquatable<TextureDescriptor>
{ {
#pragma warning disable CS0649 #pragma warning disable CS0649
public uint Word0; public uint Word0;
@ -249,5 +250,24 @@ namespace Ryujinx.Graphics.Gpu.Image
{ {
return Unsafe.As<TextureDescriptor, Vector256<byte>>(ref this).Equals(Unsafe.As<TextureDescriptor, Vector256<byte>>(ref other)); return Unsafe.As<TextureDescriptor, Vector256<byte>>(ref this).Equals(Unsafe.As<TextureDescriptor, Vector256<byte>>(ref other));
} }
/// <summary>
/// Check if two descriptors are equal.
/// </summary>
/// <param name="other">The descriptor to compare against</param>
/// <returns>True if they are equal, false otherwise</returns>
public bool Equals(TextureDescriptor other)
{
return Equals(ref other);
}
/// <summary>
/// Gets a hash code for this descriptor.
/// </summary>
/// <returns>The hash code for this descriptor.</returns>
public override int GetHashCode()
{
return Unsafe.As<TextureDescriptor, Vector256<byte>>(ref this).GetHashCode();
}
} }
} }

View file

@ -52,16 +52,21 @@ namespace Ryujinx.Graphics.Gpu.Image
if (texture == null) if (texture == null)
{ {
TextureInfo info = GetInfo(descriptor, out int layerSize); texture = PhysicalMemory.TextureCache.FindShortCache(descriptor);
ProcessDereferenceQueue();
texture = PhysicalMemory.TextureCache.FindOrCreateTexture(_channel.MemoryManager, TextureSearchFlags.ForSampler, info, layerSize);
// If this happens, then the texture address is invalid, we can't add it to the cache.
if (texture == null) if (texture == null)
{ {
return ref descriptor; TextureInfo info = GetInfo(descriptor, out int layerSize);
ProcessDereferenceQueue();
texture = PhysicalMemory.TextureCache.FindOrCreateTexture(_channel.MemoryManager, TextureSearchFlags.ForSampler, info, layerSize);
// If this happens, then the texture address is invalid, we can't add it to the cache.
if (texture == null)
{
return ref descriptor;
}
} }
texture.IncrementReferenceCount(this, id); texture.IncrementReferenceCount(this, id);
@ -208,15 +213,21 @@ namespace Ryujinx.Graphics.Gpu.Image
if (texture != null) if (texture != null)
{ {
TextureDescriptor descriptor = PhysicalMemory.Read<TextureDescriptor>(address); ref TextureDescriptor cachedDescriptor = ref DescriptorCache[id];
ref readonly TextureDescriptor descriptor = ref GetDescriptorRefAddress(address);
// If the descriptors are the same, the texture is the same, // If the descriptors are the same, the texture is the same,
// we don't need to remove as it was not modified. Just continue. // we don't need to remove as it was not modified. Just continue.
if (descriptor.Equals(ref DescriptorCache[id])) if (descriptor.Equals(ref cachedDescriptor))
{ {
continue; continue;
} }
if (texture.HasOneReference())
{
_channel.MemoryManager.Physical.TextureCache.AddShortCache(texture, ref cachedDescriptor);
}
texture.DecrementReferenceCount(this, id); texture.DecrementReferenceCount(this, id);
Items[id] = null; Items[id] = null;

View file

@ -204,6 +204,8 @@ namespace Ryujinx.Graphics.Gpu
Texture texture = pt.Cache.FindOrCreateTexture(null, TextureSearchFlags.WithUpscale, pt.Info, 0, null, pt.Range); Texture texture = pt.Cache.FindOrCreateTexture(null, TextureSearchFlags.WithUpscale, pt.Info, 0, null, pt.Range);
pt.Cache.Tick();
texture.SynchronizeMemory(); texture.SynchronizeMemory();
ImageCrop crop = pt.Crop; ImageCrop crop = pt.Crop;