using ICSharpCode.SharpZipLib.Zip; 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.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, /// /// Remove entries from the hash manifest and save it, and also deletes the temporary file. /// RemoveManifestEntryAndTempFile, /// /// 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 ZipFile _cacheArchive; /// /// Indicates if the cache collection supports modification. /// public bool IsReadOnly { get; } /// /// 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() => CacheHelper.GetCacheTempDataPath(_cacheDirectory); /// /// The path to the cache archive file. /// /// The path to the cache archive file private string GetArchivePath() => CacheHelper.GetArchivePath(_cacheDirectory); /// /// The path to the cache manifest file. /// /// The path to the cache manifest file private string GetManifestPath() => CacheHelper.GetManifestPath(_cacheDirectory); /// /// 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) => CacheHelper.GenCacheTempFilePath(_cacheDirectory, key); /// /// 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 = CacheHelper.GenerateCachePath(baseCacheDirectory, graphicsApi, shaderProvider, cacheName); _graphicsApi = graphicsApi; _hashType = hashType; _version = version; _hashTable = new HashSet(); IsReadOnly = CacheHelper.IsArchiveReadOnly(GetArchivePath()); Load(); _fileWriterWorkerQueue = new AsyncWorkQueue(HandleCacheTask, $"CacheCollection.Worker.{cacheName}"); } /// /// Load the cache manifest file and recreate it if invalid. /// private void Load() { bool isValid = false; if (Directory.Exists(_cacheDirectory)) { 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()); isValid = manifestHeader.IsValid(_graphicsApi, _hashType, hashTableRaw.Span) && _version == manifestHeader.Version; if (isValid) { ReadOnlySpan hashTable = MemoryMarshal.Cast(hashTableRaw.Span); foreach (Hash128 hash in hashTable) { _hashTable.Add(hash); } } } } } if (!isValid) { 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) { if (IsReadOnly) { Logger.Warning?.Print(LogClass.Gpu, "Trying to remove manifest entries on a read-only cache, ignoring."); return; } _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(); } } /// /// Remove given entry from the manifest and delete the temporary file. /// /// Entry to remove from the manifest private void RemoveManifestEntryAndTempFile(Hash128 entry) { lock (_hashTable) { _hashTable.Remove(entry); SaveManifest(); } File.Delete(GenCacheTempFilePath(entry)); } /// /// 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 = new ZipFile(File.OpenRead(GetArchivePath())); } /// /// 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.Close(); } string archivePath = GetArchivePath(); if (IsReadOnly) { Logger.Warning?.Print(LogClass.Gpu, $"Cache collection archive in read-only, archiving task skipped."); return; } if (CacheHelper.IsArchiveReadOnly(archivePath)) { Logger.Warning?.Print(LogClass.Gpu, $"Cache collection archive in use, archiving task skipped."); return; } if (!File.Exists(archivePath)) { using (ZipFile newZip = ZipFile.Create(archivePath)) { // Workaround for SharpZipLib issue #395 newZip.BeginUpdate(); newZip.CommitUpdate(); } } // Open the zip in read/write. _cacheArchive = new ZipFile(File.Open(archivePath, FileMode.Open, FileAccess.ReadWrite, FileShare.None)); Logger.Info?.Print(LogClass.Gpu, $"Updating cache collection archive {archivePath}..."); // Update the content of the zip. lock (_hashTable) { CacheHelper.EnsureArchiveUpToDate(_cacheDirectory, _cacheArchive, _hashTable); // Close the instance to force a flush. _cacheArchive.Close(); _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() { byte[] data; lock (_hashTable) { data = CacheHelper.ComputeManifest(_version, _graphicsApi, _hashType, _hashTable); } File.WriteAllBytes(GetManifestPath(), data); } /// /// 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) { return CacheHelper.ReadFromArchive(_cacheArchive, keyHash); } 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) { return CacheHelper.ReadFromFile(GetCacheTempDataPath(), keyHash); } 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.RemoveManifestEntryAndTempFile: RemoveManifestEntryAndTempFile((Hash128)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) { if (IsReadOnly) { Logger.Warning?.Print(LogClass.Gpu, $"Trying to add {keyHash} on a read-only cache, ignoring."); return; } 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) { if (IsReadOnly) { Logger.Warning?.Print(LogClass.Gpu, $"Trying to replace {keyHash} on a read-only cache, ignoring."); return; } Debug.Assert(value != null); // Only queue file change operations _fileWriterWorkerQueue.Add(new CacheFileOperationTask { Type = CacheFileOperation.SaveTempEntry, Data = new CacheFileSaveEntryTaskData { Key = keyHash, Value = value } }); } /// /// Removes a value at the given hash from the cache. /// /// The hash of the value in the cache public void RemoveValue(ref Hash128 keyHash) { if (IsReadOnly) { Logger.Warning?.Print(LogClass.Gpu, $"Trying to remove {keyHash} on a read-only cache, ignoring."); return; } // Only queue file change operations _fileWriterWorkerQueue.Add(new CacheFileOperationTask { Type = CacheFileOperation.RemoveManifestEntryAndTempFile, Data = keyHash }); } 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(); } } } }