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
{
///
/// Helper to manipulate the disk shader cache.
///
static class CacheHelper
{
///
/// Compute a cache manifest from runtime data.
///
/// The version of the cache
/// The graphics api used by the cache
/// The hash type of the cache
/// The entries in the cache
/// The cache manifest from runtime data
public static byte[] ComputeManifest(ulong version, CacheGraphicsApi graphicsApi, CacheHashType hashType, HashSet entries)
{
if (hashType != CacheHashType.XxHash128)
{
throw new NotImplementedException($"{hashType}");
}
CacheManifestHeader manifestHeader = new CacheManifestHeader(version, graphicsApi, hashType);
byte[] data = new byte[Unsafe.SizeOf() + entries.Count * Unsafe.SizeOf()];
// CacheManifestHeader has the same size as a Hash128.
Span dataSpan = MemoryMarshal.Cast(data.AsSpan()).Slice(1);
int i = 0;
foreach (Hash128 hash in entries)
{
dataSpan[i++] = hash;
}
manifestHeader.UpdateChecksum(data.AsSpan(Unsafe.SizeOf()));
MemoryMarshal.Write(data, ref manifestHeader);
return data;
}
///
/// Get the base directory of the shader cache for a given title id.
///
/// The title id of the target application
/// The base directory of the shader cache for a given title id
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string GetBaseCacheDirectory(string titleId) => Path.Combine(AppDataManager.GamesDirPath, titleId, "cache", "shader");
///
/// Get the temp path to the cache data directory.
///
/// The cache directory
/// The temp path to the cache data directory
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string GetCacheTempDataPath(string cacheDirectory) => Path.Combine(cacheDirectory, "temp");
///
/// The path to the cache archive file.
///
/// The cache directory
/// The path to the cache archive file
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string GetArchivePath(string cacheDirectory) => Path.Combine(cacheDirectory, "cache.zip");
///
/// The path to the cache manifest file.
///
/// The cache directory
/// The path to the cache manifest file
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string GetManifestPath(string cacheDirectory) => Path.Combine(cacheDirectory, "cache.info");
///
/// Create a new temp path to the given cached file via its hash.
///
/// The cache directory
/// The hash of the cached data
/// New path to the given cached file
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string GenCacheTempFilePath(string cacheDirectory, Hash128 key) => Path.Combine(GetCacheTempDataPath(cacheDirectory), key.ToString());
///
/// Generate the path to the cache directory.
///
/// The base of the cache directory
/// The graphics api in use
/// The name of the shader provider in use
/// The name of the cache
/// The path to the cache directory
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);
}
///
/// Read a cached file with the given hash that is present in the archive.
///
/// The archive in use
/// The given hash
/// The cached file if present or null
[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;
}
///
/// Read a cached file with the given hash that is not present in the archive.
///
/// The cache directory
/// The given hash
/// The cached file if present or null
[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;
}
///
/// Read transform feedback descriptors from guest.
///
/// The raw guest transform feedback descriptors
/// The guest shader program header
/// The transform feedback descriptors read from guest
public static TransformFeedbackDescriptorOld[] ReadTransformFeedbackInformation(ref ReadOnlySpan 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(data);
result[i] = new TransformFeedbackDescriptorOld(feedbackHeader.BufferIndex, feedbackHeader.Stride, data.Slice(Unsafe.SizeOf(), feedbackHeader.VaryingLocationsLength).ToArray());
data = data.Slice(Unsafe.SizeOf() + feedbackHeader.VaryingLocationsLength);
}
return result;
}
return null;
}
///
/// Save temporary files not in archive.
///
/// The base of the cache directory
/// The archive to use
/// The entries in the cache
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void EnsureArchiveUpToDate(string baseCacheDirectory, ZipFile archive, HashSet entries)
{
List filesToDelete = new List();
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;
}
}
}
}