DiskCacheGuestStorage.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459
  1. using Ryujinx.Common;
  2. using System;
  3. using System.Collections.Generic;
  4. using System.IO;
  5. using System.Runtime.CompilerServices;
  6. namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
  7. {
  8. /// <summary>
  9. /// On-disk shader cache storage for guest code.
  10. /// </summary>
  11. class DiskCacheGuestStorage
  12. {
  13. private const uint TocMagic = (byte)'T' | ((byte)'O' << 8) | ((byte)'C' << 16) | ((byte)'G' << 24);
  14. private const ushort VersionMajor = 1;
  15. private const ushort VersionMinor = 1;
  16. private const uint VersionPacked = ((uint)VersionMajor << 16) | VersionMinor;
  17. private const string TocFileName = "guest.toc";
  18. private const string DataFileName = "guest.data";
  19. private readonly string _basePath;
  20. /// <summary>
  21. /// TOC (Table of contents) file header.
  22. /// </summary>
  23. private struct TocHeader
  24. {
  25. /// <summary>
  26. /// Magic value, for validation and identification purposes.
  27. /// </summary>
  28. public uint Magic;
  29. /// <summary>
  30. /// File format version.
  31. /// </summary>
  32. public uint Version;
  33. /// <summary>
  34. /// Header padding.
  35. /// </summary>
  36. public uint Padding;
  37. /// <summary>
  38. /// Number of modifications to the file, also the shaders count.
  39. /// </summary>
  40. public uint ModificationsCount;
  41. /// <summary>
  42. /// Reserved space, to be used in the future. Write as zero.
  43. /// </summary>
  44. public ulong Reserved;
  45. /// <summary>
  46. /// Reserved space, to be used in the future. Write as zero.
  47. /// </summary>
  48. public ulong Reserved2;
  49. }
  50. /// <summary>
  51. /// TOC (Table of contents) file entry.
  52. /// </summary>
  53. private struct TocEntry
  54. {
  55. /// <summary>
  56. /// Offset of the data on the data file.
  57. /// </summary>
  58. public uint Offset;
  59. /// <summary>
  60. /// Code size.
  61. /// </summary>
  62. public uint CodeSize;
  63. /// <summary>
  64. /// Constant buffer 1 data size.
  65. /// </summary>
  66. public uint Cb1DataSize;
  67. /// <summary>
  68. /// Hash of the code and constant buffer data.
  69. /// </summary>
  70. public uint Hash;
  71. }
  72. /// <summary>
  73. /// TOC (Table of contents) memory cache entry.
  74. /// </summary>
  75. private struct TocMemoryEntry
  76. {
  77. /// <summary>
  78. /// Offset of the data on the data file.
  79. /// </summary>
  80. public uint Offset;
  81. /// <summary>
  82. /// Code size.
  83. /// </summary>
  84. public uint CodeSize;
  85. /// <summary>
  86. /// Constant buffer 1 data size.
  87. /// </summary>
  88. public uint Cb1DataSize;
  89. /// <summary>
  90. /// Index of the shader on the cache.
  91. /// </summary>
  92. public readonly int Index;
  93. /// <summary>
  94. /// Creates a new TOC memory entry.
  95. /// </summary>
  96. /// <param name="offset">Offset of the data on the data file</param>
  97. /// <param name="codeSize">Code size</param>
  98. /// <param name="cb1DataSize">Constant buffer 1 data size</param>
  99. /// <param name="index">Index of the shader on the cache</param>
  100. public TocMemoryEntry(uint offset, uint codeSize, uint cb1DataSize, int index)
  101. {
  102. Offset = offset;
  103. CodeSize = codeSize;
  104. Cb1DataSize = cb1DataSize;
  105. Index = index;
  106. }
  107. }
  108. private Dictionary<uint, List<TocMemoryEntry>> _toc;
  109. private uint _tocModificationsCount;
  110. private (byte[], byte[])[] _cache;
  111. /// <summary>
  112. /// Creates a new disk cache guest storage.
  113. /// </summary>
  114. /// <param name="basePath">Base path of the disk shader cache</param>
  115. public DiskCacheGuestStorage(string basePath)
  116. {
  117. _basePath = basePath;
  118. }
  119. /// <summary>
  120. /// Checks if the TOC (table of contents) file for the guest cache exists.
  121. /// </summary>
  122. /// <returns>True if the file exists, false otherwise</returns>
  123. public bool TocFileExists()
  124. {
  125. return File.Exists(Path.Combine(_basePath, TocFileName));
  126. }
  127. /// <summary>
  128. /// Checks if the data file for the guest cache exists.
  129. /// </summary>
  130. /// <returns>True if the file exists, false otherwise</returns>
  131. public bool DataFileExists()
  132. {
  133. return File.Exists(Path.Combine(_basePath, DataFileName));
  134. }
  135. /// <summary>
  136. /// Opens the guest cache TOC (table of contents) file.
  137. /// </summary>
  138. /// <returns>File stream</returns>
  139. public Stream OpenTocFileStream()
  140. {
  141. return DiskCacheCommon.OpenFile(_basePath, TocFileName, writable: false);
  142. }
  143. /// <summary>
  144. /// Opens the guest cache data file.
  145. /// </summary>
  146. /// <returns>File stream</returns>
  147. public Stream OpenDataFileStream()
  148. {
  149. return DiskCacheCommon.OpenFile(_basePath, DataFileName, writable: false);
  150. }
  151. /// <summary>
  152. /// Clear all content from the guest cache files.
  153. /// </summary>
  154. public void ClearCache()
  155. {
  156. using var tocFileStream = DiskCacheCommon.OpenFile(_basePath, TocFileName, writable: true);
  157. using var dataFileStream = DiskCacheCommon.OpenFile(_basePath, DataFileName, writable: true);
  158. tocFileStream.SetLength(0);
  159. dataFileStream.SetLength(0);
  160. }
  161. /// <summary>
  162. /// Loads the guest cache from file or memory cache.
  163. /// </summary>
  164. /// <param name="tocFileStream">Guest TOC file stream</param>
  165. /// <param name="dataFileStream">Guest data file stream</param>
  166. /// <param name="index">Guest shader index</param>
  167. /// <returns>Guest code and constant buffer 1 data</returns>
  168. public GuestCodeAndCbData LoadShader(Stream tocFileStream, Stream dataFileStream, int index)
  169. {
  170. if (_cache == null || index >= _cache.Length)
  171. {
  172. _cache = new (byte[], byte[])[Math.Max(index + 1, GetShadersCountFromLength(tocFileStream.Length))];
  173. }
  174. (byte[] guestCode, byte[] cb1Data) = _cache[index];
  175. if (guestCode == null || cb1Data == null)
  176. {
  177. BinarySerializer tocReader = new BinarySerializer(tocFileStream);
  178. tocFileStream.Seek(Unsafe.SizeOf<TocHeader>() + index * Unsafe.SizeOf<TocEntry>(), SeekOrigin.Begin);
  179. TocEntry entry = new TocEntry();
  180. tocReader.Read(ref entry);
  181. guestCode = new byte[entry.CodeSize];
  182. cb1Data = new byte[entry.Cb1DataSize];
  183. if (entry.Offset >= (ulong)dataFileStream.Length)
  184. {
  185. throw new DiskCacheLoadException(DiskCacheLoadResult.FileCorruptedGeneric);
  186. }
  187. dataFileStream.Seek((long)entry.Offset, SeekOrigin.Begin);
  188. dataFileStream.Read(cb1Data);
  189. BinarySerializer.ReadCompressed(dataFileStream, guestCode);
  190. _cache[index] = (guestCode, cb1Data);
  191. }
  192. return new GuestCodeAndCbData(guestCode, cb1Data);
  193. }
  194. /// <summary>
  195. /// Clears guest code memory cache, forcing future loads to be from file.
  196. /// </summary>
  197. public void ClearMemoryCache()
  198. {
  199. _cache = null;
  200. }
  201. /// <summary>
  202. /// Calculates the guest shaders count from the TOC file length.
  203. /// </summary>
  204. /// <param name="length">TOC file length</param>
  205. /// <returns>Shaders count</returns>
  206. private static int GetShadersCountFromLength(long length)
  207. {
  208. return (int)((length - Unsafe.SizeOf<TocHeader>()) / Unsafe.SizeOf<TocEntry>());
  209. }
  210. /// <summary>
  211. /// Adds a guest shader to the cache.
  212. /// </summary>
  213. /// <remarks>
  214. /// If the shader is already on the cache, the existing index will be returned and nothing will be written.
  215. /// </remarks>
  216. /// <param name="data">Guest code</param>
  217. /// <param name="cb1Data">Constant buffer 1 data accessed by the code</param>
  218. /// <returns>Index of the shader on the cache</returns>
  219. public int AddShader(ReadOnlySpan<byte> data, ReadOnlySpan<byte> cb1Data)
  220. {
  221. using var tocFileStream = DiskCacheCommon.OpenFile(_basePath, TocFileName, writable: true);
  222. using var dataFileStream = DiskCacheCommon.OpenFile(_basePath, DataFileName, writable: true);
  223. TocHeader header = new TocHeader();
  224. LoadOrCreateToc(tocFileStream, ref header);
  225. uint hash = CalcHash(data, cb1Data);
  226. if (_toc.TryGetValue(hash, out var list))
  227. {
  228. foreach (var entry in list)
  229. {
  230. if (data.Length != entry.CodeSize || cb1Data.Length != entry.Cb1DataSize)
  231. {
  232. continue;
  233. }
  234. dataFileStream.Seek((long)entry.Offset, SeekOrigin.Begin);
  235. byte[] cachedCode = new byte[entry.CodeSize];
  236. byte[] cachedCb1Data = new byte[entry.Cb1DataSize];
  237. dataFileStream.Read(cachedCb1Data);
  238. BinarySerializer.ReadCompressed(dataFileStream, cachedCode);
  239. if (data.SequenceEqual(cachedCode) && cb1Data.SequenceEqual(cachedCb1Data))
  240. {
  241. return entry.Index;
  242. }
  243. }
  244. }
  245. return WriteNewEntry(tocFileStream, dataFileStream, ref header, data, cb1Data, hash);
  246. }
  247. /// <summary>
  248. /// Loads the guest cache TOC file, or create a new one if not present.
  249. /// </summary>
  250. /// <param name="tocFileStream">Guest TOC file stream</param>
  251. /// <param name="header">Set to the TOC file header</param>
  252. private void LoadOrCreateToc(Stream tocFileStream, ref TocHeader header)
  253. {
  254. BinarySerializer reader = new BinarySerializer(tocFileStream);
  255. if (!reader.TryRead(ref header) || header.Magic != TocMagic || header.Version != VersionPacked)
  256. {
  257. CreateToc(tocFileStream, ref header);
  258. }
  259. if (_toc == null || header.ModificationsCount != _tocModificationsCount)
  260. {
  261. if (!LoadTocEntries(tocFileStream, ref reader))
  262. {
  263. CreateToc(tocFileStream, ref header);
  264. }
  265. _tocModificationsCount = header.ModificationsCount;
  266. }
  267. }
  268. /// <summary>
  269. /// Creates a new guest cache TOC file.
  270. /// </summary>
  271. /// <param name="tocFileStream">Guest TOC file stream</param>
  272. /// <param name="header">Set to the TOC header</param>
  273. private void CreateToc(Stream tocFileStream, ref TocHeader header)
  274. {
  275. BinarySerializer writer = new BinarySerializer(tocFileStream);
  276. header.Magic = TocMagic;
  277. header.Version = VersionPacked;
  278. header.Padding = 0;
  279. header.ModificationsCount = 0;
  280. header.Reserved = 0;
  281. header.Reserved2 = 0;
  282. if (tocFileStream.Length > 0)
  283. {
  284. tocFileStream.Seek(0, SeekOrigin.Begin);
  285. tocFileStream.SetLength(0);
  286. }
  287. writer.Write(ref header);
  288. }
  289. /// <summary>
  290. /// Reads all the entries on the guest TOC file.
  291. /// </summary>
  292. /// <param name="tocFileStream">Guest TOC file stream</param>
  293. /// <param name="reader">TOC file reader</param>
  294. /// <returns>True if the operation was successful, false otherwise</returns>
  295. private bool LoadTocEntries(Stream tocFileStream, ref BinarySerializer reader)
  296. {
  297. _toc = new Dictionary<uint, List<TocMemoryEntry>>();
  298. TocEntry entry = new TocEntry();
  299. int index = 0;
  300. while (tocFileStream.Position < tocFileStream.Length)
  301. {
  302. if (!reader.TryRead(ref entry))
  303. {
  304. return false;
  305. }
  306. AddTocMemoryEntry(entry.Offset, entry.CodeSize, entry.Cb1DataSize, entry.Hash, index++);
  307. }
  308. return true;
  309. }
  310. /// <summary>
  311. /// Writes a new guest code entry into the file.
  312. /// </summary>
  313. /// <param name="tocFileStream">TOC file stream</param>
  314. /// <param name="dataFileStream">Data file stream</param>
  315. /// <param name="header">TOC header, to be updated with the new count</param>
  316. /// <param name="data">Guest code</param>
  317. /// <param name="cb1Data">Constant buffer 1 data accessed by the guest code</param>
  318. /// <param name="hash">Code and constant buffer data hash</param>
  319. /// <returns>Entry index</returns>
  320. private int WriteNewEntry(
  321. Stream tocFileStream,
  322. Stream dataFileStream,
  323. ref TocHeader header,
  324. ReadOnlySpan<byte> data,
  325. ReadOnlySpan<byte> cb1Data,
  326. uint hash)
  327. {
  328. BinarySerializer tocWriter = new BinarySerializer(tocFileStream);
  329. dataFileStream.Seek(0, SeekOrigin.End);
  330. uint dataOffset = checked((uint)dataFileStream.Position);
  331. uint codeSize = (uint)data.Length;
  332. uint cb1DataSize = (uint)cb1Data.Length;
  333. dataFileStream.Write(cb1Data);
  334. BinarySerializer.WriteCompressed(dataFileStream, data, DiskCacheCommon.GetCompressionAlgorithm());
  335. _tocModificationsCount = ++header.ModificationsCount;
  336. tocFileStream.Seek(0, SeekOrigin.Begin);
  337. tocWriter.Write(ref header);
  338. TocEntry entry = new TocEntry()
  339. {
  340. Offset = dataOffset,
  341. CodeSize = codeSize,
  342. Cb1DataSize = cb1DataSize,
  343. Hash = hash
  344. };
  345. tocFileStream.Seek(0, SeekOrigin.End);
  346. int index = (int)((tocFileStream.Position - Unsafe.SizeOf<TocHeader>()) / Unsafe.SizeOf<TocEntry>());
  347. tocWriter.Write(ref entry);
  348. AddTocMemoryEntry(dataOffset, codeSize, cb1DataSize, hash, index);
  349. return index;
  350. }
  351. /// <summary>
  352. /// Adds an entry to the memory TOC cache. This can be used to avoid reading the TOC file all the time.
  353. /// </summary>
  354. /// <param name="dataOffset">Offset of the code and constant buffer data in the data file</param>
  355. /// <param name="codeSize">Code size</param>
  356. /// <param name="cb1DataSize">Constant buffer 1 data size</param>
  357. /// <param name="hash">Code and constant buffer data hash</param>
  358. /// <param name="index">Index of the data on the cache</param>
  359. private void AddTocMemoryEntry(uint dataOffset, uint codeSize, uint cb1DataSize, uint hash, int index)
  360. {
  361. if (!_toc.TryGetValue(hash, out var list))
  362. {
  363. _toc.Add(hash, list = new List<TocMemoryEntry>());
  364. }
  365. list.Add(new TocMemoryEntry(dataOffset, codeSize, cb1DataSize, index));
  366. }
  367. /// <summary>
  368. /// Calculates the hash for a data pair.
  369. /// </summary>
  370. /// <param name="data">Data 1</param>
  371. /// <param name="data2">Data 2</param>
  372. /// <returns>Hash of both data</returns>
  373. private static uint CalcHash(ReadOnlySpan<byte> data, ReadOnlySpan<byte> data2)
  374. {
  375. return CalcHash(data2) * 23 ^ CalcHash(data);
  376. }
  377. /// <summary>
  378. /// Calculates the hash for data.
  379. /// </summary>
  380. /// <param name="data">Data to be hashed</param>
  381. /// <returns>Hash of the data</returns>
  382. private static uint CalcHash(ReadOnlySpan<byte> data)
  383. {
  384. return (uint)XXHash128.ComputeHash(data).Low;
  385. }
  386. }
  387. }