using ICSharpCode.SharpZipLib.Zip; using Ryujinx.Common; using Ryujinx.Common.Logging; using Ryujinx.Graphics.GAL; using Ryujinx.Graphics.Gpu.Shader.Cache.Definition; using System; using System.Collections.Generic; using System.IO; namespace Ryujinx.Graphics.Gpu.Shader.Cache { /// /// Class handling shader cache migrations. /// static class CacheMigration { /// /// Check if the given cache version need to recompute its hash. /// /// The version in use /// The new version after migration /// True if a hash recompute is needed public static bool NeedHashRecompute(ulong version, out ulong newVersion) { const ulong TargetBrokenVersion = 1717; const ulong TargetFixedVersion = 1759; newVersion = TargetFixedVersion; if (version == TargetBrokenVersion) { return true; } return false; } private class StreamZipEntryDataSource : IStaticDataSource { private readonly ZipFile Archive; private readonly ZipEntry Entry; public StreamZipEntryDataSource(ZipFile archive, ZipEntry entry) { Archive = archive; Entry = entry; } public Stream GetSource() { return Archive.GetInputStream(Entry); } } /// /// Move a file with the name of a given hash to another in the cache archive. /// /// The archive in use /// The old key /// The new key private static void MoveEntry(ZipFile archive, Hash128 oldKey, Hash128 newKey) { ZipEntry oldGuestEntry = archive.GetEntry($"{oldKey}"); if (oldGuestEntry != null) { archive.Add(new StreamZipEntryDataSource(archive, oldGuestEntry), $"{newKey}", CompressionMethod.Deflated); archive.Delete(oldGuestEntry); } } /// /// Recompute all the hashes of a given cache. /// /// The guest cache directory path /// The host cache directory path /// The graphics api in use /// The hash type in use /// The version to write in the host and guest manifest after migration private static void RecomputeHashes(string guestBaseCacheDirectory, string hostBaseCacheDirectory, CacheGraphicsApi graphicsApi, CacheHashType hashType, ulong newVersion) { string guestManifestPath = CacheHelper.GetManifestPath(guestBaseCacheDirectory); string hostManifestPath = CacheHelper.GetManifestPath(hostBaseCacheDirectory); if (CacheHelper.TryReadManifestFile(guestManifestPath, CacheGraphicsApi.Guest, hashType, out _, out HashSet guestEntries)) { CacheHelper.TryReadManifestFile(hostManifestPath, graphicsApi, hashType, out _, out HashSet hostEntries); Logger.Info?.Print(LogClass.Gpu, "Shader cache hashes need to be recomputed, performing migration..."); string guestArchivePath = CacheHelper.GetArchivePath(guestBaseCacheDirectory); string hostArchivePath = CacheHelper.GetArchivePath(hostBaseCacheDirectory); ZipFile guestArchive = new ZipFile(File.Open(guestArchivePath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None)); ZipFile hostArchive = new ZipFile(File.Open(hostArchivePath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None)); CacheHelper.EnsureArchiveUpToDate(guestBaseCacheDirectory, guestArchive, guestEntries); CacheHelper.EnsureArchiveUpToDate(hostBaseCacheDirectory, hostArchive, hostEntries); int programIndex = 0; HashSet newEntries = new HashSet(); foreach (Hash128 oldHash in guestEntries) { byte[] guestProgram = CacheHelper.ReadFromArchive(guestArchive, oldHash); Logger.Info?.Print(LogClass.Gpu, $"Migrating shader {oldHash} ({programIndex + 1} / {guestEntries.Count})"); if (guestProgram != null) { ReadOnlySpan guestProgramReadOnlySpan = guestProgram; ReadOnlySpan cachedShaderEntries = GuestShaderCacheEntry.Parse(ref guestProgramReadOnlySpan, out GuestShaderCacheHeader fileHeader); TransformFeedbackDescriptor[] tfd = CacheHelper.ReadTransformFeedbackInformation(ref guestProgramReadOnlySpan, fileHeader); Hash128 newHash = CacheHelper.ComputeGuestHashFromCache(cachedShaderEntries, tfd); if (newHash != oldHash) { MoveEntry(guestArchive, oldHash, newHash); MoveEntry(hostArchive, oldHash, newHash); } else { Logger.Warning?.Print(LogClass.Gpu, $"Same hashes for shader {oldHash}"); } newEntries.Add(newHash); } programIndex++; } byte[] newGuestManifestContent = CacheHelper.ComputeManifest(newVersion, CacheGraphicsApi.Guest, hashType, newEntries); byte[] newHostManifestContent = CacheHelper.ComputeManifest(newVersion, graphicsApi, hashType, newEntries); File.WriteAllBytes(guestManifestPath, newGuestManifestContent); File.WriteAllBytes(hostManifestPath, newHostManifestContent); guestArchive.CommitUpdate(); hostArchive.CommitUpdate(); guestArchive.Close(); hostArchive.Close(); } } /// /// Check and run cache migration if needed. /// /// The base path of the cache /// The graphics api in use /// The hash type in use /// The shader provider name of the cache public static void Run(string baseCacheDirectory, CacheGraphicsApi graphicsApi, CacheHashType hashType, string shaderProvider) { string guestBaseCacheDirectory = CacheHelper.GenerateCachePath(baseCacheDirectory, CacheGraphicsApi.Guest, "", "program"); string hostBaseCacheDirectory = CacheHelper.GenerateCachePath(baseCacheDirectory, graphicsApi, shaderProvider, "host"); string guestArchivePath = CacheHelper.GetArchivePath(guestBaseCacheDirectory); string hostArchivePath = CacheHelper.GetArchivePath(hostBaseCacheDirectory); bool isReadOnly = CacheHelper.IsArchiveReadOnly(guestArchivePath) || CacheHelper.IsArchiveReadOnly(hostArchivePath); if (!isReadOnly && CacheHelper.TryReadManifestHeader(CacheHelper.GetManifestPath(guestBaseCacheDirectory), out CacheManifestHeader header)) { if (NeedHashRecompute(header.Version, out ulong newVersion)) { RecomputeHashes(guestBaseCacheDirectory, hostBaseCacheDirectory, graphicsApi, hashType, newVersion); } } } } }