[GPU] Replace the shader prewarm list with the shader cache and multi-process precompile
This commit is contained in:
parent
2920f8843b
commit
79a13903e2
|
|
@ -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) ||
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
||||
|
|
|
|||
|
|
@ -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<ColorTargetState> colors,
|
||||
|
|
|
|||
|
|
@ -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<byte> destination)
|
||||
{
|
||||
if (!inner.TryRead(virtualAddress, destination))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_reads.Add((virtualAddress, destination.ToArray()));
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source) => false;
|
||||
|
||||
public CodeRange[] TakeRanges()
|
||||
{
|
||||
_reads.Sort(static (left, right) => left.Address.CompareTo(right.Address));
|
||||
var ranges = new List<CodeRange>();
|
||||
ulong start = 0;
|
||||
var bytes = new List<byte>();
|
||||
foreach (var (address, data) in _reads)
|
||||
{
|
||||
var end = start + (ulong)bytes.Count;
|
||||
if (bytes.Count != 0 && address <= end)
|
||||
{
|
||||
var overlap = (int)(end - address);
|
||||
if (overlap < data.Length)
|
||||
{
|
||||
bytes.AddRange(data.AsSpan(overlap).ToArray());
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (bytes.Count != 0)
|
||||
{
|
||||
ranges.Add(new CodeRange(start, bytes.ToArray()));
|
||||
}
|
||||
|
||||
start = address;
|
||||
bytes.Clear();
|
||||
bytes.AddRange(data);
|
||||
}
|
||||
|
||||
if (bytes.Count != 0)
|
||||
{
|
||||
ranges.Add(new CodeRange(start, bytes.ToArray()));
|
||||
}
|
||||
|
||||
_reads.Clear();
|
||||
return ranges.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class ReplayCpuMemory(IReadOnlyList<CodeRange> ranges) : ICpuMemory
|
||||
{
|
||||
public bool TryRead(ulong virtualAddress, Span<byte> destination)
|
||||
{
|
||||
foreach (var range in ranges)
|
||||
{
|
||||
if (virtualAddress < range.Address)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var offset = virtualAddress - range.Address;
|
||||
if (offset + (ulong)destination.Length <= (ulong)range.Bytes.Length)
|
||||
{
|
||||
range.Bytes.AsSpan((int)offset, destination.Length).CopyTo(destination);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source) => false;
|
||||
}
|
||||
|
||||
internal readonly record struct CodeRange(ulong Address, byte[] Bytes);
|
||||
|
||||
internal readonly record struct FusedCodeParts(ulong EntryHeaderAddress, ulong ContinuationAddress, ulong ContinuationHeaderAddress);
|
||||
|
||||
internal 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<ulong> _computes = [];
|
||||
private readonly Dictionary<(ulong Hash, uint CodeSize, ulong Address), ShaderCodeCapture> _loadedCodes = new();
|
||||
private readonly List<ComputePrewarmRecord> _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<ulong> ReadProgress(string stamp)
|
||||
{
|
||||
var completed = new HashSet<ulong>();
|
||||
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<ulong> identities)
|
||||
{
|
||||
if (identities.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_progressGate)
|
||||
{
|
||||
try
|
||||
{
|
||||
var lines = new List<string>(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<byte> 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.");
|
||||
}
|
||||
|
|
@ -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<ProgramPermutation> 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<ProgramKey, ProgramSourceEntry> _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<ulong, (StageRecord Record, ShaderCodeCapture Code)> _stageRecords = new();
|
||||
private readonly List<uint> _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<EmbeddedVertexFetchPlan, ShaderVertexInput[]> buildVertexInputs,
|
||||
uint computeWaveSize,
|
||||
Action<ShaderResourcePlan>? 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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<string> arguments) => VulkanVideoPresenter.RunShaderCacheWorker(arguments);
|
||||
|
||||
public static int Run(string app0Root, bool full) => VulkanVideoPresenter.RunShaderPrecompile(app0Root, full);
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -26,8 +26,9 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
_vk.DeviceWaitIdle(_device);
|
||||
}
|
||||
|
||||
StopShaderPrewarm();
|
||||
StopPipelineOptimization();
|
||||
DrainPendingComputePipelines();
|
||||
StopShaderCache();
|
||||
SavePipelineCache(force: true);
|
||||
DrainFrameSlots();
|
||||
CollectCompletedGuestSubmissions(waitForOldest: false);
|
||||
|
|
|
|||
|
|
@ -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<byte> 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<Device, PipelineCreateInfoKhr*, PipelineBinaryKeyKHR*, Result> GetPipelineKey;
|
||||
public delegate* unmanaged<Device, PipelineBinaryCreateInfoKHR*, AllocationCallbacks*, PipelineBinaryHandlesInfoKHR*, Result> CreatePipelineBinaries;
|
||||
public delegate* unmanaged<Device, PipelineBinaryKHR, AllocationCallbacks*, void> DestroyPipelineBinary;
|
||||
public delegate* unmanaged<Device, PipelineBinaryDataInfoKHR*, PipelineBinaryKeyKHR*, nuint*, void*, Result> GetPipelineBinaryData;
|
||||
public delegate* unmanaged<Device, ReleaseCapturedPipelineDataInfoKHR*, AllocationCallbacks*, Result> 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<byte>(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<Device, PipelineCreateInfoKhr*, PipelineBinaryKeyKHR*, Result>)Load("vkGetPipelineKeyKHR"),
|
||||
CreatePipelineBinaries = (delegate* unmanaged<Device, PipelineBinaryCreateInfoKHR*, AllocationCallbacks*, PipelineBinaryHandlesInfoKHR*, Result>)Load("vkCreatePipelineBinariesKHR"),
|
||||
DestroyPipelineBinary = (delegate* unmanaged<Device, PipelineBinaryKHR, AllocationCallbacks*, void>)Load("vkDestroyPipelineBinaryKHR"),
|
||||
GetPipelineBinaryData = (delegate* unmanaged<Device, PipelineBinaryDataInfoKHR*, PipelineBinaryKeyKHR*, nuint*, void*, Result>)Load("vkGetPipelineBinaryDataKHR"),
|
||||
ReleaseCapturedPipelineData = (delegate* unmanaged<Device, ReleaseCapturedPipelineDataInfoKHR*, AllocationCallbacks*, Result>)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<byte>(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<byte>(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<byte>(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -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<string, byte>? _prewarmedShaderIdentities;
|
||||
private ConcurrentDictionary<string, byte>? _runtimeComputeIdentities;
|
||||
private Thread[] _shaderPrewarmThreads = [];
|
||||
private volatile bool _shaderPrewarmStopping;
|
||||
private volatile bool _shaderPrewarmFinished;
|
||||
private int _shaderPrewarmCompiled;
|
||||
private int _shaderPrewarmFailed;
|
||||
private int _shaderPrewarmRuntimeHits;
|
||||
private readonly ConcurrentQueue<ulong> _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<string, byte>(StringComparer.Ordinal);
|
||||
_runtimeComputeIdentities = new ConcurrentDictionary<string, byte>(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<ulong>();
|
||||
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<DescriptorSetLayoutBinding>();
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue