From 48f6570557fc76496936514d94e3ccddf55ec633 Mon Sep 17 00:00:00 2001 From: Mary Date: Fri, 13 Nov 2020 00:15:34 +0100 Subject: Salieri: shader cache (#1701) Here come Salieri, my implementation of a disk shader cache! "I'm sure you know why I named it that." "It doesn't really mean anything." This implementation collects shaders at runtime and cache them to be later compiled when starting a game. --- .../Shader/Cache/CacheCollection.cs | 595 +++++++++++++++++++++ 1 file changed, 595 insertions(+) create mode 100644 Ryujinx.Graphics.Gpu/Shader/Cache/CacheCollection.cs (limited to 'Ryujinx.Graphics.Gpu/Shader/Cache/CacheCollection.cs') diff --git a/Ryujinx.Graphics.Gpu/Shader/Cache/CacheCollection.cs b/Ryujinx.Graphics.Gpu/Shader/Cache/CacheCollection.cs new file mode 100644 index 00000000..effd893a --- /dev/null +++ b/Ryujinx.Graphics.Gpu/Shader/Cache/CacheCollection.cs @@ -0,0 +1,595 @@ +using Ryujinx.Common; +using Ryujinx.Common.Logging; +using Ryujinx.Graphics.Gpu.Shader.Cache.Definition; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; + +namespace Ryujinx.Graphics.Gpu.Shader.Cache +{ + /// + /// Represent a cache collection handling one shader cache. + /// + class CacheCollection : IDisposable + { + /// + /// Possible operation to do on the . + /// + private enum CacheFileOperation + { + /// + /// Save a new entry in the temp cache. + /// + SaveTempEntry, + + /// + /// Save the hash manifest. + /// + SaveManifest, + + /// + /// Flush temporary cache to archive. + /// + FlushToArchive, + + /// + /// Signal when hitting this point. This is useful to know if all previous operations were performed. + /// + Synchronize + } + + /// + /// Represent an operation to perform on the . + /// + private class CacheFileOperationTask + { + /// + /// The type of operation to perform. + /// + public CacheFileOperation Type; + + /// + /// The data associated to this operation or null. + /// + public object Data; + } + + /// + /// Data associated to the operation. + /// + private class CacheFileSaveEntryTaskData + { + /// + /// The key of the entry to cache. + /// + public Hash128 Key; + + /// + /// The value of the entry to cache. + /// + public byte[] Value; + } + + /// + /// The directory of the shader cache. + /// + private readonly string _cacheDirectory; + + /// + /// The version of the cache. + /// + private readonly ulong _version; + + /// + /// The hash type of the cache. + /// + private readonly CacheHashType _hashType; + + /// + /// The graphics API of the cache. + /// + private readonly CacheGraphicsApi _graphicsApi; + + /// + /// The table of all the hash registered in the cache. + /// + private HashSet _hashTable; + + /// + /// The queue of operations to be performed by the file writer worker. + /// + private AsyncWorkQueue _fileWriterWorkerQueue; + + /// + /// Main storage of the cache collection. + /// + private ZipArchive _cacheArchive; + + /// + /// Immutable copy of the hash table. + /// + public ReadOnlySpan HashTable => _hashTable.ToArray(); + + /// + /// Get the temp path to the cache data directory. + /// + /// The temp path to the cache data directory + private string GetCacheTempDataPath() => Path.Combine(_cacheDirectory, "temp"); + + /// + /// The path to the cache archive file. + /// + /// The path to the cache archive file + private string GetArchivePath() => Path.Combine(_cacheDirectory, "cache.zip"); + + /// + /// The path to the cache manifest file. + /// + /// The path to the cache manifest file + private string GetManifestPath() => Path.Combine(_cacheDirectory, "cache.info"); + + /// + /// Create a new temp path to the given cached file via its hash. + /// + /// The hash of the cached data + /// New path to the given cached file + private string GenCacheTempFilePath(Hash128 key) => Path.Combine(GetCacheTempDataPath(), key.ToString()); + + /// + /// Create a new cache collection. + /// + /// The directory of the shader cache + /// The hash type of the shader cache + /// The graphics api of the shader cache + /// The shader provider name of the shader cache + /// The name of the cache + /// The version of the cache + public CacheCollection(string baseCacheDirectory, CacheHashType hashType, CacheGraphicsApi graphicsApi, string shaderProvider, string cacheName, ulong version) + { + if (hashType != CacheHashType.XxHash128) + { + throw new NotImplementedException($"{hashType}"); + } + + _cacheDirectory = GenerateCachePath(baseCacheDirectory, graphicsApi, shaderProvider, cacheName); + _graphicsApi = graphicsApi; + _hashType = hashType; + _version = version; + _hashTable = new HashSet(); + + Load(); + + _fileWriterWorkerQueue = new AsyncWorkQueue(HandleCacheTask, $"CacheCollection.Worker.{cacheName}"); + } + + /// + /// Load the cache manifest file and recreate it if invalid. + /// + private void Load() + { + bool isInvalid = false; + + if (!Directory.Exists(_cacheDirectory)) + { + isInvalid = true; + } + else + { + string manifestPath = GetManifestPath(); + + if (File.Exists(manifestPath)) + { + Memory rawManifest = File.ReadAllBytes(manifestPath); + + if (MemoryMarshal.TryRead(rawManifest.Span, out CacheManifestHeader manifestHeader)) + { + Memory hashTableRaw = rawManifest.Slice(Unsafe.SizeOf()); + + isInvalid = !manifestHeader.IsValid(_version, _graphicsApi, _hashType, hashTableRaw.Span); + + if (!isInvalid) + { + ReadOnlySpan hashTable = MemoryMarshal.Cast(hashTableRaw.Span); + + foreach (Hash128 hash in hashTable) + { + _hashTable.Add(hash); + } + } + } + } + else + { + isInvalid = true; + } + } + + if (isInvalid) + { + Logger.Warning?.Print(LogClass.Gpu, $"Shader collection \"{_cacheDirectory}\" got invalidated, cache will need to be rebuilt."); + + if (Directory.Exists(_cacheDirectory)) + { + Directory.Delete(_cacheDirectory, true); + } + + Directory.CreateDirectory(_cacheDirectory); + + SaveManifest(); + } + + FlushToArchive(); + } + + /// + /// Remove given entries from the manifest. + /// + /// Entries to remove from the manifest + public void RemoveManifestEntries(HashSet entries) + { + lock (_hashTable) + { + foreach (Hash128 entry in entries) + { + _hashTable.Remove(entry); + } + + SaveManifest(); + } + } + + /// + /// Queue a task to flush temporary files to the archive on the worker. + /// + public void FlushToArchiveAsync() + { + _fileWriterWorkerQueue.Add(new CacheFileOperationTask + { + Type = CacheFileOperation.FlushToArchive + }); + } + + /// + /// Wait for all tasks before this given point to be done. + /// + public void Synchronize() + { + using (ManualResetEvent evnt = new ManualResetEvent(false)) + { + _fileWriterWorkerQueue.Add(new CacheFileOperationTask + { + Type = CacheFileOperation.Synchronize, + Data = evnt + }); + + evnt.WaitOne(); + } + } + + /// + /// Flush temporary files to the archive. + /// + /// This dispose if not null and reinstantiate it. + private void FlushToArchive() + { + EnsureArchiveUpToDate(); + + // Open the zip in readonly to avoid anyone modifying/corrupting it during normal operations. + _cacheArchive = ZipFile.Open(GetArchivePath(), ZipArchiveMode.Read); + } + + /// + /// Save temporary files not in archive. + /// + /// This dispose if not null. + public void EnsureArchiveUpToDate() + { + // First close previous opened instance if found. + if (_cacheArchive != null) + { + _cacheArchive.Dispose(); + } + + string archivePath = GetArchivePath(); + + // Open the zip in read/write. + _cacheArchive = ZipFile.Open(archivePath, ZipArchiveMode.Update); + + Logger.Info?.Print(LogClass.Gpu, $"Updating cache collection archive {archivePath}..."); + + // Update the content of the zip. + lock (_hashTable) + { + foreach (Hash128 hash in _hashTable) + { + string cacheTempFilePath = GenCacheTempFilePath(hash); + + if (File.Exists(cacheTempFilePath)) + { + string cacheHash = $"{hash}"; + + ZipArchiveEntry entry = _cacheArchive.GetEntry(cacheHash); + + entry?.Delete(); + + _cacheArchive.CreateEntryFromFile(cacheTempFilePath, cacheHash); + File.Delete(cacheTempFilePath); + } + } + + // Close the instance to force a flush. + _cacheArchive.Dispose(); + _cacheArchive = null; + + string cacheTempDataPath = GetCacheTempDataPath(); + + // Create the cache data path if missing. + if (!Directory.Exists(cacheTempDataPath)) + { + Directory.CreateDirectory(cacheTempDataPath); + } + } + + Logger.Info?.Print(LogClass.Gpu, $"Updated cache collection archive {archivePath}."); + } + + /// + /// Save the manifest file. + /// + private void SaveManifest() + { + CacheManifestHeader manifestHeader = new CacheManifestHeader(_version, _graphicsApi, _hashType); + + byte[] data; + + lock (_hashTable) + { + data = new byte[Unsafe.SizeOf() + _hashTable.Count * Unsafe.SizeOf()]; + + // CacheManifestHeader has the same size as a Hash128. + Span dataSpan = MemoryMarshal.Cast(data.AsSpan()).Slice(1); + + int i = 0; + + foreach (Hash128 hash in _hashTable) + { + dataSpan[i++] = hash; + } + } + + manifestHeader.UpdateChecksum(data.AsSpan().Slice(Unsafe.SizeOf())); + + MemoryMarshal.Write(data, ref manifestHeader); + + File.WriteAllBytes(GetManifestPath(), data); + } + + /// + /// Generate the path to the cache directory. + /// + /// The base of the cache directory + /// The graphics api in use + /// The name of the shader provider in use + /// The name of the cache + /// The path to the cache directory + private static string GenerateCachePath(string baseCacheDirectory, CacheGraphicsApi graphicsApi, string shaderProvider, string cacheName) + { + string graphicsApiName = graphicsApi switch + { + CacheGraphicsApi.OpenGL => "opengl", + CacheGraphicsApi.OpenGLES => "opengles", + CacheGraphicsApi.Vulkan => "vulkan", + CacheGraphicsApi.DirectX => "directx", + CacheGraphicsApi.Metal => "metal", + CacheGraphicsApi.Guest => "guest", + _ => throw new NotImplementedException(graphicsApi.ToString()), + }; + + return Path.Combine(baseCacheDirectory, graphicsApiName, shaderProvider, cacheName); + } + + /// + /// Get a cached file with the given hash. + /// + /// The given hash + /// The cached file if present or null + public byte[] GetValueRaw(ref Hash128 keyHash) + { + return GetValueRawFromArchive(ref keyHash) ?? GetValueRawFromFile(ref keyHash); + } + + /// + /// Get a cached file with the given hash that is present in the archive. + /// + /// The given hash + /// The cached file if present or null + private byte[] GetValueRawFromArchive(ref Hash128 keyHash) + { + bool found; + + lock (_hashTable) + { + found = _hashTable.Contains(keyHash); + } + + if (found) + { + ZipArchiveEntry archiveEntry = _cacheArchive.GetEntry($"{keyHash}"); + + if (archiveEntry != null) + { + try + { + byte[] result = new byte[archiveEntry.Length]; + + using (Stream archiveStream = archiveEntry.Open()) + { + archiveStream.Read(result); + + return result; + } + } + catch (Exception e) + { + Logger.Error?.Print(LogClass.Gpu, $"Cannot load cache file {keyHash} from archive"); + Logger.Error?.Print(LogClass.Gpu, e.ToString()); + } + } + } + + return null; + } + + /// + /// Get a cached file with the given hash that is not present in the archive. + /// + /// The given hash + /// The cached file if present or null + private byte[] GetValueRawFromFile(ref Hash128 keyHash) + { + bool found; + + lock (_hashTable) + { + found = _hashTable.Contains(keyHash); + } + + if (found) + { + string cacheTempFilePath = GenCacheTempFilePath(keyHash); + + try + { + return File.ReadAllBytes(GenCacheTempFilePath(keyHash)); + } + catch (Exception e) + { + Logger.Error?.Print(LogClass.Gpu, $"Cannot load cache file at {cacheTempFilePath}"); + Logger.Error?.Print(LogClass.Gpu, e.ToString()); + } + } + + return null; + } + + private void HandleCacheTask(CacheFileOperationTask task) + { + switch (task.Type) + { + case CacheFileOperation.SaveTempEntry: + SaveTempEntry((CacheFileSaveEntryTaskData)task.Data); + break; + case CacheFileOperation.SaveManifest: + SaveManifest(); + break; + case CacheFileOperation.FlushToArchive: + FlushToArchive(); + break; + case CacheFileOperation.Synchronize: + ((ManualResetEvent)task.Data).Set(); + break; + default: + throw new NotImplementedException($"{task.Type}"); + } + + } + + /// + /// Save a new entry in the temp cache. + /// + /// The entry to save in the temp cache + private void SaveTempEntry(CacheFileSaveEntryTaskData entry) + { + string tempPath = GenCacheTempFilePath(entry.Key); + + File.WriteAllBytes(tempPath, entry.Value); + } + + /// + /// Add a new value in the cache with a given hash. + /// + /// The hash to use for the value in the cache + /// The value to cache + public void AddValue(ref Hash128 keyHash, byte[] value) + { + Debug.Assert(value != null); + Debug.Assert(GetValueRaw(ref keyHash) != null); + + bool isAlreadyPresent; + + lock (_hashTable) + { + isAlreadyPresent = !_hashTable.Add(keyHash); + } + + if (isAlreadyPresent) + { + // NOTE: Used for debug + File.WriteAllBytes(GenCacheTempFilePath(new Hash128()), value); + + throw new InvalidOperationException($"Cache collision found on {GenCacheTempFilePath(keyHash)}"); + } + + // Queue file change operations + _fileWriterWorkerQueue.Add(new CacheFileOperationTask + { + Type = CacheFileOperation.SaveTempEntry, + Data = new CacheFileSaveEntryTaskData + { + Key = keyHash, + Value = value + } + }); + + // Save the manifest changes + _fileWriterWorkerQueue.Add(new CacheFileOperationTask + { + Type = CacheFileOperation.SaveManifest, + }); + } + + /// + /// Replace a value at the given hash in the cache. + /// + /// The hash to use for the value in the cache + /// The value to cache + public void ReplaceValue(ref Hash128 keyHash, byte[] value) + { + Debug.Assert(value != null); + + // Only queue file change operations + _fileWriterWorkerQueue.Add(new CacheFileOperationTask + { + Type = CacheFileOperation.SaveTempEntry, + Data = new CacheFileSaveEntryTaskData + { + Key = keyHash, + Value = value + } + }); + } + + public void Dispose() + { + Dispose(true); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + // Make sure all operations on _fileWriterWorkerQueue are done. + Synchronize(); + + _fileWriterWorkerQueue.Dispose(); + EnsureArchiveUpToDate(); + } + } + } +} -- cgit v1.2.3