[GPU] Add the shader cache file, shader inventory, game file scanner and shader seed
This commit is contained in:
parent
278836bc7e
commit
2920f8843b
|
|
@ -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<byte> 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<AgcRegister> ContextRegisters => Registers(ContextRegistersField, Bytes[91]);
|
||||
|
||||
public IReadOnlyList<AgcRegister> 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<byte> 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<byte> 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<int>)[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));
|
||||
}
|
||||
|
|
@ -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<string> 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<long, long>? 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<FoundProgram>();
|
||||
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<byte> 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<long> 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<byte> 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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<string> _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<object?>().ToArray();
|
||||
var right = derivedItems.Cast<object?>().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,
|
||||
};
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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<ShaderVertexInput> Inputs,
|
||||
uint ScratchDwords,
|
||||
int RequiredVertexOutputCount,
|
||||
uint PositionExportControl,
|
||||
ShaderClipSpaceTransform ClipSpace);
|
||||
|
||||
internal sealed record PixelStageInputs(
|
||||
uint ScratchDwords,
|
||||
IReadOnlyList<Gen5PixelOutputBinding> Outputs,
|
||||
uint InputEnable,
|
||||
uint CustomInterpolationMask,
|
||||
uint InputAddress,
|
||||
IReadOnlyList<uint> 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<BinaryWriter> write)
|
||||
{
|
||||
using var stream = new MemoryStream();
|
||||
using var writer = new BinaryWriter(stream);
|
||||
write(writer);
|
||||
writer.Flush();
|
||||
return stream.ToArray();
|
||||
}
|
||||
|
||||
public static T Deserialize<T>(byte[] payload, Func<BinaryReader, T> 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<byte> driverKey, UInt128 contentKey, IReadOnlyList<PipelineBinaryPart> 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<uint>();
|
||||
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.");
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<byte> destination)
|
||||
{
|
||||
if (!inner.TryRead(virtualAddress, destination))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_reads.Add((virtualAddress, destination.ToArray()));
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source) => false;
|
||||
|
||||
public CodeRange[] TakeRanges()
|
||||
{
|
||||
_reads.Sort(static (left, right) => left.Address.CompareTo(right.Address));
|
||||
var ranges = new List<CodeRange>();
|
||||
ulong start = 0;
|
||||
var bytes = new List<byte>();
|
||||
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<CodeRange> ranges) : ICpuMemory
|
||||
{
|
||||
public bool TryRead(ulong virtualAddress, Span<byte> 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<byte> 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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> _pending = new();
|
||||
private static ShaderCacheFile? _file;
|
||||
private static Action<CachedProgram>? _created;
|
||||
private static Thread? _thread;
|
||||
private static long _dropped;
|
||||
|
||||
[ThreadStatic]
|
||||
private static Pending? _lastVertex;
|
||||
|
||||
public static void Attach(ShaderCacheFile file, Action<CachedProgram>? 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<byte> 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<byte> 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<byte> 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<CachedProgram>? 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<CachedProgram>? 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ulong, SortedDictionary<ulong, (StaticGraphicsState State, int Uses)>> _groups = new();
|
||||
private readonly SortedSet<(ulong Vertex, ulong Pixel)> _pairs = [];
|
||||
private readonly SortedDictionary<ulong, StageRecord> _computes = new();
|
||||
private readonly SortedDictionary<ulong, GraphicsPipelineRecord> _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<StageRecord> Computes => _computes.Values;
|
||||
|
||||
public IReadOnlyCollection<GraphicsPipelineRecord> 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<StaticGraphicsState> 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<StageRecord>();
|
||||
for (var count = reader.ReadInt32(); count > 0; count--)
|
||||
{
|
||||
computes.Add(ShaderCacheSerializer.ReadStage(reader));
|
||||
}
|
||||
|
||||
var graphics = new List<GraphicsPipelineRecord>();
|
||||
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<Gen5PixelOutputBinding> 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<ulong, (StaticGraphicsState, int)>();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ulong> _identities;
|
||||
private readonly Dictionary<(ulong Hash, uint CodeSize), CachedProgram> _programsByCode;
|
||||
|
||||
private ShaderWorkList(
|
||||
ShaderCacheFile file,
|
||||
List<ShaderWorkItem> items,
|
||||
HashSet<ulong> identities,
|
||||
List<StaticGraphicsState> states,
|
||||
List<SeedComputeItem> seedComputes,
|
||||
List<SeedGraphicsItem> 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<ShaderWorkItem> Items { get; }
|
||||
|
||||
public IReadOnlyList<StaticGraphicsState> States { get; }
|
||||
|
||||
public IReadOnlyList<SeedComputeItem> SeedComputes { get; }
|
||||
|
||||
public IReadOnlyList<SeedGraphicsItem> 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<ShaderWorkItem>();
|
||||
var seen = new HashSet<ulong>();
|
||||
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<ulong>();
|
||||
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<ulong>();
|
||||
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<SeedComputeItem>();
|
||||
var seedGraphics = new List<SeedGraphicsItem>();
|
||||
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<StaticGraphicsState>();
|
||||
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<ShaderWorkItem> add,
|
||||
List<StaticGraphicsState> states)
|
||||
{
|
||||
var programIndexByIdentity = new Dictionary<ulong, int>();
|
||||
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<ulong, int[]>();
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Gen5PixelOutputBinding> 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<uint> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue