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 }