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 { /// <summary> /// Represent a cache collection handling one shader cache. /// </summary> class CacheCollection : IDisposable { /// <summary> /// Possible operation to do on the <see cref="_fileWriterWorkerQueue"/>. /// </summary> private enum CacheFileOperation { /// <summary> /// Save a new entry in the temp cache. /// </summary> SaveTempEntry, /// <summary> /// Save the hash manifest. /// </summary> SaveManifest, /// <summary> /// Remove entries from the hash manifest and save it. /// </summary> RemoveManifestEntries, /// <summary> /// Remove entries from the hash manifest and save it, and also deletes the temporary file. /// </summary> RemoveManifestEntryAndTempFile, /// <summary> /// Flush temporary cache to archive. /// </summary> FlushToArchive, /// <summary> /// Signal when hitting this point. This is useful to know if all previous operations were performed. /// </summary> Synchronize } /// <summary> /// Represent an operation to perform on the <see cref="_fileWriterWorkerQueue"/>. /// </summary> private class CacheFileOperationTask { /// <summary> /// The type of operation to perform. /// </summary> public CacheFileOperation Type; /// <summary> /// The data associated to this operation or null. /// </summary> public object Data; } /// <summary> /// Data associated to the <see cref="CacheFileOperation.SaveTempEntry"/> operation. /// </summary> private class CacheFileSaveEntryTaskData { /// <summary> /// The key of the entry to cache. /// </summary> public Hash128 Key; /// <summary> /// The value of the entry to cache. /// </summary> public byte[] Value; } /// <summary> /// The directory of the shader cache. /// </summary> private readonly string _cacheDirectory; /// <summary> /// The version of the cache. /// </summary> private readonly ulong _version; /// <summary> /// The hash type of the cache. /// </summary> private readonly CacheHashType _hashType; /// <summary> /// The graphics API of the cache. /// </summary> private readonly CacheGraphicsApi _graphicsApi; /// <summary> /// The table of all the hash registered in the cache. /// </summary> private HashSet<Hash128> _hashTable; /// <summary> /// The queue of operations to be performed by the file writer worker. /// </summary> private AsyncWorkQueue<CacheFileOperationTask> _fileWriterWorkerQueue; /// <summary> /// Main storage of the cache collection. /// </summary> private ZipArchive _cacheArchive; /// <summary> /// Indicates if the cache collection supports modification. /// </summary> public bool IsReadOnly { get; } /// <summary> /// Immutable copy of the hash table. /// </summary> public ReadOnlySpan<Hash128> HashTable => _hashTable.ToArray(); /// <summary> /// Get the temp path to the cache data directory. /// </summary> /// <returns>The temp path to the cache data directory</returns> private string GetCacheTempDataPath() => CacheHelper.GetCacheTempDataPath(_cacheDirectory); /// <summary> /// The path to the cache archive file. /// </summary> /// <returns>The path to the cache archive file</returns> private string GetArchivePath() => CacheHelper.GetArchivePath(_cacheDirectory); /// <summary> /// The path to the cache manifest file. /// </summary> /// <returns>The path to the cache manifest file</returns> private string GetManifestPath() => CacheHelper.GetManifestPath(_cacheDirectory); /// <summary> /// Create a new temp path to the given cached file via its hash. /// </summary> /// <param name="key">The hash of the cached data</param> /// <returns>New path to the given cached file</returns> private string GenCacheTempFilePath(Hash128 key) => CacheHelper.GenCacheTempFilePath(_cacheDirectory, key); /// <summary> /// Create a new cache collection. /// </summary> /// <param name="baseCacheDirectory">The directory of the shader cache</param> /// <param name="hashType">The hash type of the shader cache</param> /// <param name="graphicsApi">The graphics api of the shader cache</param> /// <param name="shaderProvider">The shader provider name of the shader cache</param> /// <param name="cacheName">The name of the cache</param> /// <param name="version">The version of the cache</param> 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<Hash128>(); IsReadOnly = CacheHelper.IsArchiveReadOnly(GetArchivePath()); Load(); _fileWriterWorkerQueue = new AsyncWorkQueue<CacheFileOperationTask>(HandleCacheTask, $"CacheCollection.Worker.{cacheName}"); } /// <summary> /// Load the cache manifest file and recreate it if invalid. /// </summary> private void Load() { bool isValid = false; if (Directory.Exists(_cacheDirectory)) { string manifestPath = GetManifestPath(); if (File.Exists(manifestPath)) { Memory<byte> rawManifest = File.ReadAllBytes(manifestPath); if (MemoryMarshal.TryRead(rawManifest.Span, out CacheManifestHeader manifestHeader)) { Memory<byte> hashTableRaw = rawManifest.Slice(Unsafe.SizeOf<CacheManifestHeader>()); isValid = manifestHeader.IsValid(_graphicsApi, _hashType, hashTableRaw.Span) && _version == manifestHeader.Version; if (isValid) { ReadOnlySpan<Hash128> hashTable = MemoryMarshal.Cast<byte, Hash128>(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(); } /// <summary> /// Queue a task to remove entries from the hash manifest. /// </summary> /// <param name="entries">Entries to remove from the manifest</param> public void RemoveManifestEntriesAsync(HashSet<Hash128> 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 }); } /// <summary> /// Remove given entries from the manifest. /// </summary> /// <param name="entries">Entries to remove from the manifest</param> private void RemoveManifestEntries(HashSet<Hash128> entries) { lock (_hashTable) { foreach (Hash128 entry in entries) { _hashTable.Remove(entry); } SaveManifest(); } } /// <summary> /// Remove given entry from the manifest and delete the temporary file. /// </summary> /// <param name="entry">Entry to remove from the manifest</param> private void RemoveManifestEntryAndTempFile(Hash128 entry) { lock (_hashTable) { _hashTable.Remove(entry); SaveManifest(); } File.Delete(GenCacheTempFilePath(entry)); } /// <summary> /// Queue a task to flush temporary files to the archive on the worker. /// </summary> public void FlushToArchiveAsync() { _fileWriterWorkerQueue.Add(new CacheFileOperationTask { Type = CacheFileOperation.FlushToArchive }); } /// <summary> /// Wait for all tasks before this given point to be done. /// </summary> public void Synchronize() { using (ManualResetEvent evnt = new ManualResetEvent(false)) { _fileWriterWorkerQueue.Add(new CacheFileOperationTask { Type = CacheFileOperation.Synchronize, Data = evnt }); evnt.WaitOne(); } } /// <summary> /// Flush temporary files to the archive. /// </summary> /// <remarks>This dispose <see cref="_cacheArchive"/> if not null and reinstantiate it.</remarks> private void FlushToArchive() { EnsureArchiveUpToDate(); // Open the zip in readonly to avoid anyone modifying/corrupting it during normal operations. _cacheArchive = ZipFile.Open(GetArchivePath(), ZipArchiveMode.Read); } /// <summary> /// Save temporary files not in archive. /// </summary> /// <remarks>This dispose <see cref="_cacheArchive"/> if not null.</remarks> public void EnsureArchiveUpToDate() { // First close previous opened instance if found. if (_cacheArchive != null) { _cacheArchive.Dispose(); } 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; } // 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) { CacheHelper.EnsureArchiveUpToDate(_cacheDirectory, _cacheArchive, _hashTable); // 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}."); } /// <summary> /// Save the manifest file. /// </summary> private void SaveManifest() { byte[] data; lock (_hashTable) { data = CacheHelper.ComputeManifest(_version, _graphicsApi, _hashType, _hashTable); } File.WriteAllBytes(GetManifestPath(), data); } /// <summary> /// Get a cached file with the given hash. /// </summary> /// <param name="keyHash">The given hash</param> /// <returns>The cached file if present or null</returns> public byte[] GetValueRaw(ref Hash128 keyHash) { return GetValueRawFromArchive(ref keyHash) ?? GetValueRawFromFile(ref keyHash); } /// <summary> /// Get a cached file with the given hash that is present in the archive. /// </summary> /// <param name="keyHash">The given hash</param> /// <returns>The cached file if present or null</returns> private byte[] GetValueRawFromArchive(ref Hash128 keyHash) { bool found; lock (_hashTable) { found = _hashTable.Contains(keyHash); } if (found) { return CacheHelper.ReadFromArchive(_cacheArchive, keyHash); } return null; } /// <summary> /// Get a cached file with the given hash that is not present in the archive. /// </summary> /// <param name="keyHash">The given hash</param> /// <returns>The cached file if present or null</returns> 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<Hash128>)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}"); } } /// <summary> /// Save a new entry in the temp cache. /// </summary> /// <param name="entry">The entry to save in the temp cache</param> private void SaveTempEntry(CacheFileSaveEntryTaskData entry) { string tempPath = GenCacheTempFilePath(entry.Key); File.WriteAllBytes(tempPath, entry.Value); } /// <summary> /// Add a new value in the cache with a given hash. /// </summary> /// <param name="keyHash">The hash to use for the value in the cache</param> /// <param name="value">The value to cache</param> 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, }); } /// <summary> /// Replace a value at the given hash in the cache. /// </summary> /// <param name="keyHash">The hash to use for the value in the cache</param> /// <param name="value">The value to cache</param> 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 } }); } /// <summary> /// Removes a value at the given hash from the cache. /// </summary> /// <param name="keyHash">The hash of the value in the cache</param> 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(); } } } }