From 2920f8843b1538d82946ea2565aaa58c9c7d45f0 Mon Sep 17 00:00:00 2001 From: ParantezTech <12572227+par274@users.noreply.github.com> Date: Thu, 8 Oct 2026 02:53:24 +0300 Subject: [PATCH] [GPU] Add the shader cache file, shader inventory, game file scanner and shader seed --- .../Gpu/ShaderCache/AgcShaderHeader.cs | 255 ++++ .../Gpu/ShaderCache/GameShaderScanner.cs | 189 +++ .../Gpu/ShaderCache/RecordDifference.cs | 150 +++ .../Gpu/ShaderCache/ShaderCacheFile.cs | 1146 +++++++++++++++++ .../Gpu/ShaderCache/ShaderCacheRecords.cs | 768 +++++++++++ .../Gpu/ShaderCache/ShaderCacheSettings.cs | 93 ++ .../Gpu/ShaderCache/ShaderCodeCapture.cs | 121 ++ .../Gpu/ShaderCache/ShaderInventory.cs | 215 ++++ .../Gpu/ShaderCache/ShaderSeed.cs | 401 ++++++ .../Gpu/ShaderCache/ShaderWorkList.cs | 241 ++++ .../Gpu/ShaderCache/StaticStageInputs.cs | 219 ++++ 11 files changed, 3798 insertions(+) create mode 100644 src/SharpEmu.Libs/Gpu/ShaderCache/AgcShaderHeader.cs create mode 100644 src/SharpEmu.Libs/Gpu/ShaderCache/GameShaderScanner.cs create mode 100644 src/SharpEmu.Libs/Gpu/ShaderCache/RecordDifference.cs create mode 100644 src/SharpEmu.Libs/Gpu/ShaderCache/ShaderCacheFile.cs create mode 100644 src/SharpEmu.Libs/Gpu/ShaderCache/ShaderCacheRecords.cs create mode 100644 src/SharpEmu.Libs/Gpu/ShaderCache/ShaderCacheSettings.cs create mode 100644 src/SharpEmu.Libs/Gpu/ShaderCache/ShaderCodeCapture.cs create mode 100644 src/SharpEmu.Libs/Gpu/ShaderCache/ShaderInventory.cs create mode 100644 src/SharpEmu.Libs/Gpu/ShaderCache/ShaderSeed.cs create mode 100644 src/SharpEmu.Libs/Gpu/ShaderCache/ShaderWorkList.cs create mode 100644 src/SharpEmu.Libs/Gpu/ShaderCache/StaticStageInputs.cs diff --git a/src/SharpEmu.Libs/Gpu/ShaderCache/AgcShaderHeader.cs b/src/SharpEmu.Libs/Gpu/ShaderCache/AgcShaderHeader.cs new file mode 100644 index 0000000..fe7c429 --- /dev/null +++ b/src/SharpEmu.Libs/Gpu/ShaderCache/AgcShaderHeader.cs @@ -0,0 +1,255 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using System.Buffers.Binary; + +namespace SharpEmu.Libs.Gpu.ShaderCache; + +internal enum AgcShaderType : byte +{ + Compute = 0, + Pixel = 1, + Geometry = 2, +} + +internal readonly record struct AgcRegister(uint Offset, uint Value); + +internal sealed class AgcShaderHeader +{ + public const int FixedBytes = 96; + public const int MaxHeaderBytes = 1 << 16; + public const int MaxCodeBytes = 1 << 22; + private const uint FileMagic = 0x34333231; + private const uint Version = 0x18; + private const int UserDataField = 8; + private const int CodeField = 16; + private const int ContextRegistersField = 24; + private const int ShaderRegistersField = 32; + private const int SpecialsField = 40; + private const int InputSemanticsField = 48; + private const int OutputSemanticsField = 56; + private const int UserDataBlockBytes = 56; + private const int UserDataPointerCount = 5; + private const int DispatchModifierOffset = 16; + private const int MinSpecialsBytes = 20; + private const int DirectResourceCountOffset = 44; + private const int FetchTableSlot = 8; + private const int EmbeddedFetchSlot = 10; + + public static ReadOnlySpan Signature => [0x31, 0x32, 0x33, 0x34, 0x18, 0x00, 0x00, 0x00]; + + private AgcShaderHeader(byte[] bytes) + { + Bytes = bytes; + } + + public byte[] Bytes { get; } + + public uint HeaderSize => U32(64); + + public uint CodeSize => U32(68); + + public AgcShaderType Type => (AgcShaderType)Bytes[90]; + + public uint ScratchDwords => U16(84); + + public uint InputSemanticCount => U32(80); + + public uint OutputSemanticCount => U16(86); + + public int InputSemanticsOffset => Pointer(InputSemanticsField); + + public IReadOnlyList ContextRegisters => Registers(ContextRegistersField, Bytes[91]); + + public IReadOnlyList ShaderRegisters => Registers(ShaderRegistersField, Bytes[92]); + + public uint DispatchModifier + { + get + { + var specials = Pointer(SpecialsField); + return specials != 0 && U16(88) >= MinSpecialsBytes ? U32(specials + DispatchModifierOffset) : 0; + } + } + + public uint[] InputSemantics => Words(InputSemanticsField, (int)Math.Min(InputSemanticCount, 32u)); + + public uint[] OutputSemantics => Words(OutputSemanticsField, (int)OutputSemanticCount); + + public bool HasVertexFetchTables + { + get + { + var userData = Pointer(UserDataField); + if (userData == 0) + { + return false; + } + + var count = U16(userData + DirectResourceCountOffset); + var offsets = SelfRelative(userData); + bool Bound(int slot) => count > slot && (offsets == 0 || U16(offsets + slot * 2) != ushort.MaxValue); + return Bound(FetchTableSlot) || Bound(EmbeddedFetchSlot); + } + } + + public static bool IsSupported(byte type) => type <= (byte)AgcShaderType.Geometry; + + public static bool TryPeekSizes(ReadOnlySpan fixedBytes, out int headerBytes, out int codeBytes, out byte type) + { + headerBytes = codeBytes = 0; + type = 0; + if (fixedBytes.Length < FixedBytes || !fixedBytes[..8].SequenceEqual(Signature)) + { + return false; + } + + var header = BinaryPrimitives.ReadUInt32LittleEndian(fixedBytes[64..]); + var code = BinaryPrimitives.ReadUInt32LittleEndian(fixedBytes[68..]); + type = fixedBytes[90]; + if (header < FixedBytes || header > MaxHeaderBytes || code == 0 || code > MaxCodeBytes || code % sizeof(uint) != 0) + { + return false; + } + + headerBytes = (int)header; + codeBytes = (int)code; + return true; + } + + public static bool TryParse(ReadOnlySpan bytes, ulong headerAddress, out AgcShaderHeader header) + { + header = null!; + if (!TryPeekSizes(bytes, out var headerBytes, out _, out var type) || headerBytes != bytes.Length || !IsSupported(type)) + { + return false; + } + + var normalized = new AgcShaderHeader(bytes.ToArray()); + foreach (var field in (ReadOnlySpan)[UserDataField, ContextRegistersField, ShaderRegistersField, SpecialsField, InputSemanticsField, OutputSemanticsField]) + { + if (!normalized.Normalize(field, headerAddress)) + { + return false; + } + } + + BinaryPrimitives.WriteUInt64LittleEndian(normalized.Bytes.AsSpan(CodeField), 0); + var userData = normalized.Pointer(UserDataField); + if (userData != 0) + { + if (userData + UserDataBlockBytes > normalized.Bytes.Length) + { + return false; + } + + for (var index = 0; index < UserDataPointerCount; index++) + { + var field = userData + index * sizeof(ulong); + if (!normalized.Normalize(field, headerAddress)) + { + if (index == 0) + { + return false; + } + + BinaryPrimitives.WriteUInt64LittleEndian(normalized.Bytes.AsSpan(field), 0); + } + } + } + + try + { + _ = normalized.ContextRegisters; + _ = normalized.ShaderRegisters; + _ = normalized.InputSemantics; + _ = normalized.OutputSemantics; + } + catch (InvalidDataException) + { + return false; + } + + header = normalized; + return true; + } + + private bool Normalize(int field, ulong headerAddress) + { + var value = BinaryPrimitives.ReadUInt64LittleEndian(Bytes.AsSpan(field)); + if (value == 0) + { + return true; + } + + var relative = (long)value; + if (relative > -field && relative < Bytes.Length - field) + { + return true; + } + + if (headerAddress == 0 || value < headerAddress || value - headerAddress >= (ulong)Bytes.Length) + { + return false; + } + + BinaryPrimitives.WriteInt64LittleEndian(Bytes.AsSpan(field), (long)(value - headerAddress) - field); + return true; + } + + private int SelfRelative(int field) + { + var relative = BinaryPrimitives.ReadInt64LittleEndian(Bytes.AsSpan(field)); + return relative == 0 ? 0 : checked((int)(field + relative)); + } + + private int Pointer(int field) => SelfRelative(field); + + private AgcRegister[] Registers(int field, int count) + { + var start = Pointer(field); + if (count == 0) + { + return []; + } + + if (start <= 0 || start + count * 8 > Bytes.Length) + { + throw new InvalidDataException("The AGC register table is outside the header."); + } + + var registers = new AgcRegister[count]; + for (var index = 0; index < count; index++) + { + registers[index] = new AgcRegister(U32(start + index * 8), U32(start + index * 8 + 4)); + } + + return registers; + } + + private uint[] Words(int field, int count) + { + var start = Pointer(field); + if (count == 0 || start == 0) + { + return []; + } + + if (start < 0 || start + count * 4 > Bytes.Length) + { + throw new InvalidDataException("The AGC semantic table is outside the header."); + } + + var words = new uint[count]; + for (var index = 0; index < count; index++) + { + words[index] = U32(start + index * 4); + } + + return words; + } + + private uint U32(int offset) => BinaryPrimitives.ReadUInt32LittleEndian(Bytes.AsSpan(offset)); + + private ushort U16(int offset) => BinaryPrimitives.ReadUInt16LittleEndian(Bytes.AsSpan(offset)); +} diff --git a/src/SharpEmu.Libs/Gpu/ShaderCache/GameShaderScanner.cs b/src/SharpEmu.Libs/Gpu/ShaderCache/GameShaderScanner.cs new file mode 100644 index 0000000..2d4f561 --- /dev/null +++ b/src/SharpEmu.Libs/Gpu/ShaderCache/GameShaderScanner.cs @@ -0,0 +1,189 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using Microsoft.Win32.SafeHandles; + +namespace SharpEmu.Libs.Gpu.ShaderCache; + +internal readonly record struct ShaderScanResult(int FilesScanned, int FilesSkipped, int Programs, long BytesScanned); + +internal static class GameShaderScanner +{ + public const ulong ScannedCodeAddress = 0x10_0000_0000; + public const int Version = 2; + private const int ChunkBytes = 8 << 20; + private const int CodeSlackBytes = 256; + private const int CodeAlignment = 256; + + private static readonly HashSet MediaExtensions = new(StringComparer.OrdinalIgnoreCase) + { + ".mp4", ".m4v", ".webm", ".bk2", ".bik", ".usm", ".at9", ".wem", ".bnk", ".ogg", ".wav", ".mp3", ".opus", ".flac", + }; + + private readonly record struct FoundProgram(long Offset, CachedProgram? Program, AgcShaderType Type, ulong Identity); + + public static ShaderScanResult Scan(string root, ShaderCacheFile file, Action? progress, CancellationToken cancellation) + { + var files = Directory.EnumerateFiles(root, "*", new EnumerationOptions { RecurseSubdirectories = true, IgnoreInaccessible = true }) + .Where(path => !MediaExtensions.Contains(Path.GetExtension(path))) + .Select(path => new FileInfo(path)) + .Where(info => info.Length >= AgcShaderHeader.FixedBytes) + .OrderBy(info => info.FullName, StringComparer.Ordinal) + .ToArray(); + var pending = new List<(FileInfo Info, string Relative)>(); + var skipped = 0; + foreach (var info in files) + { + var relative = Path.GetRelativePath(root, info.FullName).Replace('\\', '/'); + if (file.TryGetScannedFile(relative, out var known) && known.Size == info.Length && + known.WriteTicks == info.LastWriteTimeUtc.Ticks && known.Scanner == Version) + { + skipped++; + continue; + } + + pending.Add((info, relative)); + } + + var total = pending.Sum(entry => entry.Info.Length); + var scanned = 0L; + var programs = 0; + Parallel.ForEach( + pending, + new ParallelOptions { MaxDegreeOfParallelism = Math.Clamp(Environment.ProcessorCount / 2, 1, 8), CancellationToken = cancellation }, + entry => + { + var found = ScanFile(entry.Info, file, cancellation); + Interlocked.Add(ref programs, found); + file.RecordScannedFile(new ScannedFile(entry.Relative, entry.Info.Length, entry.Info.LastWriteTimeUtc.Ticks, found, Version)); + progress?.Invoke(Interlocked.Add(ref scanned, entry.Info.Length), total); + }); + return new ShaderScanResult(pending.Count, skipped, programs, total); + } + + private static int ScanFile(FileInfo info, ShaderCacheFile file, CancellationToken cancellation) + { + using var handle = File.OpenHandle(info.FullName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete, FileOptions.SequentialScan); + var length = RandomAccess.GetLength(handle); + var signature = AgcShaderHeader.Signature; + var buffer = new byte[ChunkBytes]; + var found = new List(); + for (var chunkStart = 0L; chunkStart < length;) + { + cancellation.ThrowIfCancellationRequested(); + var read = ReadAt(handle, buffer, chunkStart); + if (read <= 0) + { + break; + } + + var window = buffer.AsSpan(0, read); + for (var position = window.IndexOf(signature); position >= 0;) + { + var offset = chunkStart + position; + if (TryRecord(handle, length, offset, file) is { } program) + { + found.Add(program); + } + + var next = window[(position + 1)..].IndexOf(signature); + position = next < 0 ? -1 : position + 1 + next; + } + + if (chunkStart + read >= length) + { + break; + } + + chunkStart += read - (signature.Length - 1); + } + + for (var index = 1; index < found.Count; index++) + { + if (found[index - 1].Type == AgcShaderType.Geometry && found[index].Type == AgcShaderType.Pixel) + { + file.RecordPair(found[index - 1].Identity, found[index].Identity); + } + } + + return found.Count(entry => entry.Program is not null); + } + + private static FoundProgram? TryRecord(SafeFileHandle handle, long length, long offset, ShaderCacheFile file) + { + Span head = stackalloc byte[AgcShaderHeader.FixedBytes]; + if (offset + head.Length > length || ReadAt(handle, head, offset) != head.Length || + !AgcShaderHeader.TryPeekSizes(head, out var headerBytes, out var codeBytes, out var type) || + !AgcShaderHeader.IsSupported(type) || offset + headerBytes > length) + { + return null; + } + + var headerData = new byte[headerBytes]; + if (ReadAt(handle, headerData, offset) != headerBytes || !AgcShaderHeader.TryParse(headerData, 0, out var header)) + { + return null; + } + + foreach (var codeOffset in CodeCandidates(offset, headerBytes, codeBytes)) + { + if (codeOffset < 0 || codeOffset + codeBytes > length) + { + continue; + } + + var code = new byte[codeBytes + CodeSlackBytes]; + var read = ReadAt(handle, code, codeOffset); + if (read < codeBytes || !ShaderInventory.TryHash(code, (uint)codeBytes, ScannedCodeAddress, out var hash)) + { + continue; + } + + var known = ShaderCacheFile.ProgramIdentity(hash, (uint)codeBytes, header.Bytes); + if (file.TryGetProgram(known, out _)) + { + return new FoundProgram(offset, null, header.Type, known); + } + + if (!ShaderInventory.TryCreateCapture(code.AsSpan(0, Math.Max(read, codeBytes)), (uint)codeBytes, ScannedCodeAddress, out var capture)) + { + continue; + } + + var program = new InventoryProgram { Code = capture.Key, Source = InventorySource.Scanned, Header = header.Bytes }; + var identity = file.RecordProgram(program, capture); + var cached = identity is { } recorded && file.TryGetProgram(recorded, out var stored) ? stored : null; + return new FoundProgram(offset, cached, header.Type, identity ?? ShaderCacheFile.ProgramIdentity(program)); + } + + return null; + } + + private static IEnumerable CodeCandidates(long headerOffset, int headerBytes, int codeBytes) + { + yield return headerOffset - codeBytes; + yield return headerOffset + headerBytes; + var aligned = (headerOffset + headerBytes + CodeAlignment - 1) / CodeAlignment * CodeAlignment; + if (aligned != headerOffset + headerBytes) + { + yield return aligned; + } + } + + private static int ReadAt(SafeFileHandle handle, Span destination, long offset) + { + var total = 0; + while (total < destination.Length) + { + var read = RandomAccess.Read(handle, destination[total..], offset + total); + if (read <= 0) + { + break; + } + + total += read; + } + + return total; + } +} diff --git a/src/SharpEmu.Libs/Gpu/ShaderCache/RecordDifference.cs b/src/SharpEmu.Libs/Gpu/ShaderCache/RecordDifference.cs new file mode 100644 index 0000000..d06a32c --- /dev/null +++ b/src/SharpEmu.Libs/Gpu/ShaderCache/RecordDifference.cs @@ -0,0 +1,150 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using System.Collections; +using System.Globalization; +using System.Reflection; +using System.Text; +using SharpEmu.Libs.Gpu.Rendering; + +namespace SharpEmu.Libs.Gpu.ShaderCache; + +internal sealed class RecordDifference +{ + private const int MaxDepth = 8; + private const int MaxDifferences = 3; + + private readonly List _differences = []; + + public bool Full => _differences.Count >= MaxDifferences; + + public bool Any => _differences.Count != 0; + + public override string ToString() => string.Join("; ", _differences); + + public RecordDifference Compare(string path, object? recorded, object? derived) + { + Compare(path, recorded, derived, 0); + return this; + } + + public static string Category(string difference) + { + var end = difference.IndexOf(' '); + var path = end < 0 ? difference : difference[..end]; + var builder = new StringBuilder(path.Length); + var skipping = false; + foreach (var character in path) + { + if (character == '[') + { + builder.Append("[*]"); + skipping = true; + } + else if (character == ']') + { + skipping = false; + } + else if (!skipping) + { + builder.Append(character); + } + } + + return builder.ToString(); + } + + private void Compare(string path, object? recorded, object? derived, int depth) + { + if (Full) + { + return; + } + + if (recorded is null || derived is null) + { + if (recorded is not null || derived is not null) + { + Add(path, recorded, derived); + } + + return; + } + + var type = recorded.GetType(); + if (type != derived.GetType()) + { + Add(path, type.Name, derived.GetType().Name); + return; + } + + if (IsScalar(type) || depth >= MaxDepth) + { + if (!Equals(recorded, derived)) + { + Add(path, recorded, derived); + } + + return; + } + + if (recorded is IEnumerable recordedItems && derived is IEnumerable derivedItems) + { + var left = recordedItems.Cast().ToArray(); + var right = derivedItems.Cast().ToArray(); + if (left.Length != right.Length) + { + Add($"{path}.Count", left.Length, right.Length); + } + + for (var index = 0; index < Math.Min(left.Length, right.Length); index++) + { + Compare($"{path}[{index}]", left[index], right[index], depth + 1); + } + + return; + } + + foreach (var property in type.GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(static property => property.GetIndexParameters().Length == 0 && !IsRuntimeOnly(property)) + .OrderBy(static property => property.MetadataToken)) + { + object? left, right; + try + { + left = property.GetValue(recorded); + right = property.GetValue(derived); + } + catch (TargetInvocationException) + { + continue; + } + + Compare($"{path}.{property.Name}", left, right, depth + 1); + } + + foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Instance).OrderBy(static field => field.MetadataToken)) + { + Compare($"{path}.{field.Name}", field.GetValue(recorded), field.GetValue(derived), depth + 1); + } + } + + private static bool IsRuntimeOnly(PropertyInfo property) => + property.PropertyType == typeof(ShaderStageResources) || + property.DeclaringType == typeof(ComputeInputInfo) && property.Name.StartsWith("DispatchThreads", StringComparison.Ordinal); + + private static bool IsScalar(Type type) => + type.IsPrimitive || type.IsEnum || type == typeof(string) || type == typeof(decimal) || type == typeof(UInt128); + + private void Add(string path, object? recorded, object? derived) => + _differences.Add($"{path} {Format(recorded)} -> {Format(derived)}"); + + private static string Format(object? value) => value switch + { + null => "none", + uint number when number > 9 => $"0x{number:X}", + ulong number when number > 9 => $"0x{number:X}", + IFormattable formattable => formattable.ToString(null, CultureInfo.InvariantCulture), + _ => value.ToString() ?? string.Empty, + }; +} diff --git a/src/SharpEmu.Libs/Gpu/ShaderCache/ShaderCacheFile.cs b/src/SharpEmu.Libs/Gpu/ShaderCache/ShaderCacheFile.cs new file mode 100644 index 0000000..9a7fe8e --- /dev/null +++ b/src/SharpEmu.Libs/Gpu/ShaderCache/ShaderCacheFile.cs @@ -0,0 +1,1146 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using System.Buffers; +using System.Buffers.Binary; +using System.IO.Hashing; +using System.Text; +using Microsoft.Win32.SafeHandles; +using SharpEmu.Libs.Gpu.Pipelines; +using SharpEmu.Libs.Gpu.Rendering; +using SharpEmu.ShaderCompiler; +using SharpEmu.ShaderCompiler.Resources; + +namespace SharpEmu.Libs.Gpu.ShaderCache; + +internal sealed record CachedCompute(StageRecord Record, ShaderCodeCapture Code, ulong Identity); + +internal sealed record CachedGraphics( + GraphicsPipelineRecord Record, + ShaderCodeCapture VertexCode, + ShaderCodeCapture? PixelCode, + ulong Identity, + ulong VertexIdentity, + ulong PixelIdentity); + +internal sealed record CachedProgram(InventoryProgram Program, ShaderCodeCapture Code, ulong Identity); + +internal readonly record struct ShaderCacheCompaction(long BytesBefore, long BytesAfter, int RecordsDropped); + +internal sealed class ShaderCacheFile : IDisposable +{ + public const string FileName = "shader-cache.bin"; + + private const string LegacyListName = "shader-prewarm.bin"; + private static readonly string[] LegacyFileNames = [LegacyListName, "shader-prewarm.stamp", "shader-prewarm.progress"]; + + private const uint Magic = 0x48435353; + private const uint FormatVersion = 4; + private const uint LegacyMagic = 0x57504553; + private const uint LegacyFormatVersion = 1; + private const byte CodeKind = 1; + private const byte ComputeKind = 2; + private const byte GraphicsKind = 3; + private const byte CompiledStageKind = 4; + private const byte BinaryKind = 5; + private const byte ProgramKind = 6; + private const byte PairKind = 7; + private const byte ScannedFileKind = 8; + private const byte DoneKind = 9; + private const int RecordHeaderBytes = sizeof(uint) + sizeof(ulong); + private const int FileHeaderBytes = 3 * sizeof(uint); + private const int CopyChunkBytes = 1 << 20; + private const int LazyHeadBytes = 64; + private const int DoneHeadBytes = 2 * sizeof(ulong); + private const int ContentKeyBytes = 16; + private const long MinCompactionBytes = 64L << 20; + + private readonly record struct RecordSpan(long Offset, int Length, ulong Checksum); + + private readonly record struct RecordEntry( + long Offset, int TotalLength, byte Kind, ulong Stamp, ulong DriverKey, UInt128 ContentKey, bool Duplicate, ulong Checksum); + + private readonly object _gate = new(); + private readonly string _path; + private readonly bool _readOnly; + private readonly long _lengthLimit; + private SafeFileHandle _handle; + private long _length; + private bool _failed; + + private readonly Dictionary _codes = new(); + private readonly HashSet _pipelineIdentities = []; + private readonly List _computes = []; + private readonly List _graphics = []; + private readonly Dictionary<(ulong Identity, ulong Stamp), RecordSpan> _compiledStages = new(); + private readonly Dictionary<(ulong DriverKey, UInt128 ContentKey), RecordSpan> _binaries = new(); + private readonly List _programs = []; + private readonly Dictionary _programIndex = new(); + private readonly List<(ulong Vertex, ulong Pixel)> _pairs = []; + private readonly HashSet<(ulong Vertex, ulong Pixel)> _pairSet = []; + private readonly Dictionary _scannedFiles = new(StringComparer.Ordinal); + private readonly HashSet<(ulong Item, ulong DriverStamp)> _done = []; + private readonly Dictionary> _doneContentKeys = new(); + private readonly List _entries = []; + + private ShaderCacheFile(string path, SafeFileHandle handle, bool readOnly = false, long lengthLimit = long.MaxValue) + { + _path = path; + _handle = handle; + _readOnly = readOnly; + _lengthLimit = lengthLimit; + } + + public string Path => _path; + + public long Length + { + get + { + lock (_gate) + { + return _length; + } + } + } + + public IReadOnlyList Computes => _computes; + + public CachedCompute[] SnapshotComputes() + { + lock (_gate) + { + return _computes.ToArray(); + } + } + + public IReadOnlyList Graphics => _graphics; + + public IReadOnlyList Programs => _programs; + + public CachedProgram[] SnapshotPrograms() + { + lock (_gate) + { + return _programs.ToArray(); + } + } + + public CachedGraphics[] SnapshotGraphics() + { + lock (_gate) + { + return _graphics.ToArray(); + } + } + + public (ulong Vertex, ulong Pixel)[] SnapshotPairs() + { + lock (_gate) + { + return _pairs.ToArray(); + } + } + + public IReadOnlyList<(ulong Vertex, ulong Pixel)> Pairs => _pairs; + + public bool TryGetProgram(ulong identity, out CachedProgram program) + { + lock (_gate) + { + if (_programIndex.TryGetValue(identity, out var index)) + { + program = _programs[index]; + return true; + } + } + + program = null!; + return false; + } + + public int BinaryCount + { + get + { + lock (_gate) + { + return _binaries.Count; + } + } + } + + public int ImportedLegacyComputes { get; private set; } + + public static ShaderCacheFile? Open(string directory) + { + try + { + Directory.CreateDirectory(directory); + var path = System.IO.Path.Combine(directory, FileName); + var handle = File.OpenHandle(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read); + var file = new ShaderCacheFile(path, handle); + try + { + file.Load(); + file.ImportLegacyList(System.IO.Path.Combine(directory, LegacyListName)); + } + catch + { + handle.Dispose(); + throw; + } + + foreach (var legacy in LegacyFileNames) + { + File.Delete(System.IO.Path.Combine(directory, legacy)); + } + + return file; + } + catch (Exception exception) when (exception is IOException or UnauthorizedAccessException) + { + Console.Error.WriteLine($"[SHADER CACHE][WARN] The shader cache is unavailable: {exception.Message}"); + return null; + } + } + + public static ShaderCacheFile? OpenSnapshot(string path, long length) + { + try + { + var handle = File.OpenHandle(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete); + var file = new ShaderCacheFile(path, handle, readOnly: true, lengthLimit: length); + try + { + file.Load(); + } + catch + { + handle.Dispose(); + throw; + } + + return file; + } + catch (Exception exception) when (exception is IOException or UnauthorizedAccessException) + { + Console.Error.WriteLine($"[SHADER CACHE][WARN] The shader cache snapshot is unavailable: {exception.Message}"); + return null; + } + } + + public static ShaderCacheFile? OpenPart(string path) + { + try + { + var handle = File.OpenHandle(path, FileMode.Create, FileAccess.ReadWrite, FileShare.Read); + var file = new ShaderCacheFile(path, handle); + file.Load(); + return file; + } + catch (Exception exception) when (exception is IOException or UnauthorizedAccessException) + { + Console.Error.WriteLine($"[SHADER CACHE][WARN] The shader cache part is unavailable: {exception.Message}"); + return null; + } + } + + public static ulong ProgramIdentity(InventoryProgram program) => + ProgramIdentity(program.Code.Hash, program.Code.CodeSize, program.Header); + + public static ulong ProgramIdentity(ulong codeHash, uint codeSize, ReadOnlySpan header) + { + var hash = new XxHash3(); + Span head = stackalloc byte[sizeof(ulong) + sizeof(uint)]; + BinaryPrimitives.WriteUInt64LittleEndian(head, codeHash); + BinaryPrimitives.WriteUInt32LittleEndian(head[sizeof(ulong)..], codeSize); + hash.Append(head); + hash.Append(header); + return hash.GetCurrentHashAsUInt64(); + } + + public ulong? RecordProgram(InventoryProgram program, ShaderCodeCapture code) + { + var identity = ProgramIdentity(program); + var payload = ShaderCacheSerializer.Serialize(writer => ShaderCacheSerializer.WriteProgram(writer, program)); + lock (_gate) + { + if (_failed || _programIndex.ContainsKey(identity)) + { + return null; + } + + EnsureCodeLocked(code); + if (AppendLocked(ProgramKind, payload) is null) + { + return null; + } + + _programIndex[identity] = _programs.Count; + _programs.Add(new CachedProgram(program, code, identity)); + return identity; + } + } + + public void RecordPair(ulong vertex, ulong pixel) + { + var payload = new byte[2 * sizeof(ulong)]; + BinaryPrimitives.WriteUInt64LittleEndian(payload, vertex); + BinaryPrimitives.WriteUInt64LittleEndian(payload.AsSpan(sizeof(ulong)), pixel); + lock (_gate) + { + if (!_failed && !_pairSet.Contains((vertex, pixel)) && AppendLocked(PairKind, payload) is not null) + { + _pairSet.Add((vertex, pixel)); + _pairs.Add((vertex, pixel)); + } + } + } + + public bool TryGetScannedFile(string path, out ScannedFile file) + { + lock (_gate) + { + if (_scannedFiles.TryGetValue(path, out var known)) + { + file = known.File; + return true; + } + } + + file = default; + return false; + } + + public void RecordScannedFile(ScannedFile file) + { + var payload = ShaderCacheSerializer.Serialize(writer => ShaderCacheSerializer.WriteScannedFile(writer, file)); + lock (_gate) + { + if (!_failed && AppendLocked(ScannedFileKind, payload) is not null) + { + ReplaceScannedFileLocked(file, _entries.Count - 1); + } + } + } + + public bool IsDone(ulong item, ulong driverStamp) + { + lock (_gate) + { + return _done.Contains((item, driverStamp)); + } + } + + public void AddDone(ulong item, ulong driverStamp, IReadOnlyCollection? contentKeys = null) + { + contentKeys ??= []; + var payload = new byte[DoneHeadBytes + contentKeys.Count * ContentKeyBytes]; + BinaryPrimitives.WriteUInt64LittleEndian(payload, item); + BinaryPrimitives.WriteUInt64LittleEndian(payload.AsSpan(sizeof(ulong)), driverStamp); + var offset = DoneHeadBytes; + foreach (var key in contentKeys) + { + BinaryPrimitives.WriteUInt128LittleEndian(payload.AsSpan(offset), key); + offset += ContentKeyBytes; + } + + lock (_gate) + { + if (!_failed && !_done.Contains((item, driverStamp)) && AppendLocked(DoneKind, payload, stamp: driverStamp) is not null) + { + IndexDoneLocked(payload, driverStamp); + } + } + } + + private bool IndexDoneLocked(ReadOnlySpan payload, ulong driverStamp) + { + if (!_done.Add((BinaryPrimitives.ReadUInt64LittleEndian(payload), driverStamp))) + { + return false; + } + + if (payload.Length > DoneHeadBytes) + { + if (!_doneContentKeys.TryGetValue(driverStamp, out var keys)) + { + _doneContentKeys[driverStamp] = keys = []; + } + + for (var offset = DoneHeadBytes; offset < payload.Length; offset += ContentKeyBytes) + { + keys.Add(BinaryPrimitives.ReadUInt128LittleEndian(payload[offset..])); + } + } + + return true; + } + + private static bool IsDonePayload(ReadOnlySpan payload) => + payload.Length >= DoneHeadBytes && (payload.Length - DoneHeadBytes) % ContentKeyBytes == 0; + + public int MergePart(string partPath) + { + using var part = OpenSnapshot(partPath, long.MaxValue); + if (part is null) + { + return 0; + } + + var merged = 0; + foreach (var entry in part._entries) + { + if (entry.Duplicate || entry.Kind is not (CompiledStageKind or BinaryKind or DoneKind)) + { + continue; + } + + var body = part.ReadBody(new RecordSpan(entry.Offset + RecordHeaderBytes, entry.TotalLength - RecordHeaderBytes, entry.Checksum)); + if (body is null) + { + continue; + } + + lock (_gate) + { + if (_failed) + { + break; + } + + var known = entry.Kind switch + { + CompiledStageKind => _compiledStages.ContainsKey(ReadCompiledKey(body)), + BinaryKind => _binaries.ContainsKey((entry.DriverKey, entry.ContentKey)), + _ => _done.Contains((BinaryPrimitives.ReadUInt64LittleEndian(body), entry.Stamp)), + }; + if (known || AppendLocked(entry.Kind, body, entry.Stamp, entry.DriverKey, entry.ContentKey) is not { } span) + { + continue; + } + + switch (entry.Kind) + { + case CompiledStageKind: + _compiledStages[ReadCompiledKey(body)] = span; + break; + case BinaryKind: + _binaries[(entry.DriverKey, entry.ContentKey)] = span; + break; + default: + IndexDoneLocked(body, entry.Stamp); + break; + } + + merged++; + } + } + + return merged; + } + + private static (ulong Identity, ulong Stamp) ReadCompiledKey(byte[] body) => + (BinaryPrimitives.ReadUInt64LittleEndian(body), BinaryPrimitives.ReadUInt64LittleEndian(body.AsSpan(sizeof(ulong)))); + + private void ReplaceScannedFileLocked(ScannedFile file, int entry) + { + if (_scannedFiles.TryGetValue(file.Path, out var previous)) + { + _entries[previous.Entry] = _entries[previous.Entry] with { Duplicate = true }; + } + + _scannedFiles[file.Path] = (file, entry); + } + + public static ulong StageIdentity(StageRecord record) => + XxHash3.HashToUInt64(ShaderCacheSerializer.Serialize(writer => ShaderCacheSerializer.WriteStage(writer, record))); + + public static ulong DriverKeyHash(ReadOnlySpan driverKey) => XxHash3.HashToUInt64(driverKey); + + public void RecordCompute(StageRecord record, ShaderCodeCapture code) + { + lock (_gate) + { + if (RecordComputeLocked(record, code) is { } identity) + { + _computes.Add(new CachedCompute(record, code, identity)); + } + } + } + + private ulong? RecordComputeLocked(StageRecord record, ShaderCodeCapture code) + { + var payload = ShaderCacheSerializer.Serialize(writer => ShaderCacheSerializer.WriteStage(writer, record)); + var identity = XxHash3.HashToUInt64(payload); + if (_failed || !_pipelineIdentities.Add(identity)) + { + return null; + } + + EnsureCodeLocked(code); + AppendLocked(ComputeKind, payload); + return identity; + } + + private void ImportLegacyList(string path) + { + if (!File.Exists(path)) + { + return; + } + + var content = File.ReadAllBytes(path); + if (content.Length < FileHeaderBytes || + BinaryPrimitives.ReadUInt32LittleEndian(content) != LegacyMagic || + BinaryPrimitives.ReadUInt32LittleEndian(content.AsSpan(sizeof(uint))) != LegacyFormatVersion || + BinaryPrimitives.ReadUInt32LittleEndian(content.AsSpan(2 * sizeof(uint))) != LegacySchemaFingerprint) + { + if (content.Length > FileHeaderBytes) + { + Console.Error.WriteLine("[SHADER CACHE][WARN] The old shader prewarm list has an unknown layout and was dropped."); + } + + return; + } + + var codes = new Dictionary(); + var imported = 0; + lock (_gate) + { + for (var offset = FileHeaderBytes; content.Length - offset >= RecordHeaderBytes + 1;) + { + var length = BinaryPrimitives.ReadUInt32LittleEndian(content.AsSpan(offset)); + var checksum = BinaryPrimitives.ReadUInt64LittleEndian(content.AsSpan(offset + sizeof(uint))); + if (length == 0 || length > content.Length - offset - RecordHeaderBytes) + { + break; + } + + var body = content.AsSpan(offset + RecordHeaderBytes, (int)length); + if (XxHash3.HashToUInt64(body) != checksum) + { + break; + } + + offset += RecordHeaderBytes + (int)length; + var payload = body[1..].ToArray(); + try + { + if (body[0] == CodeKind) + { + var code = ShaderCacheSerializer.Deserialize(payload, ShaderCacheSerializer.ReadCode); + codes[code.Key] = code; + } + else if (body[0] == ComputeKind) + { + var record = ShaderCacheSerializer.Deserialize(payload, ShaderCacheSerializer.ReadLegacyCompute); + if (codes.TryGetValue(record.CodeKey, out var code) && RecordComputeLocked(record, code) is { } identity) + { + _computes.Add(new CachedCompute(record, code, identity)); + imported++; + } + } + } + catch (Exception exception) when (exception is EndOfStreamException or ArgumentException) + { + break; + } + } + } + + ImportedLegacyComputes = imported; + } + + public void RecordGraphics(GraphicsPipelineRecord record, ShaderCodeCapture vertexCode, ShaderCodeCapture? pixelCode) + { + var payload = ShaderCacheSerializer.Serialize(writer => ShaderCacheSerializer.WriteGraphics(writer, record)); + var identity = XxHash3.HashToUInt64(payload); + lock (_gate) + { + if (_failed || !_pipelineIdentities.Add(identity)) + { + return; + } + + EnsureCodeLocked(vertexCode); + if (pixelCode is not null) + { + EnsureCodeLocked(pixelCode); + } + + if (AppendLocked(GraphicsKind, payload) is not null) + { + _graphics.Add(new CachedGraphics( + record, + vertexCode, + pixelCode, + identity, + StageIdentity(record.Vertex), + record.Pixel is null ? 0 : StageIdentity(record.Pixel))); + } + } + } + + public bool TryGetCompiledStage(ulong stageIdentity, ulong stamp, out CompiledStageRecord stage) + { + stage = null!; + RecordSpan span; + lock (_gate) + { + if (_failed || !_compiledStages.TryGetValue((stageIdentity, stamp), out span)) + { + return false; + } + } + + var body = ReadBody(span); + if (body is null) + { + return false; + } + + try + { + stage = ShaderCacheSerializer.Deserialize(body, reader => + { + ShaderCacheSerializer.ReadCompiledStageHeader(reader); + return ShaderCacheSerializer.ReadCompiledStageBody(reader); + }); + return true; + } + catch (Exception exception) when (exception is EndOfStreamException or ArgumentException) + { + return false; + } + } + + public void AddCompiledStage(ulong stageIdentity, ulong stamp, CompiledStageRecord stage) + { + var payload = ShaderCacheSerializer.Serialize(writer => ShaderCacheSerializer.WriteCompiledStage(writer, stageIdentity, stamp, stage)); + lock (_gate) + { + if (_failed || _compiledStages.ContainsKey((stageIdentity, stamp))) + { + return; + } + + if (AppendLocked(CompiledStageKind, payload, stamp: stamp) is { } span) + { + _compiledStages[(stageIdentity, stamp)] = span; + } + } + } + + public bool HasBinary(ReadOnlySpan driverKey, UInt128 contentKey) + { + var driver = DriverKeyHash(driverKey); + lock (_gate) + { + return !_failed && _binaries.ContainsKey((driver, contentKey)); + } + } + + public bool TryReadBinary(ReadOnlySpan driverKey, UInt128 contentKey, out PipelineBinaryPart[] parts) + { + parts = []; + RecordSpan span; + var driver = DriverKeyHash(driverKey); + lock (_gate) + { + if (_failed || !_binaries.TryGetValue((driver, contentKey), out span)) + { + return false; + } + } + + var body = ReadBody(span); + if (body is null) + { + return false; + } + + try + { + parts = ShaderCacheSerializer.Deserialize(body, reader => + { + ShaderCacheSerializer.ReadBinaryHeader(reader); + return ShaderCacheSerializer.ReadBinaryParts(reader); + }); + return parts.Length != 0; + } + catch (Exception exception) when (exception is EndOfStreamException or ArgumentException) + { + return false; + } + } + + public void AddBinary(ReadOnlySpan driverKey, UInt128 contentKey, IReadOnlyList parts) + { + if (parts.Count == 0) + { + return; + } + + var driverBytes = driverKey.ToArray(); + var payload = ShaderCacheSerializer.Serialize(writer => ShaderCacheSerializer.WriteBinary(writer, driverBytes, contentKey, parts)); + var key = (DriverKeyHash(driverKey), contentKey); + lock (_gate) + { + if (_failed || _binaries.ContainsKey(key)) + { + return; + } + + if (AppendLocked(BinaryKind, payload, driverKey: key.Item1, contentKey: contentKey) is { } span) + { + _binaries[key] = span; + } + } + } + + public ShaderCacheCompaction? Compact( + ulong stamp, ReadOnlySpan driverKey, ulong driverStamp, bool force = false, bool dropUnreferencedBinaries = false) + { + var driver = driverKey.IsEmpty ? 0 : DriverKeyHash(driverKey); + lock (_gate) + { + if (_failed || _readOnly) + { + return null; + } + + var referenced = dropUnreferencedBinaries ? _doneContentKeys.GetValueOrDefault(driverStamp) ?? [] : null; + var kept = new List(_entries.Count); + var droppedBytes = 0L; + foreach (var entry in _entries) + { + var keep = !entry.Duplicate && entry.Kind switch + { + CompiledStageKind => false, + BinaryKind => driver != 0 && entry.DriverKey == driver && (referenced is null || referenced.Contains(entry.ContentKey)), + DoneKind => entry.Stamp == driverStamp, + _ => true, + }; + if (keep) + { + kept.Add(entry); + } + else + { + droppedBytes += entry.TotalLength; + } + } + + var dropped = _entries.Count - kept.Count; + if (dropped == 0 || (!force && droppedBytes < Math.Max(MinCompactionBytes, _length / 4))) + { + return null; + } + + var before = _length; + var temporary = _path + ".compact"; + try + { + using (var output = new FileStream(temporary, FileMode.Create, FileAccess.Write, FileShare.None)) + { + output.Write(CreateFileHeader()); + var buffer = ArrayPool.Shared.Rent(CopyChunkBytes); + try + { + foreach (var entry in kept) + { + for (var copied = 0; copied < entry.TotalLength;) + { + var chunk = Math.Min(CopyChunkBytes, entry.TotalLength - copied); + ReadExactly(_handle, buffer.AsSpan(0, chunk), entry.Offset + copied); + output.Write(buffer, 0, chunk); + copied += chunk; + } + } + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + _handle.Dispose(); + File.Move(temporary, _path, overwrite: true); + _handle = File.OpenHandle(_path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read); + ResetIndexLocked(); + Load(); + return new ShaderCacheCompaction(before, _length, dropped); + } + catch (Exception exception) when (exception is IOException or UnauthorizedAccessException) + { + Console.Error.WriteLine($"[SHADER CACHE][WARN] The shader cache could not be compacted: {exception.Message}"); + try + { + File.Delete(temporary); + if (_handle.IsClosed) + { + _handle = File.OpenHandle(_path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read); + ResetIndexLocked(); + Load(); + } + } + catch (Exception reopen) when (reopen is IOException or UnauthorizedAccessException) + { + _failed = true; + Console.Error.WriteLine($"[SHADER CACHE][WARN] The shader cache was closed: {reopen.Message}"); + } + + return null; + } + } + } + + public void Dispose() + { + lock (_gate) + { + _failed = true; + _handle.Dispose(); + } + } + + private void ResetIndexLocked() + { + _codes.Clear(); + _pipelineIdentities.Clear(); + _computes.Clear(); + _graphics.Clear(); + _compiledStages.Clear(); + _binaries.Clear(); + _programs.Clear(); + _programIndex.Clear(); + _pairs.Clear(); + _pairSet.Clear(); + _scannedFiles.Clear(); + _done.Clear(); + _doneContentKeys.Clear(); + _entries.Clear(); + } + + private void EnsureCodeLocked(ShaderCodeCapture code) + { + if (_codes.TryAdd(code.Key, code)) + { + AppendLocked(CodeKind, ShaderCacheSerializer.Serialize(writer => ShaderCacheSerializer.WriteCode(writer, code))); + } + } + + private RecordSpan? AppendLocked(byte kind, byte[] payload, ulong stamp = 0, ulong driverKey = 0, UInt128 contentKey = default) + { + if (_readOnly) + { + return null; + } + + try + { + var record = new byte[RecordHeaderBytes + 1 + payload.Length]; + record[RecordHeaderBytes] = kind; + payload.CopyTo(record, RecordHeaderBytes + 1); + var body = record.AsSpan(RecordHeaderBytes); + var checksum = XxHash3.HashToUInt64(body); + BinaryPrimitives.WriteUInt32LittleEndian(record, (uint)body.Length); + BinaryPrimitives.WriteUInt64LittleEndian(record.AsSpan(sizeof(uint)), checksum); + var offset = _length; + RandomAccess.Write(_handle, record, offset); + _length += record.Length; + _entries.Add(new RecordEntry(offset, record.Length, kind, stamp, driverKey, contentKey, false, checksum)); + return new RecordSpan(offset + RecordHeaderBytes, body.Length, checksum); + } + catch (Exception exception) when (exception is IOException or UnauthorizedAccessException or ObjectDisposedException) + { + _failed = true; + Console.Error.WriteLine($"[SHADER CACHE][WARN] The shader cache write failed: {exception.Message}"); + return null; + } + } + + private byte[]? ReadBody(RecordSpan span) + { + try + { + var body = new byte[span.Length]; + ReadExactly(_handle, body, span.Offset); + return XxHash3.HashToUInt64(body) == span.Checksum ? body.AsSpan(1).ToArray() : null; + } + catch (Exception exception) when (exception is IOException or ObjectDisposedException or EndOfStreamException) + { + return null; + } + } + + private static bool IsLazyKind(byte kind) => kind is CompiledStageKind or BinaryKind; + + private void Load() + { + var fileLength = RandomAccess.GetLength(_handle); + var length = Math.Min(fileLength, _lengthLimit); + var validEnd = 0L; + var header = new byte[FileHeaderBytes]; + if (length >= FileHeaderBytes && + TryReadExactly(_handle, header, 0) && + header.AsSpan().SequenceEqual(CreateFileHeader())) + { + validEnd = FileHeaderBytes; + var recordHeader = new byte[RecordHeaderBytes + 1]; + while (length - validEnd >= RecordHeaderBytes + 1 && TryReadExactly(_handle, recordHeader, validEnd)) + { + var bodyLength = BinaryPrimitives.ReadUInt32LittleEndian(recordHeader); + var checksum = BinaryPrimitives.ReadUInt64LittleEndian(recordHeader.AsSpan(sizeof(uint))); + if (bodyLength == 0 || bodyLength > length - validEnd - RecordHeaderBytes) + { + break; + } + + var lazy = IsLazyKind(recordHeader[RecordHeaderBytes]); + var body = new byte[lazy ? Math.Min(bodyLength, LazyHeadBytes) : bodyLength]; + if (!TryReadExactly(_handle, body, validEnd + RecordHeaderBytes) || + (!lazy && XxHash3.HashToUInt64(body) != checksum) || + !TryIndex(body, validEnd, RecordHeaderBytes + (int)bodyLength, checksum)) + { + break; + } + + validEnd += RecordHeaderBytes + bodyLength; + } + } + + if (validEnd == 0) + { + if (!_readOnly) + { + RandomAccess.SetLength(_handle, 0); + RandomAccess.Write(_handle, CreateFileHeader(), 0); + } + + _length = _readOnly ? 0 : FileHeaderBytes; + return; + } + + if (!_readOnly && validEnd != fileLength) + { + RandomAccess.SetLength(_handle, validEnd); + } + + _length = validEnd; + } + + private bool TryIndex(byte[] body, long offset, int totalLength, ulong checksum) + { + var kind = body[0]; + var payload = body.AsSpan(1).ToArray(); + var bodySpan = new RecordSpan(offset + RecordHeaderBytes, totalLength - RecordHeaderBytes, checksum); + RecordEntry Entry(bool duplicate, ulong stamp = 0, ulong driver = 0, UInt128 content = default) => + new(offset, totalLength, kind, stamp, driver, content, duplicate, checksum); + try + { + switch (kind) + { + case CodeKind: + { + var code = ShaderCacheSerializer.Deserialize(payload, ShaderCacheSerializer.ReadCode); + _entries.Add(Entry(!_codes.TryAdd(code.Key, code))); + return true; + } + + case ComputeKind: + { + var identity = XxHash3.HashToUInt64(payload); + var duplicate = !_pipelineIdentities.Add(identity); + _entries.Add(Entry(duplicate)); + var record = ShaderCacheSerializer.Deserialize(payload, ShaderCacheSerializer.ReadStage); + if (!duplicate && record.Stage == ShaderStage.Compute && _codes.TryGetValue(record.CodeKey, out var code)) + { + _computes.Add(new CachedCompute(record, code, identity)); + } + + return true; + } + + case GraphicsKind: + { + var identity = XxHash3.HashToUInt64(payload); + var duplicate = !_pipelineIdentities.Add(identity); + _entries.Add(Entry(duplicate)); + var record = ShaderCacheSerializer.Deserialize(payload, ShaderCacheSerializer.ReadGraphics); + if (!duplicate && _codes.TryGetValue(record.Vertex.CodeKey, out var vertexCode)) + { + ShaderCodeCapture? pixelCode = null; + if (record.Pixel is null || _codes.TryGetValue(record.Pixel.CodeKey, out pixelCode)) + { + _graphics.Add(new CachedGraphics( + record, + vertexCode, + pixelCode, + identity, + StageIdentity(record.Vertex), + record.Pixel is null ? 0 : StageIdentity(record.Pixel))); + } + } + + return true; + } + + case CompiledStageKind: + { + var (identity, stamp) = ShaderCacheSerializer.Deserialize(payload, ShaderCacheSerializer.ReadCompiledStageHeader, allowTrailing: true); + _entries.Add(Entry(!_compiledStages.TryAdd((identity, stamp), bodySpan), stamp)); + return true; + } + + case BinaryKind: + { + var (driverKey, contentKey) = ShaderCacheSerializer.Deserialize(payload, ShaderCacheSerializer.ReadBinaryHeader, allowTrailing: true); + var driver = DriverKeyHash(driverKey); + _entries.Add(Entry(!_binaries.TryAdd((driver, contentKey), bodySpan), driver: driver, content: contentKey)); + return true; + } + + case ProgramKind: + { + var program = ShaderCacheSerializer.Deserialize(payload, ShaderCacheSerializer.ReadProgram); + var identity = ProgramIdentity(program); + var duplicate = _programIndex.ContainsKey(identity) || !_codes.TryGetValue(program.Code, out var code); + _entries.Add(Entry(duplicate)); + if (!duplicate) + { + _programIndex[identity] = _programs.Count; + _programs.Add(new CachedProgram(program, _codes[program.Code], identity)); + } + + return true; + } + + case PairKind: + { + if (payload.Length != 2 * sizeof(ulong)) + { + return false; + } + + var pair = (BinaryPrimitives.ReadUInt64LittleEndian(payload), BinaryPrimitives.ReadUInt64LittleEndian(payload.AsSpan(sizeof(ulong)))); + var added = _pairSet.Add(pair); + _entries.Add(Entry(!added)); + if (added) + { + _pairs.Add(pair); + } + + return true; + } + + case ScannedFileKind: + { + var file = ShaderCacheSerializer.Deserialize(payload, ShaderCacheSerializer.ReadScannedFile); + _entries.Add(Entry(false)); + ReplaceScannedFileLocked(file, _entries.Count - 1); + return true; + } + + case DoneKind: + { + if (!IsDonePayload(payload)) + { + return false; + } + + var stamp = BinaryPrimitives.ReadUInt64LittleEndian(payload.AsSpan(sizeof(ulong))); + _entries.Add(Entry(!IndexDoneLocked(payload, stamp), stamp)); + return true; + } + + default: + return false; + } + } + catch (Exception exception) when (exception is EndOfStreamException or ArgumentException or IOException) + { + return false; + } + } + + private static byte[] CreateFileHeader() + { + var header = new byte[FileHeaderBytes]; + BinaryPrimitives.WriteUInt32LittleEndian(header, Magic); + BinaryPrimitives.WriteUInt32LittleEndian(header.AsSpan(sizeof(uint)), FormatVersion); + BinaryPrimitives.WriteUInt32LittleEndian(header.AsSpan(2 * sizeof(uint)), SchemaFingerprint); + return header; + } + + private static readonly Type[] ComputeSchemaTypes = + [ + typeof(ComputeInputInfo), typeof(Gen5ComputeSystemRegisters), typeof(ResourceSpecialization), + typeof(BufferSpecialization), typeof(ImageSpecialization), typeof(BufferCandidateTableSpecialization), + ]; + + private static readonly uint SchemaFingerprint = HashSchema( + DescribeTypes( + [ + .. ComputeSchemaTypes, + typeof(ShaderVertexInput), typeof(ShaderClipSpaceTransform), typeof(Gen5PixelOutputBinding), + typeof(PipelineRenderingState), typeof(PipelineVertexInputState), typeof(PipelineVertexBinding), + typeof(PipelineVertexAttribute), typeof(BufferDescriptorWords), typeof(StageRecord), + typeof(VertexStageInputs), typeof(PixelStageInputs), typeof(GraphicsPipelineRecord), + ]) + .Append(PipelineStaticParameters.ByteSize)); + + private static readonly uint LegacySchemaFingerprint = HashSchema(DescribeTypes(ComputeSchemaTypes)); + + public static uint RecordSchema => SchemaFingerprint; + + private static StringBuilder DescribeTypes(IEnumerable types) + { + var builder = new StringBuilder(); + foreach (var type in types) + { + builder.Append(type.FullName).Append('{'); + foreach (var property in type.GetProperties().OrderBy(static property => property.Name, StringComparer.Ordinal)) + { + builder.Append(property.PropertyType.FullName).Append(' ').Append(property.Name).Append(';'); + } + + builder.Append('}'); + } + + return builder; + } + + private static uint HashSchema(StringBuilder description) => + (uint)XxHash3.HashToUInt64(Encoding.UTF8.GetBytes(description.ToString())); + + private static bool TryReadExactly(SafeFileHandle handle, Span destination, long offset) + { + try + { + ReadExactly(handle, destination, offset); + return true; + } + catch (EndOfStreamException) + { + return false; + } + } + + private static void ReadExactly(SafeFileHandle handle, Span destination, long offset) + { + while (!destination.IsEmpty) + { + var read = RandomAccess.Read(handle, destination, offset); + if (read <= 0) + { + throw new EndOfStreamException(); + } + + destination = destination[read..]; + offset += read; + } + } +} diff --git a/src/SharpEmu.Libs/Gpu/ShaderCache/ShaderCacheRecords.cs b/src/SharpEmu.Libs/Gpu/ShaderCache/ShaderCacheRecords.cs new file mode 100644 index 0000000..d46172a --- /dev/null +++ b/src/SharpEmu.Libs/Gpu/ShaderCache/ShaderCacheRecords.cs @@ -0,0 +1,768 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using SharpEmu.HLE; +using SharpEmu.Libs.Gpu.Pipelines; +using SharpEmu.Libs.Gpu.Rendering; +using SharpEmu.ShaderCompiler; +using SharpEmu.ShaderCompiler.Resources; +using Silk.NET.Vulkan; + +namespace SharpEmu.Libs.Gpu.ShaderCache; + +internal sealed record VertexStageInputs( + bool FetchEmbedded, + int FetchAttributeRegister, + int FetchBufferRegister, + IReadOnlyList Inputs, + uint ScratchDwords, + int RequiredVertexOutputCount, + uint PositionExportControl, + ShaderClipSpaceTransform ClipSpace); + +internal sealed record PixelStageInputs( + uint ScratchDwords, + IReadOnlyList Outputs, + uint InputEnable, + uint CustomInterpolationMask, + uint InputAddress, + IReadOnlyList InputCntl); + +internal sealed class StageRecord +{ + public required ShaderStage Stage { get; init; } + public required ulong Hash { get; init; } + public required uint CodeSize { get; init; } + public required ulong Address { get; init; } + public required uint UserDataBase { get; init; } + public required uint UserDataCount { get; init; } + public required uint PushDataCursor { get; init; } + public ResourceSpecialization? Specialization { get; init; } + public ComputeInputInfo? Compute { get; init; } + public Gen5ComputeSystemRegisters? ComputeSystemRegisters { get; init; } + public VertexStageInputs? Vertex { get; init; } + public PixelStageInputs? Pixel { get; init; } + + public ShaderCodeKey CodeKey => new(Hash, CodeSize, Address); + + public StageRecord WithPushDataCursor(uint cursor) => With(Address, cursor); + + public StageRecord WithAddress(ulong address) => With(address, PushDataCursor); + + private StageRecord With(ulong address, uint cursor) => new() + { + Stage = Stage, + Hash = Hash, + CodeSize = CodeSize, + Address = address, + UserDataBase = UserDataBase, + UserDataCount = UserDataCount, + PushDataCursor = cursor, + Specialization = Specialization, + Compute = Compute, + ComputeSystemRegisters = ComputeSystemRegisters, + Vertex = Vertex, + Pixel = Pixel, + }; +} + +internal enum InventorySource : byte +{ + Created = 1, + Scanned = 2, +} + +internal sealed class InventoryProgram +{ + public required ShaderCodeKey Code { get; init; } + public required InventorySource Source { get; init; } + public required byte[] Header { get; init; } +} + +internal readonly record struct ScannedFile(string Path, long Size, long WriteTicks, int Programs, int Scanner); + +internal readonly record struct VertexAttributeFormat(BufferDescriptorWords Descriptor, int RegisterCount); + +internal sealed class GraphicsPipelineRecord +{ + public required StageRecord Vertex { get; init; } + public StageRecord? Pixel { get; init; } + public required PipelineRenderingState Rendering { get; init; } + public required PipelineVertexInputState VertexInput { get; init; } + public required PipelineStaticParameters StaticParameters { get; init; } + public required VertexAttributeFormat[] Attributes { get; init; } + + public GraphicsPipelineRecord WithoutAddresses() => new() + { + Vertex = Vertex.WithAddress(0), + Pixel = Pixel?.WithAddress(0), + Rendering = Rendering, + VertexInput = VertexInput, + StaticParameters = StaticParameters, + Attributes = Attributes, + }; +} + +internal readonly record struct PipelineBinaryPart(byte[] Key, byte[] Data); + +internal readonly record struct LayoutBindingRecord(uint Binding, int DescriptorType, uint DescriptorCount, uint StageFlags); + +internal sealed class CompiledStageRecord +{ + public required byte[] Spirv { get; init; } + public required LayoutBindingRecord[] Bindings { get; init; } + public required byte[] VertexFetchComponents { get; init; } + public uint PushDataEnd { get; init; } +} + +internal static class ShaderCacheSerializer +{ + public static byte[] Serialize(Action write) + { + using var stream = new MemoryStream(); + using var writer = new BinaryWriter(stream); + write(writer); + writer.Flush(); + return stream.ToArray(); + } + + public static T Deserialize(byte[] payload, Func read, bool allowTrailing = false) + { + using var reader = new BinaryReader(new MemoryStream(payload)); + var value = read(reader); + if (!allowTrailing && reader.BaseStream.Position != reader.BaseStream.Length) + { + throw new ArgumentException("The shader cache record has trailing bytes."); + } + + return value; + } + + public static void WriteCode(BinaryWriter writer, ShaderCodeCapture code) + { + writer.Write(code.Hash); + writer.Write(code.CodeSize); + writer.Write(code.Address); + writer.Write((int)code.Generation); + writer.Write(code.Fused is not null); + if (code.Fused is { } fused) + { + writer.Write(fused.EntryHeaderAddress); + writer.Write(fused.ContinuationAddress); + writer.Write(fused.ContinuationHeaderAddress); + } + + writer.Write(code.Ranges.Length); + foreach (var range in code.Ranges) + { + writer.Write(range.Address); + writer.Write(range.Bytes.Length); + writer.Write(range.Bytes); + } + } + + public static ShaderCodeCapture ReadCode(BinaryReader reader) + { + var hash = reader.ReadUInt64(); + var codeSize = reader.ReadUInt32(); + var address = reader.ReadUInt64(); + var generation = (Generation)reader.ReadInt32(); + FusedCodeParts? fused = reader.ReadBoolean() + ? new FusedCodeParts(reader.ReadUInt64(), reader.ReadUInt64(), reader.ReadUInt64()) + : null; + var ranges = new CodeRange[Count(reader)]; + for (var index = 0; index < ranges.Length; index++) + { + var rangeAddress = reader.ReadUInt64(); + ranges[index] = new CodeRange(rangeAddress, reader.ReadBytes(Count(reader))); + } + + return new ShaderCodeCapture + { + Hash = hash, + CodeSize = codeSize, + Address = address, + Generation = generation, + Fused = fused, + Ranges = ranges, + }; + } + + public static void WriteStage(BinaryWriter writer, StageRecord record) + { + writer.Write((byte)record.Stage); + writer.Write(record.Hash); + writer.Write(record.CodeSize); + writer.Write(record.Address); + writer.Write(record.UserDataBase); + writer.Write(record.UserDataCount); + writer.Write(record.PushDataCursor); + writer.Write(record.Specialization is not null); + if (record.Specialization is { } specialization) + { + WriteSpecialization(writer, specialization); + } + + switch (record.Stage) + { + case ShaderStage.Compute: + WriteCompute(writer, record.Compute ?? throw new ArgumentException("A compute record has no compute inputs."), + record.ComputeSystemRegisters); + break; + case ShaderStage.Vertex: + WriteVertex(writer, record.Vertex ?? throw new ArgumentException("A vertex record has no vertex inputs.")); + break; + case ShaderStage.Pixel: + WritePixel(writer, record.Pixel ?? throw new ArgumentException("A pixel record has no pixel inputs.")); + break; + default: + throw new ArgumentException($"The shader cache cannot record stage {record.Stage}."); + } + } + + public static StageRecord ReadStage(BinaryReader reader) + { + var stage = (ShaderStage)reader.ReadByte(); + var hash = reader.ReadUInt64(); + var codeSize = reader.ReadUInt32(); + var address = reader.ReadUInt64(); + var userDataBase = reader.ReadUInt32(); + var userDataCount = reader.ReadUInt32(); + var pushDataCursor = reader.ReadUInt32(); + var specialization = reader.ReadBoolean() ? ReadSpecialization(reader) : null; + ComputeInputInfo? compute = null; + Gen5ComputeSystemRegisters? registers = null; + VertexStageInputs? vertex = null; + PixelStageInputs? pixel = null; + switch (stage) + { + case ShaderStage.Compute: + (compute, registers) = ReadCompute(reader); + break; + case ShaderStage.Vertex: + vertex = ReadVertex(reader); + break; + case ShaderStage.Pixel: + pixel = ReadPixel(reader); + break; + default: + throw new ArgumentException($"The shader cache record names an unknown stage {stage}."); + } + + return new StageRecord + { + Stage = stage, + Hash = hash, + CodeSize = codeSize, + Address = address, + UserDataBase = userDataBase, + UserDataCount = userDataCount, + PushDataCursor = pushDataCursor, + Specialization = specialization, + Compute = compute, + ComputeSystemRegisters = registers, + Vertex = vertex, + Pixel = pixel, + }; + } + + public static StageRecord ReadLegacyCompute(BinaryReader reader) + { + var hash = reader.ReadUInt64(); + var codeSize = reader.ReadUInt32(); + var address = reader.ReadUInt64(); + var userDataBase = reader.ReadUInt32(); + var userDataCount = reader.ReadUInt32(); + var pushDataCursor = reader.ReadUInt32(); + var (compute, registers) = ReadCompute(reader); + return new StageRecord + { + Stage = ShaderStage.Compute, + Hash = hash, + CodeSize = codeSize, + Address = address, + UserDataBase = userDataBase, + UserDataCount = userDataCount, + PushDataCursor = pushDataCursor, + Specialization = ReadSpecialization(reader), + Compute = compute, + ComputeSystemRegisters = registers, + }; + } + + public static void WriteProgram(BinaryWriter writer, InventoryProgram program) + { + writer.Write(program.Code.Hash); + writer.Write(program.Code.CodeSize); + writer.Write(program.Code.Address); + writer.Write((byte)program.Source); + writer.Write(program.Header.Length); + writer.Write(program.Header); + } + + public static InventoryProgram ReadProgram(BinaryReader reader) + { + var code = new ShaderCodeKey(reader.ReadUInt64(), reader.ReadUInt32(), reader.ReadUInt64()); + var source = (InventorySource)reader.ReadByte(); + return new InventoryProgram { Code = code, Source = source, Header = reader.ReadBytes(Count(reader)) }; + } + + public static void WriteScannedFile(BinaryWriter writer, ScannedFile file) + { + writer.Write(file.Path); + writer.Write(file.Size); + writer.Write(file.WriteTicks); + writer.Write(file.Programs); + writer.Write(file.Scanner); + } + + public static ScannedFile ReadScannedFile(BinaryReader reader) => + new(reader.ReadString(), reader.ReadInt64(), reader.ReadInt64(), reader.ReadInt32(), reader.ReadInt32()); + + public static void WriteGraphics(BinaryWriter writer, GraphicsPipelineRecord record) + { + WriteStage(writer, record.Vertex); + writer.Write(record.Pixel is not null); + if (record.Pixel is { } pixel) + { + WriteStage(writer, pixel); + } + + var rendering = record.Rendering; + writer.Write(rendering.ColorCount); + foreach (var format in rendering.ColorFormats) + { + writer.Write((int)format); + } + + writer.Write((int)rendering.DepthFormat); + writer.Write((int)rendering.StencilFormat); + + var vertexInput = record.VertexInput; + writer.Write(vertexInput.BindingCount); + writer.Write(vertexInput.AttributeCount); + for (var index = 0; index < vertexInput.BindingCount; index++) + { + writer.Write(vertexInput.Bindings[index].Stride); + writer.Write(vertexInput.Bindings[index].Instance); + } + + for (var index = 0; index < vertexInput.AttributeCount; index++) + { + writer.Write(vertexInput.Attributes[index].Offset); + writer.Write(vertexInput.Attributes[index].Binding); + } + + writer.Write(record.StaticParameters.Bytes); + writer.Write(record.Attributes.Length); + foreach (var attribute in record.Attributes) + { + writer.Write(attribute.Descriptor.Word0); + writer.Write(attribute.Descriptor.Word1); + writer.Write(attribute.Descriptor.Word2); + writer.Write(attribute.Descriptor.Word3); + writer.Write(attribute.RegisterCount); + } + } + + public static GraphicsPipelineRecord ReadGraphics(BinaryReader reader) + { + var vertex = ReadStage(reader); + var pixel = reader.ReadBoolean() ? ReadStage(reader) : null; + if (vertex.Stage != ShaderStage.Vertex || pixel is { Stage: not ShaderStage.Pixel }) + { + throw new ArgumentException("The graphics record pairs the wrong stages."); + } + + var rendering = new PipelineRenderingState { ColorCount = reader.ReadUInt32() }; + for (var index = 0; index < rendering.ColorFormats.Length; index++) + { + rendering.ColorFormats[index] = (Format)reader.ReadInt32(); + } + + rendering.DepthFormat = (Format)reader.ReadInt32(); + rendering.StencilFormat = (Format)reader.ReadInt32(); + + var vertexInput = new PipelineVertexInputState + { + BindingCount = reader.ReadByte(), + AttributeCount = reader.ReadByte(), + }; + if (vertexInput.BindingCount > VertexInputInfo.MaxBuffers || vertexInput.AttributeCount > VertexInputInfo.MaxBuffers) + { + throw new ArgumentException("The graphics record has too many vertex inputs."); + } + + for (var index = 0; index < vertexInput.BindingCount; index++) + { + vertexInput.Bindings[index] = new PipelineVertexBinding(reader.ReadUInt32(), reader.ReadBoolean()); + } + + for (var index = 0; index < vertexInput.AttributeCount; index++) + { + vertexInput.Attributes[index] = new PipelineVertexAttribute(reader.ReadUInt32(), reader.ReadByte()); + } + + var staticParameters = PipelineStaticParameters.FromBytes(reader.ReadBytes(PipelineStaticParameters.ByteSize)); + var attributes = new VertexAttributeFormat[Count(reader)]; + for (var index = 0; index < attributes.Length; index++) + { + attributes[index] = new VertexAttributeFormat( + new BufferDescriptorWords(reader.ReadUInt32(), reader.ReadUInt32(), reader.ReadUInt32(), reader.ReadUInt32()), + reader.ReadInt32()); + } + + return new GraphicsPipelineRecord + { + Vertex = vertex, + Pixel = pixel, + Rendering = rendering, + VertexInput = vertexInput, + StaticParameters = staticParameters, + Attributes = attributes, + }; + } + + public static void WriteBinary(BinaryWriter writer, ReadOnlySpan driverKey, UInt128 contentKey, IReadOnlyList parts) + { + writer.Write((byte)driverKey.Length); + writer.Write(driverKey); + writer.Write((ulong)contentKey); + writer.Write((ulong)(contentKey >> 64)); + writer.Write(parts.Count); + foreach (var part in parts) + { + writer.Write(part.Key.Length); + writer.Write(part.Key); + writer.Write(part.Data.Length); + writer.Write(part.Data); + } + } + + public static (byte[] DriverKey, UInt128 ContentKey) ReadBinaryHeader(BinaryReader reader) + { + var driverKey = reader.ReadBytes(reader.ReadByte()); + var low = reader.ReadUInt64(); + var high = reader.ReadUInt64(); + return (driverKey, new UInt128(high, low)); + } + + public static void WriteCompiledStage(BinaryWriter writer, ulong stageIdentity, ulong stamp, CompiledStageRecord stage) + { + writer.Write(stageIdentity); + writer.Write(stamp); + writer.Write(stage.Spirv.Length); + writer.Write(stage.Spirv); + writer.Write(stage.Bindings.Length); + foreach (var binding in stage.Bindings) + { + writer.Write(binding.Binding); + writer.Write(binding.DescriptorType); + writer.Write(binding.DescriptorCount); + writer.Write(binding.StageFlags); + } + + writer.Write(stage.VertexFetchComponents.Length); + writer.Write(stage.VertexFetchComponents); + writer.Write(stage.PushDataEnd); + } + + public static (ulong StageIdentity, ulong Stamp) ReadCompiledStageHeader(BinaryReader reader) => + (reader.ReadUInt64(), reader.ReadUInt64()); + + public static CompiledStageRecord ReadCompiledStageBody(BinaryReader reader) + { + var spirv = reader.ReadBytes(Count(reader)); + var bindings = new LayoutBindingRecord[Count(reader)]; + for (var index = 0; index < bindings.Length; index++) + { + bindings[index] = new LayoutBindingRecord(reader.ReadUInt32(), reader.ReadInt32(), reader.ReadUInt32(), reader.ReadUInt32()); + } + + return new CompiledStageRecord + { + Spirv = spirv, + Bindings = bindings, + VertexFetchComponents = reader.ReadBytes(Count(reader)), + PushDataEnd = reader.ReadUInt32(), + }; + } + + public static PipelineBinaryPart[] ReadBinaryParts(BinaryReader reader) + { + var parts = new PipelineBinaryPart[Count(reader)]; + for (var index = 0; index < parts.Length; index++) + { + var key = reader.ReadBytes(Count(reader)); + parts[index] = new PipelineBinaryPart(key, reader.ReadBytes(Count(reader))); + } + + return parts; + } + + public static void WriteSpecialization(BinaryWriter writer, ResourceSpecialization specialization) + { + writer.Write(specialization.BaseBufferCount); + writer.Write(specialization.Buffers.Count); + foreach (var buffer in specialization.Buffers) + { + writer.Write(buffer.PackedStride); + writer.Write(buffer.DescriptorFormat); + writer.Write(buffer.DescriptorSwizzle); + } + + writer.Write(specialization.Images.Count); + foreach (var image in specialization.Images) + { + writer.Write((int)image.NumericClass); + writer.Write((int)image.Dimension); + writer.Write(image.MipCount); + writer.Write(image.ConversionFormat); + writer.Write(image.ShaderSwizzle); + writer.Write(image.IndirectRoot); + writer.Write(image.IndirectMappingOffset); + writer.Write(image.IndirectSearchIterations); + writer.Write(image.Cube); + writer.Write(image.EmulatedCompareFunction); + } + + writer.Write(specialization.BufferCandidateTables.Count); + foreach (var table in specialization.BufferCandidateTables) + { + writer.Write(table.FirstCandidate); + writer.Write(table.CandidateCount); + writer.Write(table.MappingOffset); + writer.Write(table.SearchIterations); + } + } + + private static ResourceSpecialization ReadSpecialization(BinaryReader reader) + { + var specialization = new ResourceSpecialization { BaseBufferCount = reader.ReadInt32() }; + for (var count = Count(reader); count > 0; count--) + { + specialization.Buffers.Add(new BufferSpecialization(reader.ReadUInt32(), reader.ReadUInt32(), reader.ReadUInt32())); + } + + for (var count = Count(reader); count > 0; count--) + { + specialization.Images.Add(new ImageSpecialization( + (ImageNumericClass)reader.ReadInt32(), + (ImageDimension)reader.ReadInt32(), + reader.ReadUInt32(), + reader.ReadUInt32(), + reader.ReadUInt32(), + reader.ReadUInt32(), + reader.ReadUInt32(), + reader.ReadUInt32(), + reader.ReadBoolean(), + reader.ReadInt32())); + } + + for (var count = Count(reader); count > 0; count--) + { + specialization.BufferCandidateTables.Add(new BufferCandidateTableSpecialization( + reader.ReadUInt32(), reader.ReadUInt32(), reader.ReadUInt32(), reader.ReadUInt32())); + } + + return specialization; + } + + public static void WriteCompute(BinaryWriter writer, ComputeInputInfo info, Gen5ComputeSystemRegisters? registers) + { + writer.Write(info.ThreadsX); + writer.Write(info.ThreadsY); + writer.Write(info.ThreadsZ); + writer.Write(info.DispatchThreadDimensions); + writer.Write(info.GroupIdX); + writer.Write(info.GroupIdY); + writer.Write(info.GroupIdZ); + writer.Write(info.ThreadIdCount); + writer.Write(info.ThreadGroupSizeEnabled); + writer.Write(info.WaveSize); + writer.Write(info.LocalDataShareDwords); + writer.Write(info.ScratchDwords); + writer.Write(info.NeedsLocalDataShareBarriers); + writer.Write(info.WorkgroupRegister); + writer.Write(registers is not null); + if (registers is { } values) + { + WriteOptional(writer, values.WorkGroupXRegister); + WriteOptional(writer, values.WorkGroupYRegister); + WriteOptional(writer, values.WorkGroupZRegister); + WriteOptional(writer, values.ThreadGroupSizeRegister); + } + } + + private static (ComputeInputInfo Info, Gen5ComputeSystemRegisters? Registers) ReadCompute(BinaryReader reader) + { + var info = new ComputeInputInfo + { + ThreadsX = reader.ReadUInt32(), + ThreadsY = reader.ReadUInt32(), + ThreadsZ = reader.ReadUInt32(), + DispatchThreadDimensions = reader.ReadBoolean(), + GroupIdX = reader.ReadBoolean(), + GroupIdY = reader.ReadBoolean(), + GroupIdZ = reader.ReadBoolean(), + ThreadIdCount = reader.ReadInt32(), + ThreadGroupSizeEnabled = reader.ReadBoolean(), + WaveSize = reader.ReadUInt32(), + LocalDataShareDwords = reader.ReadUInt32(), + ScratchDwords = reader.ReadUInt32(), + NeedsLocalDataShareBarriers = reader.ReadBoolean(), + WorkgroupRegister = reader.ReadInt32(), + }; + Gen5ComputeSystemRegisters? registers = reader.ReadBoolean() + ? new Gen5ComputeSystemRegisters(ReadOptional(reader), ReadOptional(reader), ReadOptional(reader), ReadOptional(reader)) + : null; + return (info, registers); + } + + private static void WriteVertex(BinaryWriter writer, VertexStageInputs vertex) + { + writer.Write(vertex.FetchEmbedded); + writer.Write(vertex.FetchAttributeRegister); + writer.Write(vertex.FetchBufferRegister); + writer.Write(vertex.Inputs.Count); + foreach (var input in vertex.Inputs) + { + writer.Write(input.Pc); + writer.Write(input.Location); + writer.Write(input.FetchComponentCount); + writer.Write(input.ComponentCount); + writer.Write(input.NumberFormat); + writer.Write(input.DestinationSelect); + writer.Write(input.PerInstance); + writer.Write(input.AliasPcs.Count); + foreach (var alias in input.AliasPcs) + { + writer.Write(alias); + } + } + + writer.Write(vertex.ScratchDwords); + writer.Write(vertex.RequiredVertexOutputCount); + writer.Write(vertex.PositionExportControl); + var clip = vertex.ClipSpace; + writer.Write(clip.Enabled); + writer.Write(clip.ScaleX); + writer.Write(clip.ScaleY); + writer.Write(clip.OffsetX); + writer.Write(clip.OffsetY); + writer.Write(clip.HalfExtentX); + writer.Write(clip.HalfExtentY); + } + + private static VertexStageInputs ReadVertex(BinaryReader reader) + { + var fetchEmbedded = reader.ReadBoolean(); + var attributeRegister = reader.ReadInt32(); + var bufferRegister = reader.ReadInt32(); + var inputs = new ShaderVertexInput[Count(reader)]; + for (var index = 0; index < inputs.Length; index++) + { + var pc = reader.ReadUInt32(); + var location = reader.ReadUInt32(); + var fetchComponents = reader.ReadUInt32(); + var components = reader.ReadUInt32(); + var numberFormat = reader.ReadUInt32(); + var destinationSelect = reader.ReadUInt32(); + var perInstance = reader.ReadBoolean(); + var aliases = new List(); + for (var count = Count(reader); count > 0; count--) + { + aliases.Add(reader.ReadUInt32()); + } + + inputs[index] = new ShaderVertexInput(pc, location, fetchComponents, components, numberFormat, destinationSelect, perInstance, aliases); + } + + var scratch = reader.ReadUInt32(); + var requiredOutputs = reader.ReadInt32(); + var positionExportControl = reader.ReadUInt32(); + var clip = new ShaderClipSpaceTransform( + reader.ReadBoolean(), + reader.ReadSingle(), + reader.ReadSingle(), + reader.ReadSingle(), + reader.ReadSingle(), + reader.ReadSingle(), + reader.ReadSingle()); + return new VertexStageInputs( + fetchEmbedded, attributeRegister, bufferRegister, inputs, scratch, requiredOutputs, positionExportControl, clip); + } + + private static void WritePixel(BinaryWriter writer, PixelStageInputs pixel) + { + writer.Write(pixel.ScratchDwords); + writer.Write(pixel.Outputs.Count); + foreach (var output in pixel.Outputs) + { + writer.Write(output.GuestSlot); + writer.Write(output.HostLocation); + writer.Write((int)output.Kind); + writer.Write(output.ComponentMapping.Packed); + writer.Write(output.ExportTarget); + } + + writer.Write(pixel.InputEnable); + writer.Write(pixel.CustomInterpolationMask); + writer.Write(pixel.InputAddress); + writer.Write(pixel.InputCntl.Count); + foreach (var setting in pixel.InputCntl) + { + writer.Write(setting); + } + } + + private static PixelStageInputs ReadPixel(BinaryReader reader) + { + var scratch = reader.ReadUInt32(); + var outputs = new Gen5PixelOutputBinding[Count(reader)]; + for (var index = 0; index < outputs.Length; index++) + { + outputs[index] = new Gen5PixelOutputBinding( + reader.ReadUInt32(), + reader.ReadUInt32(), + (Gen5PixelOutputKind)reader.ReadInt32(), + new Gen5ColorComponentMapping(reader.ReadByte())) + { + ExportTarget = reader.ReadUInt32(), + }; + } + + var inputEnable = reader.ReadUInt32(); + var customMask = reader.ReadUInt32(); + var inputAddress = reader.ReadUInt32(); + var cntl = new uint[Count(reader)]; + for (var index = 0; index < cntl.Length; index++) + { + cntl[index] = reader.ReadUInt32(); + } + + return new PixelStageInputs(scratch, outputs, inputEnable, customMask, inputAddress, cntl); + } + + private static void WriteOptional(BinaryWriter writer, uint? value) + { + writer.Write(value.HasValue); + writer.Write(value.GetValueOrDefault()); + } + + private static uint? ReadOptional(BinaryReader reader) + { + var hasValue = reader.ReadBoolean(); + var value = reader.ReadUInt32(); + return hasValue ? value : null; + } + + private static int Count(BinaryReader reader) + { + var count = reader.ReadInt32(); + var remaining = reader.BaseStream.Length - reader.BaseStream.Position; + return count >= 0 && count <= remaining + ? count + : throw new ArgumentException("The shader cache record count is out of range."); + } +} diff --git a/src/SharpEmu.Libs/Gpu/ShaderCache/ShaderCacheSettings.cs b/src/SharpEmu.Libs/Gpu/ShaderCache/ShaderCacheSettings.cs new file mode 100644 index 0000000..d989b6f --- /dev/null +++ b/src/SharpEmu.Libs/Gpu/ShaderCache/ShaderCacheSettings.cs @@ -0,0 +1,93 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +namespace SharpEmu.Libs.Gpu.ShaderCache; + +public static class ShaderCacheSettings +{ + public const string EnvironmentVariable = "SHARPEMU_SHADER_CACHE"; + public const string WaitEnvironmentVariable = "SHARPEMU_SHADER_CACHE_WAIT"; + public const string FullPrecompileEnvironmentVariable = "SHARPEMU_SHADER_PRECOMPILE_FULL"; + public const string LearnEnvironmentVariable = "SHARPEMU_SHADER_LEARN"; + public const string JobsEnvironmentVariable = "SHARPEMU_SHADER_PRECOMPILE_JOBS"; + + private const string LegacyEnvironmentVariable = "SHARPEMU_SHADER_PREWARM"; + + public static bool Enabled => Resolve( + Environment.GetEnvironmentVariable(EnvironmentVariable), + Environment.GetEnvironmentVariable(LegacyEnvironmentVariable)); + + public static bool HoldsGuestUntilReady => + !string.Equals(Environment.GetEnvironmentVariable(WaitEnvironmentVariable), "0", StringComparison.Ordinal); + + public static bool FullPrecompile => + TryParse(Environment.GetEnvironmentVariable(FullPrecompileEnvironmentVariable), out var full) && full; + + public static bool Learn => + TryParse(Environment.GetEnvironmentVariable(LearnEnvironmentVariable), out var learn) && learn; + + public static int PrecompileJobs => + int.TryParse(Environment.GetEnvironmentVariable(JobsEnvironmentVariable), out var jobs) + ? Math.Clamp(jobs, 1, 32) + : Math.Clamp(Environment.ProcessorCount / 2, 1, 8); + + public static bool Resolve(string? value, string? legacyValue = null) => + TryParse(value, out var enabled) ? enabled : !string.Equals(legacyValue, "0", StringComparison.Ordinal); + + public static bool TryParse(string? value, out bool enabled) + { + switch (value?.Trim().ToLowerInvariant()) + { + case "1" or "on" or "true" or "yes" or "enabled": + enabled = true; + return true; + case "0" or "off" or "false" or "no" or "disabled": + enabled = false; + return true; + default: + enabled = true; + return false; + } + } + + public static string Format(bool enabled) => enabled ? "on" : "off"; + + public static string CacheFilePath(string titleId) => + Path.Combine( + Path.GetDirectoryName(VideoOut.VulkanPipelineCacheStorage.ResolvePath( + titleId, + Environment.GetEnvironmentVariable("SHARPEMU_VK_PIPELINE_CACHE_PATH")))!, + ShaderCacheFile.FileName); + + public static string SeedPath(string titleId) + { + var cacheDirectory = Path.GetDirectoryName(CacheFilePath(titleId))!; + var userDirectory = Path.GetDirectoryName(Path.GetDirectoryName(cacheDirectory)) ?? cacheDirectory; + return Path.Combine(userDirectory, "shader_seeds", titleId + ShaderSeed.Extension); + } + + public static bool TryClear(string titleId) + { + try + { + var path = CacheFilePath(titleId); + var directory = Path.GetDirectoryName(path)!; + if (!Directory.Exists(directory)) + { + return true; + } + + File.Delete(path); + foreach (var part in Directory.EnumerateFiles(directory, ShaderCacheFile.FileName + ".part*")) + { + File.Delete(part); + } + + return true; + } + catch (Exception exception) when (exception is IOException or UnauthorizedAccessException) + { + return false; + } + } +} diff --git a/src/SharpEmu.Libs/Gpu/ShaderCache/ShaderCodeCapture.cs b/src/SharpEmu.Libs/Gpu/ShaderCache/ShaderCodeCapture.cs new file mode 100644 index 0000000..8a16297 --- /dev/null +++ b/src/SharpEmu.Libs/Gpu/ShaderCache/ShaderCodeCapture.cs @@ -0,0 +1,121 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using SharpEmu.HLE; +using SharpEmu.ShaderCompiler; + +namespace SharpEmu.Libs.Gpu.ShaderCache; + +internal sealed class RecordingCpuMemory(ICpuMemory inner) : ICpuMemory, ICpuMemoryWrapper +{ + private readonly List<(ulong Address, byte[] Bytes)> _reads = []; + + public ICpuMemory Inner => inner; + + public bool TryRead(ulong virtualAddress, Span destination) + { + if (!inner.TryRead(virtualAddress, destination)) + { + return false; + } + + _reads.Add((virtualAddress, destination.ToArray())); + return true; + } + + public bool TryWrite(ulong virtualAddress, ReadOnlySpan source) => false; + + public CodeRange[] TakeRanges() + { + _reads.Sort(static (left, right) => left.Address.CompareTo(right.Address)); + var ranges = new List(); + ulong start = 0; + var bytes = new List(); + foreach (var (address, data) in _reads) + { + var end = start + (ulong)bytes.Count; + if (bytes.Count != 0 && address <= end) + { + var overlap = (int)(end - address); + if (overlap < data.Length) + { + bytes.AddRange(data.AsSpan(overlap).ToArray()); + } + + continue; + } + + if (bytes.Count != 0) + { + ranges.Add(new CodeRange(start, bytes.ToArray())); + } + + start = address; + bytes.Clear(); + bytes.AddRange(data); + } + + if (bytes.Count != 0) + { + ranges.Add(new CodeRange(start, bytes.ToArray())); + } + + _reads.Clear(); + return ranges.ToArray(); + } +} + +internal sealed class ReplayCpuMemory(IReadOnlyList ranges) : ICpuMemory +{ + public bool TryRead(ulong virtualAddress, Span destination) + { + foreach (var range in ranges) + { + if (virtualAddress < range.Address) + { + continue; + } + + var offset = virtualAddress - range.Address; + if (offset + (ulong)destination.Length <= (ulong)range.Bytes.Length) + { + range.Bytes.AsSpan((int)offset, destination.Length).CopyTo(destination); + return true; + } + } + + return false; + } + + public bool TryWrite(ulong virtualAddress, ReadOnlySpan source) => false; +} + +internal readonly record struct CodeRange(ulong Address, byte[] Bytes); + +internal readonly record struct FusedCodeParts(ulong EntryHeaderAddress, ulong ContinuationAddress, ulong ContinuationHeaderAddress); + +internal readonly record struct ShaderCodeKey(ulong Hash, uint CodeSize, ulong Address); + +internal sealed class ShaderCodeCapture +{ + public required ulong Hash { get; init; } + public required uint CodeSize { get; init; } + public required ulong Address { get; init; } + public required Generation Generation { get; init; } + public FusedCodeParts? Fused { get; init; } + public required CodeRange[] Ranges { get; init; } + + public ShaderCodeKey Key => new(Hash, CodeSize, Address); + + public CpuContext CreateContext() + { + var context = new CpuContext(new ReplayCpuMemory(Ranges), Generation); + if (Fused is { } fused) + { + Gen5ShaderTranslator.RegisterFusedProgram( + context, Address, fused.EntryHeaderAddress, fused.ContinuationAddress, fused.ContinuationHeaderAddress); + } + + return context; + } +} diff --git a/src/SharpEmu.Libs/Gpu/ShaderCache/ShaderInventory.cs b/src/SharpEmu.Libs/Gpu/ShaderCache/ShaderInventory.cs new file mode 100644 index 0000000..669cc87 --- /dev/null +++ b/src/SharpEmu.Libs/Gpu/ShaderCache/ShaderInventory.cs @@ -0,0 +1,215 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using System.IO.Hashing; +using SharpEmu.HLE; +using SharpEmu.Libs.Gpu.Pipelines; +using SharpEmu.ShaderCompiler; + +namespace SharpEmu.Libs.Gpu.ShaderCache; + +internal static class ShaderInventory +{ + private const int MaxPending = 1 << 16; + private const int CodeSlackBytes = 256; + + private sealed class Pending + { + public required byte[] Header { get; init; } + public required byte[] Code { get; init; } + public required ulong CodeAddress { get; init; } + public required ulong HeaderAddress { get; init; } + public required uint CodeSize { get; init; } + public Pending? Vertex { get; set; } + public ulong Identity { get; set; } + } + + private static readonly object _gate = new(); + private static readonly Queue _pending = new(); + private static ShaderCacheFile? _file; + private static Action? _created; + private static Thread? _thread; + private static long _dropped; + + [ThreadStatic] + private static Pending? _lastVertex; + + public static void Attach(ShaderCacheFile file, Action? created) + { + lock (_gate) + { + _file = file; + _created = created; + if (_thread is null) + { + _thread = new Thread(Run) { IsBackground = true, Name = "SharpEmu shader inventory", Priority = ThreadPriority.BelowNormal }; + _thread.Start(); + } + + Monitor.PulseAll(_gate); + } + } + + public static void Detach(ShaderCacheFile file) + { + lock (_gate) + { + if (ReferenceEquals(_file, file)) + { + _file = null; + _created = null; + } + } + } + + public static void Capture(CpuContext context, ulong headerAddress, ulong codeAddress) + { + if (!ShaderCacheSettings.Enabled) + { + return; + } + + Span head = stackalloc byte[AgcShaderHeader.FixedBytes]; + if (!context.Memory.TryRead(headerAddress, head) || + !AgcShaderHeader.TryPeekSizes(head, out var headerBytes, out var codeBytes, out var type) || + !AgcShaderHeader.IsSupported(type)) + { + return; + } + + var header = new byte[headerBytes]; + var code = new byte[codeBytes + CodeSlackBytes]; + if (!context.Memory.TryRead(headerAddress, header) || + (!context.Memory.TryRead(codeAddress, code) && !context.Memory.TryRead(codeAddress, code.AsSpan(0, codeBytes)))) + { + return; + } + + var pending = new Pending + { + Header = header, + Code = code, + CodeAddress = codeAddress, + HeaderAddress = headerAddress, + CodeSize = (uint)codeBytes, + }; + switch ((AgcShaderType)type) + { + case AgcShaderType.Geometry: + _lastVertex = pending; + break; + case AgcShaderType.Pixel: + pending.Vertex = _lastVertex; + _lastVertex = null; + break; + } + + lock (_gate) + { + if (_pending.Count >= MaxPending) + { + _dropped++; + return; + } + + _pending.Enqueue(pending); + Monitor.PulseAll(_gate); + } + } + + public static bool TryHash(ReadOnlySpan code, uint codeSize, ulong codeAddress, out ulong hash) + { + hash = 0; + if (code.Length < codeSize) + { + return false; + } + + var replay = new ReplayCpuMemory([new CodeRange(codeAddress, code.ToArray())]); + if (!ShaderIdentity.TryReadDeclaredHash(replay, codeAddress, out var declared)) + { + return false; + } + + hash = declared != 0 ? declared : XxHash3.HashToUInt64(code[..(int)codeSize]); + return true; + } + + public static bool TryCreateCapture(ReadOnlySpan code, uint codeSize, ulong codeAddress, out ShaderCodeCapture capture) + { + capture = null!; + if (!TryHash(code, codeSize, codeAddress, out var hash)) + { + return false; + } + + var range = new CodeRange(codeAddress, code.ToArray()); + if (!Gen5ShaderTranslator.TryDecodeProgram(new CpuContext(new ReplayCpuMemory([range]), Generation.Gen5), codeAddress, out _, out _)) + { + return false; + } + + capture = new ShaderCodeCapture + { + Hash = hash, + CodeSize = codeSize, + Address = codeAddress, + Generation = Generation.Gen5, + Ranges = [range], + }; + return true; + } + + private static void Run() + { + while (true) + { + Pending pending; + ShaderCacheFile file; + Action? created; + lock (_gate) + { + while (_file is null || _pending.Count == 0) + { + Monitor.Wait(_gate); + } + + pending = _pending.Dequeue(); + file = _file; + created = _created; + } + + try + { + Process(pending, file, created); + } + catch (Exception exception) when (exception is InvalidDataException or ArgumentException or InvalidOperationException or OverflowException) + { + Console.Error.WriteLine($"[SHADER CACHE][WARN] A created shader could not be recorded: {exception.Message}"); + } + } + } + + private static void Process(Pending pending, ShaderCacheFile file, Action? created) + { + if (!AgcShaderHeader.TryParse(pending.Header, pending.HeaderAddress, out var header) || + !TryCreateCapture(pending.Code, pending.CodeSize, pending.CodeAddress, out var capture)) + { + return; + } + + var program = new InventoryProgram { Code = capture.Key, Source = InventorySource.Created, Header = header.Bytes }; + var identity = file.RecordProgram(program, capture); + pending.Identity = identity ?? ShaderCacheFile.ProgramIdentity(program); + if (header.Type == AgcShaderType.Pixel && pending.Vertex is { Identity: not 0 } vertex) + { + file.RecordPair(vertex.Identity, pending.Identity); + } + + if (identity is { } recorded && header.Type == AgcShaderType.Compute && created is not null && + file.TryGetProgram(recorded, out var cached)) + { + created(cached); + } + } +} diff --git a/src/SharpEmu.Libs/Gpu/ShaderCache/ShaderSeed.cs b/src/SharpEmu.Libs/Gpu/ShaderCache/ShaderSeed.cs new file mode 100644 index 0000000..a835a38 --- /dev/null +++ b/src/SharpEmu.Libs/Gpu/ShaderCache/ShaderSeed.cs @@ -0,0 +1,401 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using System.IO.Hashing; +using SharpEmu.Libs.Agc; +using SharpEmu.Libs.Gpu.Pipelines; +using SharpEmu.Libs.Gpu.Rendering; +using SharpEmu.ShaderCompiler; +using Silk.NET.Vulkan; + +namespace SharpEmu.Libs.Gpu.ShaderCache; + +internal sealed class ShaderSeed +{ + public const string Extension = ".seed"; + + private const uint Magic = 0x44534553; + private const uint FormatVersion = 2; + private const uint StatesOnlyFormatVersion = 1; + + private readonly SortedDictionary> _groups = new(); + private readonly SortedSet<(ulong Vertex, ulong Pixel)> _pairs = []; + private readonly SortedDictionary _computes = new(); + private readonly SortedDictionary _graphics = new(); + + public string TitleId { get; private set; } = string.Empty; + + public string GameVersion { get; private set; } = string.Empty; + + public int FirstVariantVotes { get; private set; } + + public int SecondVariantVotes { get; private set; } + + public bool SecondInterpolantVariant => SecondVariantVotes > FirstVariantVotes; + + public int GroupCount => _groups.Count; + + public int StateCount => _groups.Values.Sum(static group => group.Count); + + public IReadOnlyCollection<(ulong Vertex, ulong Pixel)> Pairs => _pairs; + + public IReadOnlyCollection Computes => _computes.Values; + + public IReadOnlyCollection Graphics => _graphics.Values; + + public bool IsEmpty => StateCount == 0 && _pairs.Count == 0 && _computes.Count == 0 && _graphics.Count == 0; + + public static ulong ComputeIdentity(StageRecord record) => + XxHash3.HashToUInt64(ShaderCacheSerializer.Serialize(writer => ShaderCacheSerializer.WriteStage(writer, record.WithAddress(0)))); + + public static ulong GraphicsIdentity(GraphicsPipelineRecord record) => + XxHash3.HashToUInt64(ShaderCacheSerializer.Serialize(writer => ShaderCacheSerializer.WriteGraphics(writer, record.WithoutAddresses()))); + + public IReadOnlyList Palette(ulong signature, int maxStates) => + _groups.TryGetValue(signature, out var group) + ? group.Values + .OrderByDescending(static entry => entry.Uses) + .ThenBy(static entry => entry.State.Identity) + .Take(maxStates) + .Select(static entry => entry.State) + .ToArray() + : []; + + public static ShaderSeed Load(string path) + { + var pack = new ShaderSeed(); + try + { + if (!File.Exists(path)) + { + return pack; + } + + using var reader = new BinaryReader(new MemoryStream(File.ReadAllBytes(path))); + var magic = reader.ReadUInt32(); + var version = reader.ReadUInt32(); + if (magic != Magic || version is not (FormatVersion or StatesOnlyFormatVersion)) + { + return pack; + } + + pack.TitleId = reader.ReadString(); + pack.GameVersion = reader.ReadString(); + + pack.FirstVariantVotes = reader.ReadInt32(); + pack.SecondVariantVotes = reader.ReadInt32(); + for (var groups = reader.ReadInt32(); groups > 0; groups--) + { + var signature = reader.ReadUInt64(); + for (var states = reader.ReadInt32(); states > 0; states--) + { + var uses = reader.ReadInt32(); + pack.Add(signature, ReadState(reader), uses); + } + } + + for (var pairs = reader.ReadInt32(); pairs > 0; pairs--) + { + pack._pairs.Add((reader.ReadUInt64(), reader.ReadUInt64())); + } + + if (version == FormatVersion) + { + var schema = reader.ReadUInt32(); + var records = reader.ReadBytes(reader.ReadInt32()); + if (schema == ShaderCacheFile.RecordSchema) + { + pack.ReadRecords(records); + } + } + + return pack; + } + catch (Exception exception) when (exception is IOException or UnauthorizedAccessException or EndOfStreamException or ArgumentException) + { + return new ShaderSeed(); + } + } + + public static ShaderSeed Learn(ShaderCacheFile file) + { + var pack = new ShaderSeed(); + foreach (var compute in file.SnapshotComputes()) + { + pack._computes.TryAdd(ComputeIdentity(compute.Record), compute.Record.WithAddress(0)); + } + + foreach (var graphics in file.SnapshotGraphics()) + { + pack._graphics.TryAdd(GraphicsIdentity(graphics.Record), graphics.Record.WithoutAddresses()); + } + + var programsByCode = new Dictionary<(ulong Hash, uint Size), CachedProgram>(); + foreach (var program in file.SnapshotPrograms()) + { + programsByCode.TryAdd((program.Code.Hash, program.Code.CodeSize), program); + } + + foreach (var pair in file.SnapshotPairs()) + { + pack._pairs.Add(pair); + } + + foreach (var recorded in file.SnapshotGraphics()) + { + var record = recorded.Record; + if (record.Pixel is not { Pixel: { } pixelInputs } pixelStage || record.Vertex.Vertex is not { } vertexInputs || + vertexInputs.FetchEmbedded || record.VertexInput.BindingCount != 0 || record.VertexInput.AttributeCount != 0 || + !programsByCode.TryGetValue((record.Vertex.Hash, record.Vertex.CodeSize), out var vertex) || + !programsByCode.TryGetValue((pixelStage.Hash, pixelStage.CodeSize), out var pixel) || + !StaticStageInputs.TryParse(vertex, out var vertexHeader) || + !StaticStageInputs.TryParse(pixel, out var pixelHeader)) + { + continue; + } + + pack._pairs.Add((vertex.Identity, pixel.Identity)); + var recordedCntl = pixelInputs.InputCntl; + if (recordedCntl.Count != 0) + { + var first = AgcExports.ComputeInterpolantMapping(pixelHeader.InputSemantics, vertexHeader.OutputSemantics, false); + var second = AgcExports.ComputeInterpolantMapping(pixelHeader.InputSemantics, vertexHeader.OutputSemantics, true); + pack.FirstVariantVotes += recordedCntl.SequenceEqual(first.Take(recordedCntl.Count)) ? 1 : 0; + pack.SecondVariantVotes += recordedCntl.SequenceEqual(second.Take(recordedCntl.Count)) ? 1 : 0; + } + + pack.Add( + StaticStageInputs.PixelSignature(pixelHeader), + CreateState(record.Rendering, record.StaticParameters, pixelInputs.Outputs, vertexInputs.ClipSpace), + 1); + } + + return pack; + } + + public ShaderSeed MergedWith(ShaderSeed other) + { + var merged = new ShaderSeed + { + TitleId = TitleId.Length != 0 ? TitleId : other.TitleId, + GameVersion = GameVersion.Length != 0 ? GameVersion : other.GameVersion, + FirstVariantVotes = Math.Max(FirstVariantVotes, other.FirstVariantVotes), + SecondVariantVotes = Math.Max(SecondVariantVotes, other.SecondVariantVotes), + }; + foreach (var source in (ShaderSeed[])[this, other]) + { + foreach (var (signature, group) in source._groups) + { + foreach (var (state, uses) in group.Values) + { + merged.Add(signature, state, uses, keepMaximum: true); + } + } + + merged._pairs.UnionWith(source._pairs); + foreach (var (identity, record) in source._computes) + { + merged._computes.TryAdd(identity, record); + } + + foreach (var (identity, record) in source._graphics) + { + merged._graphics.TryAdd(identity, record); + } + } + + return merged; + } + + public byte[] Serialize() => ShaderCacheSerializer.Serialize(writer => + { + writer.Write(Magic); + writer.Write(FormatVersion); + writer.Write(TitleId); + writer.Write(GameVersion); + writer.Write(FirstVariantVotes); + writer.Write(SecondVariantVotes); + writer.Write(_groups.Count); + foreach (var (signature, group) in _groups) + { + writer.Write(signature); + writer.Write(group.Count); + foreach (var (state, uses) in group.Values) + { + writer.Write(uses); + WriteState(writer, state); + } + } + + writer.Write(_pairs.Count); + foreach (var (vertex, pixel) in _pairs) + { + writer.Write(vertex); + writer.Write(pixel); + } + + var records = ShaderCacheSerializer.Serialize(WriteRecords); + writer.Write(ShaderCacheFile.RecordSchema); + writer.Write(records.Length); + writer.Write(records); + }); + + private void WriteRecords(BinaryWriter writer) + { + writer.Write(_computes.Count); + foreach (var record in _computes.Values) + { + ShaderCacheSerializer.WriteStage(writer, record); + } + + writer.Write(_graphics.Count); + foreach (var record in _graphics.Values) + { + ShaderCacheSerializer.WriteGraphics(writer, record); + } + } + + private void ReadRecords(byte[] records) + { + using var reader = new BinaryReader(new MemoryStream(records)); + var computes = new List(); + for (var count = reader.ReadInt32(); count > 0; count--) + { + computes.Add(ShaderCacheSerializer.ReadStage(reader)); + } + + var graphics = new List(); + for (var count = reader.ReadInt32(); count > 0; count--) + { + graphics.Add(ShaderCacheSerializer.ReadGraphics(reader)); + } + + foreach (var record in computes) + { + _computes.TryAdd(ComputeIdentity(record), record); + } + + foreach (var record in graphics) + { + _graphics.TryAdd(GraphicsIdentity(record), record); + } + } + + public ShaderSeed Stamped(string titleId, string gameVersion) + { + var stamped = MergedWith(new ShaderSeed()); + stamped.TitleId = titleId; + stamped.GameVersion = gameVersion; + return stamped; + } + + public bool TrySave(string path) + { + var temporary = path + ".tmp"; + try + { + Directory.CreateDirectory(Path.GetDirectoryName(path)!); + File.WriteAllBytes(temporary, Serialize()); + File.Move(temporary, path, overwrite: true); + return true; + } + catch (Exception exception) when (exception is IOException or UnauthorizedAccessException) + { + return false; + } + } + + public static StaticGraphicsState CreateState( + PipelineRenderingState rendering, + PipelineStaticParameters parameters, + IReadOnlyList outputs, + ShaderClipSpaceTransform clipSpace) + { + var state = new StaticGraphicsState(rendering, parameters, outputs, clipSpace, 0); + return state with { Identity = XxHash3.HashToUInt64(ShaderCacheSerializer.Serialize(writer => WriteState(writer, state))) }; + } + + private void Add(ulong signature, StaticGraphicsState state, int uses, bool keepMaximum = false) + { + if (!_groups.TryGetValue(signature, out var group)) + { + _groups[signature] = group = new SortedDictionary(); + } + + group[state.Identity] = group.TryGetValue(state.Identity, out var known) + ? (known.State, keepMaximum ? Math.Max(known.Uses, uses) : known.Uses + uses) + : (state, uses); + } + + private static void WriteState(BinaryWriter writer, StaticGraphicsState state) + { + writer.Write(state.Rendering.ColorCount); + foreach (var format in state.Rendering.ColorFormats) + { + writer.Write((int)format); + } + + writer.Write((int)state.Rendering.DepthFormat); + writer.Write((int)state.Rendering.StencilFormat); + writer.Write(state.StaticParameters.Bytes); + writer.Write(state.PixelOutputs.Count); + foreach (var output in state.PixelOutputs) + { + writer.Write(output.GuestSlot); + writer.Write(output.HostLocation); + writer.Write((int)output.Kind); + writer.Write(output.ComponentMapping.Packed); + writer.Write(output.ExportTarget); + } + + var clip = state.ClipSpace; + writer.Write(clip.Enabled); + writer.Write(clip.ScaleX); + writer.Write(clip.ScaleY); + writer.Write(clip.OffsetX); + writer.Write(clip.OffsetY); + writer.Write(clip.HalfExtentX); + writer.Write(clip.HalfExtentY); + } + + private static StaticGraphicsState ReadState(BinaryReader reader) + { + var rendering = new PipelineRenderingState { ColorCount = reader.ReadUInt32() }; + for (var index = 0; index < rendering.ColorFormats.Length; index++) + { + rendering.ColorFormats[index] = (Format)reader.ReadInt32(); + } + + rendering.DepthFormat = (Format)reader.ReadInt32(); + rendering.StencilFormat = (Format)reader.ReadInt32(); + var parameters = PipelineStaticParameters.FromBytes(reader.ReadBytes(PipelineStaticParameters.ByteSize)); + var count = reader.ReadInt32(); + if (count is < 0 or > 16) + { + throw new ArgumentException("The graphics state pack has too many pixel outputs."); + } + + var outputs = new Gen5PixelOutputBinding[count]; + for (var index = 0; index < outputs.Length; index++) + { + outputs[index] = new Gen5PixelOutputBinding( + reader.ReadUInt32(), + reader.ReadUInt32(), + (Gen5PixelOutputKind)reader.ReadInt32(), + new Gen5ColorComponentMapping(reader.ReadByte())) + { + ExportTarget = reader.ReadUInt32(), + }; + } + + var clip = new ShaderClipSpaceTransform( + reader.ReadBoolean(), + reader.ReadSingle(), + reader.ReadSingle(), + reader.ReadSingle(), + reader.ReadSingle(), + reader.ReadSingle(), + reader.ReadSingle()); + return CreateState(rendering, parameters, outputs, clip); + } +} diff --git a/src/SharpEmu.Libs/Gpu/ShaderCache/ShaderWorkList.cs b/src/SharpEmu.Libs/Gpu/ShaderCache/ShaderWorkList.cs new file mode 100644 index 0000000..97c04e1 --- /dev/null +++ b/src/SharpEmu.Libs/Gpu/ShaderCache/ShaderWorkList.cs @@ -0,0 +1,241 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using System.IO.Hashing; + +namespace SharpEmu.Libs.Gpu.ShaderCache; + +internal enum ShaderWorkKind : byte +{ + RecordedCompute = 1, + StaticCompute = 2, + RecordedGraphics = 3, + StaticGraphics = 4, + SeedCompute = 5, + SeedGraphics = 6, +} + +internal readonly record struct ShaderWorkItem(ShaderWorkKind Kind, ulong Identity, int First, int Second = -1, int State = -1) +{ + public bool IsCompute => Kind is ShaderWorkKind.RecordedCompute or ShaderWorkKind.StaticCompute or ShaderWorkKind.SeedCompute; +} + +internal sealed record SeedComputeItem(StageRecord Record, ShaderCodeCapture Code); + +internal sealed record SeedGraphicsItem(GraphicsPipelineRecord Record, ShaderCodeCapture VertexCode, ShaderCodeCapture? PixelCode); + +internal sealed class ShaderWorkList +{ + private const int MaxStatesPerSignature = 8; + + private readonly HashSet _identities; + private readonly Dictionary<(ulong Hash, uint CodeSize), CachedProgram> _programsByCode; + + private ShaderWorkList( + ShaderCacheFile file, + List items, + HashSet identities, + List states, + List seedComputes, + List seedGraphics, + Dictionary<(ulong Hash, uint CodeSize), CachedProgram> programsByCode, + bool secondVariant) + { + File = file; + Items = items; + States = states; + SeedComputes = seedComputes; + SeedGraphics = seedGraphics; + SecondInterpolantVariant = secondVariant; + _identities = identities; + _programsByCode = programsByCode; + } + + public ShaderCacheFile File { get; } + + public IReadOnlyList Items { get; } + + public IReadOnlyList States { get; } + + public IReadOnlyList SeedComputes { get; } + + public IReadOnlyList SeedGraphics { get; } + + public bool SecondInterpolantVariant { get; } + + public bool Contains(ulong identity) => _identities.Contains(identity); + + public bool TryGetProgram(ulong hash, uint codeSize, out CachedProgram program) => + _programsByCode.TryGetValue((hash, codeSize), out program!); + + public static ulong StaticGraphicsIdentity(ulong vertex, ulong pixel, ulong state) => Combine(2, vertex, pixel, state); + + public static ShaderWorkList Build(ShaderCacheFile file, bool includeStaticGraphics, ShaderSeed? seed = null) + { + var items = new List(); + var seen = new HashSet(); + void Add(ShaderWorkItem item) + { + if (seen.Add(item.Identity)) + { + items.Add(item); + } + } + + var programsByCode = new Dictionary<(ulong Hash, uint CodeSize), CachedProgram>(); + var codes = new Dictionary<(ulong Hash, uint CodeSize), ShaderCodeCapture>(); + foreach (var program in file.Programs) + { + programsByCode.TryAdd((program.Code.Hash, program.Code.CodeSize), program); + codes.TryAdd((program.Code.Hash, program.Code.CodeSize), program.Code); + } + + var recordedComputes = new HashSet(); + for (var index = 0; index < file.Computes.Count; index++) + { + var compute = file.Computes[index]; + codes.TryAdd((compute.Code.Hash, compute.Code.CodeSize), compute.Code); + recordedComputes.Add(ShaderSeed.ComputeIdentity(compute.Record)); + Add(new ShaderWorkItem(ShaderWorkKind.RecordedCompute, compute.Identity, index)); + } + + var recordedGraphics = new HashSet(); + foreach (var graphics in file.Graphics) + { + codes.TryAdd((graphics.VertexCode.Hash, graphics.VertexCode.CodeSize), graphics.VertexCode); + if (graphics.PixelCode is { } pixelCode) + { + codes.TryAdd((pixelCode.Hash, pixelCode.CodeSize), pixelCode); + } + + recordedGraphics.Add(ShaderSeed.GraphicsIdentity(graphics.Record)); + } + + var seedComputes = new List(); + var seedGraphics = new List(); + if (seed is not null) + { + foreach (var record in seed.Computes) + { + var identity = ShaderSeed.ComputeIdentity(record); + if (!recordedComputes.Contains(identity) && codes.TryGetValue((record.Hash, record.CodeSize), out var code)) + { + seedComputes.Add(new SeedComputeItem(record, code)); + Add(new ShaderWorkItem(ShaderWorkKind.SeedCompute, Combine(3, identity), seedComputes.Count - 1)); + } + } + } + + for (var index = 0; index < file.Programs.Count; index++) + { + var program = file.Programs[index]; + if (StaticStageInputs.TryParse(program, out var header) && header.Type == AgcShaderType.Compute) + { + Add(new ShaderWorkItem(ShaderWorkKind.StaticCompute, Combine(1, program.Identity), index)); + } + } + + for (var index = 0; index < file.Graphics.Count; index++) + { + Add(new ShaderWorkItem(ShaderWorkKind.RecordedGraphics, file.Graphics[index].Identity, index)); + } + + if (seed is not null) + { + foreach (var record in seed.Graphics) + { + var identity = ShaderSeed.GraphicsIdentity(record); + ShaderCodeCapture? pixelCode = null; + if (!recordedGraphics.Contains(identity) && + codes.TryGetValue((record.Vertex.Hash, record.Vertex.CodeSize), out var vertexCode) && + (record.Pixel is null || codes.TryGetValue((record.Pixel.Hash, record.Pixel.CodeSize), out pixelCode))) + { + seedGraphics.Add(new SeedGraphicsItem(record, vertexCode, pixelCode)); + Add(new ShaderWorkItem(ShaderWorkKind.SeedGraphics, Combine(4, identity), seedGraphics.Count - 1)); + } + } + } + + var states = new List(); + var pack = ShaderSeed.Learn(file); + if (seed is not null) + { + pack = pack.MergedWith(seed); + } + + if (includeStaticGraphics) + { + AddStaticGraphics(file, pack, Add, states); + } + + return new ShaderWorkList(file, items, seen, states, seedComputes, seedGraphics, programsByCode, pack.SecondInterpolantVariant); + } + + public static ulong Combine(params ulong[] values) + { + var hash = new XxHash3(); + foreach (var value in values) + { + hash.Append(BitConverter.GetBytes(value)); + } + + return hash.GetCurrentHashAsUInt64(); + } + + private static void AddStaticGraphics( + ShaderCacheFile file, + ShaderSeed pack, + Action add, + List states) + { + var programIndexByIdentity = new Dictionary(); + for (var index = 0; index < file.Programs.Count; index++) + { + programIndexByIdentity[file.Programs[index].Identity] = index; + } + + var pairs = new List<(int Vertex, int Pixel)>(); + foreach (var (vertexIdentity, pixelIdentity) in pack.Pairs) + { + if (programIndexByIdentity.TryGetValue(vertexIdentity, out var vertex) && + programIndexByIdentity.TryGetValue(pixelIdentity, out var pixel)) + { + pairs.Add((vertex, pixel)); + } + } + + var palettes = new Dictionary(); + foreach (var (vertex, pixel) in pairs) + { + if (!StaticStageInputs.TryParse(file.Programs[vertex], out var vertexHeader) || + vertexHeader.Type != AgcShaderType.Geometry || vertexHeader.HasVertexFetchTables || + !StaticStageInputs.TryParse(file.Programs[pixel], out var pixelHeader)) + { + continue; + } + + var signature = StaticStageInputs.PixelSignature(pixelHeader); + if (!palettes.TryGetValue(signature, out var palette)) + { + palette = pack.Palette(signature, MaxStatesPerSignature) + .Select(state => + { + states.Add(state); + return states.Count - 1; + }) + .ToArray(); + palettes[signature] = palette; + } + + foreach (var state in palette) + { + add(new ShaderWorkItem( + ShaderWorkKind.StaticGraphics, + StaticGraphicsIdentity(file.Programs[vertex].Identity, file.Programs[pixel].Identity, states[state].Identity), + vertex, + pixel, + state)); + } + } + } +} diff --git a/src/SharpEmu.Libs/Gpu/ShaderCache/StaticStageInputs.cs b/src/SharpEmu.Libs/Gpu/ShaderCache/StaticStageInputs.cs new file mode 100644 index 0000000..b3156a9 --- /dev/null +++ b/src/SharpEmu.Libs/Gpu/ShaderCache/StaticStageInputs.cs @@ -0,0 +1,219 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using System.IO.Hashing; +using SharpEmu.HLE; +using SharpEmu.Libs.Agc; +using SharpEmu.Libs.Gpu.GpuCommands.Registers; +using SharpEmu.Libs.Gpu.Images; +using SharpEmu.Libs.Gpu.Pipelines; +using SharpEmu.Libs.Gpu.Rendering; +using SharpEmu.ShaderCompiler; +using SharpEmu.ShaderCompiler.Resources; + +namespace SharpEmu.Libs.Gpu.ShaderCache; + +internal sealed record StaticGraphicsState( + PipelineRenderingState Rendering, + PipelineStaticParameters StaticParameters, + IReadOnlyList PixelOutputs, + ShaderClipSpaceTransform ClipSpace, + ulong Identity); + +internal static class StaticStageInputs +{ + private const uint VertexUserDataBase = 8; + private const ulong HeaderAddress = 0x20_0000_0000; + private const uint SupportedPixelInputs = 0xFFFF; + private const uint MaxPixelInputs = 32; + private const uint DispatchInitiatorMask = 0xA038; + private const uint DispatchInitiatorBase = 0x41; + + public static bool TryParse(CachedProgram program, out AgcShaderHeader header) => + AgcShaderHeader.TryParse(program.Program.Header, 0, out header); + + public static bool TryCompute(CachedProgram program, bool computeWave64Supported, out StageRecord record) + { + record = null!; + if (!TryParse(program, out var header) || header.Type != AgcShaderType.Compute || + !TryApplyRegisters(header, out var banks)) + { + return false; + } + + var compute = banks.Shader.Compute; + var shader = Registered(program, header); + var initiator = (header.DispatchModifier & DispatchInitiatorMask) | DispatchInitiatorBase; + record = new StageRecord + { + Stage = ShaderStage.Compute, + Hash = program.Code.Hash, + CodeSize = program.Code.CodeSize, + Address = program.Code.Address, + UserDataBase = 0, + UserDataCount = compute.UserScalarCount, + PushDataCursor = 0, + Compute = ComputeStageInputResolver.Resolve(compute, shader, initiator, !computeWave64Supported, 0, 0, 0), + ComputeSystemRegisters = ShaderPipelineCache.DecodeComputeSystemRegisters(compute), + }; + return true; + } + + public static ulong PixelSignature(AgcShaderHeader header) + { + uint colorFormat = 0, depthControl = 0, colorMask = 0; + foreach (var register in header.ContextRegisters) + { + switch (register.Offset) + { + case ContextRegisterOffset.SpiShaderColFormat: + colorFormat = register.Value; + break; + case ContextRegisterOffset.DbShaderControl: + depthControl = register.Value & 0x107; + break; + case ContextRegisterOffset.CbShaderMask: + colorMask = register.Value; + break; + } + } + + Span values = [colorFormat, depthControl, colorMask]; + return XxHash3.HashToUInt64(System.Runtime.InteropServices.MemoryMarshal.AsBytes(values)); + } + + public static bool TryGraphics( + CachedProgram vertex, + CachedProgram pixel, + StaticGraphicsState state, + bool secondInterpolantVariant, + out StageRecord vertexRecord, + out StageRecord pixelRecord) + { + vertexRecord = pixelRecord = null!; + if (!TryParse(vertex, out var vertexHeader) || vertexHeader.Type != AgcShaderType.Geometry || vertexHeader.HasVertexFetchTables || + !TryParse(pixel, out var pixelHeader) || pixelHeader.Type != AgcShaderType.Pixel || + !TryApplyRegisters(vertexHeader, out var banks) || !TryApplyRegisters(pixelHeader, banks)) + { + return false; + } + + var interpolators = AgcExports.ComputeInterpolantMapping(pixelHeader.InputSemantics, vertexHeader.OutputSemantics, secondInterpolantVariant); + for (var index = 0u; index < (uint)interpolators.Length; index++) + { + if (!TryWrite(RegisterWriteTable.ContextIndirect, banks, ContextRegisterOffset.SpiPsInputCntl0 + index, interpolators[index])) + { + return false; + } + } + + var shaderInterface = banks.Context.ShaderInterface; + var enable = shaderInterface.PixelInputEnable; + var address = shaderInterface.PixelInputAddress; + if (((enable | address) & ~SupportedPixelInputs) != 0 || enable != address || + !Gen5ShaderTranslator.TryDecodeProgram(pixel.Code.CreateContext(), pixel.Code.Address, out var pixelProgram, out _)) + { + return false; + } + + var attributeCount = ShaderPipelineCache.InterpolatedAttributeCount(pixelProgram); + var inputCount = shaderInterface.PixelInputControl & 0x3Fu; + if (inputCount == 0) + { + inputCount = Math.Min(attributeCount, MaxPixelInputs); + } + + if (inputCount > MaxPixelInputs) + { + return false; + } + + var headerMemory = new CpuContext(new ReplayCpuMemory([new CodeRange(HeaderAddress, pixelHeader.Bytes)]), Generation.Gen5); + var info = PixelStageInputResolver.Resolve( + headerMemory, Registered(pixel, pixelHeader), shaderInterface, + new byte[PixelInputInfo.TargetCount], new ColorComponentMap[PixelInputInfo.TargetCount], inputCount); + var requiredOutputs = Math.Max(attributeCount, ShaderPipelineCache.ReadVertexOutputCount(pixelProgram, info)); + var cntl = new uint[info.InputCount]; + Array.Copy(info.InterpolatorSettings, cntl, cntl.Length); + pixelRecord = new StageRecord + { + Stage = ShaderStage.Pixel, + Hash = pixel.Code.Hash, + CodeSize = pixel.Code.CodeSize, + Address = pixel.Code.Address, + UserDataBase = 0, + UserDataCount = banks.Shader.Pixel.Resource2.UserScalarCount, + PushDataCursor = 0, + Pixel = new PixelStageInputs(info.ScratchDwords, state.PixelOutputs, enable, info.CustomInterpolationMask, address, cntl), + }; + vertexRecord = new StageRecord + { + Stage = ShaderStage.Vertex, + Hash = vertex.Code.Hash, + CodeSize = vertex.Code.CodeSize, + Address = vertex.Code.Address, + UserDataBase = VertexUserDataBase, + UserDataCount = banks.Shader.Vertex.GeometryResource2.UserScalarCount, + PushDataCursor = 0, + Vertex = new VertexStageInputs( + false, 0, 0, [], vertexHeader.ScratchDwords, (int)requiredOutputs, shaderInterface.VertexOutputControl, state.ClipSpace), + }; + return true; + } + + private static RegisteredShader Registered(CachedProgram program, AgcShaderHeader header) => new( + program.Code.Address, + HeaderAddress, + program.Code.CodeSize, + header.ScratchDwords, + 0, + HeaderAddress + (ulong)header.InputSemanticsOffset, + header.InputSemanticCount, + 0, + 0); + + private static bool TryApplyRegisters(AgcShaderHeader header, out RegisterBanks banks) + { + banks = new RegisterBanks(static message => new InvalidDataException(message)); + return TryApplyRegisters(header, banks); + } + + private static bool TryApplyRegisters(AgcShaderHeader header, RegisterBanks banks) + { + foreach (var register in header.ShaderRegisters) + { + if (!TryWrite(RegisterWriteTable.ShaderIndirect, banks, register.Offset, register.Value)) + { + return false; + } + } + + foreach (var register in header.ContextRegisters) + { + if (!TryWrite(RegisterWriteTable.ContextIndirect, banks, register.Offset, register.Value)) + { + return false; + } + } + + return true; + } + + private static bool TryWrite(RegisterWriter?[] table, RegisterBanks banks, uint offset, uint value) + { + if (offset >= (uint)table.Length || table[offset] is not { } writer) + { + return false; + } + + try + { + writer(banks, offset, value); + return true; + } + catch (InvalidDataException) + { + return false; + } + } +}