aboutsummaryrefslogtreecommitdiff
path: root/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheGuestStorage.cs
diff options
context:
space:
mode:
Diffstat (limited to 'src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheGuestStorage.cs')
-rw-r--r--src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheGuestStorage.cs459
1 files changed, 459 insertions, 0 deletions
diff --git a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheGuestStorage.cs b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheGuestStorage.cs
new file mode 100644
index 00000000..01034b49
--- /dev/null
+++ b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheGuestStorage.cs
@@ -0,0 +1,459 @@
+using Ryujinx.Common;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Runtime.CompilerServices;
+
+namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
+{
+ /// <summary>
+ /// On-disk shader cache storage for guest code.
+ /// </summary>
+ class DiskCacheGuestStorage
+ {
+ private const uint TocMagic = (byte)'T' | ((byte)'O' << 8) | ((byte)'C' << 16) | ((byte)'G' << 24);
+
+ private const ushort VersionMajor = 1;
+ private const ushort VersionMinor = 1;
+ private const uint VersionPacked = ((uint)VersionMajor << 16) | VersionMinor;
+
+ private const string TocFileName = "guest.toc";
+ private const string DataFileName = "guest.data";
+
+ private readonly string _basePath;
+
+ /// <summary>
+ /// TOC (Table of contents) file header.
+ /// </summary>
+ private struct TocHeader
+ {
+ /// <summary>
+ /// Magic value, for validation and identification purposes.
+ /// </summary>
+ public uint Magic;
+
+ /// <summary>
+ /// File format version.
+ /// </summary>
+ public uint Version;
+
+ /// <summary>
+ /// Header padding.
+ /// </summary>
+ public uint Padding;
+
+ /// <summary>
+ /// Number of modifications to the file, also the shaders count.
+ /// </summary>
+ public uint ModificationsCount;
+
+ /// <summary>
+ /// Reserved space, to be used in the future. Write as zero.
+ /// </summary>
+ public ulong Reserved;
+
+ /// <summary>
+ /// Reserved space, to be used in the future. Write as zero.
+ /// </summary>
+ public ulong Reserved2;
+ }
+
+ /// <summary>
+ /// TOC (Table of contents) file entry.
+ /// </summary>
+ private struct TocEntry
+ {
+ /// <summary>
+ /// Offset of the data on the data file.
+ /// </summary>
+ public uint Offset;
+
+ /// <summary>
+ /// Code size.
+ /// </summary>
+ public uint CodeSize;
+
+ /// <summary>
+ /// Constant buffer 1 data size.
+ /// </summary>
+ public uint Cb1DataSize;
+
+ /// <summary>
+ /// Hash of the code and constant buffer data.
+ /// </summary>
+ public uint Hash;
+ }
+
+ /// <summary>
+ /// TOC (Table of contents) memory cache entry.
+ /// </summary>
+ private struct TocMemoryEntry
+ {
+ /// <summary>
+ /// Offset of the data on the data file.
+ /// </summary>
+ public uint Offset;
+
+ /// <summary>
+ /// Code size.
+ /// </summary>
+ public uint CodeSize;
+
+ /// <summary>
+ /// Constant buffer 1 data size.
+ /// </summary>
+ public uint Cb1DataSize;
+
+ /// <summary>
+ /// Index of the shader on the cache.
+ /// </summary>
+ public readonly int Index;
+
+ /// <summary>
+ /// Creates a new TOC memory entry.
+ /// </summary>
+ /// <param name="offset">Offset of the data on the data file</param>
+ /// <param name="codeSize">Code size</param>
+ /// <param name="cb1DataSize">Constant buffer 1 data size</param>
+ /// <param name="index">Index of the shader on the cache</param>
+ public TocMemoryEntry(uint offset, uint codeSize, uint cb1DataSize, int index)
+ {
+ Offset = offset;
+ CodeSize = codeSize;
+ Cb1DataSize = cb1DataSize;
+ Index = index;
+ }
+ }
+
+ private Dictionary<uint, List<TocMemoryEntry>> _toc;
+ private uint _tocModificationsCount;
+
+ private (byte[], byte[])[] _cache;
+
+ /// <summary>
+ /// Creates a new disk cache guest storage.
+ /// </summary>
+ /// <param name="basePath">Base path of the disk shader cache</param>
+ public DiskCacheGuestStorage(string basePath)
+ {
+ _basePath = basePath;
+ }
+
+ /// <summary>
+ /// Checks if the TOC (table of contents) file for the guest cache exists.
+ /// </summary>
+ /// <returns>True if the file exists, false otherwise</returns>
+ public bool TocFileExists()
+ {
+ return File.Exists(Path.Combine(_basePath, TocFileName));
+ }
+
+ /// <summary>
+ /// Checks if the data file for the guest cache exists.
+ /// </summary>
+ /// <returns>True if the file exists, false otherwise</returns>
+ public bool DataFileExists()
+ {
+ return File.Exists(Path.Combine(_basePath, DataFileName));
+ }
+
+ /// <summary>
+ /// Opens the guest cache TOC (table of contents) file.
+ /// </summary>
+ /// <returns>File stream</returns>
+ public Stream OpenTocFileStream()
+ {
+ return DiskCacheCommon.OpenFile(_basePath, TocFileName, writable: false);
+ }
+
+ /// <summary>
+ /// Opens the guest cache data file.
+ /// </summary>
+ /// <returns>File stream</returns>
+ public Stream OpenDataFileStream()
+ {
+ return DiskCacheCommon.OpenFile(_basePath, DataFileName, writable: false);
+ }
+
+ /// <summary>
+ /// Clear all content from the guest cache files.
+ /// </summary>
+ public void ClearCache()
+ {
+ using var tocFileStream = DiskCacheCommon.OpenFile(_basePath, TocFileName, writable: true);
+ using var dataFileStream = DiskCacheCommon.OpenFile(_basePath, DataFileName, writable: true);
+
+ tocFileStream.SetLength(0);
+ dataFileStream.SetLength(0);
+ }
+
+ /// <summary>
+ /// Loads the guest cache from file or memory cache.
+ /// </summary>
+ /// <param name="tocFileStream">Guest TOC file stream</param>
+ /// <param name="dataFileStream">Guest data file stream</param>
+ /// <param name="index">Guest shader index</param>
+ /// <returns>Guest code and constant buffer 1 data</returns>
+ public GuestCodeAndCbData LoadShader(Stream tocFileStream, Stream dataFileStream, int index)
+ {
+ if (_cache == null || index >= _cache.Length)
+ {
+ _cache = new (byte[], byte[])[Math.Max(index + 1, GetShadersCountFromLength(tocFileStream.Length))];
+ }
+
+ (byte[] guestCode, byte[] cb1Data) = _cache[index];
+
+ if (guestCode == null || cb1Data == null)
+ {
+ BinarySerializer tocReader = new BinarySerializer(tocFileStream);
+ tocFileStream.Seek(Unsafe.SizeOf<TocHeader>() + index * Unsafe.SizeOf<TocEntry>(), SeekOrigin.Begin);
+
+ TocEntry entry = new TocEntry();
+ tocReader.Read(ref entry);
+
+ guestCode = new byte[entry.CodeSize];
+ cb1Data = new byte[entry.Cb1DataSize];
+
+ if (entry.Offset >= (ulong)dataFileStream.Length)
+ {
+ throw new DiskCacheLoadException(DiskCacheLoadResult.FileCorruptedGeneric);
+ }
+
+ dataFileStream.Seek((long)entry.Offset, SeekOrigin.Begin);
+ dataFileStream.Read(cb1Data);
+ BinarySerializer.ReadCompressed(dataFileStream, guestCode);
+
+ _cache[index] = (guestCode, cb1Data);
+ }
+
+ return new GuestCodeAndCbData(guestCode, cb1Data);
+ }
+
+ /// <summary>
+ /// Clears guest code memory cache, forcing future loads to be from file.
+ /// </summary>
+ public void ClearMemoryCache()
+ {
+ _cache = null;
+ }
+
+ /// <summary>
+ /// Calculates the guest shaders count from the TOC file length.
+ /// </summary>
+ /// <param name="length">TOC file length</param>
+ /// <returns>Shaders count</returns>
+ private static int GetShadersCountFromLength(long length)
+ {
+ return (int)((length - Unsafe.SizeOf<TocHeader>()) / Unsafe.SizeOf<TocEntry>());
+ }
+
+ /// <summary>
+ /// Adds a guest shader to the cache.
+ /// </summary>
+ /// <remarks>
+ /// If the shader is already on the cache, the existing index will be returned and nothing will be written.
+ /// </remarks>
+ /// <param name="data">Guest code</param>
+ /// <param name="cb1Data">Constant buffer 1 data accessed by the code</param>
+ /// <returns>Index of the shader on the cache</returns>
+ public int AddShader(ReadOnlySpan<byte> data, ReadOnlySpan<byte> cb1Data)
+ {
+ using var tocFileStream = DiskCacheCommon.OpenFile(_basePath, TocFileName, writable: true);
+ using var dataFileStream = DiskCacheCommon.OpenFile(_basePath, DataFileName, writable: true);
+
+ TocHeader header = new TocHeader();
+
+ LoadOrCreateToc(tocFileStream, ref header);
+
+ uint hash = CalcHash(data, cb1Data);
+
+ if (_toc.TryGetValue(hash, out var list))
+ {
+ foreach (var entry in list)
+ {
+ if (data.Length != entry.CodeSize || cb1Data.Length != entry.Cb1DataSize)
+ {
+ continue;
+ }
+
+ dataFileStream.Seek((long)entry.Offset, SeekOrigin.Begin);
+ byte[] cachedCode = new byte[entry.CodeSize];
+ byte[] cachedCb1Data = new byte[entry.Cb1DataSize];
+ dataFileStream.Read(cachedCb1Data);
+ BinarySerializer.ReadCompressed(dataFileStream, cachedCode);
+
+ if (data.SequenceEqual(cachedCode) && cb1Data.SequenceEqual(cachedCb1Data))
+ {
+ return entry.Index;
+ }
+ }
+ }
+
+ return WriteNewEntry(tocFileStream, dataFileStream, ref header, data, cb1Data, hash);
+ }
+
+ /// <summary>
+ /// Loads the guest cache TOC file, or create a new one if not present.
+ /// </summary>
+ /// <param name="tocFileStream">Guest TOC file stream</param>
+ /// <param name="header">Set to the TOC file header</param>
+ private void LoadOrCreateToc(Stream tocFileStream, ref TocHeader header)
+ {
+ BinarySerializer reader = new BinarySerializer(tocFileStream);
+
+ if (!reader.TryRead(ref header) || header.Magic != TocMagic || header.Version != VersionPacked)
+ {
+ CreateToc(tocFileStream, ref header);
+ }
+
+ if (_toc == null || header.ModificationsCount != _tocModificationsCount)
+ {
+ if (!LoadTocEntries(tocFileStream, ref reader))
+ {
+ CreateToc(tocFileStream, ref header);
+ }
+
+ _tocModificationsCount = header.ModificationsCount;
+ }
+ }
+
+ /// <summary>
+ /// Creates a new guest cache TOC file.
+ /// </summary>
+ /// <param name="tocFileStream">Guest TOC file stream</param>
+ /// <param name="header">Set to the TOC header</param>
+ private void CreateToc(Stream tocFileStream, ref TocHeader header)
+ {
+ BinarySerializer writer = new BinarySerializer(tocFileStream);
+
+ header.Magic = TocMagic;
+ header.Version = VersionPacked;
+ header.Padding = 0;
+ header.ModificationsCount = 0;
+ header.Reserved = 0;
+ header.Reserved2 = 0;
+
+ if (tocFileStream.Length > 0)
+ {
+ tocFileStream.Seek(0, SeekOrigin.Begin);
+ tocFileStream.SetLength(0);
+ }
+
+ writer.Write(ref header);
+ }
+
+ /// <summary>
+ /// Reads all the entries on the guest TOC file.
+ /// </summary>
+ /// <param name="tocFileStream">Guest TOC file stream</param>
+ /// <param name="reader">TOC file reader</param>
+ /// <returns>True if the operation was successful, false otherwise</returns>
+ private bool LoadTocEntries(Stream tocFileStream, ref BinarySerializer reader)
+ {
+ _toc = new Dictionary<uint, List<TocMemoryEntry>>();
+
+ TocEntry entry = new TocEntry();
+ int index = 0;
+
+ while (tocFileStream.Position < tocFileStream.Length)
+ {
+ if (!reader.TryRead(ref entry))
+ {
+ return false;
+ }
+
+ AddTocMemoryEntry(entry.Offset, entry.CodeSize, entry.Cb1DataSize, entry.Hash, index++);
+ }
+
+ return true;
+ }
+
+ /// <summary>
+ /// Writes a new guest code entry into the file.
+ /// </summary>
+ /// <param name="tocFileStream">TOC file stream</param>
+ /// <param name="dataFileStream">Data file stream</param>
+ /// <param name="header">TOC header, to be updated with the new count</param>
+ /// <param name="data">Guest code</param>
+ /// <param name="cb1Data">Constant buffer 1 data accessed by the guest code</param>
+ /// <param name="hash">Code and constant buffer data hash</param>
+ /// <returns>Entry index</returns>
+ private int WriteNewEntry(
+ Stream tocFileStream,
+ Stream dataFileStream,
+ ref TocHeader header,
+ ReadOnlySpan<byte> data,
+ ReadOnlySpan<byte> cb1Data,
+ uint hash)
+ {
+ BinarySerializer tocWriter = new BinarySerializer(tocFileStream);
+
+ dataFileStream.Seek(0, SeekOrigin.End);
+ uint dataOffset = checked((uint)dataFileStream.Position);
+ uint codeSize = (uint)data.Length;
+ uint cb1DataSize = (uint)cb1Data.Length;
+ dataFileStream.Write(cb1Data);
+ BinarySerializer.WriteCompressed(dataFileStream, data, DiskCacheCommon.GetCompressionAlgorithm());
+
+ _tocModificationsCount = ++header.ModificationsCount;
+ tocFileStream.Seek(0, SeekOrigin.Begin);
+ tocWriter.Write(ref header);
+
+ TocEntry entry = new TocEntry()
+ {
+ Offset = dataOffset,
+ CodeSize = codeSize,
+ Cb1DataSize = cb1DataSize,
+ Hash = hash
+ };
+
+ tocFileStream.Seek(0, SeekOrigin.End);
+ int index = (int)((tocFileStream.Position - Unsafe.SizeOf<TocHeader>()) / Unsafe.SizeOf<TocEntry>());
+
+ tocWriter.Write(ref entry);
+
+ AddTocMemoryEntry(dataOffset, codeSize, cb1DataSize, hash, index);
+
+ return index;
+ }
+
+ /// <summary>
+ /// Adds an entry to the memory TOC cache. This can be used to avoid reading the TOC file all the time.
+ /// </summary>
+ /// <param name="dataOffset">Offset of the code and constant buffer data in the data file</param>
+ /// <param name="codeSize">Code size</param>
+ /// <param name="cb1DataSize">Constant buffer 1 data size</param>
+ /// <param name="hash">Code and constant buffer data hash</param>
+ /// <param name="index">Index of the data on the cache</param>
+ private void AddTocMemoryEntry(uint dataOffset, uint codeSize, uint cb1DataSize, uint hash, int index)
+ {
+ if (!_toc.TryGetValue(hash, out var list))
+ {
+ _toc.Add(hash, list = new List<TocMemoryEntry>());
+ }
+
+ list.Add(new TocMemoryEntry(dataOffset, codeSize, cb1DataSize, index));
+ }
+
+ /// <summary>
+ /// Calculates the hash for a data pair.
+ /// </summary>
+ /// <param name="data">Data 1</param>
+ /// <param name="data2">Data 2</param>
+ /// <returns>Hash of both data</returns>
+ private static uint CalcHash(ReadOnlySpan<byte> data, ReadOnlySpan<byte> data2)
+ {
+ return CalcHash(data2) * 23 ^ CalcHash(data);
+ }
+
+ /// <summary>
+ /// Calculates the hash for data.
+ /// </summary>
+ /// <param name="data">Data to be hashed</param>
+ /// <returns>Hash of the data</returns>
+ private static uint CalcHash(ReadOnlySpan<byte> data)
+ {
+ return (uint)XXHash128.ComputeHash(data).Low;
+ }
+ }
+} \ No newline at end of file