diff options
Diffstat (limited to 'Ryujinx.Graphics.Gpu/Shader/Cache/CacheHelper.cs')
-rw-r--r-- | Ryujinx.Graphics.Gpu/Shader/Cache/CacheHelper.cs | 482 |
1 files changed, 482 insertions, 0 deletions
diff --git a/Ryujinx.Graphics.Gpu/Shader/Cache/CacheHelper.cs b/Ryujinx.Graphics.Gpu/Shader/Cache/CacheHelper.cs new file mode 100644 index 00000000..1d492214 --- /dev/null +++ b/Ryujinx.Graphics.Gpu/Shader/Cache/CacheHelper.cs @@ -0,0 +1,482 @@ +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 +{ + /// <summary> + /// Helper to manipulate the disk shader cache. + /// </summary> + static class CacheHelper + { + /// <summary> + /// Try to read the manifest header from a given file path. + /// </summary> + /// <param name="manifestPath">The path to the manifest file</param> + /// <param name="header">The manifest header read</param> + /// <returns>Return true if the manifest header was read</returns> + public static bool TryReadManifestHeader(string manifestPath, out CacheManifestHeader header) + { + header = default; + + if (File.Exists(manifestPath)) + { + Memory<byte> rawManifest = File.ReadAllBytes(manifestPath); + + if (MemoryMarshal.TryRead(rawManifest.Span, out header)) + { + return true; + } + } + + return false; + } + + /// <summary> + /// Try to read the manifest from a given file path. + /// </summary> + /// <param name="manifestPath">The path to the manifest file</param> + /// <param name="graphicsApi">The graphics api used by the cache</param> + /// <param name="hashType">The hash type of the cache</param> + /// <param name="header">The manifest header read</param> + /// <param name="entries">The entries read from the cache manifest</param> + /// <returns>Return true if the manifest was read</returns> + public static bool TryReadManifestFile(string manifestPath, CacheGraphicsApi graphicsApi, CacheHashType hashType, out CacheManifestHeader header, out HashSet<Hash128> entries) + { + header = default; + entries = new HashSet<Hash128>(); + + if (File.Exists(manifestPath)) + { + Memory<byte> rawManifest = File.ReadAllBytes(manifestPath); + + if (MemoryMarshal.TryRead(rawManifest.Span, out header)) + { + Memory<byte> hashTableRaw = rawManifest.Slice(Unsafe.SizeOf<CacheManifestHeader>()); + + bool isValid = header.IsValid(graphicsApi, hashType, hashTableRaw.Span); + + if (isValid) + { + ReadOnlySpan<Hash128> hashTable = MemoryMarshal.Cast<byte, Hash128>(hashTableRaw.Span); + + foreach (Hash128 hash in hashTable) + { + entries.Add(hash); + } + } + + return isValid; + } + } + + return false; + } + + /// <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().Slice(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(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; + } + + /// <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> + /// Compute the guest program code for usage while dumping to disk or hash. + /// </summary> + /// <param name="cachedShaderEntries">The guest shader entries to use</param> + /// <param name="tfd">The transform feedback descriptors</param> + /// <param name="forHashCompute">Used to determine if the guest program code is generated for hashing</param> + /// <returns>The guest program code for usage while dumping to disk or hash</returns> + private static byte[] ComputeGuestProgramCode(ReadOnlySpan<GuestShaderCacheEntry> 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); + } + } + } + + // Transformation 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(); + } + } + + /// <summary> + /// Compute a guest hash from shader entries. + /// </summary> + /// <param name="cachedShaderEntries">The guest shader entries to use</param> + /// <param name="tfd">The optional transform feedback descriptors</param> + /// <returns>A guest hash from shader entries</returns> + public static Hash128 ComputeGuestHashFromCache(ReadOnlySpan<GuestShaderCacheEntry> cachedShaderEntries, TransformFeedbackDescriptor[] tfd = null) + { + return XXHash128.ComputeHash(ComputeGuestProgramCode(cachedShaderEntries, tfd, true)); + } + + /// <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 TransformFeedbackDescriptor[] ReadTransformationFeedbackInformations(ref ReadOnlySpan<byte> 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<GuestShaderCacheTransformFeedbackHeader>(data); + + result[i] = new TransformFeedbackDescriptor(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> + /// Create a new instance of <see cref="GuestGpuAccessorHeader"/> from an gpu accessor. + /// </summary> + /// <param name="gpuAccessor">The gpu accessor</param> + /// <returns>A new instance of <see cref="GuestGpuAccessorHeader"/></returns> + 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(), + }; + } + + /// <summary> + /// Create guest shader cache entries from the runtime contexts. + /// </summary> + /// <param name="memoryManager">The GPU memory manager in use</param> + /// <param name="shaderContexts">The runtime contexts</param> + /// <returns>Guest shader cahe entries from the runtime contexts</returns> + public static GuestShaderCacheEntry[] CreateShaderCacheEntries(MemoryManager memoryManager, ReadOnlySpan<TranslatorContext> shaderContexts) + { + GuestShaderCacheEntry[] entries = new GuestShaderCacheEntry[shaderContexts.Length]; + + for (int i = 0; i < shaderContexts.Length; i++) + { + TranslatorContext context = shaderContexts[i]; + + if (context == null) + { + continue; + } + + int sizeA = context.AddressA == 0 ? 0 : context.SizeA; + + byte[] code = new byte[context.Size + sizeA]; + + memoryManager.GetSpan(context.Address, context.Size).CopyTo(code); + + if (context.AddressA != 0) + { + memoryManager.GetSpan(context.AddressA, context.SizeA).CopyTo(code.AsSpan().Slice(context.Size, context.SizeA)); + } + + GuestGpuAccessorHeader gpuAccessorHeader = CreateGuestGpuAccessorCache(context.GpuAccessor); + + if (context.GpuAccessor is GpuAccessor) + { + gpuAccessorHeader.TextureDescriptorCount = context.TextureHandlesForCache.Count; + } + + GuestShaderCacheEntryHeader header = new GuestShaderCacheEntryHeader(context.Stage, context.Size, sizeA, gpuAccessorHeader); + + GuestShaderCacheEntry entry = new GuestShaderCacheEntry(header, code); + + if (context.GpuAccessor is GpuAccessor gpuAccessor) + { + foreach (int textureHandle in context.TextureHandlesForCache) + { + GuestTextureDescriptor textureDescriptor = ((Image.TextureDescriptor)gpuAccessor.GetTextureDescriptor(textureHandle)).ToCache(); + + textureDescriptor.Handle = (uint)textureHandle; + + entry.TextureDescriptors.Add(textureHandle, textureDescriptor); + } + } + + entries[i] = entry; + } + + return entries; + } + + /// <summary> + /// Create a guest shader program. + /// </summary> + /// <param name="shaderCacheEntries">The entries composing the guest program dump</param> + /// <param name="tfd">The transform feedback descriptors in use</param> + /// <returns>The resulting guest shader program</returns> + 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(); + } + } + + /// <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, ZipArchive archive, HashSet<Hash128> 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); + } + } + } + } +} |