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, /// /// Remove entries from the hash manifest and save it. /// RemoveManifestEntries, /// /// 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(); } /// /// Queue a task to remove entries from the hash manifest. /// /// Entries to remove from the manifest public void RemoveManifestEntriesAsync(HashSet entries) { _fileWriterWorkerQueue.Add(new CacheFileOperationTask { Type = CacheFileOperation.RemoveManifestEntries, Data = entries }); } /// /// Remove given entries from the manifest. /// /// Entries to remove from the manifest private 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.RemoveManifestEntries: RemoveManifestEntries((HashSet)task.Data); 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); 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(); } } } }