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