[GPU] Replace the shader prewarm list with the shader cache and multi-process precompile

This commit is contained in:
ParantezTech 2026-10-08 02:53:25 +03:00
parent 2920f8843b
commit 79a13903e2
15 changed files with 2298 additions and 1163 deletions

View File

@ -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) ||

View File

@ -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; }

View File

@ -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,

View File

@ -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.");
}

View File

@ -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,

View File

@ -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;

View File

@ -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);
}

View File

@ -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;

View File

@ -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);

View File

@ -26,8 +26,9 @@ internal static unsafe partial class VulkanVideoPresenter
_vk.DeviceWaitIdle(_device);
}
StopShaderPrewarm();
StopPipelineOptimization();
DrainPendingComputePipelines();
StopShaderCache();
SavePipelineCache(force: true);
DrainFrameSlots();
CollectCompletedGuestSubmissions(waitForOldest: false);

View File

@ -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);
}
}
}
}

View File

@ -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)

View File

@ -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

View File

@ -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();
}
}
}
}