using Ryujinx.Common;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging;
using Ryujinx.Graphics.GAL;
using Ryujinx.Graphics.Gpu.Memory;
using Ryujinx.Graphics.Gpu.Shader.Cache.Definition;
using Ryujinx.Graphics.Shader;
using Ryujinx.Graphics.Shader.Translation;
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Ryujinx.Graphics.Gpu.Shader.Cache
{
///
/// Helper to manipulate the disk shader cache.
///
static class CacheHelper
{
///
/// Try to read the manifest header from a given file path.
///
/// The path to the manifest file
/// The manifest header read
/// Return true if the manifest header was read
public static bool TryReadManifestHeader(string manifestPath, out CacheManifestHeader header)
{
header = default;
if (File.Exists(manifestPath))
{
Memory rawManifest = File.ReadAllBytes(manifestPath);
if (MemoryMarshal.TryRead(rawManifest.Span, out header))
{
return true;
}
}
return false;
}
///
/// Try to read the manifest from a given file path.
///
/// The path to the manifest file
/// The graphics api used by the cache
/// The hash type of the cache
/// The manifest header read
/// The entries read from the cache manifest
/// Return true if the manifest was read
public static bool TryReadManifestFile(string manifestPath, CacheGraphicsApi graphicsApi, CacheHashType hashType, out CacheManifestHeader header, out HashSet entries)
{
header = default;
entries = new HashSet();
if (File.Exists(manifestPath))
{
Memory rawManifest = File.ReadAllBytes(manifestPath);
if (MemoryMarshal.TryRead(rawManifest.Span, out header))
{
Memory hashTableRaw = rawManifest.Slice(Unsafe.SizeOf());
bool isValid = header.IsValid(graphicsApi, hashType, hashTableRaw.Span);
if (isValid)
{
ReadOnlySpan hashTable = MemoryMarshal.Cast(hashTableRaw.Span);
foreach (Hash128 hash in hashTable)
{
entries.Add(hash);
}
}
return isValid;
}
}
return false;
}
///
/// 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().Slice(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(ZipArchive archive, Hash128 entry)
{
if (archive != null)
{
ZipArchiveEntry archiveEntry = archive.GetEntry($"{entry}");
if (archiveEntry != null)
{
try
{
byte[] result = new byte[archiveEntry.Length];
using (Stream archiveStream = archiveEntry.Open())
{
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;
}
///
/// Compute the guest program code for usage while dumping to disk or hash.
///
/// The guest shader entries to use
/// The transform feedback descriptors
/// Used to determine if the guest program code is generated for hashing
/// The guest program code for usage while dumping to disk or hash
private static byte[] ComputeGuestProgramCode(ReadOnlySpan cachedShaderEntries, TransformFeedbackDescriptor[] tfd, bool forHashCompute = false)
{
using (MemoryStream stream = new MemoryStream())
{
BinaryWriter writer = new BinaryWriter(stream);
foreach (GuestShaderCacheEntry cachedShaderEntry in cachedShaderEntries)
{
if (cachedShaderEntry != null)
{
// Code (and Code A if present)
stream.Write(cachedShaderEntry.Code);
if (forHashCompute)
{
// Guest GPU accessor header (only write this for hashes, already present in the header for dumps)
writer.WriteStruct(cachedShaderEntry.Header.GpuAccessorHeader);
}
// Texture descriptors
foreach (GuestTextureDescriptor textureDescriptor in cachedShaderEntry.TextureDescriptors.Values)
{
writer.WriteStruct(textureDescriptor);
}
}
}
// Transform feedback
if (tfd != null)
{
foreach (TransformFeedbackDescriptor transform in tfd)
{
writer.WriteStruct(new GuestShaderCacheTransformFeedbackHeader(transform.BufferIndex, transform.Stride, transform.VaryingLocations.Length));
writer.Write(transform.VaryingLocations);
}
}
return stream.ToArray();
}
}
///
/// Compute a guest hash from shader entries.
///
/// The guest shader entries to use
/// The optional transform feedback descriptors
/// A guest hash from shader entries
public static Hash128 ComputeGuestHashFromCache(ReadOnlySpan cachedShaderEntries, TransformFeedbackDescriptor[] tfd = null)
{
return XXHash128.ComputeHash(ComputeGuestProgramCode(cachedShaderEntries, tfd, true));
}
///
/// 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 TransformFeedbackDescriptor[] ReadTransformFeedbackInformation(ref ReadOnlySpan data, GuestShaderCacheHeader header)
{
if (header.TransformFeedbackCount != 0)
{
TransformFeedbackDescriptor[] result = new TransformFeedbackDescriptor[header.TransformFeedbackCount];
for (int i = 0; i < result.Length; i++)
{
GuestShaderCacheTransformFeedbackHeader feedbackHeader = MemoryMarshal.Read(data);
result[i] = new TransformFeedbackDescriptor(feedbackHeader.BufferIndex, feedbackHeader.Stride, data.Slice(Unsafe.SizeOf(), feedbackHeader.VaryingLocationsLength).ToArray());
data = data.Slice(Unsafe.SizeOf() + feedbackHeader.VaryingLocationsLength);
}
return result;
}
return null;
}
///
/// Builds gpu state flags using information from the given gpu accessor.
///
/// The gpu accessor
/// The gpu state flags
private static GuestGpuStateFlags GetGpuStateFlags(IGpuAccessor gpuAccessor)
{
GuestGpuStateFlags flags = 0;
if (gpuAccessor.QueryEarlyZForce())
{
flags |= GuestGpuStateFlags.EarlyZForce;
}
return flags;
}
///
/// Packs the tessellation parameters from the gpu accessor.
///
/// The gpu accessor
/// The packed tessellation parameters
private static byte GetTessellationModePacked(IGpuAccessor gpuAccessor)
{
byte value;
value = (byte)((int)gpuAccessor.QueryTessPatchType() & 3);
value |= (byte)(((int)gpuAccessor.QueryTessSpacing() & 3) << 2);
if (gpuAccessor.QueryTessCw())
{
value |= 0x10;
}
return value;
}
///
/// Create a new instance of from an gpu accessor.
///
/// The gpu accessor
/// A new instance of
public static GuestGpuAccessorHeader CreateGuestGpuAccessorCache(IGpuAccessor gpuAccessor)
{
return new GuestGpuAccessorHeader
{
ComputeLocalSizeX = gpuAccessor.QueryComputeLocalSizeX(),
ComputeLocalSizeY = gpuAccessor.QueryComputeLocalSizeY(),
ComputeLocalSizeZ = gpuAccessor.QueryComputeLocalSizeZ(),
ComputeLocalMemorySize = gpuAccessor.QueryComputeLocalMemorySize(),
ComputeSharedMemorySize = gpuAccessor.QueryComputeSharedMemorySize(),
PrimitiveTopology = gpuAccessor.QueryPrimitiveTopology(),
TessellationModePacked = GetTessellationModePacked(gpuAccessor),
StateFlags = GetGpuStateFlags(gpuAccessor)
};
}
///
/// Create guest shader cache entries from the runtime contexts.
///
/// The GPU channel in use
/// The runtime contexts
/// Guest shader cahe entries from the runtime contexts
public static GuestShaderCacheEntry[] CreateShaderCacheEntries(GpuChannel channel, ReadOnlySpan shaderContexts)
{
MemoryManager memoryManager = channel.MemoryManager;
int startIndex = shaderContexts.Length > 1 ? 1 : 0;
GuestShaderCacheEntry[] entries = new GuestShaderCacheEntry[shaderContexts.Length - startIndex];
for (int i = startIndex; i < shaderContexts.Length; i++)
{
TranslatorContext context = shaderContexts[i];
if (context == null)
{
continue;
}
GpuAccessor gpuAccessor = context.GpuAccessor as GpuAccessor;
ulong cb1DataAddress;
int cb1DataSize = gpuAccessor?.Cb1DataSize ?? 0;
if (context.Stage == ShaderStage.Compute)
{
cb1DataAddress = channel.BufferManager.GetComputeUniformBufferAddress(1);
}
else
{
int stageIndex = context.Stage switch
{
ShaderStage.TessellationControl => 1,
ShaderStage.TessellationEvaluation => 2,
ShaderStage.Geometry => 3,
ShaderStage.Fragment => 4,
_ => 0
};
cb1DataAddress = channel.BufferManager.GetGraphicsUniformBufferAddress(stageIndex, 1);
}
int size = context.Size;
TranslatorContext translatorContext2 = i == 1 ? shaderContexts[0] : null;
int sizeA = translatorContext2 != null ? translatorContext2.Size : 0;
byte[] code = new byte[size + cb1DataSize + sizeA];
memoryManager.GetSpan(context.Address, size).CopyTo(code);
if (cb1DataAddress != 0 && cb1DataSize != 0)
{
memoryManager.Physical.GetSpan(cb1DataAddress, cb1DataSize).CopyTo(code.AsSpan().Slice(size, cb1DataSize));
}
if (translatorContext2 != null)
{
memoryManager.GetSpan(translatorContext2.Address, sizeA).CopyTo(code.AsSpan().Slice(size + cb1DataSize, sizeA));
}
GuestGpuAccessorHeader gpuAccessorHeader = CreateGuestGpuAccessorCache(context.GpuAccessor);
if (gpuAccessor != null)
{
gpuAccessorHeader.TextureDescriptorCount = context.TextureHandlesForCache.Count;
}
GuestShaderCacheEntryHeader header = new GuestShaderCacheEntryHeader(
context.Stage,
size + cb1DataSize,
sizeA,
cb1DataSize,
gpuAccessorHeader);
GuestShaderCacheEntry entry = new GuestShaderCacheEntry(header, code);
if (gpuAccessor != null)
{
foreach (int textureHandle in context.TextureHandlesForCache)
{
GuestTextureDescriptor textureDescriptor = ((Image.TextureDescriptor)gpuAccessor.GetTextureDescriptor(textureHandle, -1)).ToCache();
textureDescriptor.Handle = (uint)textureHandle;
entry.TextureDescriptors.Add(textureHandle, textureDescriptor);
}
}
entries[i - startIndex] = entry;
}
return entries;
}
///
/// Create a guest shader program.
///
/// The entries composing the guest program dump
/// The transform feedback descriptors in use
/// The resulting guest shader program
public static byte[] CreateGuestProgramDump(GuestShaderCacheEntry[] shaderCacheEntries, TransformFeedbackDescriptor[] tfd = null)
{
using (MemoryStream resultStream = new MemoryStream())
{
BinaryWriter resultStreamWriter = new BinaryWriter(resultStream);
byte transformFeedbackCount = 0;
if (tfd != null)
{
transformFeedbackCount = (byte)tfd.Length;
}
// Header
resultStreamWriter.WriteStruct(new GuestShaderCacheHeader((byte)shaderCacheEntries.Length, transformFeedbackCount));
// Write all entries header
foreach (GuestShaderCacheEntry entry in shaderCacheEntries)
{
if (entry == null)
{
resultStreamWriter.WriteStruct(new GuestShaderCacheEntryHeader());
}
else
{
resultStreamWriter.WriteStruct(entry.Header);
}
}
// Finally, write all program code and all transform feedback information.
resultStreamWriter.Write(ComputeGuestProgramCode(shaderCacheEntries, tfd));
return resultStream.ToArray();
}
}
///
/// 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, ZipArchive archive, HashSet entries)
{
foreach (Hash128 hash in entries)
{
string cacheTempFilePath = GenCacheTempFilePath(baseCacheDirectory, hash);
if (File.Exists(cacheTempFilePath))
{
string cacheHash = $"{hash}";
ZipArchiveEntry entry = archive.GetEntry(cacheHash);
entry?.Delete();
archive.CreateEntryFromFile(cacheTempFilePath, cacheHash);
File.Delete(cacheTempFilePath);
}
}
}
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;
}
}
}
}