using Ryujinx.Common; using System; using System.Collections.Generic; using System.IO; using System.Runtime.CompilerServices; namespace Ryujinx.Graphics.Gpu.Shader.DiskCache { /// /// On-disk shader cache storage for guest code. /// 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; /// /// TOC (Table of contents) file header. /// private struct TocHeader { /// /// Magic value, for validation and identification purposes. /// public uint Magic; /// /// File format version. /// public uint Version; /// /// Header padding. /// public uint Padding; /// /// Number of modifications to the file, also the shaders count. /// public uint ModificationsCount; /// /// Reserved space, to be used in the future. Write as zero. /// public ulong Reserved; /// /// Reserved space, to be used in the future. Write as zero. /// public ulong Reserved2; } /// /// TOC (Table of contents) file entry. /// private struct TocEntry { /// /// Offset of the data on the data file. /// public uint Offset; /// /// Code size. /// public uint CodeSize; /// /// Constant buffer 1 data size. /// public uint Cb1DataSize; /// /// Hash of the code and constant buffer data. /// public uint Hash; } /// /// TOC (Table of contents) memory cache entry. /// private struct TocMemoryEntry { /// /// Offset of the data on the data file. /// public uint Offset; /// /// Code size. /// public uint CodeSize; /// /// Constant buffer 1 data size. /// public uint Cb1DataSize; /// /// Index of the shader on the cache. /// public readonly int Index; /// /// Creates a new TOC memory entry. /// /// Offset of the data on the data file /// Code size /// Constant buffer 1 data size /// Index of the shader on the cache public TocMemoryEntry(uint offset, uint codeSize, uint cb1DataSize, int index) { Offset = offset; CodeSize = codeSize; Cb1DataSize = cb1DataSize; Index = index; } } private Dictionary> _toc; private uint _tocModificationsCount; private (byte[], byte[])[] _cache; /// /// Creates a new disk cache guest storage. /// /// Base path of the disk shader cache public DiskCacheGuestStorage(string basePath) { _basePath = basePath; } /// /// Checks if the TOC (table of contents) file for the guest cache exists. /// /// True if the file exists, false otherwise public bool TocFileExists() { return File.Exists(Path.Combine(_basePath, TocFileName)); } /// /// Checks if the data file for the guest cache exists. /// /// True if the file exists, false otherwise public bool DataFileExists() { return File.Exists(Path.Combine(_basePath, DataFileName)); } /// /// Opens the guest cache TOC (table of contents) file. /// /// File stream public Stream OpenTocFileStream() { return DiskCacheCommon.OpenFile(_basePath, TocFileName, writable: false); } /// /// Opens the guest cache data file. /// /// File stream public Stream OpenDataFileStream() { return DiskCacheCommon.OpenFile(_basePath, DataFileName, writable: false); } /// /// Clear all content from the guest cache files. /// 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); } /// /// Loads the guest cache from file or memory cache. /// /// Guest TOC file stream /// Guest data file stream /// Guest shader index /// Guest code and constant buffer 1 data 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(tocFileStream); tocFileStream.Seek(Unsafe.SizeOf() + index * Unsafe.SizeOf(), SeekOrigin.Begin); TocEntry entry = new(); 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); } /// /// Clears guest code memory cache, forcing future loads to be from file. /// public void ClearMemoryCache() { _cache = null; } /// /// Calculates the guest shaders count from the TOC file length. /// /// TOC file length /// Shaders count private static int GetShadersCountFromLength(long length) { return (int)((length - Unsafe.SizeOf()) / Unsafe.SizeOf()); } /// /// Adds a guest shader to the cache. /// /// /// If the shader is already on the cache, the existing index will be returned and nothing will be written. /// /// Guest code /// Constant buffer 1 data accessed by the code /// Index of the shader on the cache public int AddShader(ReadOnlySpan data, ReadOnlySpan cb1Data) { using var tocFileStream = DiskCacheCommon.OpenFile(_basePath, TocFileName, writable: true); using var dataFileStream = DiskCacheCommon.OpenFile(_basePath, DataFileName, writable: true); TocHeader header = new(); 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); } /// /// Loads the guest cache TOC file, or create a new one if not present. /// /// Guest TOC file stream /// Set to the TOC file header private void LoadOrCreateToc(Stream tocFileStream, ref TocHeader header) { BinarySerializer reader = new(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; } } /// /// Creates a new guest cache TOC file. /// /// Guest TOC file stream /// Set to the TOC header private static void CreateToc(Stream tocFileStream, ref TocHeader header) { BinarySerializer writer = new(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); } /// /// Reads all the entries on the guest TOC file. /// /// Guest TOC file stream /// TOC file reader /// True if the operation was successful, false otherwise private bool LoadTocEntries(Stream tocFileStream, ref BinarySerializer reader) { _toc = new Dictionary>(); TocEntry entry = new(); 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; } /// /// Writes a new guest code entry into the file. /// /// TOC file stream /// Data file stream /// TOC header, to be updated with the new count /// Guest code /// Constant buffer 1 data accessed by the guest code /// Code and constant buffer data hash /// Entry index private int WriteNewEntry( Stream tocFileStream, Stream dataFileStream, ref TocHeader header, ReadOnlySpan data, ReadOnlySpan cb1Data, uint hash) { BinarySerializer tocWriter = new(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() { Offset = dataOffset, CodeSize = codeSize, Cb1DataSize = cb1DataSize, Hash = hash, }; tocFileStream.Seek(0, SeekOrigin.End); int index = (int)((tocFileStream.Position - Unsafe.SizeOf()) / Unsafe.SizeOf()); tocWriter.Write(ref entry); AddTocMemoryEntry(dataOffset, codeSize, cb1DataSize, hash, index); return index; } /// /// Adds an entry to the memory TOC cache. This can be used to avoid reading the TOC file all the time. /// /// Offset of the code and constant buffer data in the data file /// Code size /// Constant buffer 1 data size /// Code and constant buffer data hash /// Index of the data on the cache 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()); } list.Add(new TocMemoryEntry(dataOffset, codeSize, cb1DataSize, index)); } /// /// Calculates the hash for a data pair. /// /// Data 1 /// Data 2 /// Hash of both data private static uint CalcHash(ReadOnlySpan data, ReadOnlySpan data2) { return CalcHash(data2) * 23 ^ CalcHash(data); } /// /// Calculates the hash for data. /// /// Data to be hashed /// Hash of the data private static uint CalcHash(ReadOnlySpan data) { return (uint)XXHash128.ComputeHash(data).Low; } } }