CacheHelper.cs 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526
  1. using Ryujinx.Common;
  2. using Ryujinx.Common.Configuration;
  3. using Ryujinx.Common.Logging;
  4. using Ryujinx.Graphics.GAL;
  5. using Ryujinx.Graphics.Gpu.Memory;
  6. using Ryujinx.Graphics.Gpu.Shader.Cache.Definition;
  7. using Ryujinx.Graphics.Shader;
  8. using Ryujinx.Graphics.Shader.Translation;
  9. using System;
  10. using System.Collections.Generic;
  11. using System.IO;
  12. using System.IO.Compression;
  13. using System.Runtime.CompilerServices;
  14. using System.Runtime.InteropServices;
  15. namespace Ryujinx.Graphics.Gpu.Shader.Cache
  16. {
  17. /// <summary>
  18. /// Helper to manipulate the disk shader cache.
  19. /// </summary>
  20. static class CacheHelper
  21. {
  22. /// <summary>
  23. /// Try to read the manifest header from a given file path.
  24. /// </summary>
  25. /// <param name="manifestPath">The path to the manifest file</param>
  26. /// <param name="header">The manifest header read</param>
  27. /// <returns>Return true if the manifest header was read</returns>
  28. public static bool TryReadManifestHeader(string manifestPath, out CacheManifestHeader header)
  29. {
  30. header = default;
  31. if (File.Exists(manifestPath))
  32. {
  33. Memory<byte> rawManifest = File.ReadAllBytes(manifestPath);
  34. if (MemoryMarshal.TryRead(rawManifest.Span, out header))
  35. {
  36. return true;
  37. }
  38. }
  39. return false;
  40. }
  41. /// <summary>
  42. /// Try to read the manifest from a given file path.
  43. /// </summary>
  44. /// <param name="manifestPath">The path to the manifest file</param>
  45. /// <param name="graphicsApi">The graphics api used by the cache</param>
  46. /// <param name="hashType">The hash type of the cache</param>
  47. /// <param name="header">The manifest header read</param>
  48. /// <param name="entries">The entries read from the cache manifest</param>
  49. /// <returns>Return true if the manifest was read</returns>
  50. public static bool TryReadManifestFile(string manifestPath, CacheGraphicsApi graphicsApi, CacheHashType hashType, out CacheManifestHeader header, out HashSet<Hash128> entries)
  51. {
  52. header = default;
  53. entries = new HashSet<Hash128>();
  54. if (File.Exists(manifestPath))
  55. {
  56. Memory<byte> rawManifest = File.ReadAllBytes(manifestPath);
  57. if (MemoryMarshal.TryRead(rawManifest.Span, out header))
  58. {
  59. Memory<byte> hashTableRaw = rawManifest.Slice(Unsafe.SizeOf<CacheManifestHeader>());
  60. bool isValid = header.IsValid(graphicsApi, hashType, hashTableRaw.Span);
  61. if (isValid)
  62. {
  63. ReadOnlySpan<Hash128> hashTable = MemoryMarshal.Cast<byte, Hash128>(hashTableRaw.Span);
  64. foreach (Hash128 hash in hashTable)
  65. {
  66. entries.Add(hash);
  67. }
  68. }
  69. return isValid;
  70. }
  71. }
  72. return false;
  73. }
  74. /// <summary>
  75. /// Compute a cache manifest from runtime data.
  76. /// </summary>
  77. /// <param name="version">The version of the cache</param>
  78. /// <param name="graphicsApi">The graphics api used by the cache</param>
  79. /// <param name="hashType">The hash type of the cache</param>
  80. /// <param name="entries">The entries in the cache</param>
  81. /// <returns>The cache manifest from runtime data</returns>
  82. public static byte[] ComputeManifest(ulong version, CacheGraphicsApi graphicsApi, CacheHashType hashType, HashSet<Hash128> entries)
  83. {
  84. if (hashType != CacheHashType.XxHash128)
  85. {
  86. throw new NotImplementedException($"{hashType}");
  87. }
  88. CacheManifestHeader manifestHeader = new CacheManifestHeader(version, graphicsApi, hashType);
  89. byte[] data = new byte[Unsafe.SizeOf<CacheManifestHeader>() + entries.Count * Unsafe.SizeOf<Hash128>()];
  90. // CacheManifestHeader has the same size as a Hash128.
  91. Span<Hash128> dataSpan = MemoryMarshal.Cast<byte, Hash128>(data.AsSpan()).Slice(1);
  92. int i = 0;
  93. foreach (Hash128 hash in entries)
  94. {
  95. dataSpan[i++] = hash;
  96. }
  97. manifestHeader.UpdateChecksum(data.AsSpan().Slice(Unsafe.SizeOf<CacheManifestHeader>()));
  98. MemoryMarshal.Write(data, ref manifestHeader);
  99. return data;
  100. }
  101. /// <summary>
  102. /// Get the base directory of the shader cache for a given title id.
  103. /// </summary>
  104. /// <param name="titleId">The title id of the target application</param>
  105. /// <returns>The base directory of the shader cache for a given title id</returns>
  106. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  107. public static string GetBaseCacheDirectory(string titleId) => Path.Combine(AppDataManager.GamesDirPath, titleId, "cache", "shader");
  108. /// <summary>
  109. /// Get the temp path to the cache data directory.
  110. /// </summary>
  111. /// <param name="cacheDirectory">The cache directory</param>
  112. /// <returns>The temp path to the cache data directory</returns>
  113. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  114. public static string GetCacheTempDataPath(string cacheDirectory) => Path.Combine(cacheDirectory, "temp");
  115. /// <summary>
  116. /// The path to the cache archive file.
  117. /// </summary>
  118. /// <param name="cacheDirectory">The cache directory</param>
  119. /// <returns>The path to the cache archive file</returns>
  120. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  121. public static string GetArchivePath(string cacheDirectory) => Path.Combine(cacheDirectory, "cache.zip");
  122. /// <summary>
  123. /// The path to the cache manifest file.
  124. /// </summary>
  125. /// <param name="cacheDirectory">The cache directory</param>
  126. /// <returns>The path to the cache manifest file</returns>
  127. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  128. public static string GetManifestPath(string cacheDirectory) => Path.Combine(cacheDirectory, "cache.info");
  129. /// <summary>
  130. /// Create a new temp path to the given cached file via its hash.
  131. /// </summary>
  132. /// <param name="cacheDirectory">The cache directory</param>
  133. /// <param name="key">The hash of the cached data</param>
  134. /// <returns>New path to the given cached file</returns>
  135. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  136. public static string GenCacheTempFilePath(string cacheDirectory, Hash128 key) => Path.Combine(GetCacheTempDataPath(cacheDirectory), key.ToString());
  137. /// <summary>
  138. /// Generate the path to the cache directory.
  139. /// </summary>
  140. /// <param name="baseCacheDirectory">The base of the cache directory</param>
  141. /// <param name="graphicsApi">The graphics api in use</param>
  142. /// <param name="shaderProvider">The name of the shader provider in use</param>
  143. /// <param name="cacheName">The name of the cache</param>
  144. /// <returns>The path to the cache directory</returns>
  145. public static string GenerateCachePath(string baseCacheDirectory, CacheGraphicsApi graphicsApi, string shaderProvider, string cacheName)
  146. {
  147. string graphicsApiName = graphicsApi switch
  148. {
  149. CacheGraphicsApi.OpenGL => "opengl",
  150. CacheGraphicsApi.OpenGLES => "opengles",
  151. CacheGraphicsApi.Vulkan => "vulkan",
  152. CacheGraphicsApi.DirectX => "directx",
  153. CacheGraphicsApi.Metal => "metal",
  154. CacheGraphicsApi.Guest => "guest",
  155. _ => throw new NotImplementedException(graphicsApi.ToString()),
  156. };
  157. return Path.Combine(baseCacheDirectory, graphicsApiName, shaderProvider, cacheName);
  158. }
  159. /// <summary>
  160. /// Read a cached file with the given hash that is present in the archive.
  161. /// </summary>
  162. /// <param name="archive">The archive in use</param>
  163. /// <param name="entry">The given hash</param>
  164. /// <returns>The cached file if present or null</returns>
  165. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  166. public static byte[] ReadFromArchive(ZipArchive archive, Hash128 entry)
  167. {
  168. if (archive != null)
  169. {
  170. ZipArchiveEntry archiveEntry = archive.GetEntry($"{entry}");
  171. if (archiveEntry != null)
  172. {
  173. try
  174. {
  175. byte[] result = new byte[archiveEntry.Length];
  176. using (Stream archiveStream = archiveEntry.Open())
  177. {
  178. archiveStream.Read(result);
  179. return result;
  180. }
  181. }
  182. catch (Exception e)
  183. {
  184. Logger.Error?.Print(LogClass.Gpu, $"Cannot load cache file {entry} from archive");
  185. Logger.Error?.Print(LogClass.Gpu, e.ToString());
  186. }
  187. }
  188. }
  189. return null;
  190. }
  191. /// <summary>
  192. /// Read a cached file with the given hash that is not present in the archive.
  193. /// </summary>
  194. /// <param name="cacheDirectory">The cache directory</param>
  195. /// <param name="entry">The given hash</param>
  196. /// <returns>The cached file if present or null</returns>
  197. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  198. public static byte[] ReadFromFile(string cacheDirectory, Hash128 entry)
  199. {
  200. string cacheTempFilePath = GenCacheTempFilePath(cacheDirectory, entry);
  201. try
  202. {
  203. return File.ReadAllBytes(cacheTempFilePath);
  204. }
  205. catch (Exception e)
  206. {
  207. Logger.Error?.Print(LogClass.Gpu, $"Cannot load cache file at {cacheTempFilePath}");
  208. Logger.Error?.Print(LogClass.Gpu, e.ToString());
  209. }
  210. return null;
  211. }
  212. /// <summary>
  213. /// Compute the guest program code for usage while dumping to disk or hash.
  214. /// </summary>
  215. /// <param name="cachedShaderEntries">The guest shader entries to use</param>
  216. /// <param name="tfd">The transform feedback descriptors</param>
  217. /// <param name="forHashCompute">Used to determine if the guest program code is generated for hashing</param>
  218. /// <returns>The guest program code for usage while dumping to disk or hash</returns>
  219. private static byte[] ComputeGuestProgramCode(ReadOnlySpan<GuestShaderCacheEntry> cachedShaderEntries, TransformFeedbackDescriptor[] tfd, bool forHashCompute = false)
  220. {
  221. using (MemoryStream stream = new MemoryStream())
  222. {
  223. BinaryWriter writer = new BinaryWriter(stream);
  224. foreach (GuestShaderCacheEntry cachedShaderEntry in cachedShaderEntries)
  225. {
  226. if (cachedShaderEntry != null)
  227. {
  228. // Code (and Code A if present)
  229. stream.Write(cachedShaderEntry.Code);
  230. if (forHashCompute)
  231. {
  232. // Guest GPU accessor header (only write this for hashes, already present in the header for dumps)
  233. writer.WriteStruct(cachedShaderEntry.Header.GpuAccessorHeader);
  234. }
  235. // Texture descriptors
  236. foreach (GuestTextureDescriptor textureDescriptor in cachedShaderEntry.TextureDescriptors.Values)
  237. {
  238. writer.WriteStruct(textureDescriptor);
  239. }
  240. }
  241. }
  242. // Transform feedback
  243. if (tfd != null)
  244. {
  245. foreach (TransformFeedbackDescriptor transform in tfd)
  246. {
  247. writer.WriteStruct(new GuestShaderCacheTransformFeedbackHeader(transform.BufferIndex, transform.Stride, transform.VaryingLocations.Length));
  248. writer.Write(transform.VaryingLocations);
  249. }
  250. }
  251. return stream.ToArray();
  252. }
  253. }
  254. /// <summary>
  255. /// Compute a guest hash from shader entries.
  256. /// </summary>
  257. /// <param name="cachedShaderEntries">The guest shader entries to use</param>
  258. /// <param name="tfd">The optional transform feedback descriptors</param>
  259. /// <returns>A guest hash from shader entries</returns>
  260. public static Hash128 ComputeGuestHashFromCache(ReadOnlySpan<GuestShaderCacheEntry> cachedShaderEntries, TransformFeedbackDescriptor[] tfd = null)
  261. {
  262. return XXHash128.ComputeHash(ComputeGuestProgramCode(cachedShaderEntries, tfd, true));
  263. }
  264. /// <summary>
  265. /// Read transform feedback descriptors from guest.
  266. /// </summary>
  267. /// <param name="data">The raw guest transform feedback descriptors</param>
  268. /// <param name="header">The guest shader program header</param>
  269. /// <returns>The transform feedback descriptors read from guest</returns>
  270. public static TransformFeedbackDescriptor[] ReadTransformFeedbackInformation(ref ReadOnlySpan<byte> data, GuestShaderCacheHeader header)
  271. {
  272. if (header.TransformFeedbackCount != 0)
  273. {
  274. TransformFeedbackDescriptor[] result = new TransformFeedbackDescriptor[header.TransformFeedbackCount];
  275. for (int i = 0; i < result.Length; i++)
  276. {
  277. GuestShaderCacheTransformFeedbackHeader feedbackHeader = MemoryMarshal.Read<GuestShaderCacheTransformFeedbackHeader>(data);
  278. result[i] = new TransformFeedbackDescriptor(feedbackHeader.BufferIndex, feedbackHeader.Stride, data.Slice(Unsafe.SizeOf<GuestShaderCacheTransformFeedbackHeader>(), feedbackHeader.VaryingLocationsLength).ToArray());
  279. data = data.Slice(Unsafe.SizeOf<GuestShaderCacheTransformFeedbackHeader>() + feedbackHeader.VaryingLocationsLength);
  280. }
  281. return result;
  282. }
  283. return null;
  284. }
  285. /// <summary>
  286. /// Builds gpu state flags using information from the given gpu accessor.
  287. /// </summary>
  288. /// <param name="gpuAccessor">The gpu accessor</param>
  289. /// <returns>The gpu state flags</returns>
  290. private static GuestGpuStateFlags GetGpuStateFlags(IGpuAccessor gpuAccessor)
  291. {
  292. GuestGpuStateFlags flags = 0;
  293. if (gpuAccessor.QueryEarlyZForce())
  294. {
  295. flags |= GuestGpuStateFlags.EarlyZForce;
  296. }
  297. return flags;
  298. }
  299. /// <summary>
  300. /// Create a new instance of <see cref="GuestGpuAccessorHeader"/> from an gpu accessor.
  301. /// </summary>
  302. /// <param name="gpuAccessor">The gpu accessor</param>
  303. /// <returns>A new instance of <see cref="GuestGpuAccessorHeader"/></returns>
  304. public static GuestGpuAccessorHeader CreateGuestGpuAccessorCache(IGpuAccessor gpuAccessor)
  305. {
  306. return new GuestGpuAccessorHeader
  307. {
  308. ComputeLocalSizeX = gpuAccessor.QueryComputeLocalSizeX(),
  309. ComputeLocalSizeY = gpuAccessor.QueryComputeLocalSizeY(),
  310. ComputeLocalSizeZ = gpuAccessor.QueryComputeLocalSizeZ(),
  311. ComputeLocalMemorySize = gpuAccessor.QueryComputeLocalMemorySize(),
  312. ComputeSharedMemorySize = gpuAccessor.QueryComputeSharedMemorySize(),
  313. PrimitiveTopology = gpuAccessor.QueryPrimitiveTopology(),
  314. StateFlags = GetGpuStateFlags(gpuAccessor)
  315. };
  316. }
  317. /// <summary>
  318. /// Create guest shader cache entries from the runtime contexts.
  319. /// </summary>
  320. /// <param name="memoryManager">The GPU memory manager in use</param>
  321. /// <param name="shaderContexts">The runtime contexts</param>
  322. /// <returns>Guest shader cahe entries from the runtime contexts</returns>
  323. public static GuestShaderCacheEntry[] CreateShaderCacheEntries(MemoryManager memoryManager, ReadOnlySpan<TranslatorContext> shaderContexts)
  324. {
  325. int startIndex = shaderContexts.Length > 1 ? 1 : 0;
  326. GuestShaderCacheEntry[] entries = new GuestShaderCacheEntry[shaderContexts.Length - startIndex];
  327. for (int i = startIndex; i < shaderContexts.Length; i++)
  328. {
  329. TranslatorContext context = shaderContexts[i];
  330. if (context == null)
  331. {
  332. continue;
  333. }
  334. TranslatorContext translatorContext2 = i == 1 ? shaderContexts[0] : null;
  335. int sizeA = translatorContext2 != null ? translatorContext2.Size : 0;
  336. byte[] code = new byte[context.Size + sizeA];
  337. memoryManager.GetSpan(context.Address, context.Size).CopyTo(code);
  338. if (translatorContext2 != null)
  339. {
  340. memoryManager.GetSpan(translatorContext2.Address, sizeA).CopyTo(code.AsSpan().Slice(context.Size, sizeA));
  341. }
  342. GuestGpuAccessorHeader gpuAccessorHeader = CreateGuestGpuAccessorCache(context.GpuAccessor);
  343. if (context.GpuAccessor is GpuAccessor)
  344. {
  345. gpuAccessorHeader.TextureDescriptorCount = context.TextureHandlesForCache.Count;
  346. }
  347. GuestShaderCacheEntryHeader header = new GuestShaderCacheEntryHeader(context.Stage, context.Size, sizeA, gpuAccessorHeader);
  348. GuestShaderCacheEntry entry = new GuestShaderCacheEntry(header, code);
  349. if (context.GpuAccessor is GpuAccessor gpuAccessor)
  350. {
  351. foreach (int textureHandle in context.TextureHandlesForCache)
  352. {
  353. GuestTextureDescriptor textureDescriptor = ((Image.TextureDescriptor)gpuAccessor.GetTextureDescriptor(textureHandle, -1)).ToCache();
  354. textureDescriptor.Handle = (uint)textureHandle;
  355. entry.TextureDescriptors.Add(textureHandle, textureDescriptor);
  356. }
  357. }
  358. entries[i - startIndex] = entry;
  359. }
  360. return entries;
  361. }
  362. /// <summary>
  363. /// Create a guest shader program.
  364. /// </summary>
  365. /// <param name="shaderCacheEntries">The entries composing the guest program dump</param>
  366. /// <param name="tfd">The transform feedback descriptors in use</param>
  367. /// <returns>The resulting guest shader program</returns>
  368. public static byte[] CreateGuestProgramDump(GuestShaderCacheEntry[] shaderCacheEntries, TransformFeedbackDescriptor[] tfd = null)
  369. {
  370. using (MemoryStream resultStream = new MemoryStream())
  371. {
  372. BinaryWriter resultStreamWriter = new BinaryWriter(resultStream);
  373. byte transformFeedbackCount = 0;
  374. if (tfd != null)
  375. {
  376. transformFeedbackCount = (byte)tfd.Length;
  377. }
  378. // Header
  379. resultStreamWriter.WriteStruct(new GuestShaderCacheHeader((byte)shaderCacheEntries.Length, transformFeedbackCount));
  380. // Write all entries header
  381. foreach (GuestShaderCacheEntry entry in shaderCacheEntries)
  382. {
  383. if (entry == null)
  384. {
  385. resultStreamWriter.WriteStruct(new GuestShaderCacheEntryHeader());
  386. }
  387. else
  388. {
  389. resultStreamWriter.WriteStruct(entry.Header);
  390. }
  391. }
  392. // Finally, write all program code and all transform feedback information.
  393. resultStreamWriter.Write(ComputeGuestProgramCode(shaderCacheEntries, tfd));
  394. return resultStream.ToArray();
  395. }
  396. }
  397. /// <summary>
  398. /// Save temporary files not in archive.
  399. /// </summary>
  400. /// <param name="baseCacheDirectory">The base of the cache directory</param>
  401. /// <param name="archive">The archive to use</param>
  402. /// <param name="entries">The entries in the cache</param>
  403. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  404. public static void EnsureArchiveUpToDate(string baseCacheDirectory, ZipArchive archive, HashSet<Hash128> entries)
  405. {
  406. foreach (Hash128 hash in entries)
  407. {
  408. string cacheTempFilePath = GenCacheTempFilePath(baseCacheDirectory, hash);
  409. if (File.Exists(cacheTempFilePath))
  410. {
  411. string cacheHash = $"{hash}";
  412. ZipArchiveEntry entry = archive.GetEntry(cacheHash);
  413. entry?.Delete();
  414. archive.CreateEntryFromFile(cacheTempFilePath, cacheHash);
  415. File.Delete(cacheTempFilePath);
  416. }
  417. }
  418. }
  419. public static bool IsArchiveReadOnly(string archivePath)
  420. {
  421. FileInfo info = new FileInfo(archivePath);
  422. if (!info.Exists)
  423. {
  424. return false;
  425. }
  426. try
  427. {
  428. using (FileStream stream = info.Open(FileMode.Open, FileAccess.Read, FileShare.None))
  429. {
  430. return false;
  431. }
  432. }
  433. catch (IOException)
  434. {
  435. return true;
  436. }
  437. }
  438. }
  439. }