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;
}
}
}