From 79a13903e29170698543331dc140eb9a28c12412 Mon Sep 17 00:00:00 2001 From: ParantezTech <12572227+par274@users.noreply.github.com> Date: Thu, 8 Oct 2026 02:53:25 +0300 Subject: [PATCH] [GPU] Replace the shader prewarm list with the shader cache and multi-process precompile --- .../Agc/AgcExports.ShaderState.cs | 2 + .../Gpu/Pipelines/IShaderPipelineHost.cs | 3 +- .../Gpu/Pipelines/ShaderPipelineCache.cs | 39 + .../Gpu/Pipelines/ShaderPrewarmList.cs | 697 -------- .../Gpu/Pipelines/ShaderProgramCache.cs | 357 +++-- .../Gpu/Rendering/RenderExecutor.Compute.cs | 2 +- .../VideoOut/ShaderPrecompiler.cs | 13 + .../VulkanVideoPresenter.CommandStream.cs | 4 +- .../VulkanVideoPresenter.Device.Setup.cs | 31 +- .../VulkanVideoPresenter.Device.Teardown.cs | 3 +- .../VulkanVideoPresenter.PipelineBinaries.cs | 435 +++++ .../VulkanVideoPresenter.Pipelines.cs | 149 +- .../VideoOut/VulkanVideoPresenter.Present.cs | 46 +- .../VulkanVideoPresenter.ShaderCache.cs | 1401 +++++++++++++++++ .../VulkanVideoPresenter.ShaderPrewarm.cs | 279 ---- 15 files changed, 2298 insertions(+), 1163 deletions(-) delete mode 100644 src/SharpEmu.Libs/Gpu/Pipelines/ShaderPrewarmList.cs create mode 100644 src/SharpEmu.Libs/VideoOut/ShaderPrecompiler.cs create mode 100644 src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.PipelineBinaries.cs create mode 100644 src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.ShaderCache.cs delete mode 100644 src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.ShaderPrewarm.cs diff --git a/src/SharpEmu.Libs/Agc/AgcExports.ShaderState.cs b/src/SharpEmu.Libs/Agc/AgcExports.ShaderState.cs index b3f576d..0fbc5df 100644 --- a/src/SharpEmu.Libs/Agc/AgcExports.ShaderState.cs +++ b/src/SharpEmu.Libs/Agc/AgcExports.ShaderState.cs @@ -92,6 +92,8 @@ public static partial class AgcExports return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); } + Gpu.ShaderCache.ShaderInventory.Capture(ctx, headerAddress, codeAddress); + if (!RelocatePointerField(ctx, headerAddress + ShaderCxRegistersOffset) || !RelocatePointerField(ctx, headerAddress + ShaderShRegistersOffset) || !RelocatePointerField(ctx, headerAddress + ShaderUserDataOffset) || diff --git a/src/SharpEmu.Libs/Gpu/Pipelines/IShaderPipelineHost.cs b/src/SharpEmu.Libs/Gpu/Pipelines/IShaderPipelineHost.cs index 2eff4e4..257a439 100644 --- a/src/SharpEmu.Libs/Gpu/Pipelines/IShaderPipelineHost.cs +++ b/src/SharpEmu.Libs/Gpu/Pipelines/IShaderPipelineHost.cs @@ -43,9 +43,10 @@ internal interface IShaderPipelineHost bool SharedInt64AtomicsEnabled { get; } bool ShaderSignedZeroInfNanPreserveFloat32Supported => false; bool ExecGuardElisionEnabled => true; - ShaderPrewarmList? ShaderPrewarm => null; + ShaderCache.ShaderCacheFile? ShaderCache => null; bool PerVertexPixelInputsSupported => true; bool ClipDistanceEnabled => false; + bool PortableBufferDescriptors => false; RenderHostLimits Limits { get; } diff --git a/src/SharpEmu.Libs/Gpu/Pipelines/ShaderPipelineCache.cs b/src/SharpEmu.Libs/Gpu/Pipelines/ShaderPipelineCache.cs index 3ce3ccb..bbb8199 100644 --- a/src/SharpEmu.Libs/Gpu/Pipelines/ShaderPipelineCache.cs +++ b/src/SharpEmu.Libs/Gpu/Pipelines/ShaderPipelineCache.cs @@ -6,6 +6,7 @@ using SharpEmu.Libs.Gpu.GpuCommands.Registers; using SharpEmu.Libs.Gpu.Images; using SharpEmu.Libs.Gpu.Rendering; using SharpEmu.Libs.Gpu.Scheduling; +using SharpEmu.Libs.Gpu.ShaderCache; using SharpEmu.Libs.VideoOut; using SharpEmu.ShaderCompiler; using SharpEmu.ShaderCompiler.Resources; @@ -439,10 +440,48 @@ internal sealed partial class ShaderPipelineCache : IShaderPipelineProvider var created = _host.CreateGraphicsPipeline(description); _graphicsPipelines.Add(key, created); ShaderCacheCounters.CountGraphicsPipeline(); + RecordGraphicsPipeline(description); return created; } } + private void RecordGraphicsPipeline(GraphicsPipelineDescription description) + { + if (_host.ShaderCache is not { } cache || + !_programs.TryGetStageRecord(description.VertexProgram.Id, out var vertex, out var vertexCode)) + { + return; + } + + StageRecord? pixel = null; + ShaderCodeCapture? pixelCode = null; + if (description.PixelStage is not null && + !_programs.TryGetStageRecord(description.PixelProgram.Id, out pixel, out pixelCode)) + { + return; + } + + var attributes = new VertexAttributeFormat[description.VertexInput.AttributeCount]; + for (var index = 0; index < attributes.Length; index++) + { + var attribute = description.VertexInfo.Attributes[index]; + attributes[index] = new VertexAttributeFormat(attribute.Descriptor, attribute.RegisterCount); + } + + cache.RecordGraphics( + new GraphicsPipelineRecord + { + Vertex = vertex, + Pixel = pixel, + Rendering = description.Rendering, + VertexInput = description.VertexInput, + StaticParameters = description.StaticParameters, + Attributes = attributes, + }, + vertexCode, + pixelCode); + } + // The full static state of one graphics pipeline from the draw's targets, registers and programs. internal static GraphicsPipelineDescription BuildGraphicsDescription( ReadOnlySpan colors, diff --git a/src/SharpEmu.Libs/Gpu/Pipelines/ShaderPrewarmList.cs b/src/SharpEmu.Libs/Gpu/Pipelines/ShaderPrewarmList.cs deleted file mode 100644 index 980bcb1..0000000 --- a/src/SharpEmu.Libs/Gpu/Pipelines/ShaderPrewarmList.cs +++ /dev/null @@ -1,697 +0,0 @@ -// Copyright (C) 2026 SharpEmu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -using System.Globalization; -using System.IO.Hashing; -using System.Text; -using SharpEmu.HLE; -using SharpEmu.Libs.Gpu.Rendering; -using SharpEmu.ShaderCompiler; -using SharpEmu.ShaderCompiler.Resources; - -namespace SharpEmu.Libs.Gpu.Pipelines; - -internal sealed class RecordingCpuMemory(ICpuMemory inner) : ICpuMemory, ICpuMemoryWrapper -{ - private readonly List<(ulong Address, byte[] Bytes)> _reads = []; - - public ICpuMemory Inner => inner; - - public bool TryRead(ulong virtualAddress, Span destination) - { - if (!inner.TryRead(virtualAddress, destination)) - { - return false; - } - - _reads.Add((virtualAddress, destination.ToArray())); - return true; - } - - public bool TryWrite(ulong virtualAddress, ReadOnlySpan source) => false; - - public CodeRange[] TakeRanges() - { - _reads.Sort(static (left, right) => left.Address.CompareTo(right.Address)); - var ranges = new List(); - ulong start = 0; - var bytes = new List(); - foreach (var (address, data) in _reads) - { - var end = start + (ulong)bytes.Count; - if (bytes.Count != 0 && address <= end) - { - var overlap = (int)(end - address); - if (overlap < data.Length) - { - bytes.AddRange(data.AsSpan(overlap).ToArray()); - } - - continue; - } - - if (bytes.Count != 0) - { - ranges.Add(new CodeRange(start, bytes.ToArray())); - } - - start = address; - bytes.Clear(); - bytes.AddRange(data); - } - - if (bytes.Count != 0) - { - ranges.Add(new CodeRange(start, bytes.ToArray())); - } - - _reads.Clear(); - return ranges.ToArray(); - } -} - -internal sealed class ReplayCpuMemory(IReadOnlyList ranges) : ICpuMemory -{ - public bool TryRead(ulong virtualAddress, Span destination) - { - foreach (var range in ranges) - { - if (virtualAddress < range.Address) - { - continue; - } - - var offset = virtualAddress - range.Address; - if (offset + (ulong)destination.Length <= (ulong)range.Bytes.Length) - { - range.Bytes.AsSpan((int)offset, destination.Length).CopyTo(destination); - return true; - } - } - - return false; - } - - public bool TryWrite(ulong virtualAddress, ReadOnlySpan source) => false; -} - -internal readonly record struct CodeRange(ulong Address, byte[] Bytes); - -internal readonly record struct FusedCodeParts(ulong EntryHeaderAddress, ulong ContinuationAddress, ulong ContinuationHeaderAddress); - -internal 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 (ulong Hash, uint CodeSize, ulong Address) Key => (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; - } -} - -internal sealed class ComputePrewarmRecord -{ - 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 required ComputeInputInfo Info { get; init; } - public Gen5ComputeSystemRegisters? SystemRegisters { get; init; } - public required ResourceSpecialization Specialization { get; init; } - - public (ulong Hash, uint CodeSize, ulong Address) CodeKey => (Hash, CodeSize, Address); -} - -internal sealed class ShaderPrewarmList : IDisposable -{ - public const string FileName = "shader-prewarm.bin"; - public const string StampFileName = "shader-prewarm.stamp"; - public const string ProgressFileName = "shader-prewarm.progress"; - - private const uint Magic = 0x57504553; - private const uint FormatVersion = 1; - private const byte CodeKind = 1; - private const byte ComputeKind = 2; - private const int RecordHeaderBytes = sizeof(uint) + sizeof(ulong); - - private readonly object _gate = new(); - private readonly FileStream _file; - private readonly string _stampPath; - private readonly string _progressPath; - private readonly object _progressGate = new(); - private readonly HashSet<(ulong Hash, uint CodeSize, ulong Address)> _codes = []; - private readonly HashSet _computes = []; - private readonly Dictionary<(ulong Hash, uint CodeSize, ulong Address), ShaderCodeCapture> _loadedCodes = new(); - private readonly List _loadedComputes = []; - private bool _failed; - - private ShaderPrewarmList(FileStream file, string stampPath, string progressPath) - { - _file = file; - _stampPath = stampPath; - _progressPath = progressPath; - } - - public int LoadedComputeCount => _loadedComputes.Count; - - public static ShaderPrewarmList? Open(string directory) - { - try - { - Directory.CreateDirectory(directory); - var path = Path.Combine(directory, FileName); - var file = new FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read); - var list = new ShaderPrewarmList(file, Path.Combine(directory, StampFileName), Path.Combine(directory, ProgressFileName)); - try - { - list.Load(); - } - catch - { - file.Dispose(); - throw; - } - - return list; - } - catch (Exception exception) when (exception is IOException or UnauthorizedAccessException) - { - Console.Error.WriteLine($"[LOADER][WARN] Shader prewarm list unavailable: {exception.Message}"); - return null; - } - } - - public IReadOnlyList<(ComputePrewarmRecord Record, ShaderCodeCapture Code)> LoadedComputes() - { - var result = new List<(ComputePrewarmRecord, ShaderCodeCapture)>(_loadedComputes.Count); - foreach (var record in _loadedComputes) - { - if (_loadedCodes.TryGetValue(record.CodeKey, out var code)) - { - result.Add((record, code)); - } - } - - return result; - } - - public bool IsStampCurrent(string stamp) - { - try - { - return File.Exists(_stampPath) && File.ReadAllText(_stampPath) == stamp; - } - catch (Exception exception) when (exception is IOException or UnauthorizedAccessException) - { - return false; - } - } - - public void WriteStamp(string stamp) - { - try - { - File.WriteAllText(_stampPath, stamp); - } - catch (Exception exception) when (exception is IOException or UnauthorizedAccessException) - { - Console.Error.WriteLine($"[LOADER][WARN] Shader prewarm stamp write failed: {exception.Message}"); - } - } - - public static ulong Identity(ComputePrewarmRecord record) => XxHash3.HashToUInt64(SerializeCompute(record)); - - public HashSet ReadProgress(string stamp) - { - var completed = new HashSet(); - lock (_progressGate) - { - try - { - if (!File.Exists(_progressPath)) - { - return completed; - } - - var lines = File.ReadAllLines(_progressPath); - if (lines.Length == 0 || lines[0] != ProgressKey(stamp)) - { - File.Delete(_progressPath); - return completed; - } - - for (var index = 1; index < lines.Length; index++) - { - if (ulong.TryParse(lines[index], NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var identity)) - { - completed.Add(identity); - } - } - } - catch (Exception exception) when (exception is IOException or UnauthorizedAccessException) - { - completed.Clear(); - } - } - - return completed; - } - - public void AppendProgress(string stamp, IReadOnlyCollection identities) - { - if (identities.Count == 0) - { - return; - } - - lock (_progressGate) - { - try - { - var lines = new List(identities.Count + 1); - if (!File.Exists(_progressPath)) - { - lines.Add(ProgressKey(stamp)); - } - - foreach (var identity in identities) - { - lines.Add(identity.ToString("X16", CultureInfo.InvariantCulture)); - } - - File.AppendAllLines(_progressPath, lines); - } - catch (Exception exception) when (exception is IOException or UnauthorizedAccessException) - { - Console.Error.WriteLine($"[LOADER][WARN] Shader prewarm progress write failed: {exception.Message}"); - } - } - } - - public void ClearProgress() - { - lock (_progressGate) - { - try - { - File.Delete(_progressPath); - } - catch (Exception exception) when (exception is IOException or UnauthorizedAccessException) - { - Console.Error.WriteLine($"[LOADER][WARN] Shader prewarm progress clear failed: {exception.Message}"); - } - } - } - - private static string ProgressKey(string stamp) => - XxHash3.HashToUInt64(Encoding.UTF8.GetBytes(stamp)).ToString("X16", CultureInfo.InvariantCulture); - - public void RecordCompute(ShaderCodeCapture code, ComputePrewarmRecord record) - { - var payload = SerializeCompute(record); - var identity = XxHash3.HashToUInt64(payload); - lock (_gate) - { - if (_failed || !_computes.Add(identity)) - { - return; - } - - if (_codes.Add(code.Key)) - { - Append(CodeKind, SerializeCode(code)); - } - - Append(ComputeKind, payload); - } - } - - public void Dispose() - { - lock (_gate) - { - _failed = true; - _file.Dispose(); - } - } - - private void Append(byte kind, byte[] payload) - { - try - { - var record = new byte[RecordHeaderBytes + 1 + payload.Length]; - record[RecordHeaderBytes] = kind; - payload.CopyTo(record, RecordHeaderBytes + 1); - var body = record.AsSpan(RecordHeaderBytes); - BitConverter.TryWriteBytes(record.AsSpan(0, sizeof(uint)), (uint)body.Length); - BitConverter.TryWriteBytes(record.AsSpan(sizeof(uint), sizeof(ulong)), XxHash3.HashToUInt64(body)); - _file.Write(record); - _file.Flush(); - } - catch (Exception exception) when (exception is IOException or UnauthorizedAccessException or ObjectDisposedException) - { - _failed = true; - Console.Error.WriteLine($"[LOADER][WARN] Shader prewarm list write failed: {exception.Message}"); - } - } - - private void Load() - { - var content = new byte[_file.Length]; - _file.Position = 0; - _file.ReadExactly(content); - var validEnd = 0L; - if (content.Length >= 3 * sizeof(uint) && - BitConverter.ToUInt32(content, 0) == Magic && - BitConverter.ToUInt32(content, sizeof(uint)) == FormatVersion && - BitConverter.ToUInt32(content, 2 * sizeof(uint)) == SchemaFingerprint) - { - validEnd = 3 * sizeof(uint); - var offset = (int)validEnd; - while (content.Length - offset >= RecordHeaderBytes + 1) - { - var length = BitConverter.ToUInt32(content, offset); - var checksum = BitConverter.ToUInt64(content, offset + sizeof(uint)); - if (length == 0 || length > content.Length - offset - RecordHeaderBytes) - { - break; - } - - var body = content.AsSpan(offset + RecordHeaderBytes, (int)length); - if (XxHash3.HashToUInt64(body) != checksum || !TryLoadRecord(body[0], body[1..].ToArray())) - { - break; - } - - offset += RecordHeaderBytes + (int)length; - validEnd = offset; - } - } - - if (validEnd == 0) - { - _file.SetLength(0); - _file.Position = 0; - Span header = stackalloc byte[3 * sizeof(uint)]; - BitConverter.TryWriteBytes(header, Magic); - BitConverter.TryWriteBytes(header[sizeof(uint)..], FormatVersion); - BitConverter.TryWriteBytes(header[(2 * sizeof(uint))..], SchemaFingerprint); - _file.Write(header); - _file.Flush(); - return; - } - - _file.SetLength(validEnd); - _file.Position = validEnd; - } - - private bool TryLoadRecord(byte kind, byte[] payload) - { - try - { - switch (kind) - { - case CodeKind: - { - var code = DeserializeCode(payload); - _codes.Add(code.Key); - _loadedCodes[code.Key] = code; - return true; - } - - case ComputeKind: - if (_computes.Add(XxHash3.HashToUInt64(payload))) - { - _loadedComputes.Add(DeserializeCompute(payload)); - } - - return true; - - default: - return false; - } - } - catch (Exception exception) when (exception is EndOfStreamException or ArgumentException or IOException) - { - return false; - } - } - - private static readonly uint SchemaFingerprint = ComputeSchemaFingerprint(); - - private static uint ComputeSchemaFingerprint() - { - var builder = new StringBuilder(); - foreach (var type in new[] - { - typeof(ComputeInputInfo), typeof(Gen5ComputeSystemRegisters), typeof(ResourceSpecialization), - typeof(BufferSpecialization), typeof(ImageSpecialization), typeof(BufferCandidateTableSpecialization), - }) - { - builder.Append(type.FullName).Append('{'); - foreach (var property in type.GetProperties().OrderBy(static property => property.Name, StringComparer.Ordinal)) - { - builder.Append(property.PropertyType.FullName).Append(' ').Append(property.Name).Append(';'); - } - - builder.Append('}'); - } - - return (uint)XxHash3.HashToUInt64(Encoding.UTF8.GetBytes(builder.ToString())); - } - - private static byte[] SerializeCode(ShaderCodeCapture code) - { - using var stream = new MemoryStream(); - using var writer = new BinaryWriter(stream); - 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); - } - - writer.Flush(); - return stream.ToArray(); - } - - private static ShaderCodeCapture DeserializeCode(byte[] payload) - { - using var reader = new BinaryReader(new MemoryStream(payload)); - 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[CheckedCount(reader.ReadInt32(), payload.Length)]; - for (var index = 0; index < ranges.Length; index++) - { - var rangeAddress = reader.ReadUInt64(); - var bytes = reader.ReadBytes(CheckedCount(reader.ReadInt32(), payload.Length)); - ranges[index] = new CodeRange(rangeAddress, bytes); - } - - return new ShaderCodeCapture - { - Hash = hash, - CodeSize = codeSize, - Address = address, - Generation = generation, - Fused = fused, - Ranges = ranges, - }; - } - - private static byte[] SerializeCompute(ComputePrewarmRecord record) - { - using var stream = new MemoryStream(); - using var writer = new BinaryWriter(stream); - writer.Write(record.Hash); - writer.Write(record.CodeSize); - writer.Write(record.Address); - writer.Write(record.UserDataBase); - writer.Write(record.UserDataCount); - writer.Write(record.PushDataCursor); - - var info = record.Info; - 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(record.SystemRegisters is not null); - if (record.SystemRegisters is { } registers) - { - WriteOptional(writer, registers.WorkGroupXRegister); - WriteOptional(writer, registers.WorkGroupYRegister); - WriteOptional(writer, registers.WorkGroupZRegister); - WriteOptional(writer, registers.ThreadGroupSizeRegister); - } - - var specialization = record.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); - } - - writer.Flush(); - return stream.ToArray(); - } - - private static ComputePrewarmRecord DeserializeCompute(byte[] payload) - { - using var reader = new BinaryReader(new MemoryStream(payload)); - 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 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; - - var specialization = new ResourceSpecialization { BaseBufferCount = reader.ReadInt32() }; - for (var count = CheckedCount(reader.ReadInt32(), payload.Length); count > 0; count--) - { - specialization.Buffers.Add(new BufferSpecialization(reader.ReadUInt32(), reader.ReadUInt32(), reader.ReadUInt32())); - } - - for (var count = CheckedCount(reader.ReadInt32(), payload.Length); 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 = CheckedCount(reader.ReadInt32(), payload.Length); count > 0; count--) - { - specialization.BufferCandidateTables.Add(new BufferCandidateTableSpecialization( - reader.ReadUInt32(), reader.ReadUInt32(), reader.ReadUInt32(), reader.ReadUInt32())); - } - - return new ComputePrewarmRecord - { - Hash = hash, - CodeSize = codeSize, - Address = address, - UserDataBase = userDataBase, - UserDataCount = userDataCount, - PushDataCursor = pushDataCursor, - Info = info, - SystemRegisters = registers, - Specialization = specialization, - }; - } - - 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 CheckedCount(int count, int limit) => - count >= 0 && count <= limit ? count : throw new ArgumentException("The prewarm record count is out of range."); -} diff --git a/src/SharpEmu.Libs/Gpu/Pipelines/ShaderProgramCache.cs b/src/SharpEmu.Libs/Gpu/Pipelines/ShaderProgramCache.cs index 611b9c8..6200bfa 100644 --- a/src/SharpEmu.Libs/Gpu/Pipelines/ShaderProgramCache.cs +++ b/src/SharpEmu.Libs/Gpu/Pipelines/ShaderProgramCache.cs @@ -3,6 +3,7 @@ using SharpEmu.HLE; using SharpEmu.Libs.Gpu.Rendering; +using SharpEmu.Libs.Gpu.ShaderCache; using SharpEmu.Libs.Gpu.Scheduling; using SharpEmu.Libs.VideoOut; using SharpEmu.ShaderCompiler; @@ -90,11 +91,33 @@ internal sealed class ProgramSourceEntry public BoundedCopy? BoundedCopy { get; init; } public EmbeddedVertexFetchPlan? EmbeddedFetch { get; init; } public ShaderVertexInput[] VertexInputs { get; init; } = []; + public bool FetchEmbedded { get; init; } + public int FetchAttributeRegister { get; init; } + public int FetchBufferRegister { get; init; } public List Permutations { get; } = new(8); } // Programs keyed by identity and static state; each draw materializes its resources and reuses // the permutation whose specialization and push-data start match, else compiles one more. +internal readonly record struct ShaderCompileHostFlags( + bool GraphicsSubgroups, + bool SharedInt64Atomics, + bool ExecGuardElision, + bool ClipDistance, + bool PerVertexPixelInputs, + bool PortableBuffers, + bool SignedZeroInfNanPreserveFloat32 = false) +{ + public static ShaderCompileHostFlags From(IShaderPipelineHost host) => new( + host.GraphicsSubgroupOperationsEnabled, + host.SharedInt64AtomicsEnabled, + host.ExecGuardElisionEnabled, + host.ClipDistanceEnabled, + host.PerVertexPixelInputsSupported, + host.PortableBufferDescriptors, + host.ShaderSignedZeroInfNanPreserveFloat32Supported); +} + internal sealed class ShaderProgramCache { private const uint MaxInstructionScan = 16384; @@ -105,6 +128,7 @@ internal sealed class ShaderProgramCache private readonly Dictionary _programs = new(); private readonly Dictionary<(ulong Hash, uint CodeSize), Gen5ShaderProgram> _decoded = new(); private readonly Dictionary<(ulong Hash, uint CodeSize), ShaderCodeCapture> _codeCaptures = new(); + private readonly Dictionary _stageRecords = new(); private readonly List _staticState = new(StageStaticKey.MaxWords); // Draws that re-bind unchanged resources reuse the last materialization. // SHARPEMU_RESOURCE_CACHE=0 materializes every draw, for A/B comparisons. @@ -149,7 +173,7 @@ internal sealed class ShaderProgramCache return program; } - var recording = _host.ShaderPrewarm is not null ? new RecordingCpuMemory(_context.Memory) : null; + var recording = _host.ShaderCache is not null ? new RecordingCpuMemory(_context.Memory) : null; var context = recording is null ? _context : new CpuContext(recording, _context.TargetGeneration); if (!Gen5ShaderTranslator.TryDecodeProgram(context, source.Address, out program, out var error)) { @@ -237,6 +261,7 @@ internal sealed class ShaderProgramCache ReadMemory = _readGuestWord, ReadCleanMemory = _readCleanGuestWord, ReadResidentMemory = _prefetchResidentGuestBytes, + PortableBuffers = _host.PortableBufferDescriptors, ComputeState = source.Stage == ShaderStage.Compute && options.ComputeInfo is { } computeState ? new ComputeSelectorState(computeState.WaveSize, Math.Max(computeState.ThreadsX, 1), Math.Max(computeState.ThreadsY, 1), Math.Max(computeState.ThreadsZ, 1), computeState.DispatchThreadDimensions, @@ -341,35 +366,58 @@ internal sealed class ShaderProgramCache var program = Decode(source); var dumpPlanning = CompiledShaderDump.ShouldWrite(source.Address, source.Hash); if (dumpPlanning) ShaderPlanningDump.WriteInput(source, program); - EmbeddedVertexFetchPlan? fetch = null; - ShaderVertexInput[] vertexInputs = []; - if (source.Stage == ShaderStage.Vertex && options.VertexInfo is { FetchEmbedded: true } vertexInfo) - { - fetch = EmbeddedVertexFetchDetector.Detect( - program, - (int)source.UserDataBase + vertexInfo.FetchAttributeRegister, - (int)source.UserDataBase + vertexInfo.FetchBufferRegister, - source.UserDataBase, - (uint)source.UserData.Length, - waveSize: 32); - vertexInputs = BuildVertexInputs(fetch, vertexInfo, source); - program = fetch.RemoveReplacedTableLoads(program); - } - - ShaderResourcePlan plan; + var vertexInfo = source.Stage == ShaderStage.Vertex ? options.VertexInfo : null; try { - plan = ShaderResourcePlan.Extract(program, source.Stage, source.Hash, source.UserDataBase, (uint)source.UserData.Length, - fetch?.Loads.Select(load => load.Pc).ToHashSet(), - beforeResourceTracking: dumpPlanning ? resourcePlan => ShaderPlanningDump.WriteGraph(source, resourcePlan) : null, - // Graphics stages compile as wave32 (see the compile request); compute follows the dispatch. - waveSize: source.Stage == ShaderStage.Compute ? options.ComputeInfo?.WaveSize ?? 64u : 32u); + return BuildEntry( + program, + source.Stage, + source.Hash, + source.UserDataBase, + (uint)source.UserData.Length, + vertexInfo is { FetchEmbedded: true } ? (vertexInfo.FetchAttributeRegister, vertexInfo.FetchBufferRegister) : null, + fetch => BuildVertexInputs(fetch, vertexInfo!, source), + options.ComputeInfo?.WaveSize ?? 64u, + dumpPlanning ? resourcePlan => ShaderPlanningDump.WriteGraph(source, resourcePlan) : null); } catch (ResourcePlanException exception) { if (dumpPlanning) ShaderPlanningDump.WriteFailure(source, exception.Message); throw new ShaderProgramRejectedException($"The shader resource plan is invalid: stage={source.Label} hash=0x{source.Hash:X16} shader=0x{source.Address:X16} error={exception.Message}."); } + } + + private static ProgramSourceEntry BuildEntry( + Gen5ShaderProgram program, + ShaderStage stage, + ulong hash, + uint userDataBase, + uint userDataCount, + (int AttributeRegister, int BufferRegister)? fetchRegisters, + Func buildVertexInputs, + uint computeWaveSize, + Action? beforeResourceTracking) + { + EmbeddedVertexFetchPlan? fetch = null; + ShaderVertexInput[] vertexInputs = []; + if (stage == ShaderStage.Vertex && fetchRegisters is { } registers) + { + fetch = EmbeddedVertexFetchDetector.Detect( + program, + (int)userDataBase + registers.AttributeRegister, + (int)userDataBase + registers.BufferRegister, + userDataBase, + userDataCount, + waveSize: 32); + vertexInputs = buildVertexInputs(fetch); + program = fetch.RemoveReplacedTableLoads(program); + } + + var plan = ShaderResourcePlan.Extract(program, stage, hash, userDataBase, userDataCount, + fetch?.Loads.Select(load => load.Pc).ToHashSet(), + beforeResourceTracking: beforeResourceTracking, + // Graphics stages compile as wave32 (see the compile request); compute follows the dispatch. + waveSize: stage == ShaderStage.Compute ? computeWaveSize : 32u); var exclusiveOr = false; foreach (var instruction in program.Instructions) @@ -382,11 +430,14 @@ internal sealed class ShaderProgramCache Plan = plan, Program = program, HasBitwiseExclusiveOr = exclusiveOr, - ConstantFill = source.Stage == ShaderStage.Compute ? ConstantFillDetector.Detect(program) : null, - BoundedFill = source.Stage == ShaderStage.Compute ? BoundedFillDetector.Detect(program) : null, - BoundedCopy = source.Stage == ShaderStage.Compute ? BoundedFillDetector.DetectCopy(program) : null, + ConstantFill = stage == ShaderStage.Compute ? ConstantFillDetector.Detect(program) : null, + BoundedFill = stage == ShaderStage.Compute ? BoundedFillDetector.Detect(program) : null, + BoundedCopy = stage == ShaderStage.Compute ? BoundedFillDetector.DetectCopy(program) : null, EmbeddedFetch = fetch, VertexInputs = vertexInputs, + FetchEmbedded = fetchRegisters is not null, + FetchAttributeRegister = fetchRegisters?.AttributeRegister ?? 0, + FetchBufferRegister = fetchRegisters?.BufferRegister ?? 0, }; } @@ -513,14 +564,18 @@ internal sealed class ShaderProgramCache { resources = ResourceMaterializer.ApplyTo(plan, specialization); layout = AllocateLayout(program, plan, resources, source.UserDataBase, (uint)source.UserData.Length, pushDataCursor, - source.Stage == ShaderStage.Compute && options.ComputeInfo!.DispatchThreadDimensions); + source.Stage == ShaderStage.Compute && options.ComputeInfo!.DispatchThreadDimensions, _host.PortableBufferDescriptors); } catch (ResourcePlanException exception) { throw SubmissionScheduler.Fatal($"The shader binding layout is invalid: stage={source.Label} hash=0x{source.Hash:X16} error={exception.Message}."); } - var request = BuildRequest(source, options, entry, resources, layout); + var hostFlags = ShaderCompileHostFlags.From(_host); + var vertexInputs = source.Stage == ShaderStage.Vertex ? VertexInputsOf(options, entry) : null; + var pixelInputs = source.Stage == ShaderStage.Pixel ? PixelInputsOf(options) : null; + var request = BuildRequest(source.Stage, entry.Plan, resources, layout, vertexInputs, pixelInputs, + options.ComputeInfo, options.ComputeSystemRegisters, hostFlags); var permutationDump = ShaderPermutationDump.WriteInputs( source, key, sourceWasCached, _programs.Keys, entry.Permutations, specialization, request, pushDataCursor, _nextProgramId + 1); @@ -556,23 +611,32 @@ internal sealed class ShaderProgramCache CompiledShaderDump.Write(source.Label, source.Address, source.Hash, compiled, program); var id = ++_nextProgramId; var module = _host.CreateShaderModule(compiled, source.Stage, source.Hash, id); - var info = CreateProgramInfo(source, entry, resources, layout, request); - if (source.Stage == ShaderStage.Compute && - _host.ShaderPrewarm is { } prewarm && - _codeCaptures.TryGetValue((source.Hash, source.CodeSize), out var capture)) + var info = CreateProgramInfo(source.Stage, source.Hash, source.UserDataBase, (uint)source.UserData.Length, entry, resources, layout, request); + if (_host.ShaderCache is { } shaderCache && _codeCaptures.TryGetValue((source.Hash, source.CodeSize), out var capture)) { - prewarm.RecordCompute(capture, new ComputePrewarmRecord + var record = new StageRecord { + Stage = source.Stage, Hash = source.Hash, CodeSize = source.CodeSize, Address = capture.Address, UserDataBase = source.UserDataBase, UserDataCount = (uint)source.UserData.Length, PushDataCursor = pushDataCursor, - Info = options.ComputeInfo!, - SystemRegisters = options.ComputeSystemRegisters, Specialization = specialization.Clone(), - }); + Compute = source.Stage == ShaderStage.Compute ? options.ComputeInfo : null, + ComputeSystemRegisters = source.Stage == ShaderStage.Compute ? options.ComputeSystemRegisters : null, + Vertex = vertexInputs, + Pixel = pixelInputs, + }; + if (source.Stage == ShaderStage.Compute) + { + shaderCache.RecordCompute(record, capture); + } + else + { + _stageRecords[id] = (record, capture); + } } return new ProgramPermutation @@ -584,74 +648,101 @@ internal sealed class ShaderProgramCache }; } - private ShaderCompileRequest BuildRequest( - ShaderSource source, - StageCompileOptions options, - ProgramSourceEntry entry, - SpecializedResourceInfo resources, - BindingLayout layout) + private static VertexStageInputs VertexInputsOf(StageCompileOptions options, ProgramSourceEntry entry) { - var enableGraphicsSubgroups = _host.GraphicsSubgroupOperationsEnabled; - var sharedInt64Atomics = _host.SharedInt64AtomicsEnabled; - var signedZeroInfNanPreserve = _host.ShaderSignedZeroInfNanPreserveFloat32Supported; - switch (source.Stage) + var info = options.VertexInfo!; + return new VertexStageInputs( + entry.FetchEmbedded, + entry.FetchAttributeRegister, + entry.FetchBufferRegister, + entry.VertexInputs, + info.ScratchDwords, + options.RequiredVertexOutputCount, + info.PositionExportControl, + new ShaderClipSpaceTransform( + info.ClipSpace.Enabled, + info.ClipSpace.ScaleX, + info.ClipSpace.ScaleY, + info.ClipSpace.OffsetX, + info.ClipSpace.OffsetY, + info.ClipSpace.HalfExtentX, + info.ClipSpace.HalfExtentY)); + } + + private static PixelStageInputs PixelInputsOf(StageCompileOptions options) + { + var info = options.PixelInfo!; + var interpolators = new uint[info.InputCount]; + Array.Copy(info.InterpolatorSettings, interpolators, interpolators.Length); + return new PixelStageInputs( + info.ScratchDwords, + options.PixelOutputs.ToArray(), + options.PixelInputEnable, + info.CustomInterpolationMask, + options.PixelInputAddress, + interpolators); + } + + private static ShaderCompileRequest BuildRequest( + ShaderStage stage, + ShaderResourcePlan plan, + SpecializedResourceInfo resources, + BindingLayout layout, + VertexStageInputs? vertex, + PixelStageInputs? pixel, + ComputeInputInfo? compute, + Gen5ComputeSystemRegisters? systemRegisters, + ShaderCompileHostFlags flags) + { + switch (stage) { case ShaderStage.Vertex: { - var info = options.VertexInfo!; - return new ShaderCompileRequest(entry.Plan, resources, layout) + var inputs = vertex!; + return new ShaderCompileRequest(plan, resources, layout) { WaveSize = 32, TraceDeviceAddressFaults = SharpEmu.HLE.GpuMemory.GuestGpuMemoryHook.TraceEnabled, - ScratchDwords = info.ScratchDwords, - EnableGraphicsSubgroupOperations = enableGraphicsSubgroups, - SupportsSharedInt64Atomics = sharedInt64Atomics, - ShaderSignedZeroInfNanPreserveFloat32Supported = signedZeroInfNanPreserve, - RequiredVertexOutputCount = options.RequiredVertexOutputCount, - VertexInputs = entry.VertexInputs, - PositionExportControl = info.PositionExportControl, - SupportsClipDistance = _host.ClipDistanceEnabled, - ClipSpace = new ShaderClipSpaceTransform( - info.ClipSpace.Enabled, - info.ClipSpace.ScaleX, - info.ClipSpace.ScaleY, - info.ClipSpace.OffsetX, - info.ClipSpace.OffsetY, - info.ClipSpace.HalfExtentX, - info.ClipSpace.HalfExtentY), + ScratchDwords = inputs.ScratchDwords, + EnableGraphicsSubgroupOperations = flags.GraphicsSubgroups, + SupportsSharedInt64Atomics = flags.SharedInt64Atomics, + ShaderSignedZeroInfNanPreserveFloat32Supported = flags.SignedZeroInfNanPreserveFloat32, + RequiredVertexOutputCount = inputs.RequiredVertexOutputCount, + VertexInputs = inputs.Inputs, + PositionExportControl = inputs.PositionExportControl, + SupportsClipDistance = flags.ClipDistance, + ClipSpace = inputs.ClipSpace, }; } case ShaderStage.Pixel: { - var info = options.PixelInfo!; - var interpolators = new uint[info.InputCount]; - Array.Copy(info.InterpolatorSettings, interpolators, interpolators.Length); - return new ShaderCompileRequest(entry.Plan, resources, layout) + var inputs = pixel!; + return new ShaderCompileRequest(plan, resources, layout) { WaveSize = 32, TraceDeviceAddressFaults = SharpEmu.HLE.GpuMemory.GuestGpuMemoryHook.TraceEnabled, - ScratchDwords = info.ScratchDwords, - EnableGraphicsSubgroupOperations = enableGraphicsSubgroups, - SupportsSharedInt64Atomics = sharedInt64Atomics, - ShaderSignedZeroInfNanPreserveFloat32Supported = signedZeroInfNanPreserve, - PixelOutputs = options.PixelOutputs, - PixelInputEnable = options.PixelInputEnable, - PixelCustomInterpolationMask = info.CustomInterpolationMask, - SupportsPerVertexPixelInputs = _host.PerVertexPixelInputsSupported, - PixelInputAddress = options.PixelInputAddress, - PixelInputCntl = interpolators, + ScratchDwords = inputs.ScratchDwords, + EnableGraphicsSubgroupOperations = flags.GraphicsSubgroups, + SupportsSharedInt64Atomics = flags.SharedInt64Atomics, + ShaderSignedZeroInfNanPreserveFloat32Supported = flags.SignedZeroInfNanPreserveFloat32, + PixelOutputs = inputs.Outputs, + PixelInputEnable = inputs.InputEnable, + PixelCustomInterpolationMask = inputs.CustomInterpolationMask, + SupportsPerVertexPixelInputs = flags.PerVertexPixelInputs, + PixelInputAddress = inputs.InputAddress, + PixelInputCntl = inputs.InputCntl, }; } default: - return BuildComputeRequest(entry.Plan, resources, layout, options.ComputeInfo!, options.ComputeSystemRegisters, - sharedInt64Atomics, _host.ExecGuardElisionEnabled, signedZeroInfNanPreserve); + return BuildComputeRequest(plan, resources, layout, compute!, systemRegisters, + flags.SharedInt64Atomics, flags.ExecGuardElision, flags.SignedZeroInfNanPreserveFloat32); } } private static BindingLayout AllocateLayout(Gen5ShaderProgram program, ShaderResourcePlan plan, SpecializedResourceInfo resources, - uint userDataBase, uint userDataCount, uint pushDataCursor, bool usesDispatchThreadLimits) => + uint userDataBase, uint userDataCount, uint pushDataCursor, bool usesDispatchThreadLimits, bool portableBuffers) => BindingLayout.Allocate( resources.Info, BindingLayout.CollectUserDataRegisters(program, userDataBase, userDataCount), @@ -659,7 +750,8 @@ internal sealed class ShaderProgramCache ShaderCompileRequest.RequiresFlattenedTable(plan, resources), BindingLayout.ReadsShaderBase(program), pushDataCursor, - usesDispatchThreadLimits: usesDispatchThreadLimits); + usesDispatchThreadLimits: usesDispatchThreadLimits, + portableBuffers: portableBuffers); private static ShaderCompileRequest BuildComputeRequest(ShaderResourcePlan plan, SpecializedResourceInfo resources, BindingLayout layout, ComputeInputInfo info, Gen5ComputeSystemRegisters? systemRegisters, bool sharedInt64Atomics, bool execGuardElision, @@ -679,12 +771,30 @@ internal sealed class ShaderProgramCache LocalSizeZ = Math.Max(info.ThreadsZ, 1), }; - internal static bool TryCompilePrewarm(ComputePrewarmRecord record, ShaderCodeCapture code, IGuestGpuBackend compiler, - bool sharedInt64Atomics, bool execGuardElision, bool signedZeroInfNanPreserve, out IGuestCompiledShader? compiled, - out BindingLayout? layout, out string error) + internal bool TryGetStageRecord(ulong programId, out StageRecord record, out ShaderCodeCapture code) + { + if (_stageRecords.TryGetValue(programId, out var stored)) + { + (record, code) = stored; + return true; + } + + record = null!; + code = null!; + return false; + } + + internal static bool TryReplay( + StageRecord record, + ShaderCodeCapture code, + IGuestGpuBackend compiler, + ShaderCompileHostFlags flags, + out IGuestCompiledShader? compiled, + out ShaderProgramInfo? info, + out string error) { compiled = null; - layout = null; + info = null; try { if (!Gen5ShaderTranslator.TryDecodeProgram(code.CreateContext(), code.Address, out var program, out error)) @@ -692,14 +802,29 @@ internal sealed class ShaderProgramCache return false; } - var plan = ShaderResourcePlan.Extract(program, ShaderStage.Compute, record.Hash, record.UserDataBase, record.UserDataCount, - waveSize: record.Info.WaveSize); - var resources = ResourceMaterializer.ApplyTo(plan, record.Specialization); - layout = AllocateLayout(program, plan, resources, record.UserDataBase, record.UserDataCount, record.PushDataCursor, - record.Info.DispatchThreadDimensions); - var request = BuildComputeRequest(plan, resources, layout, record.Info, record.SystemRegisters, - sharedInt64Atomics, execGuardElision, signedZeroInfNanPreserve); - return compiler.TryCompileProgram(request, out compiled, out error) && compiled is not null; + var vertex = record.Vertex; + var entry = BuildEntry( + program, + record.Stage, + record.Hash, + record.UserDataBase, + record.UserDataCount, + vertex is { FetchEmbedded: true } ? (vertex.FetchAttributeRegister, vertex.FetchBufferRegister) : null, + _ => vertex!.Inputs.ToArray(), + record.Compute?.WaveSize ?? 64u, + null); + var resources = ResourceMaterializer.ApplyTo(entry.Plan, record.Specialization ?? ResourceSpecialization.Predicted(entry.Plan.Info)); + var layout = AllocateLayout(entry.Program, entry.Plan, resources, record.UserDataBase, record.UserDataCount, record.PushDataCursor, + record.Stage == ShaderStage.Compute && record.Compute!.DispatchThreadDimensions, flags.PortableBuffers); + var request = BuildRequest(record.Stage, entry.Plan, resources, layout, vertex, record.Pixel, record.Compute, + record.ComputeSystemRegisters, flags); + if (!compiler.TryCompileProgram(request, out compiled, out error) || compiled is null) + { + return false; + } + + info = CreateProgramInfo(record.Stage, record.Hash, record.UserDataBase, record.UserDataCount, entry, resources, layout, request); + return true; } catch (Exception exception) { @@ -708,6 +833,39 @@ internal sealed class ShaderProgramCache } } + internal static ResourceSpecialization? EffectiveSpecialization(StageRecord record, ShaderCodeCapture code) + { + if (record.Specialization is { } specialization) + { + return specialization; + } + + try + { + if (!Gen5ShaderTranslator.TryDecodeProgram(code.CreateContext(), code.Address, out var program, out _)) + { + return null; + } + + var vertex = record.Vertex; + var entry = BuildEntry( + program, + record.Stage, + record.Hash, + record.UserDataBase, + record.UserDataCount, + vertex is { FetchEmbedded: true } ? (vertex.FetchAttributeRegister, vertex.FetchBufferRegister) : null, + _ => vertex!.Inputs.ToArray(), + record.Compute?.WaveSize ?? 64u, + null); + return ResourceSpecialization.Predicted(entry.Plan.Info); + } + catch (Exception exception) when (exception is ResourcePlanException or InvalidOperationException or ArgumentException) + { + return null; + } + } + // A fill kernel computes its thread index, moves one constant into a VGPR and stores it once. internal static uint? FindConstantStoreValue(Gen5ShaderProgram program) { @@ -790,7 +948,10 @@ internal sealed class ShaderProgramCache } private static ShaderProgramInfo CreateProgramInfo( - ShaderSource source, + ShaderStage stage, + ulong hash, + uint userDataBase, + uint userDataCount, ProgramSourceEntry entry, SpecializedResourceInfo resources, BindingLayout layout, @@ -828,15 +989,15 @@ internal sealed class ShaderProgramCache return new ShaderProgramInfo { - Stage = source.Stage switch + Stage = stage switch { ShaderStage.Vertex => ShaderStageKind.Vertex, ShaderStage.Pixel => ShaderStageKind.Pixel, _ => ShaderStageKind.Compute, }, - Hash = source.Hash, - UserDataBase = source.UserDataBase, - UserDataCount = (uint)source.UserData.Length, + Hash = hash, + UserDataBase = userDataBase, + UserDataCount = userDataCount, ParameterExportMask = entry.Program.ParameterExportMask, PixelColorExportMasks = entry.Program.PixelColorExportMasks, VertexOffsetScalarRegister = entry.EmbeddedFetch?.VertexOffsetScalarRegister ?? ShaderProgramInfo.NoScalarRegister, diff --git a/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.Compute.cs b/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.Compute.cs index bf29ca8..63de853 100644 --- a/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.Compute.cs +++ b/src/SharpEmu.Libs/Gpu/Rendering/RenderExecutor.Compute.cs @@ -384,7 +384,7 @@ public sealed partial class RenderExecutor var descriptor = BufferDescriptorWords.From(words); if (!resource.Formatted || !resource.Written || resource.Read || resource.Atomic || resource.Scalar || resource.MaxByteExtent != ImageClearStride || descriptor.Stride != ImageClearStride || descriptor.Format != BufferDescriptorWords.Format32x4UInt || descriptor.SwizzleEnabled || - descriptor.IndexStride != 0 || descriptor.AddThreadId || resource.PackedStride != descriptor.PackedStride || + descriptor.IndexStride != 0 || descriptor.AddThreadId || program.UserDataBase != 0 || resources.UserData.Length != ImageClearUserDataCount) { return null; diff --git a/src/SharpEmu.Libs/VideoOut/ShaderPrecompiler.cs b/src/SharpEmu.Libs/VideoOut/ShaderPrecompiler.cs new file mode 100644 index 0000000..5556947 --- /dev/null +++ b/src/SharpEmu.Libs/VideoOut/ShaderPrecompiler.cs @@ -0,0 +1,13 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +namespace SharpEmu.Libs.VideoOut; + +public static class ShaderPrecompiler +{ + public const string WorkerFlag = VulkanVideoPresenter.ShaderCacheWorkerFlag; + + public static int RunWorker(IReadOnlyList arguments) => VulkanVideoPresenter.RunShaderCacheWorker(arguments); + + public static int Run(string app0Root, bool full) => VulkanVideoPresenter.RunShaderPrecompile(app0Root, full); +} diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.CommandStream.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.CommandStream.cs index 36e741d..e37a674 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.CommandStream.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.CommandStream.cs @@ -40,7 +40,7 @@ internal static unsafe partial class VulkanVideoPresenter } EnsureStarted(1280, 720); - WaitForShaderPrewarm("first GPU submission"); + WaitForShaderCache("first GPU submission"); // Queue publication precedes device initialization; only the render thread consumes it. lock (_gate) { @@ -138,7 +138,7 @@ internal static unsafe partial class VulkanVideoPresenter _ = width; _ = height; _ = pitchInPixel; - WaitForShaderPrewarm("first flip"); + WaitForShaderCache("first flip"); if (!IsKnownDisplayBuffer(address) || !TryGetActivePresenter(out var presenter)) { return false; diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Device.Setup.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Device.Setup.cs index 9d0d1d0..190ff17 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Device.Setup.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Device.Setup.cs @@ -93,7 +93,7 @@ internal static unsafe partial class VulkanVideoPresenter AttachGuestGpuMemory(); Console.Error.WriteLine( $"[LOADER][INFO] Vulkan VideoOut ready: {_extent.Width}x{_extent.Height}, format={_swapchainFormat}"); - StartShaderPrewarm(); + StartShaderCache(); } private static void WaitForRenderDocAttachIfRequested() @@ -867,6 +867,8 @@ internal static unsafe partial class VulkanVideoPresenter supportsMaintenance5 = maintenance5Features.Maintenance5; } + _supportsPipelineBinaries = QueryPipelineBinarySupport(supportsMaintenance5); + var imageViewMinLodFeatures = new PhysicalDeviceImageViewMinLodFeaturesEXT { SType = StructureType.PhysicalDeviceImageViewMinLodFeaturesExt, @@ -992,9 +994,10 @@ internal static unsafe partial class VulkanVideoPresenter var maintenance5Extension = (byte*)SilkMarshal.StringToPtr(Maintenance5ExtensionName); var imageViewMinLodExtension = (byte*)SilkMarshal.StringToPtr(ImageViewMinLodExtensionName); var fillRectangleExtension = (byte*)SilkMarshal.StringToPtr(FillRectangleExtensionName); + var pipelineBinaryExtension = (byte*)SilkMarshal.StringToPtr(PipelineBinaryExtensionName); try { - var extensions = stackalloc byte*[15]; + var extensions = stackalloc byte*[16]; var extensionCount = 0u; extensions[extensionCount++] = swapchainExtension; extensions[extensionCount++] = pushDescriptorExtension; @@ -1031,6 +1034,11 @@ internal static unsafe partial class VulkanVideoPresenter extensions[extensionCount++] = maintenance5Extension; } + if (_supportsPipelineBinaries) + { + extensions[extensionCount++] = pipelineBinaryExtension; + } + if (_supportsImageViewMinLod) { extensions[extensionCount++] = imageViewMinLodExtension; @@ -1109,6 +1117,17 @@ internal static unsafe partial class VulkanVideoPresenter }; renderingChain = &maintenance5Features; } + + var pipelineBinaryFeatures = new PhysicalDevicePipelineBinaryFeaturesKHR + { + SType = StructureType.PhysicalDevicePipelineBinaryFeaturesKhr, + PipelineBinaries = true, + PNext = renderingChain, + }; + if (_supportsPipelineBinaries) + { + renderingChain = &pipelineBinaryFeatures; + } if (_supportsImageViewMinLod) { imageViewMinLodFeatures = new PhysicalDeviceImageViewMinLodFeaturesEXT @@ -1185,6 +1204,7 @@ internal static unsafe partial class VulkanVideoPresenter SilkMarshal.Free((nint)maintenance5Extension); SilkMarshal.Free((nint)imageViewMinLodExtension); SilkMarshal.Free((nint)fillRectangleExtension); + SilkMarshal.Free((nint)pipelineBinaryExtension); SilkMarshal.Free((nint)robustness2Extension); SilkMarshal.Free((nint)portabilitySubsetExtension); SilkMarshal.Free((nint)colorWriteEnableExtension); @@ -1196,7 +1216,14 @@ internal static unsafe partial class VulkanVideoPresenter } _vk.GetDeviceQueue(_device, _queueFamilyIndex, 0, out _queue); + InitializePipelineBinaries(); _deviceInfo = new GpuDeviceInfo(_vk, _physicalDevice, _device) { ImageViewMinLodSupported = _supportsImageViewMinLod }; + if (_shaderCompileOnly) + { + LoadRenderingCommands(supportsColorWriteEnable, deviceName); + return; + } + if (_readbackQueueFamilyIndex is { } readbackQueueFamily) { _vk.GetDeviceQueue(_device, readbackQueueFamily, 0, out _readbackQueue); diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Device.Teardown.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Device.Teardown.cs index cb8bbdb..045ba7a 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Device.Teardown.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Device.Teardown.cs @@ -26,8 +26,9 @@ internal static unsafe partial class VulkanVideoPresenter _vk.DeviceWaitIdle(_device); } - StopShaderPrewarm(); + StopPipelineOptimization(); DrainPendingComputePipelines(); + StopShaderCache(); SavePipelineCache(force: true); DrainFrameSlots(); CollectCompletedGuestSubmissions(waitForOldest: false); diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.PipelineBinaries.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.PipelineBinaries.cs new file mode 100644 index 0000000..0acd49e --- /dev/null +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.PipelineBinaries.cs @@ -0,0 +1,435 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +namespace SharpEmu.Libs.VideoOut; + +using System.IO.Hashing; +using System.Runtime.InteropServices; +using SharpEmu.Libs.Gpu.ShaderCache; +using Silk.NET.Vulkan; + +internal static unsafe partial class VulkanVideoPresenter +{ + private const string PipelineBinaryExtensionName = "VK_KHR_pipeline_binary"; + private const StructureType PipelineCreateInfoKhrType = (StructureType)1000483007; + private const PipelineCreateFlags2 PipelineCaptureDataFlag = (PipelineCreateFlags2)0x8000_0000UL; + private const uint MaxPipelineBinaries = 64; + private static ReadOnlySpan NvidiaDictionaryTag => "_NVDICT_"u8; + + [StructLayout(LayoutKind.Sequential)] + private struct PipelineCreateInfoKhr + { + public StructureType SType; + public void* PNext; + } + + [StructLayout(LayoutKind.Sequential)] + private struct PipelineCreateInfoHeader + { + public StructureType SType; + public void* PNext; + public PipelineCreateFlags Flags; + } + + private sealed class PipelineBinaryApi + { + public required Device Device; + public required byte[] DriverKey; + public delegate* unmanaged GetPipelineKey; + public delegate* unmanaged CreatePipelineBinaries; + public delegate* unmanaged DestroyPipelineBinary; + public delegate* unmanaged GetPipelineBinaryData; + public delegate* unmanaged ReleaseCapturedPipelineData; + } + + private sealed partial class Presenter + { + private bool _supportsPipelineBinaries; + private PipelineBinaryApi? _pipelineBinaries; + private ShaderCacheFile[] _pipelineStoreSources = []; + private ShaderCacheFile? _pipelineStoreTarget; + private volatile bool _pipelineBinaryDictionaryReached; + + private bool UsesPipelineStore => _pipelineStoreTarget is not null; + + private void SetPipelineStore(ShaderCacheFile? target, params ShaderCacheFile[] sources) + { + _pipelineStoreSources = sources; + Volatile.Write(ref _pipelineStoreTarget, target); + } + + private bool HasStoredPipeline(void* createInfo) + { + if (_pipelineBinaries is not { } api || !TryGetPipelineStoreKey(api, createInfo, out var key)) + { + return false; + } + + foreach (var source in _pipelineStoreSources) + { + if (source.HasBinary(api.DriverKey, key)) + { + return true; + } + } + + return false; + } + + private static bool TryGetPipelineStoreKey(PipelineBinaryApi api, void* createInfo, out UInt128 key) + { + key = default; + var info = new PipelineCreateInfoKhr { SType = PipelineCreateInfoKhrType, PNext = createInfo }; + var driverKey = new PipelineBinaryKeyKHR { SType = StructureType.PipelineBinaryKeyKhr }; + if (api.GetPipelineKey(api.Device, &info, &driverKey) != Result.Success || driverKey.KeySize is 0 or > 32) + { + return false; + } + + key = XxHash128.HashToUInt128(new ReadOnlySpan(driverKey.Key, (int)driverKey.KeySize)); + _itemContentKeys?.Add(key); + return true; + } + + private bool QueryPipelineBinarySupport(bool supportsMaintenance5) + { + if (!ShaderCacheSettings.Enabled || !supportsMaintenance5 || !IsDeviceExtensionAvailable(PipelineBinaryExtensionName)) + { + return false; + } + + var features = new PhysicalDevicePipelineBinaryFeaturesKHR { SType = StructureType.PhysicalDevicePipelineBinaryFeaturesKhr }; + var query = new PhysicalDeviceFeatures2 { SType = StructureType.PhysicalDeviceFeatures2, PNext = &features }; + _vk.GetPhysicalDeviceFeatures2(_physicalDevice, &query); + return features.PipelineBinaries; + } + + private void InitializePipelineBinaries() + { + if (!_supportsPipelineBinaries) + { + return; + } + + nint Load(string name) + { + var pointer = (nint)_vk.GetDeviceProcAddr(_device, name); + return pointer != 0 ? pointer : throw new InvalidOperationException($"{name} is unavailable."); + } + + try + { + var api = new PipelineBinaryApi + { + Device = _device, + DriverKey = [], + GetPipelineKey = (delegate* unmanaged)Load("vkGetPipelineKeyKHR"), + CreatePipelineBinaries = (delegate* unmanaged)Load("vkCreatePipelineBinariesKHR"), + DestroyPipelineBinary = (delegate* unmanaged)Load("vkDestroyPipelineBinaryKHR"), + GetPipelineBinaryData = (delegate* unmanaged)Load("vkGetPipelineBinaryDataKHR"), + ReleaseCapturedPipelineData = (delegate* unmanaged)Load("vkReleaseCapturedPipelineDataKHR"), + }; + var globalKey = new PipelineBinaryKeyKHR { SType = StructureType.PipelineBinaryKeyKhr }; + if (api.GetPipelineKey(_device, null, &globalKey) != Result.Success || globalKey.KeySize is 0 or > 32) + { + throw new InvalidOperationException("vkGetPipelineKeyKHR returned no global key."); + } + + _pipelineBinaries = new PipelineBinaryApi + { + Device = api.Device, + DriverKey = new ReadOnlySpan(globalKey.Key, (int)globalKey.KeySize).ToArray(), + GetPipelineKey = api.GetPipelineKey, + CreatePipelineBinaries = api.CreatePipelineBinaries, + DestroyPipelineBinary = api.DestroyPipelineBinary, + GetPipelineBinaryData = api.GetPipelineBinaryData, + ReleaseCapturedPipelineData = api.ReleaseCapturedPipelineData, + }; + } + catch (InvalidOperationException exception) + { + Console.Error.WriteLine($"[SHADER CACHE][WARN] Vulkan pipeline binaries are unavailable: {exception.Message}"); + } + } + + private Result CreatePipelineObject(bool graphics, void* createInfo, PipelineCache cache, out Pipeline pipeline) => + graphics + ? _vk.CreateGraphicsPipelines(_device, cache, 1, (GraphicsPipelineCreateInfo*)createInfo, null, out pipeline) + : _vk.CreateComputePipelines(_device, cache, 1, (ComputePipelineCreateInfo*)createInfo, null, out pipeline); + + private Result CreateCachedPipelineObject(bool graphics, void* createInfo, out Pipeline pipeline, out bool unoptimized, bool fast) + { + unoptimized = false; + var contentKey = default(UInt128); + if (_pipelineBinaries is not { } api || Volatile.Read(ref _pipelineStoreTarget) is not { } file || + !TryGetPipelineStoreKey(api, createInfo, out contentKey)) + { + if (fast && TryCreateFastPipeline(graphics, createInfo, out pipeline)) + { + unoptimized = true; + Interlocked.Increment(ref _shaderCacheCompiled); + Interlocked.Increment(ref _shaderCacheUnoptimized); + return Result.Success; + } + + Interlocked.Increment(ref _shaderCacheCompiled); + return CreatePipelineObject(graphics, createInfo, _pipelineCache, out pipeline); + } + + foreach (var source in _pipelineStoreSources) + { + if (TryCreatePipelineFromBinaries(api, source, graphics, createInfo, contentKey, out pipeline)) + { + Interlocked.Increment(ref _shaderCacheFromBinaries); + return Result.Success; + } + } + + if (fast && TryCreateFastPipeline(graphics, createInfo, out pipeline)) + { + unoptimized = true; + Interlocked.Increment(ref _shaderCacheCompiled); + Interlocked.Increment(ref _shaderCacheUnoptimized); + return Result.Success; + } + + var header = (PipelineCreateInfoHeader*)createInfo; + var original = header->PNext; + var flags = new PipelineCreateFlags2CreateInfoKHR + { + SType = StructureType.PipelineCreateFlags2CreateInfoKhr, + PNext = original, + Flags = (PipelineCreateFlags2)(ulong)header->Flags | PipelineCaptureDataFlag, + }; + header->PNext = &flags; + var result = CreatePipelineObject(graphics, createInfo, default, out pipeline); + header->PNext = original; + if (result == Result.Success) + { + Interlocked.Increment(ref _shaderCacheCompiled); + StorePipelineBinaries(api, file, contentKey, pipeline); + return result; + } + + return CreatePipelineObject(graphics, createInfo, _pipelineCache, out pipeline); + } + + private bool TryCreateFastPipeline(bool graphics, void* createInfo, out Pipeline pipeline) + { + var header = (PipelineCreateInfoHeader*)createInfo; + var originalFlags = header->Flags; + header->Flags |= PipelineCreateFlags.CreateDisableOptimizationBit; + try + { + var result = CreatePipelineObject(graphics, createInfo, default, out pipeline); + if (result == Result.Success) + { + return true; + } + + if (pipeline.Handle != 0) + { + _vk.DestroyPipeline(_device, pipeline, null); + pipeline = default; + } + return false; + } + finally + { + header->Flags = originalFlags; + } + } + + private bool TryCreatePipelineFromBinaries( + PipelineBinaryApi api, + ShaderCacheFile file, + bool graphics, + void* createInfo, + UInt128 contentKey, + out Pipeline pipeline) + { + pipeline = default; + if (!file.TryReadBinary(api.DriverKey, contentKey, out var parts) || parts.Length > MaxPipelineBinaries) + { + return false; + } + + var keys = new PipelineBinaryKeyKHR[parts.Length]; + var data = new PipelineBinaryDataKHR[parts.Length]; + var binaries = new PipelineBinaryKHR[parts.Length]; + var pins = new GCHandle[parts.Length]; + try + { + for (var index = 0; index < parts.Length; index++) + { + var part = parts[index]; + if (part.Key.Length is 0 or > 32 || part.Data.Length == 0) + { + return false; + } + + keys[index] = new PipelineBinaryKeyKHR { SType = StructureType.PipelineBinaryKeyKhr, KeySize = (uint)part.Key.Length }; + fixed (byte* destination = keys[index].Key) + { + part.Key.CopyTo(new Span(destination, 32)); + } + + pins[index] = GCHandle.Alloc(part.Data, GCHandleType.Pinned); + data[index] = new PipelineBinaryDataKHR + { + DataSize = (nuint)part.Data.Length, + PData = (void*)pins[index].AddrOfPinnedObject(), + }; + } + + fixed (PipelineBinaryKeyKHR* keyPointer = keys) + fixed (PipelineBinaryDataKHR* dataPointer = data) + fixed (PipelineBinaryKHR* binaryPointer = binaries) + { + var keysAndData = new PipelineBinaryKeysAndDataKHR + { + BinaryCount = (uint)parts.Length, + PPipelineBinaryKeys = keyPointer, + PPipelineBinaryData = dataPointer, + }; + var create = new PipelineBinaryCreateInfoKHR + { + SType = StructureType.PipelineBinaryCreateInfoKhr, + PKeysAndDataInfo = &keysAndData, + }; + var handles = new PipelineBinaryHandlesInfoKHR + { + SType = StructureType.PipelineBinaryHandlesInfoKhr, + PipelineBinaryCount = (uint)parts.Length, + PPipelineBinaries = binaryPointer, + }; + if (api.CreatePipelineBinaries(api.Device, &create, null, &handles) != Result.Success) + { + Interlocked.Increment(ref _shaderCacheBinaryRejected); + return false; + } + + var header = (PipelineCreateInfoHeader*)createInfo; + var original = header->PNext; + var binaryInfo = new PipelineBinaryInfoKHR + { + SType = StructureType.PipelineBinaryInfoKhr, + PNext = original, + BinaryCount = (uint)parts.Length, + PPipelineBinaries = binaryPointer, + }; + header->PNext = &binaryInfo; + var result = CreatePipelineObject(graphics, createInfo, default, out pipeline); + header->PNext = original; + if (result != Result.Success) + { + Interlocked.Increment(ref _shaderCacheBinaryRejected); + return false; + } + + return true; + } + } + finally + { + foreach (var binary in binaries) + { + if (binary.Handle != 0) + { + api.DestroyPipelineBinary(api.Device, binary, null); + } + } + + foreach (var pin in pins) + { + if (pin.IsAllocated) + { + pin.Free(); + } + } + } + } + + private void StorePipelineBinaries(PipelineBinaryApi api, ShaderCacheFile file, UInt128 contentKey, Pipeline pipeline) + { + var create = new PipelineBinaryCreateInfoKHR + { + SType = StructureType.PipelineBinaryCreateInfoKhr, + Pipeline = pipeline, + }; + var handles = new PipelineBinaryHandlesInfoKHR { SType = StructureType.PipelineBinaryHandlesInfoKhr }; + PipelineBinaryKHR[] binaries = []; + try + { + if (api.CreatePipelineBinaries(api.Device, &create, null, &handles) != Result.Success || + handles.PipelineBinaryCount is 0 or > MaxPipelineBinaries) + { + return; + } + + binaries = new PipelineBinaryKHR[handles.PipelineBinaryCount]; + fixed (PipelineBinaryKHR* binaryPointer = binaries) + { + handles.PPipelineBinaries = binaryPointer; + if (api.CreatePipelineBinaries(api.Device, &create, null, &handles) != Result.Success) + { + return; + } + } + + var parts = new PipelineBinaryPart[binaries.Length]; + for (var index = 0; index < binaries.Length; index++) + { + var info = new PipelineBinaryDataInfoKHR + { + SType = StructureType.PipelineBinaryDataInfoKhr, + PipelineBinary = binaries[index], + }; + var key = new PipelineBinaryKeyKHR { SType = StructureType.PipelineBinaryKeyKhr }; + nuint size = 0; + if (api.GetPipelineBinaryData(api.Device, &info, &key, &size, null) != Result.Success || size == 0 || size > int.MaxValue) + { + return; + } + + var bytes = new byte[(int)size]; + fixed (byte* bytePointer = bytes) + { + if (api.GetPipelineBinaryData(api.Device, &info, &key, &size, bytePointer) != Result.Success) + { + return; + } + } + + parts[index] = new PipelineBinaryPart(new ReadOnlySpan(key.Key, (int)Math.Min(key.KeySize, 32u)).ToArray(), bytes); + } + + if (parts.Any(static part => part.Key.AsSpan().IndexOf(NvidiaDictionaryTag) >= 0)) + { + _pipelineBinaryDictionaryReached = true; + _lastCaptureReachedDictionary = true; + return; + } + + file.AddBinary(api.DriverKey, contentKey, parts); + } + finally + { + foreach (var binary in binaries) + { + if (binary.Handle != 0) + { + api.DestroyPipelineBinary(api.Device, binary, null); + } + } + + var release = new ReleaseCapturedPipelineDataInfoKHR + { + SType = StructureType.ReleaseCapturedPipelineDataInfoKhr, + Pipeline = pipeline, + }; + api.ReleaseCapturedPipelineData(api.Device, &release, null); + } + } + } +} diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Pipelines.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Pipelines.cs index 3b0e354..485b2a6 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Pipelines.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Pipelines.cs @@ -9,6 +9,7 @@ using SharpEmu.Libs.Gpu; using SharpEmu.Libs.Gpu.Images; using SharpEmu.Libs.Gpu.Pipelines; using SharpEmu.Libs.Gpu.Rendering; +using SharpEmu.Libs.Gpu.ShaderCache; using SharpEmu.Libs.Gpu.Scheduling; using SharpEmu.ShaderCompiler; using SharpEmu.ShaderCompiler.Resources; @@ -93,6 +94,7 @@ internal static unsafe partial class VulkanVideoPresenter bool IShaderPipelineHost.ExecGuardElisionEnabled => _physicalDeviceVendorId != NvidiaVendorId; bool IShaderPipelineHost.PerVertexPixelInputsSupported => _supportsPerVertexPixelInputs; bool IShaderPipelineHost.ClipDistanceEnabled => _supportsShaderClipDistance; + bool IShaderPipelineHost.PortableBufferDescriptors => true; RenderHostLimits IShaderPipelineHost.Limits => _renderHostLimits; @@ -294,12 +296,7 @@ internal static unsafe partial class VulkanVideoPresenter SetDebugName(ObjectType.ShaderModule, module.Handle, $"SharpEmu {stage} 0x{hash:X16}"); _shaderModules.Add(programId, module); _shaderModuleSpirvBytes[module.Handle] = shader.Payload.Length; - var identity = VulkanPipelineCacheStorage.CompiledShaderIdentity(shader.Payload); - _shaderModuleCacheIdentities[module.Handle] = identity; - if (stage == ShaderStage.Compute) - { - NoteRuntimeComputeModule(identity); - } + _shaderModuleCacheIdentities[module.Handle] = VulkanPipelineCacheStorage.CompiledShaderIdentity(shader.Payload); return module.Handle; } @@ -312,7 +309,7 @@ internal static unsafe partial class VulkanVideoPresenter private int SpirvBytesOf(ulong moduleHandle) => _shaderModuleSpirvBytes.TryGetValue(moduleHandle, out var bytes) ? bytes : -1; - private void ReportPipelineCreation(long elapsedMilliseconds, string kind, string stages, string spirv) + private void ReportPipelineCreation(long elapsedMilliseconds, string kind, string stages, string spirv, bool unoptimized = false) { Interlocked.Add(ref _pipelineCreationMilliseconds, elapsedMilliseconds); if (elapsedMilliseconds < SlowPipelineCreationMilliseconds) @@ -322,7 +319,8 @@ internal static unsafe partial class VulkanVideoPresenter Console.Error.WriteLine( $"[GPU][WARN] Slow pipeline creation: kind={kind} {stages} spirv_bytes={spirv} " + - $"ms={elapsedMilliseconds} total_s={Interlocked.Read(ref _pipelineCreationMilliseconds) / 1000.0:F1}"); + $"ms={elapsedMilliseconds}{(unoptimized ? " unoptimized" : string.Empty)} " + + $"total_s={Interlocked.Read(ref _pipelineCreationMilliseconds) / 1000.0:F1}"); } private void CreateBarycentricPipeline() @@ -583,11 +581,12 @@ internal static unsafe partial class VulkanVideoPresenter } var setLayout = CreateDescriptorSetLayout(bindings, out var usesPushDescriptors, out var demand); + const ShaderStageFlags graphicsStages = ShaderStageFlags.VertexBit | ShaderStageFlags.FragmentBit; var entry = new RenderPipelineEntry { SetLayout = setLayout, Demand = demand, - Layout = CreatePipelineLayout(setLayout, ShaderStageFlags.VertexBit | ShaderStageFlags.FragmentBit), + Layout = CreatePipelineLayout(setLayout, graphicsStages), UsesPushDescriptors = usesPushDescriptors, Description = description, ProfileVertexHash = description.VertexStage.Hash, @@ -728,7 +727,7 @@ internal static unsafe partial class VulkanVideoPresenter // One graphics pipeline for dynamic rendering: the attachment formats travel in the create info. private Pipeline CreateRenderPipeline(GraphicsPipelineDescription description, PrimitiveTopology topology, PipelineLayout layout, - PolygonMode polygonMode = PolygonMode.Fill) + PolygonMode polygonMode = PolygonMode.Fill, bool precompile = false, bool optimize = false) { var parameters = description.StaticParameters; var rendering = description.Rendering; @@ -921,11 +920,38 @@ internal static unsafe partial class VulkanVideoPresenter PDynamicState = &dynamicState, Layout = layout, }; + if (precompile || optimize) + { + if (precompile && HasStoredPipeline(&pipelineInfo)) + { + return default; + } + + Check(CreateCachedPipelineObject(true, &pipelineInfo, out var prepared, out _, fast: false), + $"vkCreateGraphicsPipelines(precompile vs=0x{description.VertexStage.Hash:X16} ps=0x{description.PixelStage?.Hash ?? 0:X16})"); + return prepared; + } + var graphicsStart = Stopwatch.GetTimestamp(); - var cache = GetGuestPipelineCache(GraphicsCacheKey(description.VertexStage.Hash, - description.PixelStage?.Hash ?? 0, vertexModule.Handle, pixelModule.Handle)); - Check(_vk.CreateGraphicsPipelines(_device, cache, 1, &pipelineInfo, null, out var pipeline), - $"vkCreateGraphicsPipelines(rendering vs=0x{description.VertexStage.Hash:X16} ps=0x{description.PixelStage?.Hash ?? 0:X16})"); + Pipeline pipeline; + var unoptimized = false; + if (UsesPipelineStore || FastPipelineBuildEnabled) + { + Check(CreateCachedPipelineObject(true, &pipelineInfo, out pipeline, out unoptimized, FastPipelineBuildEnabled), + $"vkCreateGraphicsPipelines(rendering vs=0x{description.VertexStage.Hash:X16} ps=0x{description.PixelStage?.Hash ?? 0:X16})"); + if (unoptimized) + { + QueuePipelineOptimization(pipeline, () => CreateRenderPipeline(description, topology, layout, polygonMode, optimize: true)); + } + } + else + { + var cache = GetGuestPipelineCache(GraphicsCacheKey(description.VertexStage.Hash, + description.PixelStage?.Hash ?? 0, vertexModule.Handle, pixelModule.Handle)); + Check(_vk.CreateGraphicsPipelines(_device, cache, 1, &pipelineInfo, null, out pipeline), + $"vkCreateGraphicsPipelines(rendering vs=0x{description.VertexStage.Hash:X16} ps=0x{description.PixelStage?.Hash ?? 0:X16})"); + } + ReportPipelineCreation( (long)Stopwatch.GetElapsedTime(graphicsStart).TotalMilliseconds, "graphics", @@ -934,7 +960,8 @@ internal static unsafe partial class VulkanVideoPresenter '/', Enumerable .Range(0, (int)stageCount) - .Select(index => SpirvBytesOf(shaderStages[index].Module.Handle)))); + .Select(index => SpirvBytesOf(shaderStages[index].Module.Handle))), + unoptimized); MarkPipelineCacheDirty(); Interlocked.Increment(ref _perfPipelineCreations); SetDebugName( @@ -963,37 +990,36 @@ internal static unsafe partial class VulkanVideoPresenter throw SubmissionScheduler.Fatal($"The compute pipeline has no module: hash=0x{description.Stage.Hash:X16}."); } + var requireSubgroup32 = RequiresComputeSubgroup32(description.Input); var entryPoint = (byte*)SilkMarshal.StringToPtr("main"); Pipeline pipeline; try { - var requiredSubgroupSize = new PipelineShaderStageRequiredSubgroupSizeCreateInfo - { - SType = StructureType.PipelineShaderStageRequiredSubgroupSizeCreateInfo, - RequiredSubgroupSize = RdnaSubgroupSize, - }; - var stageInfo = new PipelineShaderStageCreateInfo - { - SType = StructureType.PipelineShaderStageCreateInfo, - PNext = RequiresComputeSubgroup32(description.Input) ? &requiredSubgroupSize : null, - Stage = ShaderStageFlags.ComputeBit, - Module = computeModule, - PName = entryPoint, - }; - var pipelineInfo = new ComputePipelineCreateInfo - { - SType = StructureType.ComputePipelineCreateInfo, - Stage = stageInfo, - Layout = layout, - }; + var requiredSubgroupSize = RequiredSubgroupSize(); + var pipelineInfo = ComputeCreateInfo(computeModule, layout, entryPoint, requireSubgroup32 ? &requiredSubgroupSize : null); var computeStart = Stopwatch.GetTimestamp(); - var cache = GetGuestPipelineCache(ComputeCacheKey(description.Stage.Hash, computeModule.Handle)); - Check(_vk.CreateComputePipelines(_device, cache, 1, &pipelineInfo, null, out pipeline), $"vkCreateComputePipelines(rendering) hash=0x{description.Stage.Hash:X16}"); + var unoptimized = false; + if (UsesPipelineStore || FastPipelineBuildEnabled) + { + Check(CreateCachedPipelineObject(false, &pipelineInfo, out pipeline, out unoptimized, FastPipelineBuildEnabled), + $"vkCreateComputePipelines(rendering) hash=0x{description.Stage.Hash:X16}"); + if (unoptimized) + { + QueuePipelineOptimization(pipeline, () => CompileStoredComputePipeline(computeModule, layout, requireSubgroup32)); + } + } + else + { + var cache = GetGuestPipelineCache(ComputeCacheKey(description.Stage.Hash, computeModule.Handle)); + Check(_vk.CreateComputePipelines(_device, cache, 1, &pipelineInfo, null, out pipeline), $"vkCreateComputePipelines(rendering) hash=0x{description.Stage.Hash:X16}"); + } + ReportPipelineCreation( (long)Stopwatch.GetElapsedTime(computeStart).TotalMilliseconds, "compute", $"cs=0x{description.Stage.Hash:X16}", - SpirvBytesOf(computeModule.Handle).ToString()); + SpirvBytesOf(computeModule.Handle).ToString(), + unoptimized); MarkPipelineCacheDirty(); Interlocked.Increment(ref _perfPipelineCreations); SetDebugName(ObjectType.Pipeline, pipeline.Handle, $"SharpEmu compute cs=0x{description.Stage.Hash:X16}"); @@ -1096,6 +1122,8 @@ internal static unsafe partial class VulkanVideoPresenter throw SubmissionScheduler.Fatal($"The compute pipeline has no module: hash=0x{description.Stage.Hash:X16}."); } + var requireSubgroup32 = RequiresComputeSubgroup32(description.Input); + var usesStore = UsesPipelineStore; var device = _device; var cacheSource = GetGuestPipelineCacheSource(ComputeCacheKey(description.Stage.Hash, computeModule.Handle)); var vk = _vk; @@ -1121,8 +1149,13 @@ internal static unsafe partial class VulkanVideoPresenter { // Importing a MoltenVK cache compiles its MSL libraries. // Keep that work inside the same bounded compiler slot. + if (usesStore || FastPipelineBuildEnabled) + { + return CompileStoredComputePipeline(computeModule, layout, requireSubgroup32, FastPipelineBuildEnabled); + } + var cache = ResolveGuestPipelineCache(cacheSource); - return CompileComputePipeline(vk, device, cache, computeModule, layout); + return CompileComputePipeline(vk, device, cache, computeModule, layout, requireSubgroup32); } finally { @@ -1146,24 +1179,14 @@ internal static unsafe partial class VulkanVideoPresenter Device device, PipelineCache cache, ShaderModule module, - PipelineLayout layout) + PipelineLayout layout, + bool requireSubgroup32) { var entryPoint = (byte*)SilkMarshal.StringToPtr("main"); try { - var stageInfo = new PipelineShaderStageCreateInfo - { - SType = StructureType.PipelineShaderStageCreateInfo, - Stage = ShaderStageFlags.ComputeBit, - Module = module, - PName = entryPoint, - }; - var pipelineInfo = new ComputePipelineCreateInfo - { - SType = StructureType.ComputePipelineCreateInfo, - Stage = stageInfo, - Layout = layout, - }; + var requiredSubgroupSize = RequiredSubgroupSize(); + var pipelineInfo = ComputeCreateInfo(module, layout, entryPoint, requireSubgroup32 ? &requiredSubgroupSize : null); var result = vk.CreateComputePipelines(device, cache, 1, &pipelineInfo, null, out var pipeline); if (result != Result.Success) { @@ -1178,6 +1201,28 @@ internal static unsafe partial class VulkanVideoPresenter } } + private Pipeline CompileStoredComputePipeline(ShaderModule module, PipelineLayout layout, bool requireSubgroup32, bool fast = false) + { + var entryPoint = (byte*)SilkMarshal.StringToPtr("main"); + try + { + var requiredSubgroupSize = RequiredSubgroupSize(); + var pipelineInfo = ComputeCreateInfo(module, layout, entryPoint, requireSubgroup32 ? &requiredSubgroupSize : null); + var result = CreateCachedPipelineObject(false, &pipelineInfo, out var pipeline, out var unoptimized, fast); + Check(result, "vkCreateComputePipelines(async)"); + + if (unoptimized) + { + QueuePipelineOptimization(pipeline, () => CompileStoredComputePipeline(module, layout, requireSubgroup32)); + } + return pipeline; + } + finally + { + SilkMarshal.Free((nint)entryPoint); + } + } + private void DrainPendingComputePipelines() { foreach (var pending in _pendingComputePipelines.Values) diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Present.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Present.cs index e89fc5b..fa13e52 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Present.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Present.cs @@ -157,59 +157,45 @@ internal static unsafe partial class VulkanVideoPresenter _guestFlipVersionSequence = 0; } - private static readonly bool _shaderPrewarmWaitDisabled = - Environment.GetEnvironmentVariable("SHARPEMU_SHADER_PREWARM_WAIT") == "0"; - private static bool _shaderPrewarmDecided; - private static bool _shaderPrewarmRunning; - private static bool _shaderPrewarmReleased; - private static int _shaderPrewarmHoldReported; - private static int _shaderPrewarmProgress; - private static int _shaderPrewarmTotal; + private static bool _shaderCacheDecided; + private static bool _shaderCacheRunning; + private static bool _shaderCacheReleased; + private static int _shaderCacheHoldReported; - private static void SetShaderPrewarmState(bool running) + private static void SetShaderCacheState(bool running) { lock (_gate) { - _shaderPrewarmDecided = true; - _shaderPrewarmRunning = running; - Volatile.Write(ref _shaderPrewarmReleased, !running); + _shaderCacheDecided = true; + _shaderCacheRunning = running; + Volatile.Write(ref _shaderCacheReleased, !running); System.Threading.Monitor.PulseAll(_gate); } } - internal static void WaitForShaderPrewarm(string holder) + internal static void WaitForShaderCache(string holder) { - if (_shaderPrewarmWaitDisabled || Volatile.Read(ref _shaderPrewarmReleased)) + if (!Gpu.ShaderCache.ShaderCacheSettings.HoldsGuestUntilReady || Volatile.Read(ref _shaderCacheReleased)) { return; } var started = System.Diagnostics.Stopwatch.GetTimestamp(); - var reported = started; var reporter = false; lock (_gate) { - while (_thread is not null && (!_shaderPrewarmDecided || _shaderPrewarmRunning) && + while (_thread is not null && (!_shaderCacheDecided || _shaderCacheRunning) && !_closed && _presenterStartupFailure is null && !HostSessionControl.IsShutdownRequested && !Volatile.Read(ref _presenterCloseRequested)) { - if (!reporter && _shaderPrewarmRunning && - Interlocked.Exchange(ref _shaderPrewarmHoldReported, 1) == 0) + if (!reporter && _shaderCacheRunning && + Interlocked.Exchange(ref _shaderCacheHoldReported, 1) == 0) { reporter = true; - Console.Error.WriteLine( - $"[LOADER][INFO] Shader prewarm: holding the game at its {holder} until it finishes."); + Console.Error.WriteLine($"[SHADER CACHE] Holding the game at its {holder} until the shader precompile finishes."); } System.Threading.Monitor.Wait(_gate, 1000); - if (reporter && System.Diagnostics.Stopwatch.GetElapsedTime(reported) >= TimeSpan.FromSeconds(5)) - { - reported = System.Diagnostics.Stopwatch.GetTimestamp(); - var total = Volatile.Read(ref _shaderPrewarmTotal); - var done = Volatile.Read(ref _shaderPrewarmProgress); - Console.Error.WriteLine( - $"[LOADER][INFO] Shader prewarm: {done}/{total} ({(total == 0 ? 0 : done * 100 / total)}%)"); - } } } @@ -217,13 +203,13 @@ internal static unsafe partial class VulkanVideoPresenter { Console.Error.WriteLine(string.Create( System.Globalization.CultureInfo.InvariantCulture, - $"[LOADER][INFO] Shader prewarm: game released after {System.Diagnostics.Stopwatch.GetElapsedTime(started).TotalSeconds:F1}s.")); + $"[SHADER CACHE] Game released after {System.Diagnostics.Stopwatch.GetElapsedTime(started).TotalSeconds:F1} s.")); } } public static void HideSplashScreen() { - WaitForShaderPrewarm("splash screen"); + WaitForShaderCache("splash screen"); lock (_gate) { _splashHidden = true; diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.ShaderCache.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.ShaderCache.cs new file mode 100644 index 0000000..7e0e852 --- /dev/null +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.ShaderCache.cs @@ -0,0 +1,1401 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +namespace SharpEmu.Libs.VideoOut; + +using System.Buffers.Binary; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Globalization; +using System.IO.Hashing; +using System.Reflection; +using System.Text; +using SharpEmu.Libs.Gpu; +using SharpEmu.Libs.Gpu.Pipelines; +using SharpEmu.Libs.Gpu.Rendering; +using SharpEmu.Libs.Gpu.Scheduling; +using SharpEmu.Libs.Gpu.ShaderCache; +using SharpEmu.ShaderCompiler; +using SharpEmu.ShaderCompiler.Resources; +using SharpEmu.ShaderCompiler.Vulkan; +using Silk.NET.Core.Native; +using Silk.NET.Vulkan; + +internal enum ShaderCacheOutcome +{ + FromStore, + Compiled, + Failed, + Dictionary, +} + +internal static unsafe partial class VulkanVideoPresenter +{ + internal const string ShaderCacheWorkerFlag = "--shader-cache-worker"; + private const int ShaderCacheDictionaryExitCode = 3; + private const int ShaderCacheItemsPerWorker = 96; + private const int ShaderCacheMinItemsPerWorker = 8; + private const int ShaderCacheWorkerThreads = 2; + private const int ShaderCacheMaxPasses = 4; + private const string ShaderCacheItemPrefix = "[SHADER CACHE][ITEM] "; + private const string ShaderCacheDriverPrefix = "[SHADER CACHE][DRIVER] "; + private const string ShaderCacheBeginPrefix = "[SHADER CACHE][BEGIN] "; + private const string ShaderCacheDerivePrefix = "[SHADER CACHE][DERIVE] "; + private static readonly TimeSpan ShaderCacheWorkerStallTimeout = TimeSpan.FromMinutes(3); + private static readonly TimeSpan ShaderCacheProgressInterval = TimeSpan.FromSeconds(2); + private static readonly TimeSpan ShaderCacheSessionInterval = TimeSpan.FromSeconds(30); + + internal static int RunShaderCacheWorker(IReadOnlyList arguments) + { + if (arguments.Count != 7 || !long.TryParse(arguments[1], CultureInfo.InvariantCulture, out var length) || + !int.TryParse(arguments[2], CultureInfo.InvariantCulture, out var first) || + !int.TryParse(arguments[3], CultureInfo.InvariantCulture, out var count) || arguments[5] is not ("0" or "1")) + { + Console.Error.WriteLine("[SHADER CACHE][ERROR] The precompile worker arguments are invalid."); + return 2; + } + + SubmissionScheduler.OnFatal = static message => throw new InvalidOperationException(message); + using var presenter = new Presenter(1, 1, hidden: true); + presenter.InitializeShaderCompileDevice(); + return presenter.RunShaderCacheWorker(arguments[0], length, first, count, arguments[4], arguments[5] == "1", arguments[6]); + } + + internal static int RunShaderPrecompile(string app0Root, bool full) + { + SubmissionScheduler.OnFatal = static message => throw new InvalidOperationException(message); + using var presenter = new Presenter(1, 1, hidden: true); + presenter.InitializeShaderCompileDevice(); + return presenter.RunStandalonePrecompile(app0Root, full); + } + + private sealed partial class Presenter + { + private const int MaxLoggedShaderCacheFailures = 8; + private const int MaxForwardedWorkerLines = 32; + private const int MaxDerivationExamples = 12; + + [ThreadStatic] + private static bool _lastCaptureReachedDictionary; + + [ThreadStatic] + private static List? _itemContentKeys; + + private bool _shaderCompileOnly; + private string? _shaderSeedPath; + private ShaderCacheFile? _shaderCache; + private ulong _shaderCacheStamp; + private ulong _shaderCacheDriverStamp; + private ulong _shaderCacheBinaryKey; + private ShaderCompileHostFlags _shaderCacheFlags; + private readonly ConcurrentDictionary> _warmStages = new(); + private readonly CancellationTokenSource _shaderCacheCancel = new(); + private readonly ConcurrentDictionary _shaderCacheWorkers = new(); + private Thread? _shaderCacheThread; + private volatile bool _shaderCacheDriverMismatch; + private int _shaderCacheFailed; + private int _shaderCacheFromBinaries; + private int _shaderCacheCompiled; + private int _shaderCacheBinaryRejected; + private int _shaderCacheUnoptimized; + private Timer? _shaderCacheSessionTimer; + private (int FromStore, int Compiled, int Rejected) _shaderCacheSessionReported; + private int _shaderSeedRecordsSaved = -1; + private int _precompileComputeTotal; + private int _precompileGraphicsTotal; + private int _precompileComputeDone; + private int _precompileGraphicsDone; + private int _precompileFromStore; + private int _precompileCompiled; + private int _precompileFailed; + private int _precompileDictionary; + private int _precompileForwardedLines; + private long _precompileStarted; + private readonly ConcurrentDictionary _derivationResults = new(); + private readonly ConcurrentDictionary _derivationDifferences = new(); + private readonly ConcurrentQueue _derivationExamples = new(); + + ShaderCacheFile? IShaderPipelineHost.ShaderCache => Volatile.Read(ref _shaderCache); + + internal void InitializeShaderCompileDevice() + { + _shaderCompileOnly = true; + _relay.BindCurrentThread(); + _vk = Vk.GetApi(); + uint loaderVersion = Vk.Version10; + Check(_vk.EnumerateInstanceVersion(ref loaderVersion), "vkEnumerateInstanceVersion"); + if (loaderVersion < Vk.Version13) + { + throw new InvalidOperationException("SharpEmu requires Vulkan 1.3."); + } + + CreateInstance(); + CreateSurface(); + SelectPhysicalDevice(); + CreateDevice(); + CreatePipelineCache(); + InitializeShaderCacheStamps(); + } + + private void InitializeShaderCacheStamps() + { + _shaderCacheFlags = ShaderCompileHostFlags.From(this); + _shaderCacheStamp = ShaderCacheStamp(GuestGpu.Current, _shaderCacheFlags); + _vk.GetPhysicalDeviceProperties(_physicalDevice, out var properties); + var device = new byte[16 + 3 * sizeof(uint) + sizeof(ulong) + 1]; + new ReadOnlySpan(properties.PipelineCacheUuid, 16).CopyTo(device); + BinaryPrimitives.WriteUInt32LittleEndian(device.AsSpan(16), properties.VendorID); + BinaryPrimitives.WriteUInt32LittleEndian(device.AsSpan(20), properties.DeviceID); + BinaryPrimitives.WriteUInt32LittleEndian(device.AsSpan(24), properties.DriverVersion); + BinaryPrimitives.WriteUInt64LittleEndian(device.AsSpan(28), _shaderCacheStamp); + device[36] = _pipelineBinaries is null ? (byte)0 : (byte)1; + var pipelineState = string.Join('|', + _colorWriteEnableApi is not null, _supportsDepthClipControl, _supportsDepthClipEnable, _supportsDepthBounds, + _supportsFillRectangle, _supportsShaderClipDistance, _maxPushDescriptors, _maxColorAttachments); + _shaderCacheDriverStamp = XxHash3.HashToUInt64([.. device, .. Encoding.UTF8.GetBytes(pipelineState)]); + _shaderCacheBinaryKey = _pipelineBinaries is { } api ? ShaderCacheFile.DriverKeyHash(api.DriverKey) : 0; + } + + private void StartShaderCache() + { + if (!StartShaderCacheCore()) + { + SetShaderCacheState(false); + } + } + + private bool StartShaderCacheCore() + { + var title = VideoOutExports.GetApplicationDisplayName(); + if (!ShaderCacheSettings.Enabled) + { + Console.Error.WriteLine($"[SHADER CACHE] {title}: off."); + return false; + } + + if (_pipelineCacheShardDirectory is not null) + { + Console.Error.WriteLine($"[SHADER CACHE] {title}: off, the driver cache shards own pipeline caching on this platform."); + return false; + } + + if (ShaderCacheDirectory(VideoOutExports.GetApplicationTitleId()) is not { } directory || + ShaderCacheFile.Open(directory) is not { } file) + { + return false; + } + + InitializeShaderCacheStamps(); + if (file.ImportedLegacyComputes > 0) + { + Console.Error.WriteLine($"[SHADER CACHE] {title}: imported {file.ImportedLegacyComputes} compute pipelines from the old prewarm list."); + } + + Volatile.Write(ref _shaderCache, file); + UseShaderCacheStores(file, file); + ShaderInventory.Attach(file, null); + _shaderSeedPath = ShaderCacheSettings.SeedPath(VideoOutExports.GetApplicationTitleId()); + LogShaderCacheSummary(title, file); + var app0Root = Environment.GetEnvironmentVariable("SHARPEMU_APP0_DIR"); + var seedExists = File.Exists(_shaderSeedPath); + if (ShaderCacheSettings.Learn && !seedExists) + { + Console.Error.WriteLine( + $"[SHADER CACHE] {title}: learning run, nothing is precompiled now. The shader seed is written to {_shaderSeedPath} " + + "while you play, and the next launch prepares every shader and pipeline from it."); + StartShaderCacheSession(title); + return false; + } + + var full = ShaderCacheSettings.FullPrecompile || seedExists; + SetShaderCacheState(true); + _shaderCacheThread = new Thread(() => + { + try + { + Precompile(file, title, app0Root, full, _shaderCacheCancel.Token); + } + catch (OperationCanceledException) + { + } + catch (Exception exception) when (exception is IOException or UnauthorizedAccessException or InvalidOperationException) + { + Console.Error.WriteLine($"[SHADER CACHE][WARN] {title}: the shader precompile stopped: {exception.Message}"); + } + finally + { + SetShaderCacheState(false); + StartShaderCacheSession(title); + } + }) + { + IsBackground = true, + Name = "SharpEmu shader precompile", + }; + _shaderCacheThread.Start(); + return true; + } + + internal int RunStandalonePrecompile(string app0Root, bool full) + { + var title = VideoOutExports.GetApplicationDisplayName(); + if (ShaderCacheDirectory(VideoOutExports.GetApplicationTitleId()) is not { } directory || + ShaderCacheFile.Open(directory) is not { } file) + { + return 1; + } + + using (file) + { + _shaderSeedPath = ShaderCacheSettings.SeedPath(VideoOutExports.GetApplicationTitleId()); + UseShaderCacheStores(file, file); + LogShaderCacheSummary(title, file); + using var cancel = new CancellationTokenSource(); + ConsoleCancelEventHandler stop = (_, arguments) => + { + arguments.Cancel = true; + cancel.Cancel(); + KillShaderCacheWorkers(); + }; + Console.CancelKeyPress += stop; + try + { + Precompile(file, title, app0Root, full, cancel.Token); + CompactShaderCache(file); + return _shaderCacheDriverMismatch ? 1 : 0; + } + catch (OperationCanceledException) + { + Console.Error.WriteLine($"[SHADER CACHE] {title}: precompile stopped; finished work is kept and the next run continues."); + return 1; + } + finally + { + Console.CancelKeyPress -= stop; + } + } + } + + private ShaderSeed LoadShaderSeed() => + _shaderSeedPath is { Length: > 0 } path ? ShaderSeed.Load(path) : new ShaderSeed(); + + private void SaveShaderSeed(ShaderCacheFile file, string title) + { + if (!ShaderCacheSettings.Learn || _shaderSeedPath is not { Length: > 0 } path) + { + return; + } + + var existing = ShaderSeed.Load(path); + var merged = existing.MergedWith(ShaderSeed.Learn(file)) + .Stamped(VideoOutExports.GetApplicationTitleId(), VideoOutExports.GetApplicationVersion()); + if (merged.IsEmpty || merged.Serialize().AsSpan().SequenceEqual(existing.Serialize())) + { + return; + } + + if (merged.TrySave(path)) + { + Console.Error.WriteLine( + $"[SHADER CACHE] {title}: shader seed {merged.StateCount} graphics states in {merged.GroupCount} passes, " + + $"{merged.Pairs.Count} shader pairs, played pipelines compute {merged.Computes.Count}, graphics {merged.Graphics.Count} " + + $"(was {existing.StateCount} states, {existing.Pairs.Count} pairs, {existing.Computes.Count + existing.Graphics.Count} pipelines) at {path}"); + } + } + + private static string? ShaderCacheDirectory(string titleId) => + Path.GetDirectoryName(VulkanPipelineCacheStorage.ResolvePath( + titleId, + Environment.GetEnvironmentVariable("SHARPEMU_VK_PIPELINE_CACHE_PATH"))); + + private void UseShaderCacheStores(ShaderCacheFile? target, params ShaderCacheFile[] sources) => + SetPipelineStore(target, sources); + + private void LogShaderCacheSummary(string title, ShaderCacheFile file) + { + int compute = 0, vertex = 0, pixel = 0, scanned = 0; + var programs = file.SnapshotPrograms(); + foreach (var program in programs) + { + if (!StaticStageInputs.TryParse(program, out var header)) + { + continue; + } + + scanned += program.Program.Source == InventorySource.Scanned ? 1 : 0; + switch (header.Type) + { + case AgcShaderType.Compute: + compute++; + break; + case AgcShaderType.Geometry: + vertex++; + break; + case AgcShaderType.Pixel: + pixel++; + break; + } + } + + var binaries = _pipelineBinaries is null ? "unsupported by the driver" : file.BinaryCount.ToString(CultureInfo.InvariantCulture); + var seed = LoadShaderSeed(); + var learned = ShaderSeed.Learn(file).MergedWith(seed); + var version = VideoOutExports.GetApplicationVersion(); + if (!seed.IsEmpty && seed.GameVersion.Length != 0 && version.Length != 0 && + !string.Equals(seed.GameVersion, version, StringComparison.OrdinalIgnoreCase)) + { + Console.Error.WriteLine( + $"[SHADER CACHE][WARN] {title}: the shader seed was made with game version {seed.GameVersion}, this is {version}; " + + "shaders that changed between the versions are not paired."); + } + + Console.Error.WriteLine( + $"[SHADER CACHE] {title}: shaders compute {compute}, vertex {vertex}, pixel {pixel} " + + $"({scanned} from game files, {programs.Length - scanned} from the running game); " + + $"recorded pipelines compute {file.Computes.Count}, graphics {file.Graphics.Count}; " + + $"graphics states {learned.StateCount} in {learned.GroupCount} passes " + + $"({(seed.IsEmpty ? "no seed" : $"seed {seed.StateCount} states, {seed.Pairs.Count} pairs, {seed.Computes.Count + seed.Graphics.Count} played pipelines")}); " + + $"pipeline binaries {binaries}; {FormatCacheBytes(file.Length)} at {file.Path}"); + } + + private void Precompile(ShaderCacheFile file, string title, string? app0Root, bool full, CancellationToken cancellation) + { + _precompileStarted = Stopwatch.GetTimestamp(); + MergeLeftoverParts(file); + if (!string.IsNullOrEmpty(app0Root) && Directory.Exists(app0Root)) + { + ScanGameFiles(file, title, app0Root, cancellation); + } + + var prepared = false; + var complete = false; + for (var pass = 0; !_shaderCacheDriverMismatch; pass++) + { + cancellation.ThrowIfCancellationRequested(); + var length = file.Length; + List pending; + using (var snapshot = ShaderCacheFile.OpenSnapshot(file.Path, length)) + { + if (snapshot is null) + { + return; + } + + var list = ShaderWorkList.Build(snapshot, full, LoadShaderSeed()); + pending = list.Items.Where(item => !snapshot.IsDone(item.Identity, _shaderCacheDriverStamp)).ToList(); + if (pass == 0) + { + LogWorkList(title, list, pending, full); + } + } + + if (pending.Count == 0) + { + complete = true; + break; + } + + if (pass == ShaderCacheMaxPasses) + { + break; + } + + var doneBefore = Volatile.Read(ref _precompileComputeDone) + Volatile.Read(ref _precompileGraphicsDone); + _precompileComputeTotal = pending.Count(static item => item.IsCompute); + _precompileGraphicsTotal = pending.Count - _precompileComputeTotal; + _precompileComputeDone = _precompileGraphicsDone = 0; + RunPrecompileWorkers(file, title, length, pending.Count, full, cancellation); + prepared = true; + if (Volatile.Read(ref _precompileComputeDone) + Volatile.Read(ref _precompileGraphicsDone) == 0 && doneBefore == 0) + { + break; + } + } + + if (prepared) + { + Console.Error.WriteLine(string.Create( + CultureInfo.InvariantCulture, + $"[SHADER CACHE] {title}: precompile finished in {Stopwatch.GetElapsedTime(_precompileStarted).TotalSeconds:F1} s; " + + $"pipelines compiled {Volatile.Read(ref _precompileCompiled)}, already stored {Volatile.Read(ref _precompileFromStore)}, " + + $"skipped {Volatile.Read(ref _precompileFailed)} the translator cannot compile; pipeline binaries {file.BinaryCount}; {FormatCacheBytes(file.Length)}")); + LogDerivationCheck(title); + } + else + { + Console.Error.WriteLine($"[SHADER CACHE] {title}: every known shader is already prepared."); + } + + if (complete && prepared) + { + CompactShaderCache(file, dropUnreferencedBinaries: true); + } + } + + private void LogDerivationCheck(string title) + { + if (_derivationResults.IsEmpty) + { + return; + } + + int Count(string key) => _derivationResults.GetValueOrDefault(key); + var differences = string.Join(", ", _derivationDifferences + .OrderByDescending(static entry => entry.Value) + .ThenBy(static entry => entry.Key, StringComparer.Ordinal) + .Select(static entry => $"{entry.Key} {entry.Value}")); + Console.Error.WriteLine( + $"[SHADER CACHE] {title}: shader-file derivation of played pipelines: " + + $"compute same {Count("compute same")}, differs {Count("compute differs")}, not derivable {Count("compute underivable")}; " + + $"graphics same {Count("graphics same")} (outside the learned states {Count("graphics uncovered")}), " + + $"differs {Count("graphics differs")}, not derivable {Count("graphics underivable")}" + + (differences.Length == 0 ? "." : $"; differing fields: {differences}.")); + foreach (var example in _derivationExamples) + { + Console.Error.WriteLine($"{ShaderCacheDerivePrefix}{example}"); + } + } + + private static void MergeLeftoverParts(ShaderCacheFile file) + { + var directory = Path.GetDirectoryName(file.Path)!; + foreach (var part in Directory.EnumerateFiles(directory, Path.GetFileName(file.Path) + ".part*")) + { + file.MergePart(part); + try + { + File.Delete(part); + } + catch (Exception exception) when (exception is IOException or UnauthorizedAccessException) + { + } + } + } + + private void ScanGameFiles(ShaderCacheFile file, string title, string app0Root, CancellationToken cancellation) + { + var started = Stopwatch.GetTimestamp(); + var lastReport = started; + var result = GameShaderScanner.Scan(app0Root, file, (done, total) => + { + var now = Stopwatch.GetTimestamp(); + if (Stopwatch.GetElapsedTime(Interlocked.Read(ref lastReport), now) < ShaderCacheProgressInterval) + { + return; + } + + Interlocked.Exchange(ref lastReport, now); + Console.Error.WriteLine(string.Create( + CultureInfo.InvariantCulture, + $"[SHADER CACHE] {title}: scanning game files {done / (1024.0 * 1024.0 * 1024.0):F1}/{total / (1024.0 * 1024.0 * 1024.0):F1} GB")); + }, cancellation); + if (result.FilesScanned != 0) + { + Console.Error.WriteLine(string.Create( + CultureInfo.InvariantCulture, + $"[SHADER CACHE] {title}: scanned {result.FilesScanned} game files ({result.FilesSkipped} unchanged) " + + $"in {Stopwatch.GetElapsedTime(started).TotalSeconds:F1} s; {result.Programs} new shaders found.")); + } + } + + private static void LogWorkList(string title, ShaderWorkList list, List pending, bool full) + { + int Count(IEnumerable items, ShaderWorkKind kind) => items.Count(item => item.Kind == kind); + Console.Error.WriteLine( + $"[SHADER CACHE] {title}: to prepare compute {pending.Count(static item => item.IsCompute)} " + + $"(recorded {Count(pending, ShaderWorkKind.RecordedCompute)}, seed {Count(pending, ShaderWorkKind.SeedCompute)}, " + + $"from shaders {Count(pending, ShaderWorkKind.StaticCompute)}), " + + $"graphics {pending.Count(static item => !item.IsCompute)} " + + $"(recorded {Count(pending, ShaderWorkKind.RecordedGraphics)}, seed {Count(pending, ShaderWorkKind.SeedGraphics)}, " + + $"learned {Count(pending, ShaderWorkKind.StaticGraphics)}); " + + $"already prepared {list.Items.Count - pending.Count}." + + (full + ? $" Graphics states {list.States.Count}." + : " Learned graphics pipelines are prepared when a shader seed is present, with learning on, or by a full precompile.")); + } + + private void RunPrecompileWorkers(ShaderCacheFile file, string title, long length, int pendingCount, bool full, CancellationToken cancellation) + { + var batches = new ConcurrentQueue<(int First, int Count)>(); + var batchSize = Math.Clamp( + (pendingCount + ShaderCacheSettings.PrecompileJobs - 1) / ShaderCacheSettings.PrecompileJobs, + ShaderCacheMinItemsPerWorker, ShaderCacheItemsPerWorker); + for (var first = 0; first < pendingCount; first += batchSize) + { + batches.Enqueue((first, Math.Min(batchSize, pendingCount - first))); + } + + var jobs = Math.Min(ShaderCacheSettings.PrecompileJobs, batches.Count); + using var progress = new Timer(_ => ReportPrecompileProgress(title, jobs), null, ShaderCacheProgressInterval, ShaderCacheProgressInterval); + var threads = new Thread[jobs]; + for (var slot = 0; slot < jobs; slot++) + { + var part = file.Path + ".part" + slot.ToString(CultureInfo.InvariantCulture); + threads[slot] = new Thread(() => + { + while (!cancellation.IsCancellationRequested && !_shaderCacheDriverMismatch && batches.TryDequeue(out var batch)) + { + RunPrecompileWorker(file, length, batch.First, batch.Count, part, full, cancellation); + } + }) + { + IsBackground = true, + Name = $"SharpEmu shader precompile {slot}", + }; + threads[slot].Start(); + } + + foreach (var thread in threads) + { + thread.Join(); + } + + cancellation.ThrowIfCancellationRequested(); + } + + private void RunPrecompileWorker(ShaderCacheFile file, long length, int first, int count, string part, bool full, CancellationToken cancellation) + { + var start = new ProcessStartInfo(Environment.ProcessPath ?? throw new InvalidOperationException("The emulator path is unknown.")) + { + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = true, + }; + if (string.Equals(Path.GetFileNameWithoutExtension(start.FileName), "dotnet", StringComparison.OrdinalIgnoreCase) && + Assembly.GetEntryAssembly()?.Location is { Length: > 0 } entry) + { + start.ArgumentList.Add(entry); + } + + foreach (var argument in (string[])[ + ShaderCacheWorkerFlag, file.Path, length.ToString(CultureInfo.InvariantCulture), + first.ToString(CultureInfo.InvariantCulture), count.ToString(CultureInfo.InvariantCulture), part, full ? "1" : "0", + _shaderSeedPath ?? string.Empty]) + { + start.ArgumentList.Add(argument); + } + + start.Environment.Remove("SHARPEMU_RENDERDOC_WAIT"); + start.Environment.Remove("SHARPEMU_RENDERDOC_CAPTURE"); + var driverCache = Path.Combine(Path.GetTempPath(), "SharpEmu", "precompile", Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(driverCache); + start.Environment["__GL_SHADER_DISK_CACHE"] = "1"; + start.Environment["__GL_SHADER_DISK_CACHE_PATH"] = driverCache; + using var process = Process.Start(start) ?? throw new InvalidOperationException("The shader precompile worker could not start."); + _shaderCacheWorkers[process.Id] = process; + try + { + try + { + process.PriorityClass = ProcessPriorityClass.BelowNormal; + } + catch (Exception exception) when (exception is InvalidOperationException or System.ComponentModel.Win32Exception) + { + } + + var inFlight = new ConcurrentDictionary(); + var lastActivity = Stopwatch.GetTimestamp(); + process.OutputDataReceived += (_, line) => + { + Interlocked.Exchange(ref lastActivity, Stopwatch.GetTimestamp()); + HandleWorkerLine(line.Data, inFlight); + }; + process.ErrorDataReceived += (_, line) => ForwardWorkerError(line.Data); + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + var stalled = false; + while (!process.WaitForExit(250)) + { + if (cancellation.IsCancellationRequested) + { + KillShaderCacheWorker(process); + break; + } + + if (Stopwatch.GetElapsedTime(Interlocked.Read(ref lastActivity)) > ShaderCacheWorkerStallTimeout) + { + stalled = true; + KillShaderCacheWorker(process); + break; + } + } + + process.WaitForExit(); + if (stalled) + { + foreach (var identity in inFlight.Keys) + { + file.AddDone(identity, _shaderCacheDriverStamp); + Interlocked.Increment(ref _precompileFailed); + Console.Error.WriteLine( + $"[SHADER CACHE][WARN] Pipeline 0x{identity:X16} did not finish in {ShaderCacheWorkerStallTimeout.TotalMinutes:F0} minutes and is skipped."); + } + + return; + } + + if (process.ExitCode is not (0 or ShaderCacheDictionaryExitCode) && !cancellation.IsCancellationRequested) + { + Console.Error.WriteLine($"[SHADER CACHE][WARN] A precompile worker exited with {process.ExitCode}."); + } + } + finally + { + _shaderCacheWorkers.TryRemove(process.Id, out _); + try + { + Directory.Delete(driverCache, recursive: true); + } + catch (Exception exception) when (exception is IOException or UnauthorizedAccessException) + { + } + + if (File.Exists(part)) + { + file.MergePart(part); + try + { + File.Delete(part); + } + catch (Exception exception) when (exception is IOException or UnauthorizedAccessException) + { + } + } + } + } + + private void HandleWorkerLine(string? line, ConcurrentDictionary inFlight) + { + if (line is null) + { + return; + } + + if (line.StartsWith(ShaderCacheBeginPrefix, StringComparison.Ordinal)) + { + if (ulong.TryParse(line[ShaderCacheBeginPrefix.Length..], NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var started)) + { + inFlight.TryAdd(started, 0); + } + + return; + } + + if (line.StartsWith(ShaderCacheDerivePrefix, StringComparison.Ordinal)) + { + NoteDerivation(line[ShaderCacheDerivePrefix.Length..]); + return; + } + + if (line.StartsWith(ShaderCacheDriverPrefix, StringComparison.Ordinal)) + { + var fields = line[ShaderCacheDriverPrefix.Length..].Split(' '); + if (fields.Length == 2 && + ulong.TryParse(fields[0], NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var stamp) && + ulong.TryParse(fields[1], NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var binaryKey) && + (stamp != _shaderCacheDriverStamp || binaryKey != _shaderCacheBinaryKey) && !_shaderCacheDriverMismatch) + { + _shaderCacheDriverMismatch = true; + Console.Error.WriteLine( + $"[SHADER CACHE][WARN] A precompile worker sees another driver identity " + + $"(stamp {stamp:X16}/{_shaderCacheDriverStamp:X16}, pipeline key {binaryKey:X16}/{_shaderCacheBinaryKey:X16}); " + + "the precompile stops so it does not rebuild the same pipelines on every launch."); + } + + return; + } + + if (!line.StartsWith(ShaderCacheItemPrefix, StringComparison.Ordinal)) + { + return; + } + + var parts = line[ShaderCacheItemPrefix.Length..].Split(' '); + if (parts.Length != 3 || !Enum.TryParse(parts[1], out var outcome) || + !ulong.TryParse(parts[2], NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var finished)) + { + return; + } + + inFlight.TryRemove(finished, out _); + + if (outcome != ShaderCacheOutcome.Dictionary) + { + Interlocked.Increment(ref parts[0] == "compute" ? ref _precompileComputeDone : ref _precompileGraphicsDone); + } + + switch (outcome) + { + case ShaderCacheOutcome.FromStore: + Interlocked.Increment(ref _precompileFromStore); + break; + case ShaderCacheOutcome.Compiled: + Interlocked.Increment(ref _precompileCompiled); + break; + case ShaderCacheOutcome.Failed: + Interlocked.Increment(ref _precompileFailed); + break; + default: + Interlocked.Increment(ref _precompileDictionary); + break; + } + } + + private void NoteDerivation(string result) + { + var fields = result.Split(' ', 4); + if (fields.Length < 3) + { + return; + } + + _derivationResults.AddOrUpdate($"{fields[0]} {fields[1]}", 1, static (_, count) => count + 1); + if (fields[1] == "differs" && fields.Length == 4) + { + _derivationDifferences.AddOrUpdate($"{fields[0]} {RecordDifference.Category(fields[3])}", 1, static (_, count) => count + 1); + } + + if (fields[1] is "differs" or "underivable" && _derivationExamples.Count < MaxDerivationExamples) + { + _derivationExamples.Enqueue(result); + } + } + + private void ForwardWorkerError(string? line) + { + if (line is not null && + (line.Contains("[SHADER CACHE]", StringComparison.Ordinal) || line.Contains("[ERROR]", StringComparison.Ordinal) || + line.Contains("[FATAL]", StringComparison.Ordinal)) && + Interlocked.Increment(ref _precompileForwardedLines) <= MaxForwardedWorkerLines) + { + Console.Error.WriteLine(line); + } + } + + private void ReportPrecompileProgress(string title, int jobs) + { + Console.Error.WriteLine(string.Create( + CultureInfo.InvariantCulture, + $"[SHADER CACHE] {title}: precompiling compute {Volatile.Read(ref _precompileComputeDone)}/{_precompileComputeTotal} | " + + $"graphics {Volatile.Read(ref _precompileGraphicsDone)}/{_precompileGraphicsTotal} | " + + $"compiled {Volatile.Read(ref _precompileCompiled)} | stored {Volatile.Read(ref _precompileFromStore)} | " + + $"skipped {Volatile.Read(ref _precompileFailed)} | {jobs} workers | {Stopwatch.GetElapsedTime(_precompileStarted).TotalSeconds:F0} s")); + } + + private void StartShaderCacheSession(string title) + { + Interlocked.Exchange(ref _shaderCacheFromBinaries, 0); + Interlocked.Exchange(ref _shaderCacheCompiled, 0); + Interlocked.Exchange(ref _shaderCacheBinaryRejected, 0); + Interlocked.Exchange(ref _shaderCacheUnoptimized, 0); + _shaderCacheSessionTimer = new Timer(_ => ReportShaderCacheSession(title), null, ShaderCacheSessionInterval, ShaderCacheSessionInterval); + } + + private void ReportShaderCacheSession(string title) + { + if (Volatile.Read(ref _shaderCache) is { } file && + file.SnapshotGraphics().Length + file.SnapshotComputes().Length is var recorded && recorded != _shaderSeedRecordsSaved) + { + _shaderSeedRecordsSaved = recorded; + SaveShaderSeed(file, title); + } + + var current = (Volatile.Read(ref _shaderCacheFromBinaries), Volatile.Read(ref _shaderCacheCompiled), Volatile.Read(ref _shaderCacheBinaryRejected)); + if (current == _shaderCacheSessionReported) + { + return; + } + + _shaderCacheSessionReported = current; + Console.Error.WriteLine( + $"[SHADER CACHE] {title}: this session pipelines from the store {current.Item1}, compiled {current.Item2} " + + $"({Volatile.Read(ref _shaderCacheUnoptimized)} of them unoptimized first), rejected binaries {current.Item3}."); + } + + private void KillShaderCacheWorkers() + { + foreach (var process in _shaderCacheWorkers.Values) + { + KillShaderCacheWorker(process); + } + } + + private static void KillShaderCacheWorker(Process process) + { + try + { + process.Kill(entireProcessTree: true); + } + catch (Exception exception) when (exception is InvalidOperationException or System.ComponentModel.Win32Exception) + { + } + } + + internal int RunShaderCacheWorker(string cachePath, long length, int first, int count, string partPath, bool full, string seedPath) + { + _shaderSeedPath = seedPath; + Console.Out.WriteLine($"{ShaderCacheDriverPrefix}{_shaderCacheDriverStamp:X16} {_shaderCacheBinaryKey:X16}"); + using var snapshot = ShaderCacheFile.OpenSnapshot(cachePath, length); + using var part = ShaderCacheFile.OpenPart(partPath); + if (snapshot is null || part is null) + { + return 1; + } + + UseShaderCacheStores(part, snapshot, part); + var list = ShaderWorkList.Build(snapshot, full, LoadShaderSeed()); + var slice = list.Items + .Where(item => !snapshot.IsDone(item.Identity, _shaderCacheDriverStamp)) + .Skip(first) + .Take(count) + .ToArray(); + var next = -1; + var output = new object(); + var threads = new Thread[Math.Min(ShaderCacheWorkerThreads, Math.Max(slice.Length, 1))]; + for (var index = 0; index < threads.Length; index++) + { + threads[index] = new Thread(() => + { + while (!_pipelineBinaryDictionaryReached) + { + var position = Interlocked.Increment(ref next); + if (position >= slice.Length) + { + break; + } + + var item = slice[position]; + lock (output) + { + Console.Out.WriteLine($"{ShaderCacheBeginPrefix}{item.Identity:X16}"); + } + + _itemContentKeys = []; + var outcome = ProcessWorkItem(list, item); + var derivation = outcome == ShaderCacheOutcome.Dictionary ? null : CheckDerivation(list, item); + if (outcome != ShaderCacheOutcome.Dictionary) + { + part.AddDone(item.Identity, _shaderCacheDriverStamp, _itemContentKeys); + } + + _itemContentKeys = null; + lock (output) + { + if (derivation is not null) + { + Console.Out.WriteLine($"{ShaderCacheDerivePrefix}{derivation}"); + } + + Console.Out.WriteLine($"{ShaderCacheItemPrefix}{(item.IsCompute ? "compute" : "graphics")} {outcome} {item.Identity:X16}"); + } + } + }) + { + Name = $"SharpEmu shader precompile worker {index}", + }; + threads[index].Start(); + } + + foreach (var thread in threads) + { + thread.Join(); + } + + Console.Out.Flush(); + return _pipelineBinaryDictionaryReached ? ShaderCacheDictionaryExitCode : 0; + } + + private ShaderCacheOutcome ProcessWorkItem(ShaderWorkList list, ShaderWorkItem item) + { + try + { + var file = list.File; + switch (item.Kind) + { + case ShaderWorkKind.RecordedCompute: + { + var compute = file.Computes[item.First]; + return CompileComputeItem(compute.Record, compute.Code); + } + + case ShaderWorkKind.StaticCompute: + { + var program = file.Programs[item.First]; + return StaticStageInputs.TryCompute(program, ((IShaderPipelineHost)this).ComputeWave64Supported, out var record) + ? CompileComputeItem(record, program.Code) + : ShaderCacheOutcome.Failed; + } + + case ShaderWorkKind.SeedCompute: + { + var compute = list.SeedComputes[item.First]; + return CompileComputeItem(compute.Record, compute.Code); + } + + case ShaderWorkKind.RecordedGraphics: + { + var graphics = file.Graphics[item.First]; + return CompileGraphicsItem(graphics.Record, graphics.VertexCode, graphics.PixelCode, deriveVertexCursor: false); + } + + case ShaderWorkKind.SeedGraphics: + { + var graphics = list.SeedGraphics[item.First]; + return CompileGraphicsItem(graphics.Record, graphics.VertexCode, graphics.PixelCode, deriveVertexCursor: false); + } + + default: + { + var vertex = file.Programs[item.First]; + var pixel = file.Programs[item.Second]; + var state = list.States[item.State]; + if (!StaticStageInputs.TryGraphics(vertex, pixel, state, list.SecondInterpolantVariant, out var vertexRecord, out var pixelRecord)) + { + return ShaderCacheOutcome.Failed; + } + + var record = new GraphicsPipelineRecord + { + Vertex = vertexRecord, + Pixel = pixelRecord, + Rendering = state.Rendering, + VertexInput = new PipelineVertexInputState(), + StaticParameters = state.StaticParameters, + Attributes = [], + }; + return CompileGraphicsItem(record, vertex.Code, pixel.Code, deriveVertexCursor: true); + } + } + } + catch (Exception exception) when (exception is InvalidOperationException or InvalidDataException or ArgumentException or + ResourcePlanException or IndexOutOfRangeException) + { + NoteShaderCacheFailure(item.Kind.ToString(), item.Identity, exception.Message); + return ShaderCacheOutcome.Failed; + } + } + + private string? CheckDerivation(ShaderWorkList list, ShaderWorkItem item) + { + try + { + switch (item.Kind) + { + case ShaderWorkKind.RecordedCompute: + { + var compute = list.File.Computes[item.First]; + return CheckComputeDerivation(list, compute.Record, compute.Code); + } + + case ShaderWorkKind.SeedCompute: + { + var compute = list.SeedComputes[item.First]; + return CheckComputeDerivation(list, compute.Record, compute.Code); + } + + case ShaderWorkKind.RecordedGraphics: + { + var graphics = list.File.Graphics[item.First]; + return CheckGraphicsDerivation(list, graphics.Record, graphics.VertexCode, graphics.PixelCode); + } + + case ShaderWorkKind.SeedGraphics: + { + var graphics = list.SeedGraphics[item.First]; + return CheckGraphicsDerivation(list, graphics.Record, graphics.VertexCode, graphics.PixelCode); + } + + default: + return null; + } + } + catch (Exception exception) when (exception is InvalidOperationException or InvalidDataException or ArgumentException or + ResourcePlanException or IndexOutOfRangeException) + { + return $"{(item.IsCompute ? "compute" : "graphics")} underivable 0x{item.Identity:X16} {exception.Message}"; + } + } + + private string CheckComputeDerivation(ShaderWorkList list, StageRecord record, ShaderCodeCapture code) + { + var name = $"0x{record.Hash:X16}"; + if (!list.TryGetProgram(record.Hash, record.CodeSize, out var program)) + { + return $"compute underivable {name} no shader header"; + } + + if (!StaticStageInputs.TryCompute(program, ((IShaderPipelineHost)this).ComputeWave64Supported, out var derived)) + { + return $"compute underivable {name} header registers"; + } + + return CompareDerivedStage(string.Empty, record, code, derived, program.Code) is { } difference + ? $"compute differs {name} {difference}" + : $"compute same {name}"; + } + + private string CheckGraphicsDerivation( + ShaderWorkList list, + GraphicsPipelineRecord record, + ShaderCodeCapture vertexCode, + ShaderCodeCapture? pixelCode) + { + var name = $"vs=0x{record.Vertex.Hash:X16}/ps=0x{record.Pixel?.Hash ?? 0:X16}"; + if (record.Pixel is not { Pixel: { } pixelInputs } pixelRecord || pixelCode is null || + record.Vertex.Vertex is not { FetchEmbedded: false } vertexInputs || + record.VertexInput.BindingCount != 0 || record.VertexInput.AttributeCount != 0) + { + return $"graphics underivable {name} vertex fetch or no pixel shader"; + } + + if (!list.TryGetProgram(record.Vertex.Hash, record.Vertex.CodeSize, out var vertex) || + !list.TryGetProgram(pixelRecord.Hash, pixelRecord.CodeSize, out var pixel)) + { + return $"graphics underivable {name} no shader header"; + } + + var state = ShaderSeed.CreateState(record.Rendering, record.StaticParameters, pixelInputs.Outputs, vertexInputs.ClipSpace); + if (!StaticStageInputs.TryGraphics(vertex, pixel, state, list.SecondInterpolantVariant, out var derivedVertex, out var derivedPixel)) + { + return $"graphics underivable {name} header registers"; + } + + if (CompareDerivedStage("pixel", pixelRecord, pixelCode, derivedPixel, pixel.Code) is { } pixelDifference) + { + return $"graphics differs {name} {pixelDifference}"; + } + + var derivedPixelStage = GetCompiledStage(derivedPixel, pixel.Code)!; + if (CompareDerivedStage("vertex", record.Vertex, vertexCode, derivedVertex.WithPushDataCursor(derivedPixelStage.PushDataEnd), vertex.Code) + is { } vertexDifference) + { + return $"graphics differs {name} {vertexDifference}"; + } + + return list.Contains(ShaderWorkList.StaticGraphicsIdentity(vertex.Identity, pixel.Identity, state.Identity)) + ? $"graphics same {name}" + : $"graphics uncovered {name}"; + } + + private string? CompareDerivedStage(string stage, StageRecord recorded, ShaderCodeCapture recordedCode, StageRecord derived, ShaderCodeCapture derivedCode) + { + var prefix = stage.Length == 0 ? string.Empty : stage + "."; + var left = GetCompiledStage(recorded, recordedCode); + var right = GetCompiledStage(derived, derivedCode); + if (left is null || right is null) + { + return $"{prefix}Translation {(left is null ? "failed" : "ok")} -> {(right is null ? "failed" : "ok")}"; + } + + if (left.Spirv.AsSpan().SequenceEqual(right.Spirv) && left.Bindings.AsSpan().SequenceEqual(right.Bindings) && + left.VertexFetchComponents.AsSpan().SequenceEqual(right.VertexFetchComponents) && left.PushDataEnd == right.PushDataEnd) + { + return null; + } + + var difference = new RecordDifference() + .Compare($"{prefix}Specialization", + ShaderProgramCache.EffectiveSpecialization(recorded, recordedCode), + ShaderProgramCache.EffectiveSpecialization(derived, derivedCode)) + .Compare($"{prefix}UserDataBase", recorded.UserDataBase, derived.UserDataBase) + .Compare($"{prefix}UserDataCount", recorded.UserDataCount, derived.UserDataCount) + .Compare($"{prefix}PushDataCursor", recorded.PushDataCursor, derived.PushDataCursor) + .Compare($"{prefix}Compute", recorded.Compute, derived.Compute) + .Compare($"{prefix}ComputeSystemRegisters", recorded.ComputeSystemRegisters, derived.ComputeSystemRegisters) + .Compare($"{prefix}Vertex", recorded.Vertex, derived.Vertex) + .Compare($"{prefix}Pixel", recorded.Pixel, derived.Pixel); + return difference.Any ? difference.ToString() : $"{prefix}Spirv {left.Spirv.Length} -> {right.Spirv.Length}"; + } + + private static string ShaderCacheStampPart(Type type) => type.Assembly.ManifestModule.ModuleVersionId.ToString("N"); + + private static ulong ShaderCacheStamp(IGuestGpuBackend compiler, ShaderCompileHostFlags flags) => + XxHash3.HashToUInt64(Encoding.UTF8.GetBytes(string.Join( + '|', + ShaderCacheStampPart(typeof(Gen5ShaderTranslator)), + ShaderCacheStampPart(typeof(Gen5SpirvTranslator)), + compiler.GetType().FullName, + flags.ToString()))); + + private CompiledStageRecord? GetCompiledStage(StageRecord record, ShaderCodeCapture code) + { + var identity = ShaderCacheFile.StageIdentity(record); + return _warmStages.GetOrAdd( + identity, + _ => new Lazy( + () => TranslateStage(record, code), + LazyThreadSafetyMode.ExecutionAndPublication)) + .Value; + } + + private CompiledStageRecord? TranslateStage(StageRecord record, ShaderCodeCapture code) + { + if (!ShaderProgramCache.TryReplay(record, code, GuestGpu.Current, _shaderCacheFlags, out var compiled, out var info, out var error)) + { + NoteShaderCacheFailure(record.Stage.ToString(), record.Hash, error); + return null; + } + + var bindings = new List(); + CollectLayoutBindings(bindings, info!, record.Stage); + var pushDataEnd = record.PushDataCursor; + info!.Bindings!.AdvancePushData(ref pushDataEnd); + var stage = new CompiledStageRecord + { + Spirv = compiled!.Payload, + Bindings = bindings + .Select(static binding => new LayoutBindingRecord( + binding.Binding, (int)binding.DescriptorType, binding.DescriptorCount, (uint)binding.StageFlags)) + .ToArray(), + VertexFetchComponents = info.VertexFetchComponents, + PushDataEnd = pushDataEnd, + }; + return stage; + } + + private static List ToLayoutBindings(params CompiledStageRecord?[] stages) + { + var bindings = new List(); + foreach (var stage in stages) + { + if (stage is null) + { + continue; + } + + foreach (var binding in stage.Bindings) + { + bindings.Add(new DescriptorSetLayoutBinding + { + Binding = binding.Binding, + DescriptorType = (DescriptorType)binding.DescriptorType, + DescriptorCount = binding.DescriptorCount, + StageFlags = (ShaderStageFlags)binding.StageFlags, + }); + } + } + + return bindings; + } + + private ShaderModule CreateWarmModule(byte[] spirv) => CreateShaderModule(spirv); + + private void DestroyWarmModule(ShaderModule module) + { + if (module.Handle == 0) + { + return; + } + + _vk.DestroyShaderModule(_device, module, null); + } + + private ShaderCacheOutcome CompileComputeItem(StageRecord record, ShaderCodeCapture code) + { + if (GetCompiledStage(record, code) is not { } stage) + { + return ShaderCacheOutcome.Failed; + } + + var bindings = ToLayoutBindings(stage); + var setLayout = CreateDescriptorSetLayout(bindings, out var usesPushDescriptors, out _); + var layout = CreatePipelineLayout(setLayout, ShaderStageFlags.ComputeBit); + var module = CreateWarmModule(stage.Spirv); + var entryPoint = (byte*)SilkMarshal.StringToPtr("main"); + try + { + var requireSubgroup32 = RequiresComputeSubgroup32(record.Compute!); + var subgroup = RequiredSubgroupSize(); + var info = ComputeCreateInfo(module, layout, entryPoint, requireSubgroup32 ? &subgroup : null); + if (HasStoredPipeline(&info)) + { + return ShaderCacheOutcome.FromStore; + } + + _lastCaptureReachedDictionary = false; + if (CreateCachedPipelineObject(false, &info, out var pipeline, out _, fast: false) != Result.Success) + { + return ShaderCacheOutcome.Failed; + } + + _vk.DestroyPipeline(_device, pipeline, null); + return _lastCaptureReachedDictionary ? ShaderCacheOutcome.Dictionary : ShaderCacheOutcome.Compiled; + } + finally + { + SilkMarshal.Free((nint)entryPoint); + DestroyWarmModule(module); + _vk.DestroyPipelineLayout(_device, layout, null); + _vk.DestroyDescriptorSetLayout(_device, setLayout, null); + } + } + + private ShaderCacheOutcome CompileGraphicsItem( + GraphicsPipelineRecord record, + ShaderCodeCapture vertexCode, + ShaderCodeCapture? pixelCode, + bool deriveVertexCursor) + { + CompiledStageRecord? pixel = null; + if (record.Pixel is { } pixelRecord && (pixel = GetCompiledStage(pixelRecord, pixelCode!)) is null) + { + return ShaderCacheOutcome.Failed; + } + + var vertexRecord = deriveVertexCursor && pixel is not null ? record.Vertex.WithPushDataCursor(pixel.PushDataEnd) : record.Vertex; + if (GetCompiledStage(vertexRecord, vertexCode) is not { } vertex || record.Rendering.ColorCount > _maxColorAttachments) + { + return ShaderCacheOutcome.Failed; + } + + var bindings = ToLayoutBindings(vertex, pixel); + var pushStages = ShaderStageFlags.VertexBit | ShaderStageFlags.FragmentBit; + var setLayout = CreateDescriptorSetLayout(bindings, out var usesPushDescriptors, out _); + var layout = CreatePipelineLayout(setLayout, pushStages); + var vertexModule = CreateWarmModule(vertex.Spirv); + var pixelModule = pixel is null ? default : CreateWarmModule(pixel.Spirv); + try + { + var attributes = new VertexAttributeResource[record.Attributes.Length]; + for (var index = 0; index < attributes.Length; index++) + { + var input = record.VertexInput.Attributes[index]; + attributes[index] = new VertexAttributeResource( + record.Attributes[index].Descriptor, 0, record.Attributes[index].RegisterCount, index, 0, input.Binding, input.Offset); + } + + var description = new GraphicsPipelineDescription + { + Rendering = record.Rendering, + VertexInput = record.VertexInput, + VertexInfo = new VertexInputInfo { Attributes = attributes }, + VertexProgram = new ShaderProgram(0, vertexModule.Handle), + VertexStage = new ShaderProgramInfo + { + Stage = ShaderStageKind.Vertex, + Hash = record.Vertex.Hash, + VertexFetchComponents = vertex.VertexFetchComponents, + }, + PixelInfo = pixel is null ? null : new PixelInputInfo(), + PixelProgram = new ShaderProgram(0, pixelModule.Handle), + PixelStage = pixel is null ? null : new ShaderProgramInfo { Stage = ShaderStageKind.Pixel, Hash = record.Pixel!.Hash }, + StaticParameters = record.StaticParameters, + }; + var outcome = ShaderCacheOutcome.FromStore; + foreach (var (topology, polygonMode) in PrecompileTopologies(record.StaticParameters.Topology)) + { + _lastCaptureReachedDictionary = false; + var pipeline = CreateRenderPipeline(description, topology, layout, polygonMode, precompile: true); + if (pipeline.Handle == 0) + { + continue; + } + + _vk.DestroyPipeline(_device, pipeline, null); + if (_lastCaptureReachedDictionary) + { + outcome = ShaderCacheOutcome.Dictionary; + } + else if (outcome == ShaderCacheOutcome.FromStore) + { + outcome = ShaderCacheOutcome.Compiled; + } + } + + return outcome; + } + finally + { + DestroyWarmModule(vertexModule); + DestroyWarmModule(pixelModule); + _vk.DestroyPipelineLayout(_device, layout, null); + _vk.DestroyDescriptorSetLayout(_device, setLayout, null); + } + } + + private IEnumerable<(PrimitiveTopology Topology, PolygonMode PolygonMode)> PrecompileTopologies(PrimitiveTopology topology) + { + if (topology != PrimitiveTopology.PatchList) + { + yield return (topology, PolygonMode.Fill); + yield break; + } + + yield return (PrimitiveTopology.TriangleStrip, PolygonMode.Fill); + yield return (PrimitiveTopology.TriangleList, PolygonMode.Fill); + if (_supportsFillRectangle) + { + yield return (PrimitiveTopology.TriangleList, PolygonMode.FillRectangleNV); + } + } + + private static PipelineShaderStageRequiredSubgroupSizeCreateInfo RequiredSubgroupSize() => new() + { + SType = StructureType.PipelineShaderStageRequiredSubgroupSizeCreateInfo, + RequiredSubgroupSize = RdnaSubgroupSize, + }; + + private static ComputePipelineCreateInfo ComputeCreateInfo( + ShaderModule module, + PipelineLayout layout, + byte* entryPoint, + PipelineShaderStageRequiredSubgroupSizeCreateInfo* subgroup) => new() + { + SType = StructureType.ComputePipelineCreateInfo, + Stage = new PipelineShaderStageCreateInfo + { + SType = StructureType.PipelineShaderStageCreateInfo, + PNext = subgroup, + Stage = ShaderStageFlags.ComputeBit, + Module = module, + PName = entryPoint, + }, + Layout = layout, + }; + + private void NoteShaderCacheFailure(string kind, ulong identity, string error) + { + if (Interlocked.Increment(ref _shaderCacheFailed) <= MaxLoggedShaderCacheFailures) + { + Console.Error.WriteLine($"[SHADER CACHE] {kind} 0x{identity:X16} is skipped, the translator cannot compile it: {error}"); + } + } + + private static string FormatCacheBytes(long bytes) => + string.Create(CultureInfo.InvariantCulture, $"{bytes / (1024.0 * 1024.0):F1} MB"); + + private void StopShaderCache() + { + Interlocked.Exchange(ref _shaderCacheSessionTimer, null)?.Dispose(); + _shaderCacheCancel.Cancel(); + KillShaderCacheWorkers(); + _shaderCacheThread?.Join(TimeSpan.FromSeconds(30)); + SetShaderCacheState(false); + if (Interlocked.Exchange(ref _shaderCache, null) is not { } file) + { + return; + } + + ShaderInventory.Detach(file); + UseShaderCacheStores(null); + Console.Error.WriteLine( + $"[SHADER CACHE] Session: pipelines from the store {Volatile.Read(ref _shaderCacheFromBinaries)}, " + + $"compiled {Volatile.Read(ref _shaderCacheCompiled)}, rejected binaries {Volatile.Read(ref _shaderCacheBinaryRejected)}."); + SaveShaderSeed(file, VideoOutExports.GetApplicationDisplayName()); + CompactShaderCache(file); + file.Dispose(); + } + + private void CompactShaderCache(ShaderCacheFile file, bool dropUnreferencedBinaries = false) + { + if (file.Compact(_shaderCacheStamp, _pipelineBinaries?.DriverKey ?? [], _shaderCacheDriverStamp, + dropUnreferencedBinaries: dropUnreferencedBinaries) is { } result) + { + Console.Error.WriteLine( + $"[SHADER CACHE] Dropped {result.RecordsDropped} stale records, " + + $"{FormatCacheBytes(result.BytesBefore)} -> {FormatCacheBytes(result.BytesAfter)}."); + } + } + } +} diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.ShaderPrewarm.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.ShaderPrewarm.cs deleted file mode 100644 index d063eae..0000000 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.ShaderPrewarm.cs +++ /dev/null @@ -1,279 +0,0 @@ -// Copyright (C) 2026 SharpEmu Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -namespace SharpEmu.Libs.VideoOut; - -using System.Collections.Concurrent; -using System.Diagnostics; -using SharpEmu.Libs.Gpu; -using SharpEmu.Libs.Gpu.Pipelines; -using SharpEmu.ShaderCompiler; -using SharpEmu.ShaderCompiler.Resources; -using SharpEmu.ShaderCompiler.Vulkan; -using Silk.NET.Vulkan; - -internal static unsafe partial class VulkanVideoPresenter -{ - private sealed partial class Presenter - { - private const int MaxLoggedPrewarmFailures = 8; - private const int PrewarmProgressBatch = 256; - - private ShaderPrewarmList? _shaderPrewarmList; - private ConcurrentDictionary? _prewarmedShaderIdentities; - private ConcurrentDictionary? _runtimeComputeIdentities; - private Thread[] _shaderPrewarmThreads = []; - private volatile bool _shaderPrewarmStopping; - private volatile bool _shaderPrewarmFinished; - private int _shaderPrewarmCompiled; - private int _shaderPrewarmFailed; - private int _shaderPrewarmRuntimeHits; - private readonly ConcurrentQueue _shaderPrewarmCompleted = new(); - - ShaderPrewarmList? IShaderPipelineHost.ShaderPrewarm => Volatile.Read(ref _shaderPrewarmList); - - private void StartShaderPrewarm() - { - if (!StartShaderPrewarmCore()) - { - SetShaderPrewarmState(false); - } - } - - private bool StartShaderPrewarmCore() - { - if (_shaderPrewarmList is not null || - _pipelineCacheShardDirectory is not null || - Environment.GetEnvironmentVariable("SHARPEMU_SHADER_PREWARM") == "0") - { - return false; - } - - var directory = Path.GetDirectoryName(VulkanPipelineCacheStorage.ResolvePath( - VideoOutExports.GetApplicationTitleId(), - Environment.GetEnvironmentVariable("SHARPEMU_VK_PIPELINE_CACHE_PATH"))); - if (string.IsNullOrEmpty(directory) || ShaderPrewarmList.Open(directory) is not { } list) - { - return false; - } - - _prewarmedShaderIdentities = new ConcurrentDictionary(StringComparer.Ordinal); - _runtimeComputeIdentities = new ConcurrentDictionary(StringComparer.Ordinal); - - Volatile.Write(ref _shaderPrewarmList, list); - var compiler = GuestGpu.Current; - var stamp = ShaderPrewarmStamp(compiler); - if (list.LoadedComputeCount == 0) - { - list.WriteStamp(stamp); - _shaderPrewarmFinished = true; - return false; - } - - if (list.IsStampCurrent(stamp)) - { - _shaderPrewarmFinished = true; - Console.Error.WriteLine( - $"[LOADER][INFO] Shader prewarm skipped: compute={list.LoadedComputeCount} reason=same-build-and-driver"); - return false; - } - - var completed = list.ReadProgress(stamp); - var pending = list.LoadedComputes() - .Where(item => !completed.Contains(ShaderPrewarmList.Identity(item.Record))) - .ToList(); - var started = Stopwatch.GetTimestamp(); - if (pending.Count == 0) - { - FinishShaderPrewarm(list, stamp, started); - return false; - } - - var work = new ConcurrentQueue<(ComputePrewarmRecord Record, ShaderCodeCapture Code)>(pending); - var workerCount = Math.Clamp(Environment.ProcessorCount / 4, 1, 4); - var remaining = workerCount; - var host = (IShaderPipelineHost)this; - var sharedInt64Atomics = host.SharedInt64AtomicsEnabled; - var execGuardElision = host.ExecGuardElisionEnabled; - Volatile.Write(ref _shaderPrewarmTotal, pending.Count); - Volatile.Write(ref _shaderPrewarmProgress, 0); - Console.Error.WriteLine( - $"[LOADER][INFO] Shader prewarm started: compute={pending.Count} resumed={completed.Count} workers={workerCount}"); - SetShaderPrewarmState(true); - _shaderPrewarmThreads = new Thread[workerCount]; - for (var index = 0; index < workerCount; index++) - { - _shaderPrewarmThreads[index] = new Thread(() => - { - while (!_shaderPrewarmStopping && work.TryDequeue(out var item)) - { - PrewarmComputePipeline(item.Record, item.Code, compiler, sharedInt64Atomics, execGuardElision); - _shaderPrewarmCompleted.Enqueue(ShaderPrewarmList.Identity(item.Record)); - if (Interlocked.Increment(ref _shaderPrewarmProgress) % PrewarmProgressBatch == 0) - { - FlushShaderPrewarmProgress(list, stamp); - } - } - - FlushShaderPrewarmProgress(list, stamp); - if (Interlocked.Decrement(ref remaining) == 0) - { - FinishShaderPrewarm(list, stamp, started); - } - }) - { - IsBackground = true, - Priority = ThreadPriority.BelowNormal, - Name = $"SharpEmu shader prewarm {index}", - }; - _shaderPrewarmThreads[index].Start(); - } - - return true; - } - - private void FlushShaderPrewarmProgress(ShaderPrewarmList list, string stamp) - { - var batch = new List(); - while (_shaderPrewarmCompleted.TryDequeue(out var identity)) - { - batch.Add(identity); - } - - list.AppendProgress(stamp, batch); - } - - private static string ShaderPrewarmStampPart(Type type) => - type.Assembly.ManifestModule.ModuleVersionId.ToString("N"); - - private string ShaderPrewarmStamp(IGuestGpuBackend compiler) => - string.Join( - '|', - ShaderPrewarmStampPart(typeof(Gen5ShaderTranslator)), - ShaderPrewarmStampPart(typeof(Gen5SpirvTranslator)), - compiler.GetType().FullName, - DriverCacheSignature().TrimEnd('\n')); - - private void PrewarmComputePipeline( - ComputePrewarmRecord record, - ShaderCodeCapture code, - IGuestGpuBackend compiler, - bool sharedInt64Atomics, - bool execGuardElision) - { - if (!ShaderProgramCache.TryCompilePrewarm( - record, code, compiler, sharedInt64Atomics, execGuardElision, - _supportsShaderSignedZeroInfNanPreserveFloat32, out var compiled, out var layout, out var error)) - { - NoteShaderPrewarmFailure(record, error); - return; - } - - var payload = compiled!.Payload; - _prewarmedShaderIdentities![VulkanPipelineCacheStorage.CompiledShaderIdentity(payload)] = 0; - var bindings = new List(); - CollectLayoutBindings(bindings, layout!, ShaderStage.Compute); - DescriptorSetLayout setLayout = default; - PipelineLayout pipelineLayout = default; - ShaderModule module = default; - try - { - setLayout = CreateDescriptorSetLayout(bindings, out _, out _); - pipelineLayout = CreatePipelineLayout(setLayout, ShaderStageFlags.ComputeBit); - module = CreateShaderModule(payload); - var pipeline = CompileComputePipeline(_vk, _device, _pipelineCache, module, pipelineLayout); - _vk.DestroyPipeline(_device, pipeline, null); - Interlocked.Increment(ref _shaderPrewarmCompiled); - } - catch (Exception exception) - { - NoteShaderPrewarmFailure(record, exception.Message); - } - finally - { - if (module.Handle != 0) - { - _vk.DestroyShaderModule(_device, module, null); - } - - if (pipelineLayout.Handle != 0) - { - _vk.DestroyPipelineLayout(_device, pipelineLayout, null); - } - - if (setLayout.Handle != 0) - { - _vk.DestroyDescriptorSetLayout(_device, setLayout, null); - } - } - } - - private void NoteShaderPrewarmFailure(ComputePrewarmRecord record, string error) - { - if (Interlocked.Increment(ref _shaderPrewarmFailed) <= MaxLoggedPrewarmFailures) - { - Console.Error.WriteLine( - $"[LOADER][WARN] Shader prewarm failed: cs=0x{record.Hash:X16} shader=0x{record.Address:X16} error={error}"); - } - } - - private void FinishShaderPrewarm(ShaderPrewarmList list, string stamp, long started) - { - var runtime = _runtimeComputeIdentities!; - var prewarmed = _prewarmedShaderIdentities!; - var matched = runtime.Keys.Count(prewarmed.ContainsKey); - if (_shaderPrewarmStopping) - { - SetShaderPrewarmState(false); - return; - } - - list.WriteStamp(stamp); - list.ClearProgress(); - _shaderPrewarmFinished = true; - SetShaderPrewarmState(false); - _pipelineCacheDirty = true; - Console.Error.WriteLine( - $"[LOADER][INFO] Shader prewarm done: compiled={Volatile.Read(ref _shaderPrewarmCompiled)} " + - $"failed={Volatile.Read(ref _shaderPrewarmFailed)} ms={Stopwatch.GetElapsedTime(started).TotalMilliseconds:F0} " + - $"runtime_compute_modules={runtime.Count} matched={matched}"); - } - - private void NoteRuntimeComputeModule(string identity) - { - if (_runtimeComputeIdentities is not { } runtime || _prewarmedShaderIdentities is not { } prewarmed) - { - return; - } - - runtime[identity] = 0; - if (!_shaderPrewarmFinished || !prewarmed.ContainsKey(identity)) - { - return; - } - - var hits = Interlocked.Increment(ref _shaderPrewarmRuntimeHits); - if (hits is 1 or 10 or 100 or 1000) - { - Console.Error.WriteLine( - $"[LOADER][INFO] Shader prewarm hits: {hits} of runtime_compute_modules={runtime.Count}"); - } - } - - private void StopShaderPrewarm() - { - _shaderPrewarmStopping = true; - SetShaderPrewarmState(false); - foreach (var thread in _shaderPrewarmThreads) - { - thread.Join(TimeSpan.FromSeconds(10)); - } - - _shaderPrewarmThreads = []; - if (Interlocked.Exchange(ref _shaderPrewarmList, null) is { } list) - { - list.Dispose(); - } - } - } -}