459 lines
16 KiB
C#
459 lines
16 KiB
C#
|
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 = 0;
|
||
|
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>Tuple with the guest code and constant buffer 1 data, respectively</returns>
|
||
|
public (byte[], byte[]) 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 (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;
|
||
|
}
|
||
|
}
|
||
|
}
|