274 lines
11 KiB
C#
274 lines
11 KiB
C#
using ICSharpCode.SharpZipLib.Zip;
|
|
using Ryujinx.Common;
|
|
using Ryujinx.Common.Configuration;
|
|
using Ryujinx.Common.Logging;
|
|
using Ryujinx.Graphics.Gpu.Shader.Cache.Definition;
|
|
using Ryujinx.Graphics.Shader;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Runtime.InteropServices;
|
|
|
|
namespace Ryujinx.Graphics.Gpu.Shader.Cache
|
|
{
|
|
/// <summary>
|
|
/// Helper to manipulate the disk shader cache.
|
|
/// </summary>
|
|
static class CacheHelper
|
|
{
|
|
/// <summary>
|
|
/// Compute a cache manifest from runtime data.
|
|
/// </summary>
|
|
/// <param name="version">The version of the cache</param>
|
|
/// <param name="graphicsApi">The graphics api used by the cache</param>
|
|
/// <param name="hashType">The hash type of the cache</param>
|
|
/// <param name="entries">The entries in the cache</param>
|
|
/// <returns>The cache manifest from runtime data</returns>
|
|
public static byte[] ComputeManifest(ulong version, CacheGraphicsApi graphicsApi, CacheHashType hashType, HashSet<Hash128> entries)
|
|
{
|
|
if (hashType != CacheHashType.XxHash128)
|
|
{
|
|
throw new NotImplementedException($"{hashType}");
|
|
}
|
|
|
|
CacheManifestHeader manifestHeader = new CacheManifestHeader(version, graphicsApi, hashType);
|
|
|
|
byte[] data = new byte[Unsafe.SizeOf<CacheManifestHeader>() + entries.Count * Unsafe.SizeOf<Hash128>()];
|
|
|
|
// CacheManifestHeader has the same size as a Hash128.
|
|
Span<Hash128> dataSpan = MemoryMarshal.Cast<byte, Hash128>(data.AsSpan()).Slice(1);
|
|
|
|
int i = 0;
|
|
|
|
foreach (Hash128 hash in entries)
|
|
{
|
|
dataSpan[i++] = hash;
|
|
}
|
|
|
|
manifestHeader.UpdateChecksum(data.AsSpan(Unsafe.SizeOf<CacheManifestHeader>()));
|
|
|
|
MemoryMarshal.Write(data, ref manifestHeader);
|
|
|
|
return data;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the base directory of the shader cache for a given title id.
|
|
/// </summary>
|
|
/// <param name="titleId">The title id of the target application</param>
|
|
/// <returns>The base directory of the shader cache for a given title id</returns>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static string GetBaseCacheDirectory(string titleId) => Path.Combine(AppDataManager.GamesDirPath, titleId, "cache", "shader");
|
|
|
|
/// <summary>
|
|
/// Get the temp path to the cache data directory.
|
|
/// </summary>
|
|
/// <param name="cacheDirectory">The cache directory</param>
|
|
/// <returns>The temp path to the cache data directory</returns>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static string GetCacheTempDataPath(string cacheDirectory) => Path.Combine(cacheDirectory, "temp");
|
|
|
|
/// <summary>
|
|
/// The path to the cache archive file.
|
|
/// </summary>
|
|
/// <param name="cacheDirectory">The cache directory</param>
|
|
/// <returns>The path to the cache archive file</returns>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static string GetArchivePath(string cacheDirectory) => Path.Combine(cacheDirectory, "cache.zip");
|
|
|
|
/// <summary>
|
|
/// The path to the cache manifest file.
|
|
/// </summary>
|
|
/// <param name="cacheDirectory">The cache directory</param>
|
|
/// <returns>The path to the cache manifest file</returns>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static string GetManifestPath(string cacheDirectory) => Path.Combine(cacheDirectory, "cache.info");
|
|
|
|
/// <summary>
|
|
/// Create a new temp path to the given cached file via its hash.
|
|
/// </summary>
|
|
/// <param name="cacheDirectory">The cache directory</param>
|
|
/// <param name="key">The hash of the cached data</param>
|
|
/// <returns>New path to the given cached file</returns>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static string GenCacheTempFilePath(string cacheDirectory, Hash128 key) => Path.Combine(GetCacheTempDataPath(cacheDirectory), key.ToString());
|
|
|
|
/// <summary>
|
|
/// Generate the path to the cache directory.
|
|
/// </summary>
|
|
/// <param name="baseCacheDirectory">The base of the cache directory</param>
|
|
/// <param name="graphicsApi">The graphics api in use</param>
|
|
/// <param name="shaderProvider">The name of the shader provider in use</param>
|
|
/// <param name="cacheName">The name of the cache</param>
|
|
/// <returns>The path to the cache directory</returns>
|
|
public static string GenerateCachePath(string baseCacheDirectory, CacheGraphicsApi graphicsApi, string shaderProvider, string cacheName)
|
|
{
|
|
string graphicsApiName = graphicsApi switch
|
|
{
|
|
CacheGraphicsApi.OpenGL => "opengl",
|
|
CacheGraphicsApi.OpenGLES => "opengles",
|
|
CacheGraphicsApi.Vulkan => "vulkan",
|
|
CacheGraphicsApi.DirectX => "directx",
|
|
CacheGraphicsApi.Metal => "metal",
|
|
CacheGraphicsApi.Guest => "guest",
|
|
_ => throw new NotImplementedException(graphicsApi.ToString()),
|
|
};
|
|
|
|
return Path.Combine(baseCacheDirectory, graphicsApiName, shaderProvider, cacheName);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Read a cached file with the given hash that is present in the archive.
|
|
/// </summary>
|
|
/// <param name="archive">The archive in use</param>
|
|
/// <param name="entry">The given hash</param>
|
|
/// <returns>The cached file if present or null</returns>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static byte[] ReadFromArchive(ZipFile archive, Hash128 entry)
|
|
{
|
|
if (archive != null)
|
|
{
|
|
ZipEntry archiveEntry = archive.GetEntry($"{entry}");
|
|
|
|
if (archiveEntry != null)
|
|
{
|
|
try
|
|
{
|
|
byte[] result = new byte[archiveEntry.Size];
|
|
|
|
using (Stream archiveStream = archive.GetInputStream(archiveEntry))
|
|
{
|
|
archiveStream.Read(result);
|
|
|
|
return result;
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Logger.Error?.Print(LogClass.Gpu, $"Cannot load cache file {entry} from archive");
|
|
Logger.Error?.Print(LogClass.Gpu, e.ToString());
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Read a cached file with the given hash that is not present in the archive.
|
|
/// </summary>
|
|
/// <param name="cacheDirectory">The cache directory</param>
|
|
/// <param name="entry">The given hash</param>
|
|
/// <returns>The cached file if present or null</returns>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static byte[] ReadFromFile(string cacheDirectory, Hash128 entry)
|
|
{
|
|
string cacheTempFilePath = GenCacheTempFilePath(cacheDirectory, entry);
|
|
|
|
try
|
|
{
|
|
return File.ReadAllBytes(cacheTempFilePath);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Logger.Error?.Print(LogClass.Gpu, $"Cannot load cache file at {cacheTempFilePath}");
|
|
Logger.Error?.Print(LogClass.Gpu, e.ToString());
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Read transform feedback descriptors from guest.
|
|
/// </summary>
|
|
/// <param name="data">The raw guest transform feedback descriptors</param>
|
|
/// <param name="header">The guest shader program header</param>
|
|
/// <returns>The transform feedback descriptors read from guest</returns>
|
|
public static TransformFeedbackDescriptorOld[] ReadTransformFeedbackInformation(ref ReadOnlySpan<byte> data, GuestShaderCacheHeader header)
|
|
{
|
|
if (header.TransformFeedbackCount != 0)
|
|
{
|
|
TransformFeedbackDescriptorOld[] result = new TransformFeedbackDescriptorOld[header.TransformFeedbackCount];
|
|
|
|
for (int i = 0; i < result.Length; i++)
|
|
{
|
|
GuestShaderCacheTransformFeedbackHeader feedbackHeader = MemoryMarshal.Read<GuestShaderCacheTransformFeedbackHeader>(data);
|
|
|
|
result[i] = new TransformFeedbackDescriptorOld(feedbackHeader.BufferIndex, feedbackHeader.Stride, data.Slice(Unsafe.SizeOf<GuestShaderCacheTransformFeedbackHeader>(), feedbackHeader.VaryingLocationsLength).ToArray());
|
|
|
|
data = data.Slice(Unsafe.SizeOf<GuestShaderCacheTransformFeedbackHeader>() + feedbackHeader.VaryingLocationsLength);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Save temporary files not in archive.
|
|
/// </summary>
|
|
/// <param name="baseCacheDirectory">The base of the cache directory</param>
|
|
/// <param name="archive">The archive to use</param>
|
|
/// <param name="entries">The entries in the cache</param>
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static void EnsureArchiveUpToDate(string baseCacheDirectory, ZipFile archive, HashSet<Hash128> entries)
|
|
{
|
|
List<string> filesToDelete = new List<string>();
|
|
|
|
archive.BeginUpdate();
|
|
|
|
foreach (Hash128 hash in entries)
|
|
{
|
|
string cacheTempFilePath = GenCacheTempFilePath(baseCacheDirectory, hash);
|
|
|
|
if (File.Exists(cacheTempFilePath))
|
|
{
|
|
string cacheHash = $"{hash}";
|
|
|
|
ZipEntry entry = archive.GetEntry(cacheHash);
|
|
|
|
if (entry != null)
|
|
{
|
|
archive.Delete(entry);
|
|
}
|
|
|
|
// We enforce deflate compression here to avoid possible incompatibilities on older version of Ryujinx that use System.IO.Compression.
|
|
archive.Add(new StaticDiskDataSource(cacheTempFilePath), cacheHash, CompressionMethod.Deflated);
|
|
filesToDelete.Add(cacheTempFilePath);
|
|
}
|
|
}
|
|
|
|
archive.CommitUpdate();
|
|
|
|
foreach (string filePath in filesToDelete)
|
|
{
|
|
File.Delete(filePath);
|
|
}
|
|
}
|
|
|
|
public static bool IsArchiveReadOnly(string archivePath)
|
|
{
|
|
FileInfo info = new FileInfo(archivePath);
|
|
|
|
if (!info.Exists)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
try
|
|
{
|
|
using (FileStream stream = info.Open(FileMode.Open, FileAccess.Read, FileShare.None))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
catch (IOException)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|