1402 lines
63 KiB
C#
1402 lines
63 KiB
C#
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
namespace SharpEmu.Libs.VideoOut;
|
|
|
|
using System.Buffers.Binary;
|
|
using System.Collections.Concurrent;
|
|
using System.Diagnostics;
|
|
using System.Globalization;
|
|
using System.IO.Hashing;
|
|
using System.Reflection;
|
|
using System.Text;
|
|
using SharpEmu.Libs.Gpu;
|
|
using SharpEmu.Libs.Gpu.Pipelines;
|
|
using SharpEmu.Libs.Gpu.Rendering;
|
|
using SharpEmu.Libs.Gpu.Scheduling;
|
|
using SharpEmu.Libs.Gpu.ShaderCache;
|
|
using SharpEmu.ShaderCompiler;
|
|
using SharpEmu.ShaderCompiler.Resources;
|
|
using SharpEmu.ShaderCompiler.Vulkan;
|
|
using Silk.NET.Core.Native;
|
|
using Silk.NET.Vulkan;
|
|
|
|
internal enum ShaderCacheOutcome
|
|
{
|
|
FromStore,
|
|
Compiled,
|
|
Failed,
|
|
Dictionary,
|
|
}
|
|
|
|
internal static unsafe partial class VulkanVideoPresenter
|
|
{
|
|
internal const string ShaderCacheWorkerFlag = "--shader-cache-worker";
|
|
private const int ShaderCacheDictionaryExitCode = 3;
|
|
private const int ShaderCacheItemsPerWorker = 96;
|
|
private const int ShaderCacheMinItemsPerWorker = 8;
|
|
private const int ShaderCacheWorkerThreads = 2;
|
|
private const int ShaderCacheMaxPasses = 4;
|
|
private const string ShaderCacheItemPrefix = "[SHADER CACHE][ITEM] ";
|
|
private const string ShaderCacheDriverPrefix = "[SHADER CACHE][DRIVER] ";
|
|
private const string ShaderCacheBeginPrefix = "[SHADER CACHE][BEGIN] ";
|
|
private const string ShaderCacheDerivePrefix = "[SHADER CACHE][DERIVE] ";
|
|
private static readonly TimeSpan ShaderCacheWorkerStallTimeout = TimeSpan.FromMinutes(3);
|
|
private static readonly TimeSpan ShaderCacheProgressInterval = TimeSpan.FromSeconds(2);
|
|
private static readonly TimeSpan ShaderCacheSessionInterval = TimeSpan.FromSeconds(30);
|
|
|
|
internal static int RunShaderCacheWorker(IReadOnlyList<string> arguments)
|
|
{
|
|
if (arguments.Count != 7 || !long.TryParse(arguments[1], CultureInfo.InvariantCulture, out var length) ||
|
|
!int.TryParse(arguments[2], CultureInfo.InvariantCulture, out var first) ||
|
|
!int.TryParse(arguments[3], CultureInfo.InvariantCulture, out var count) || arguments[5] is not ("0" or "1"))
|
|
{
|
|
Console.Error.WriteLine("[SHADER CACHE][ERROR] The precompile worker arguments are invalid.");
|
|
return 2;
|
|
}
|
|
|
|
SubmissionScheduler.OnFatal = static message => throw new InvalidOperationException(message);
|
|
using var presenter = new Presenter(1, 1, hidden: true);
|
|
presenter.InitializeShaderCompileDevice();
|
|
return presenter.RunShaderCacheWorker(arguments[0], length, first, count, arguments[4], arguments[5] == "1", arguments[6]);
|
|
}
|
|
|
|
internal static int RunShaderPrecompile(string app0Root, bool full)
|
|
{
|
|
SubmissionScheduler.OnFatal = static message => throw new InvalidOperationException(message);
|
|
using var presenter = new Presenter(1, 1, hidden: true);
|
|
presenter.InitializeShaderCompileDevice();
|
|
return presenter.RunStandalonePrecompile(app0Root, full);
|
|
}
|
|
|
|
private sealed partial class Presenter
|
|
{
|
|
private const int MaxLoggedShaderCacheFailures = 8;
|
|
private const int MaxForwardedWorkerLines = 32;
|
|
private const int MaxDerivationExamples = 12;
|
|
|
|
[ThreadStatic]
|
|
private static bool _lastCaptureReachedDictionary;
|
|
|
|
[ThreadStatic]
|
|
private static List<UInt128>? _itemContentKeys;
|
|
|
|
private bool _shaderCompileOnly;
|
|
private string? _shaderSeedPath;
|
|
private ShaderCacheFile? _shaderCache;
|
|
private ulong _shaderCacheStamp;
|
|
private ulong _shaderCacheDriverStamp;
|
|
private ulong _shaderCacheBinaryKey;
|
|
private ShaderCompileHostFlags _shaderCacheFlags;
|
|
private readonly ConcurrentDictionary<ulong, Lazy<CompiledStageRecord?>> _warmStages = new();
|
|
private readonly CancellationTokenSource _shaderCacheCancel = new();
|
|
private readonly ConcurrentDictionary<int, Process> _shaderCacheWorkers = new();
|
|
private Thread? _shaderCacheThread;
|
|
private volatile bool _shaderCacheDriverMismatch;
|
|
private int _shaderCacheFailed;
|
|
private int _shaderCacheFromBinaries;
|
|
private int _shaderCacheCompiled;
|
|
private int _shaderCacheBinaryRejected;
|
|
private int _shaderCacheUnoptimized;
|
|
private Timer? _shaderCacheSessionTimer;
|
|
private (int FromStore, int Compiled, int Rejected) _shaderCacheSessionReported;
|
|
private int _shaderSeedRecordsSaved = -1;
|
|
private int _precompileComputeTotal;
|
|
private int _precompileGraphicsTotal;
|
|
private int _precompileComputeDone;
|
|
private int _precompileGraphicsDone;
|
|
private int _precompileFromStore;
|
|
private int _precompileCompiled;
|
|
private int _precompileFailed;
|
|
private int _precompileDictionary;
|
|
private int _precompileForwardedLines;
|
|
private long _precompileStarted;
|
|
private readonly ConcurrentDictionary<string, int> _derivationResults = new();
|
|
private readonly ConcurrentDictionary<string, int> _derivationDifferences = new();
|
|
private readonly ConcurrentQueue<string> _derivationExamples = new();
|
|
|
|
ShaderCacheFile? IShaderPipelineHost.ShaderCache => Volatile.Read(ref _shaderCache);
|
|
|
|
internal void InitializeShaderCompileDevice()
|
|
{
|
|
_shaderCompileOnly = true;
|
|
_relay.BindCurrentThread();
|
|
_vk = Vk.GetApi();
|
|
uint loaderVersion = Vk.Version10;
|
|
Check(_vk.EnumerateInstanceVersion(ref loaderVersion), "vkEnumerateInstanceVersion");
|
|
if (loaderVersion < Vk.Version13)
|
|
{
|
|
throw new InvalidOperationException("SharpEmu requires Vulkan 1.3.");
|
|
}
|
|
|
|
CreateInstance();
|
|
CreateSurface();
|
|
SelectPhysicalDevice();
|
|
CreateDevice();
|
|
CreatePipelineCache();
|
|
InitializeShaderCacheStamps();
|
|
}
|
|
|
|
private void InitializeShaderCacheStamps()
|
|
{
|
|
_shaderCacheFlags = ShaderCompileHostFlags.From(this);
|
|
_shaderCacheStamp = ShaderCacheStamp(GuestGpu.Current, _shaderCacheFlags);
|
|
_vk.GetPhysicalDeviceProperties(_physicalDevice, out var properties);
|
|
var device = new byte[16 + 3 * sizeof(uint) + sizeof(ulong) + 1];
|
|
new ReadOnlySpan<byte>(properties.PipelineCacheUuid, 16).CopyTo(device);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(device.AsSpan(16), properties.VendorID);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(device.AsSpan(20), properties.DeviceID);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(device.AsSpan(24), properties.DriverVersion);
|
|
BinaryPrimitives.WriteUInt64LittleEndian(device.AsSpan(28), _shaderCacheStamp);
|
|
device[36] = _pipelineBinaries is null ? (byte)0 : (byte)1;
|
|
var pipelineState = string.Join('|',
|
|
_colorWriteEnableApi is not null, _supportsDepthClipControl, _supportsDepthClipEnable, _supportsDepthBounds,
|
|
_supportsFillRectangle, _supportsShaderClipDistance, _maxPushDescriptors, _maxColorAttachments);
|
|
_shaderCacheDriverStamp = XxHash3.HashToUInt64([.. device, .. Encoding.UTF8.GetBytes(pipelineState)]);
|
|
_shaderCacheBinaryKey = _pipelineBinaries is { } api ? ShaderCacheFile.DriverKeyHash(api.DriverKey) : 0;
|
|
}
|
|
|
|
private void StartShaderCache()
|
|
{
|
|
if (!StartShaderCacheCore())
|
|
{
|
|
SetShaderCacheState(false);
|
|
}
|
|
}
|
|
|
|
private bool StartShaderCacheCore()
|
|
{
|
|
var title = VideoOutExports.GetApplicationDisplayName();
|
|
if (!ShaderCacheSettings.Enabled)
|
|
{
|
|
Console.Error.WriteLine($"[SHADER CACHE] {title}: off.");
|
|
return false;
|
|
}
|
|
|
|
if (_pipelineCacheShardDirectory is not null)
|
|
{
|
|
Console.Error.WriteLine($"[SHADER CACHE] {title}: off, the driver cache shards own pipeline caching on this platform.");
|
|
return false;
|
|
}
|
|
|
|
if (ShaderCacheDirectory(VideoOutExports.GetApplicationTitleId()) is not { } directory ||
|
|
ShaderCacheFile.Open(directory) is not { } file)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
InitializeShaderCacheStamps();
|
|
if (file.ImportedLegacyComputes > 0)
|
|
{
|
|
Console.Error.WriteLine($"[SHADER CACHE] {title}: imported {file.ImportedLegacyComputes} compute pipelines from the old prewarm list.");
|
|
}
|
|
|
|
Volatile.Write(ref _shaderCache, file);
|
|
UseShaderCacheStores(file, file);
|
|
ShaderInventory.Attach(file, null);
|
|
_shaderSeedPath = ShaderCacheSettings.SeedPath(VideoOutExports.GetApplicationTitleId());
|
|
LogShaderCacheSummary(title, file);
|
|
var app0Root = Environment.GetEnvironmentVariable("SHARPEMU_APP0_DIR");
|
|
var seedExists = File.Exists(_shaderSeedPath);
|
|
if (ShaderCacheSettings.Learn && !seedExists)
|
|
{
|
|
Console.Error.WriteLine(
|
|
$"[SHADER CACHE] {title}: learning run, nothing is precompiled now. The shader seed is written to {_shaderSeedPath} " +
|
|
"while you play, and the next launch prepares every shader and pipeline from it.");
|
|
StartShaderCacheSession(title);
|
|
return false;
|
|
}
|
|
|
|
var full = ShaderCacheSettings.FullPrecompile || seedExists;
|
|
SetShaderCacheState(true);
|
|
_shaderCacheThread = new Thread(() =>
|
|
{
|
|
try
|
|
{
|
|
Precompile(file, title, app0Root, full, _shaderCacheCancel.Token);
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
}
|
|
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException or InvalidOperationException)
|
|
{
|
|
Console.Error.WriteLine($"[SHADER CACHE][WARN] {title}: the shader precompile stopped: {exception.Message}");
|
|
}
|
|
finally
|
|
{
|
|
SetShaderCacheState(false);
|
|
StartShaderCacheSession(title);
|
|
}
|
|
})
|
|
{
|
|
IsBackground = true,
|
|
Name = "SharpEmu shader precompile",
|
|
};
|
|
_shaderCacheThread.Start();
|
|
return true;
|
|
}
|
|
|
|
internal int RunStandalonePrecompile(string app0Root, bool full)
|
|
{
|
|
var title = VideoOutExports.GetApplicationDisplayName();
|
|
if (ShaderCacheDirectory(VideoOutExports.GetApplicationTitleId()) is not { } directory ||
|
|
ShaderCacheFile.Open(directory) is not { } file)
|
|
{
|
|
return 1;
|
|
}
|
|
|
|
using (file)
|
|
{
|
|
_shaderSeedPath = ShaderCacheSettings.SeedPath(VideoOutExports.GetApplicationTitleId());
|
|
UseShaderCacheStores(file, file);
|
|
LogShaderCacheSummary(title, file);
|
|
using var cancel = new CancellationTokenSource();
|
|
ConsoleCancelEventHandler stop = (_, arguments) =>
|
|
{
|
|
arguments.Cancel = true;
|
|
cancel.Cancel();
|
|
KillShaderCacheWorkers();
|
|
};
|
|
Console.CancelKeyPress += stop;
|
|
try
|
|
{
|
|
Precompile(file, title, app0Root, full, cancel.Token);
|
|
CompactShaderCache(file);
|
|
return _shaderCacheDriverMismatch ? 1 : 0;
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
Console.Error.WriteLine($"[SHADER CACHE] {title}: precompile stopped; finished work is kept and the next run continues.");
|
|
return 1;
|
|
}
|
|
finally
|
|
{
|
|
Console.CancelKeyPress -= stop;
|
|
}
|
|
}
|
|
}
|
|
|
|
private ShaderSeed LoadShaderSeed() =>
|
|
_shaderSeedPath is { Length: > 0 } path ? ShaderSeed.Load(path) : new ShaderSeed();
|
|
|
|
private void SaveShaderSeed(ShaderCacheFile file, string title)
|
|
{
|
|
if (!ShaderCacheSettings.Learn || _shaderSeedPath is not { Length: > 0 } path)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var existing = ShaderSeed.Load(path);
|
|
var merged = existing.MergedWith(ShaderSeed.Learn(file))
|
|
.Stamped(VideoOutExports.GetApplicationTitleId(), VideoOutExports.GetApplicationVersion());
|
|
if (merged.IsEmpty || merged.Serialize().AsSpan().SequenceEqual(existing.Serialize()))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (merged.TrySave(path))
|
|
{
|
|
Console.Error.WriteLine(
|
|
$"[SHADER CACHE] {title}: shader seed {merged.StateCount} graphics states in {merged.GroupCount} passes, " +
|
|
$"{merged.Pairs.Count} shader pairs, played pipelines compute {merged.Computes.Count}, graphics {merged.Graphics.Count} " +
|
|
$"(was {existing.StateCount} states, {existing.Pairs.Count} pairs, {existing.Computes.Count + existing.Graphics.Count} pipelines) at {path}");
|
|
}
|
|
}
|
|
|
|
private static string? ShaderCacheDirectory(string titleId) =>
|
|
Path.GetDirectoryName(VulkanPipelineCacheStorage.ResolvePath(
|
|
titleId,
|
|
Environment.GetEnvironmentVariable("SHARPEMU_VK_PIPELINE_CACHE_PATH")));
|
|
|
|
private void UseShaderCacheStores(ShaderCacheFile? target, params ShaderCacheFile[] sources) =>
|
|
SetPipelineStore(target, sources);
|
|
|
|
private void LogShaderCacheSummary(string title, ShaderCacheFile file)
|
|
{
|
|
int compute = 0, vertex = 0, pixel = 0, scanned = 0;
|
|
var programs = file.SnapshotPrograms();
|
|
foreach (var program in programs)
|
|
{
|
|
if (!StaticStageInputs.TryParse(program, out var header))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
scanned += program.Program.Source == InventorySource.Scanned ? 1 : 0;
|
|
switch (header.Type)
|
|
{
|
|
case AgcShaderType.Compute:
|
|
compute++;
|
|
break;
|
|
case AgcShaderType.Geometry:
|
|
vertex++;
|
|
break;
|
|
case AgcShaderType.Pixel:
|
|
pixel++;
|
|
break;
|
|
}
|
|
}
|
|
|
|
var binaries = _pipelineBinaries is null ? "unsupported by the driver" : file.BinaryCount.ToString(CultureInfo.InvariantCulture);
|
|
var seed = LoadShaderSeed();
|
|
var learned = ShaderSeed.Learn(file).MergedWith(seed);
|
|
var version = VideoOutExports.GetApplicationVersion();
|
|
if (!seed.IsEmpty && seed.GameVersion.Length != 0 && version.Length != 0 &&
|
|
!string.Equals(seed.GameVersion, version, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
Console.Error.WriteLine(
|
|
$"[SHADER CACHE][WARN] {title}: the shader seed was made with game version {seed.GameVersion}, this is {version}; " +
|
|
"shaders that changed between the versions are not paired.");
|
|
}
|
|
|
|
Console.Error.WriteLine(
|
|
$"[SHADER CACHE] {title}: shaders compute {compute}, vertex {vertex}, pixel {pixel} " +
|
|
$"({scanned} from game files, {programs.Length - scanned} from the running game); " +
|
|
$"recorded pipelines compute {file.Computes.Count}, graphics {file.Graphics.Count}; " +
|
|
$"graphics states {learned.StateCount} in {learned.GroupCount} passes " +
|
|
$"({(seed.IsEmpty ? "no seed" : $"seed {seed.StateCount} states, {seed.Pairs.Count} pairs, {seed.Computes.Count + seed.Graphics.Count} played pipelines")}); " +
|
|
$"pipeline binaries {binaries}; {FormatCacheBytes(file.Length)} at {file.Path}");
|
|
}
|
|
|
|
private void Precompile(ShaderCacheFile file, string title, string? app0Root, bool full, CancellationToken cancellation)
|
|
{
|
|
_precompileStarted = Stopwatch.GetTimestamp();
|
|
MergeLeftoverParts(file);
|
|
if (!string.IsNullOrEmpty(app0Root) && Directory.Exists(app0Root))
|
|
{
|
|
ScanGameFiles(file, title, app0Root, cancellation);
|
|
}
|
|
|
|
var prepared = false;
|
|
var complete = false;
|
|
for (var pass = 0; !_shaderCacheDriverMismatch; pass++)
|
|
{
|
|
cancellation.ThrowIfCancellationRequested();
|
|
var length = file.Length;
|
|
List<ShaderWorkItem> pending;
|
|
using (var snapshot = ShaderCacheFile.OpenSnapshot(file.Path, length))
|
|
{
|
|
if (snapshot is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var list = ShaderWorkList.Build(snapshot, full, LoadShaderSeed());
|
|
pending = list.Items.Where(item => !snapshot.IsDone(item.Identity, _shaderCacheDriverStamp)).ToList();
|
|
if (pass == 0)
|
|
{
|
|
LogWorkList(title, list, pending, full);
|
|
}
|
|
}
|
|
|
|
if (pending.Count == 0)
|
|
{
|
|
complete = true;
|
|
break;
|
|
}
|
|
|
|
if (pass == ShaderCacheMaxPasses)
|
|
{
|
|
break;
|
|
}
|
|
|
|
var doneBefore = Volatile.Read(ref _precompileComputeDone) + Volatile.Read(ref _precompileGraphicsDone);
|
|
_precompileComputeTotal = pending.Count(static item => item.IsCompute);
|
|
_precompileGraphicsTotal = pending.Count - _precompileComputeTotal;
|
|
_precompileComputeDone = _precompileGraphicsDone = 0;
|
|
RunPrecompileWorkers(file, title, length, pending.Count, full, cancellation);
|
|
prepared = true;
|
|
if (Volatile.Read(ref _precompileComputeDone) + Volatile.Read(ref _precompileGraphicsDone) == 0 && doneBefore == 0)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (prepared)
|
|
{
|
|
Console.Error.WriteLine(string.Create(
|
|
CultureInfo.InvariantCulture,
|
|
$"[SHADER CACHE] {title}: precompile finished in {Stopwatch.GetElapsedTime(_precompileStarted).TotalSeconds:F1} s; " +
|
|
$"pipelines compiled {Volatile.Read(ref _precompileCompiled)}, already stored {Volatile.Read(ref _precompileFromStore)}, " +
|
|
$"skipped {Volatile.Read(ref _precompileFailed)} the translator cannot compile; pipeline binaries {file.BinaryCount}; {FormatCacheBytes(file.Length)}"));
|
|
LogDerivationCheck(title);
|
|
}
|
|
else
|
|
{
|
|
Console.Error.WriteLine($"[SHADER CACHE] {title}: every known shader is already prepared.");
|
|
}
|
|
|
|
if (complete && prepared)
|
|
{
|
|
CompactShaderCache(file, dropUnreferencedBinaries: true);
|
|
}
|
|
}
|
|
|
|
private void LogDerivationCheck(string title)
|
|
{
|
|
if (_derivationResults.IsEmpty)
|
|
{
|
|
return;
|
|
}
|
|
|
|
int Count(string key) => _derivationResults.GetValueOrDefault(key);
|
|
var differences = string.Join(", ", _derivationDifferences
|
|
.OrderByDescending(static entry => entry.Value)
|
|
.ThenBy(static entry => entry.Key, StringComparer.Ordinal)
|
|
.Select(static entry => $"{entry.Key} {entry.Value}"));
|
|
Console.Error.WriteLine(
|
|
$"[SHADER CACHE] {title}: shader-file derivation of played pipelines: " +
|
|
$"compute same {Count("compute same")}, differs {Count("compute differs")}, not derivable {Count("compute underivable")}; " +
|
|
$"graphics same {Count("graphics same")} (outside the learned states {Count("graphics uncovered")}), " +
|
|
$"differs {Count("graphics differs")}, not derivable {Count("graphics underivable")}" +
|
|
(differences.Length == 0 ? "." : $"; differing fields: {differences}."));
|
|
foreach (var example in _derivationExamples)
|
|
{
|
|
Console.Error.WriteLine($"{ShaderCacheDerivePrefix}{example}");
|
|
}
|
|
}
|
|
|
|
private static void MergeLeftoverParts(ShaderCacheFile file)
|
|
{
|
|
var directory = Path.GetDirectoryName(file.Path)!;
|
|
foreach (var part in Directory.EnumerateFiles(directory, Path.GetFileName(file.Path) + ".part*"))
|
|
{
|
|
file.MergePart(part);
|
|
try
|
|
{
|
|
File.Delete(part);
|
|
}
|
|
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException)
|
|
{
|
|
}
|
|
}
|
|
}
|
|
|
|
private void ScanGameFiles(ShaderCacheFile file, string title, string app0Root, CancellationToken cancellation)
|
|
{
|
|
var started = Stopwatch.GetTimestamp();
|
|
var lastReport = started;
|
|
var result = GameShaderScanner.Scan(app0Root, file, (done, total) =>
|
|
{
|
|
var now = Stopwatch.GetTimestamp();
|
|
if (Stopwatch.GetElapsedTime(Interlocked.Read(ref lastReport), now) < ShaderCacheProgressInterval)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Interlocked.Exchange(ref lastReport, now);
|
|
Console.Error.WriteLine(string.Create(
|
|
CultureInfo.InvariantCulture,
|
|
$"[SHADER CACHE] {title}: scanning game files {done / (1024.0 * 1024.0 * 1024.0):F1}/{total / (1024.0 * 1024.0 * 1024.0):F1} GB"));
|
|
}, cancellation);
|
|
if (result.FilesScanned != 0)
|
|
{
|
|
Console.Error.WriteLine(string.Create(
|
|
CultureInfo.InvariantCulture,
|
|
$"[SHADER CACHE] {title}: scanned {result.FilesScanned} game files ({result.FilesSkipped} unchanged) " +
|
|
$"in {Stopwatch.GetElapsedTime(started).TotalSeconds:F1} s; {result.Programs} new shaders found."));
|
|
}
|
|
}
|
|
|
|
private static void LogWorkList(string title, ShaderWorkList list, List<ShaderWorkItem> pending, bool full)
|
|
{
|
|
int Count(IEnumerable<ShaderWorkItem> items, ShaderWorkKind kind) => items.Count(item => item.Kind == kind);
|
|
Console.Error.WriteLine(
|
|
$"[SHADER CACHE] {title}: to prepare compute {pending.Count(static item => item.IsCompute)} " +
|
|
$"(recorded {Count(pending, ShaderWorkKind.RecordedCompute)}, seed {Count(pending, ShaderWorkKind.SeedCompute)}, " +
|
|
$"from shaders {Count(pending, ShaderWorkKind.StaticCompute)}), " +
|
|
$"graphics {pending.Count(static item => !item.IsCompute)} " +
|
|
$"(recorded {Count(pending, ShaderWorkKind.RecordedGraphics)}, seed {Count(pending, ShaderWorkKind.SeedGraphics)}, " +
|
|
$"learned {Count(pending, ShaderWorkKind.StaticGraphics)}); " +
|
|
$"already prepared {list.Items.Count - pending.Count}." +
|
|
(full
|
|
? $" Graphics states {list.States.Count}."
|
|
: " Learned graphics pipelines are prepared when a shader seed is present, with learning on, or by a full precompile."));
|
|
}
|
|
|
|
private void RunPrecompileWorkers(ShaderCacheFile file, string title, long length, int pendingCount, bool full, CancellationToken cancellation)
|
|
{
|
|
var batches = new ConcurrentQueue<(int First, int Count)>();
|
|
var batchSize = Math.Clamp(
|
|
(pendingCount + ShaderCacheSettings.PrecompileJobs - 1) / ShaderCacheSettings.PrecompileJobs,
|
|
ShaderCacheMinItemsPerWorker, ShaderCacheItemsPerWorker);
|
|
for (var first = 0; first < pendingCount; first += batchSize)
|
|
{
|
|
batches.Enqueue((first, Math.Min(batchSize, pendingCount - first)));
|
|
}
|
|
|
|
var jobs = Math.Min(ShaderCacheSettings.PrecompileJobs, batches.Count);
|
|
using var progress = new Timer(_ => ReportPrecompileProgress(title, jobs), null, ShaderCacheProgressInterval, ShaderCacheProgressInterval);
|
|
var threads = new Thread[jobs];
|
|
for (var slot = 0; slot < jobs; slot++)
|
|
{
|
|
var part = file.Path + ".part" + slot.ToString(CultureInfo.InvariantCulture);
|
|
threads[slot] = new Thread(() =>
|
|
{
|
|
while (!cancellation.IsCancellationRequested && !_shaderCacheDriverMismatch && batches.TryDequeue(out var batch))
|
|
{
|
|
RunPrecompileWorker(file, length, batch.First, batch.Count, part, full, cancellation);
|
|
}
|
|
})
|
|
{
|
|
IsBackground = true,
|
|
Name = $"SharpEmu shader precompile {slot}",
|
|
};
|
|
threads[slot].Start();
|
|
}
|
|
|
|
foreach (var thread in threads)
|
|
{
|
|
thread.Join();
|
|
}
|
|
|
|
cancellation.ThrowIfCancellationRequested();
|
|
}
|
|
|
|
private void RunPrecompileWorker(ShaderCacheFile file, long length, int first, int count, string part, bool full, CancellationToken cancellation)
|
|
{
|
|
var start = new ProcessStartInfo(Environment.ProcessPath ?? throw new InvalidOperationException("The emulator path is unknown."))
|
|
{
|
|
UseShellExecute = false,
|
|
CreateNoWindow = true,
|
|
RedirectStandardOutput = true,
|
|
RedirectStandardError = true,
|
|
};
|
|
if (string.Equals(Path.GetFileNameWithoutExtension(start.FileName), "dotnet", StringComparison.OrdinalIgnoreCase) &&
|
|
Assembly.GetEntryAssembly()?.Location is { Length: > 0 } entry)
|
|
{
|
|
start.ArgumentList.Add(entry);
|
|
}
|
|
|
|
foreach (var argument in (string[])[
|
|
ShaderCacheWorkerFlag, file.Path, length.ToString(CultureInfo.InvariantCulture),
|
|
first.ToString(CultureInfo.InvariantCulture), count.ToString(CultureInfo.InvariantCulture), part, full ? "1" : "0",
|
|
_shaderSeedPath ?? string.Empty])
|
|
{
|
|
start.ArgumentList.Add(argument);
|
|
}
|
|
|
|
start.Environment.Remove("SHARPEMU_RENDERDOC_WAIT");
|
|
start.Environment.Remove("SHARPEMU_RENDERDOC_CAPTURE");
|
|
var driverCache = Path.Combine(Path.GetTempPath(), "SharpEmu", "precompile", Guid.NewGuid().ToString("N"));
|
|
Directory.CreateDirectory(driverCache);
|
|
start.Environment["__GL_SHADER_DISK_CACHE"] = "1";
|
|
start.Environment["__GL_SHADER_DISK_CACHE_PATH"] = driverCache;
|
|
using var process = Process.Start(start) ?? throw new InvalidOperationException("The shader precompile worker could not start.");
|
|
_shaderCacheWorkers[process.Id] = process;
|
|
try
|
|
{
|
|
try
|
|
{
|
|
process.PriorityClass = ProcessPriorityClass.BelowNormal;
|
|
}
|
|
catch (Exception exception) when (exception is InvalidOperationException or System.ComponentModel.Win32Exception)
|
|
{
|
|
}
|
|
|
|
var inFlight = new ConcurrentDictionary<ulong, byte>();
|
|
var lastActivity = Stopwatch.GetTimestamp();
|
|
process.OutputDataReceived += (_, line) =>
|
|
{
|
|
Interlocked.Exchange(ref lastActivity, Stopwatch.GetTimestamp());
|
|
HandleWorkerLine(line.Data, inFlight);
|
|
};
|
|
process.ErrorDataReceived += (_, line) => ForwardWorkerError(line.Data);
|
|
process.BeginOutputReadLine();
|
|
process.BeginErrorReadLine();
|
|
var stalled = false;
|
|
while (!process.WaitForExit(250))
|
|
{
|
|
if (cancellation.IsCancellationRequested)
|
|
{
|
|
KillShaderCacheWorker(process);
|
|
break;
|
|
}
|
|
|
|
if (Stopwatch.GetElapsedTime(Interlocked.Read(ref lastActivity)) > ShaderCacheWorkerStallTimeout)
|
|
{
|
|
stalled = true;
|
|
KillShaderCacheWorker(process);
|
|
break;
|
|
}
|
|
}
|
|
|
|
process.WaitForExit();
|
|
if (stalled)
|
|
{
|
|
foreach (var identity in inFlight.Keys)
|
|
{
|
|
file.AddDone(identity, _shaderCacheDriverStamp);
|
|
Interlocked.Increment(ref _precompileFailed);
|
|
Console.Error.WriteLine(
|
|
$"[SHADER CACHE][WARN] Pipeline 0x{identity:X16} did not finish in {ShaderCacheWorkerStallTimeout.TotalMinutes:F0} minutes and is skipped.");
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (process.ExitCode is not (0 or ShaderCacheDictionaryExitCode) && !cancellation.IsCancellationRequested)
|
|
{
|
|
Console.Error.WriteLine($"[SHADER CACHE][WARN] A precompile worker exited with {process.ExitCode}.");
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_shaderCacheWorkers.TryRemove(process.Id, out _);
|
|
try
|
|
{
|
|
Directory.Delete(driverCache, recursive: true);
|
|
}
|
|
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException)
|
|
{
|
|
}
|
|
|
|
if (File.Exists(part))
|
|
{
|
|
file.MergePart(part);
|
|
try
|
|
{
|
|
File.Delete(part);
|
|
}
|
|
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException)
|
|
{
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void HandleWorkerLine(string? line, ConcurrentDictionary<ulong, byte> inFlight)
|
|
{
|
|
if (line is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (line.StartsWith(ShaderCacheBeginPrefix, StringComparison.Ordinal))
|
|
{
|
|
if (ulong.TryParse(line[ShaderCacheBeginPrefix.Length..], NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var started))
|
|
{
|
|
inFlight.TryAdd(started, 0);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (line.StartsWith(ShaderCacheDerivePrefix, StringComparison.Ordinal))
|
|
{
|
|
NoteDerivation(line[ShaderCacheDerivePrefix.Length..]);
|
|
return;
|
|
}
|
|
|
|
if (line.StartsWith(ShaderCacheDriverPrefix, StringComparison.Ordinal))
|
|
{
|
|
var fields = line[ShaderCacheDriverPrefix.Length..].Split(' ');
|
|
if (fields.Length == 2 &&
|
|
ulong.TryParse(fields[0], NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var stamp) &&
|
|
ulong.TryParse(fields[1], NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var binaryKey) &&
|
|
(stamp != _shaderCacheDriverStamp || binaryKey != _shaderCacheBinaryKey) && !_shaderCacheDriverMismatch)
|
|
{
|
|
_shaderCacheDriverMismatch = true;
|
|
Console.Error.WriteLine(
|
|
$"[SHADER CACHE][WARN] A precompile worker sees another driver identity " +
|
|
$"(stamp {stamp:X16}/{_shaderCacheDriverStamp:X16}, pipeline key {binaryKey:X16}/{_shaderCacheBinaryKey:X16}); " +
|
|
"the precompile stops so it does not rebuild the same pipelines on every launch.");
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (!line.StartsWith(ShaderCacheItemPrefix, StringComparison.Ordinal))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var parts = line[ShaderCacheItemPrefix.Length..].Split(' ');
|
|
if (parts.Length != 3 || !Enum.TryParse<ShaderCacheOutcome>(parts[1], out var outcome) ||
|
|
!ulong.TryParse(parts[2], NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var finished))
|
|
{
|
|
return;
|
|
}
|
|
|
|
inFlight.TryRemove(finished, out _);
|
|
|
|
if (outcome != ShaderCacheOutcome.Dictionary)
|
|
{
|
|
Interlocked.Increment(ref parts[0] == "compute" ? ref _precompileComputeDone : ref _precompileGraphicsDone);
|
|
}
|
|
|
|
switch (outcome)
|
|
{
|
|
case ShaderCacheOutcome.FromStore:
|
|
Interlocked.Increment(ref _precompileFromStore);
|
|
break;
|
|
case ShaderCacheOutcome.Compiled:
|
|
Interlocked.Increment(ref _precompileCompiled);
|
|
break;
|
|
case ShaderCacheOutcome.Failed:
|
|
Interlocked.Increment(ref _precompileFailed);
|
|
break;
|
|
default:
|
|
Interlocked.Increment(ref _precompileDictionary);
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void NoteDerivation(string result)
|
|
{
|
|
var fields = result.Split(' ', 4);
|
|
if (fields.Length < 3)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_derivationResults.AddOrUpdate($"{fields[0]} {fields[1]}", 1, static (_, count) => count + 1);
|
|
if (fields[1] == "differs" && fields.Length == 4)
|
|
{
|
|
_derivationDifferences.AddOrUpdate($"{fields[0]} {RecordDifference.Category(fields[3])}", 1, static (_, count) => count + 1);
|
|
}
|
|
|
|
if (fields[1] is "differs" or "underivable" && _derivationExamples.Count < MaxDerivationExamples)
|
|
{
|
|
_derivationExamples.Enqueue(result);
|
|
}
|
|
}
|
|
|
|
private void ForwardWorkerError(string? line)
|
|
{
|
|
if (line is not null &&
|
|
(line.Contains("[SHADER CACHE]", StringComparison.Ordinal) || line.Contains("[ERROR]", StringComparison.Ordinal) ||
|
|
line.Contains("[FATAL]", StringComparison.Ordinal)) &&
|
|
Interlocked.Increment(ref _precompileForwardedLines) <= MaxForwardedWorkerLines)
|
|
{
|
|
Console.Error.WriteLine(line);
|
|
}
|
|
}
|
|
|
|
private void ReportPrecompileProgress(string title, int jobs)
|
|
{
|
|
Console.Error.WriteLine(string.Create(
|
|
CultureInfo.InvariantCulture,
|
|
$"[SHADER CACHE] {title}: precompiling compute {Volatile.Read(ref _precompileComputeDone)}/{_precompileComputeTotal} | " +
|
|
$"graphics {Volatile.Read(ref _precompileGraphicsDone)}/{_precompileGraphicsTotal} | " +
|
|
$"compiled {Volatile.Read(ref _precompileCompiled)} | stored {Volatile.Read(ref _precompileFromStore)} | " +
|
|
$"skipped {Volatile.Read(ref _precompileFailed)} | {jobs} workers | {Stopwatch.GetElapsedTime(_precompileStarted).TotalSeconds:F0} s"));
|
|
}
|
|
|
|
private void StartShaderCacheSession(string title)
|
|
{
|
|
Interlocked.Exchange(ref _shaderCacheFromBinaries, 0);
|
|
Interlocked.Exchange(ref _shaderCacheCompiled, 0);
|
|
Interlocked.Exchange(ref _shaderCacheBinaryRejected, 0);
|
|
Interlocked.Exchange(ref _shaderCacheUnoptimized, 0);
|
|
_shaderCacheSessionTimer = new Timer(_ => ReportShaderCacheSession(title), null, ShaderCacheSessionInterval, ShaderCacheSessionInterval);
|
|
}
|
|
|
|
private void ReportShaderCacheSession(string title)
|
|
{
|
|
if (Volatile.Read(ref _shaderCache) is { } file &&
|
|
file.SnapshotGraphics().Length + file.SnapshotComputes().Length is var recorded && recorded != _shaderSeedRecordsSaved)
|
|
{
|
|
_shaderSeedRecordsSaved = recorded;
|
|
SaveShaderSeed(file, title);
|
|
}
|
|
|
|
var current = (Volatile.Read(ref _shaderCacheFromBinaries), Volatile.Read(ref _shaderCacheCompiled), Volatile.Read(ref _shaderCacheBinaryRejected));
|
|
if (current == _shaderCacheSessionReported)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_shaderCacheSessionReported = current;
|
|
Console.Error.WriteLine(
|
|
$"[SHADER CACHE] {title}: this session pipelines from the store {current.Item1}, compiled {current.Item2} " +
|
|
$"({Volatile.Read(ref _shaderCacheUnoptimized)} of them unoptimized first), rejected binaries {current.Item3}.");
|
|
}
|
|
|
|
private void KillShaderCacheWorkers()
|
|
{
|
|
foreach (var process in _shaderCacheWorkers.Values)
|
|
{
|
|
KillShaderCacheWorker(process);
|
|
}
|
|
}
|
|
|
|
private static void KillShaderCacheWorker(Process process)
|
|
{
|
|
try
|
|
{
|
|
process.Kill(entireProcessTree: true);
|
|
}
|
|
catch (Exception exception) when (exception is InvalidOperationException or System.ComponentModel.Win32Exception)
|
|
{
|
|
}
|
|
}
|
|
|
|
internal int RunShaderCacheWorker(string cachePath, long length, int first, int count, string partPath, bool full, string seedPath)
|
|
{
|
|
_shaderSeedPath = seedPath;
|
|
Console.Out.WriteLine($"{ShaderCacheDriverPrefix}{_shaderCacheDriverStamp:X16} {_shaderCacheBinaryKey:X16}");
|
|
using var snapshot = ShaderCacheFile.OpenSnapshot(cachePath, length);
|
|
using var part = ShaderCacheFile.OpenPart(partPath);
|
|
if (snapshot is null || part is null)
|
|
{
|
|
return 1;
|
|
}
|
|
|
|
UseShaderCacheStores(part, snapshot, part);
|
|
var list = ShaderWorkList.Build(snapshot, full, LoadShaderSeed());
|
|
var slice = list.Items
|
|
.Where(item => !snapshot.IsDone(item.Identity, _shaderCacheDriverStamp))
|
|
.Skip(first)
|
|
.Take(count)
|
|
.ToArray();
|
|
var next = -1;
|
|
var output = new object();
|
|
var threads = new Thread[Math.Min(ShaderCacheWorkerThreads, Math.Max(slice.Length, 1))];
|
|
for (var index = 0; index < threads.Length; index++)
|
|
{
|
|
threads[index] = new Thread(() =>
|
|
{
|
|
while (!_pipelineBinaryDictionaryReached)
|
|
{
|
|
var position = Interlocked.Increment(ref next);
|
|
if (position >= slice.Length)
|
|
{
|
|
break;
|
|
}
|
|
|
|
var item = slice[position];
|
|
lock (output)
|
|
{
|
|
Console.Out.WriteLine($"{ShaderCacheBeginPrefix}{item.Identity:X16}");
|
|
}
|
|
|
|
_itemContentKeys = [];
|
|
var outcome = ProcessWorkItem(list, item);
|
|
var derivation = outcome == ShaderCacheOutcome.Dictionary ? null : CheckDerivation(list, item);
|
|
if (outcome != ShaderCacheOutcome.Dictionary)
|
|
{
|
|
part.AddDone(item.Identity, _shaderCacheDriverStamp, _itemContentKeys);
|
|
}
|
|
|
|
_itemContentKeys = null;
|
|
lock (output)
|
|
{
|
|
if (derivation is not null)
|
|
{
|
|
Console.Out.WriteLine($"{ShaderCacheDerivePrefix}{derivation}");
|
|
}
|
|
|
|
Console.Out.WriteLine($"{ShaderCacheItemPrefix}{(item.IsCompute ? "compute" : "graphics")} {outcome} {item.Identity:X16}");
|
|
}
|
|
}
|
|
})
|
|
{
|
|
Name = $"SharpEmu shader precompile worker {index}",
|
|
};
|
|
threads[index].Start();
|
|
}
|
|
|
|
foreach (var thread in threads)
|
|
{
|
|
thread.Join();
|
|
}
|
|
|
|
Console.Out.Flush();
|
|
return _pipelineBinaryDictionaryReached ? ShaderCacheDictionaryExitCode : 0;
|
|
}
|
|
|
|
private ShaderCacheOutcome ProcessWorkItem(ShaderWorkList list, ShaderWorkItem item)
|
|
{
|
|
try
|
|
{
|
|
var file = list.File;
|
|
switch (item.Kind)
|
|
{
|
|
case ShaderWorkKind.RecordedCompute:
|
|
{
|
|
var compute = file.Computes[item.First];
|
|
return CompileComputeItem(compute.Record, compute.Code);
|
|
}
|
|
|
|
case ShaderWorkKind.StaticCompute:
|
|
{
|
|
var program = file.Programs[item.First];
|
|
return StaticStageInputs.TryCompute(program, ((IShaderPipelineHost)this).ComputeWave64Supported, out var record)
|
|
? CompileComputeItem(record, program.Code)
|
|
: ShaderCacheOutcome.Failed;
|
|
}
|
|
|
|
case ShaderWorkKind.SeedCompute:
|
|
{
|
|
var compute = list.SeedComputes[item.First];
|
|
return CompileComputeItem(compute.Record, compute.Code);
|
|
}
|
|
|
|
case ShaderWorkKind.RecordedGraphics:
|
|
{
|
|
var graphics = file.Graphics[item.First];
|
|
return CompileGraphicsItem(graphics.Record, graphics.VertexCode, graphics.PixelCode, deriveVertexCursor: false);
|
|
}
|
|
|
|
case ShaderWorkKind.SeedGraphics:
|
|
{
|
|
var graphics = list.SeedGraphics[item.First];
|
|
return CompileGraphicsItem(graphics.Record, graphics.VertexCode, graphics.PixelCode, deriveVertexCursor: false);
|
|
}
|
|
|
|
default:
|
|
{
|
|
var vertex = file.Programs[item.First];
|
|
var pixel = file.Programs[item.Second];
|
|
var state = list.States[item.State];
|
|
if (!StaticStageInputs.TryGraphics(vertex, pixel, state, list.SecondInterpolantVariant, out var vertexRecord, out var pixelRecord))
|
|
{
|
|
return ShaderCacheOutcome.Failed;
|
|
}
|
|
|
|
var record = new GraphicsPipelineRecord
|
|
{
|
|
Vertex = vertexRecord,
|
|
Pixel = pixelRecord,
|
|
Rendering = state.Rendering,
|
|
VertexInput = new PipelineVertexInputState(),
|
|
StaticParameters = state.StaticParameters,
|
|
Attributes = [],
|
|
};
|
|
return CompileGraphicsItem(record, vertex.Code, pixel.Code, deriveVertexCursor: true);
|
|
}
|
|
}
|
|
}
|
|
catch (Exception exception) when (exception is InvalidOperationException or InvalidDataException or ArgumentException or
|
|
ResourcePlanException or IndexOutOfRangeException)
|
|
{
|
|
NoteShaderCacheFailure(item.Kind.ToString(), item.Identity, exception.Message);
|
|
return ShaderCacheOutcome.Failed;
|
|
}
|
|
}
|
|
|
|
private string? CheckDerivation(ShaderWorkList list, ShaderWorkItem item)
|
|
{
|
|
try
|
|
{
|
|
switch (item.Kind)
|
|
{
|
|
case ShaderWorkKind.RecordedCompute:
|
|
{
|
|
var compute = list.File.Computes[item.First];
|
|
return CheckComputeDerivation(list, compute.Record, compute.Code);
|
|
}
|
|
|
|
case ShaderWorkKind.SeedCompute:
|
|
{
|
|
var compute = list.SeedComputes[item.First];
|
|
return CheckComputeDerivation(list, compute.Record, compute.Code);
|
|
}
|
|
|
|
case ShaderWorkKind.RecordedGraphics:
|
|
{
|
|
var graphics = list.File.Graphics[item.First];
|
|
return CheckGraphicsDerivation(list, graphics.Record, graphics.VertexCode, graphics.PixelCode);
|
|
}
|
|
|
|
case ShaderWorkKind.SeedGraphics:
|
|
{
|
|
var graphics = list.SeedGraphics[item.First];
|
|
return CheckGraphicsDerivation(list, graphics.Record, graphics.VertexCode, graphics.PixelCode);
|
|
}
|
|
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
catch (Exception exception) when (exception is InvalidOperationException or InvalidDataException or ArgumentException or
|
|
ResourcePlanException or IndexOutOfRangeException)
|
|
{
|
|
return $"{(item.IsCompute ? "compute" : "graphics")} underivable 0x{item.Identity:X16} {exception.Message}";
|
|
}
|
|
}
|
|
|
|
private string CheckComputeDerivation(ShaderWorkList list, StageRecord record, ShaderCodeCapture code)
|
|
{
|
|
var name = $"0x{record.Hash:X16}";
|
|
if (!list.TryGetProgram(record.Hash, record.CodeSize, out var program))
|
|
{
|
|
return $"compute underivable {name} no shader header";
|
|
}
|
|
|
|
if (!StaticStageInputs.TryCompute(program, ((IShaderPipelineHost)this).ComputeWave64Supported, out var derived))
|
|
{
|
|
return $"compute underivable {name} header registers";
|
|
}
|
|
|
|
return CompareDerivedStage(string.Empty, record, code, derived, program.Code) is { } difference
|
|
? $"compute differs {name} {difference}"
|
|
: $"compute same {name}";
|
|
}
|
|
|
|
private string CheckGraphicsDerivation(
|
|
ShaderWorkList list,
|
|
GraphicsPipelineRecord record,
|
|
ShaderCodeCapture vertexCode,
|
|
ShaderCodeCapture? pixelCode)
|
|
{
|
|
var name = $"vs=0x{record.Vertex.Hash:X16}/ps=0x{record.Pixel?.Hash ?? 0:X16}";
|
|
if (record.Pixel is not { Pixel: { } pixelInputs } pixelRecord || pixelCode is null ||
|
|
record.Vertex.Vertex is not { FetchEmbedded: false } vertexInputs ||
|
|
record.VertexInput.BindingCount != 0 || record.VertexInput.AttributeCount != 0)
|
|
{
|
|
return $"graphics underivable {name} vertex fetch or no pixel shader";
|
|
}
|
|
|
|
if (!list.TryGetProgram(record.Vertex.Hash, record.Vertex.CodeSize, out var vertex) ||
|
|
!list.TryGetProgram(pixelRecord.Hash, pixelRecord.CodeSize, out var pixel))
|
|
{
|
|
return $"graphics underivable {name} no shader header";
|
|
}
|
|
|
|
var state = ShaderSeed.CreateState(record.Rendering, record.StaticParameters, pixelInputs.Outputs, vertexInputs.ClipSpace);
|
|
if (!StaticStageInputs.TryGraphics(vertex, pixel, state, list.SecondInterpolantVariant, out var derivedVertex, out var derivedPixel))
|
|
{
|
|
return $"graphics underivable {name} header registers";
|
|
}
|
|
|
|
if (CompareDerivedStage("pixel", pixelRecord, pixelCode, derivedPixel, pixel.Code) is { } pixelDifference)
|
|
{
|
|
return $"graphics differs {name} {pixelDifference}";
|
|
}
|
|
|
|
var derivedPixelStage = GetCompiledStage(derivedPixel, pixel.Code)!;
|
|
if (CompareDerivedStage("vertex", record.Vertex, vertexCode, derivedVertex.WithPushDataCursor(derivedPixelStage.PushDataEnd), vertex.Code)
|
|
is { } vertexDifference)
|
|
{
|
|
return $"graphics differs {name} {vertexDifference}";
|
|
}
|
|
|
|
return list.Contains(ShaderWorkList.StaticGraphicsIdentity(vertex.Identity, pixel.Identity, state.Identity))
|
|
? $"graphics same {name}"
|
|
: $"graphics uncovered {name}";
|
|
}
|
|
|
|
private string? CompareDerivedStage(string stage, StageRecord recorded, ShaderCodeCapture recordedCode, StageRecord derived, ShaderCodeCapture derivedCode)
|
|
{
|
|
var prefix = stage.Length == 0 ? string.Empty : stage + ".";
|
|
var left = GetCompiledStage(recorded, recordedCode);
|
|
var right = GetCompiledStage(derived, derivedCode);
|
|
if (left is null || right is null)
|
|
{
|
|
return $"{prefix}Translation {(left is null ? "failed" : "ok")} -> {(right is null ? "failed" : "ok")}";
|
|
}
|
|
|
|
if (left.Spirv.AsSpan().SequenceEqual(right.Spirv) && left.Bindings.AsSpan().SequenceEqual(right.Bindings) &&
|
|
left.VertexFetchComponents.AsSpan().SequenceEqual(right.VertexFetchComponents) && left.PushDataEnd == right.PushDataEnd)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var difference = new RecordDifference()
|
|
.Compare($"{prefix}Specialization",
|
|
ShaderProgramCache.EffectiveSpecialization(recorded, recordedCode),
|
|
ShaderProgramCache.EffectiveSpecialization(derived, derivedCode))
|
|
.Compare($"{prefix}UserDataBase", recorded.UserDataBase, derived.UserDataBase)
|
|
.Compare($"{prefix}UserDataCount", recorded.UserDataCount, derived.UserDataCount)
|
|
.Compare($"{prefix}PushDataCursor", recorded.PushDataCursor, derived.PushDataCursor)
|
|
.Compare($"{prefix}Compute", recorded.Compute, derived.Compute)
|
|
.Compare($"{prefix}ComputeSystemRegisters", recorded.ComputeSystemRegisters, derived.ComputeSystemRegisters)
|
|
.Compare($"{prefix}Vertex", recorded.Vertex, derived.Vertex)
|
|
.Compare($"{prefix}Pixel", recorded.Pixel, derived.Pixel);
|
|
return difference.Any ? difference.ToString() : $"{prefix}Spirv {left.Spirv.Length} -> {right.Spirv.Length}";
|
|
}
|
|
|
|
private static string ShaderCacheStampPart(Type type) => type.Assembly.ManifestModule.ModuleVersionId.ToString("N");
|
|
|
|
private static ulong ShaderCacheStamp(IGuestGpuBackend compiler, ShaderCompileHostFlags flags) =>
|
|
XxHash3.HashToUInt64(Encoding.UTF8.GetBytes(string.Join(
|
|
'|',
|
|
ShaderCacheStampPart(typeof(Gen5ShaderTranslator)),
|
|
ShaderCacheStampPart(typeof(Gen5SpirvTranslator)),
|
|
compiler.GetType().FullName,
|
|
flags.ToString())));
|
|
|
|
private CompiledStageRecord? GetCompiledStage(StageRecord record, ShaderCodeCapture code)
|
|
{
|
|
var identity = ShaderCacheFile.StageIdentity(record);
|
|
return _warmStages.GetOrAdd(
|
|
identity,
|
|
_ => new Lazy<CompiledStageRecord?>(
|
|
() => TranslateStage(record, code),
|
|
LazyThreadSafetyMode.ExecutionAndPublication))
|
|
.Value;
|
|
}
|
|
|
|
private CompiledStageRecord? TranslateStage(StageRecord record, ShaderCodeCapture code)
|
|
{
|
|
if (!ShaderProgramCache.TryReplay(record, code, GuestGpu.Current, _shaderCacheFlags, out var compiled, out var info, out var error))
|
|
{
|
|
NoteShaderCacheFailure(record.Stage.ToString(), record.Hash, error);
|
|
return null;
|
|
}
|
|
|
|
var bindings = new List<DescriptorSetLayoutBinding>();
|
|
CollectLayoutBindings(bindings, info!, record.Stage);
|
|
var pushDataEnd = record.PushDataCursor;
|
|
info!.Bindings!.AdvancePushData(ref pushDataEnd);
|
|
var stage = new CompiledStageRecord
|
|
{
|
|
Spirv = compiled!.Payload,
|
|
Bindings = bindings
|
|
.Select(static binding => new LayoutBindingRecord(
|
|
binding.Binding, (int)binding.DescriptorType, binding.DescriptorCount, (uint)binding.StageFlags))
|
|
.ToArray(),
|
|
VertexFetchComponents = info.VertexFetchComponents,
|
|
PushDataEnd = pushDataEnd,
|
|
};
|
|
return stage;
|
|
}
|
|
|
|
private static List<DescriptorSetLayoutBinding> ToLayoutBindings(params CompiledStageRecord?[] stages)
|
|
{
|
|
var bindings = new List<DescriptorSetLayoutBinding>();
|
|
foreach (var stage in stages)
|
|
{
|
|
if (stage is null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
foreach (var binding in stage.Bindings)
|
|
{
|
|
bindings.Add(new DescriptorSetLayoutBinding
|
|
{
|
|
Binding = binding.Binding,
|
|
DescriptorType = (DescriptorType)binding.DescriptorType,
|
|
DescriptorCount = binding.DescriptorCount,
|
|
StageFlags = (ShaderStageFlags)binding.StageFlags,
|
|
});
|
|
}
|
|
}
|
|
|
|
return bindings;
|
|
}
|
|
|
|
private ShaderModule CreateWarmModule(byte[] spirv) => CreateShaderModule(spirv);
|
|
|
|
private void DestroyWarmModule(ShaderModule module)
|
|
{
|
|
if (module.Handle == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_vk.DestroyShaderModule(_device, module, null);
|
|
}
|
|
|
|
private ShaderCacheOutcome CompileComputeItem(StageRecord record, ShaderCodeCapture code)
|
|
{
|
|
if (GetCompiledStage(record, code) is not { } stage)
|
|
{
|
|
return ShaderCacheOutcome.Failed;
|
|
}
|
|
|
|
var bindings = ToLayoutBindings(stage);
|
|
var setLayout = CreateDescriptorSetLayout(bindings, out var usesPushDescriptors, out _);
|
|
var layout = CreatePipelineLayout(setLayout, ShaderStageFlags.ComputeBit);
|
|
var module = CreateWarmModule(stage.Spirv);
|
|
var entryPoint = (byte*)SilkMarshal.StringToPtr("main");
|
|
try
|
|
{
|
|
var requireSubgroup32 = RequiresComputeSubgroup32(record.Compute!);
|
|
var subgroup = RequiredSubgroupSize();
|
|
var info = ComputeCreateInfo(module, layout, entryPoint, requireSubgroup32 ? &subgroup : null);
|
|
if (HasStoredPipeline(&info))
|
|
{
|
|
return ShaderCacheOutcome.FromStore;
|
|
}
|
|
|
|
_lastCaptureReachedDictionary = false;
|
|
if (CreateCachedPipelineObject(false, &info, out var pipeline, out _, fast: false) != Result.Success)
|
|
{
|
|
return ShaderCacheOutcome.Failed;
|
|
}
|
|
|
|
_vk.DestroyPipeline(_device, pipeline, null);
|
|
return _lastCaptureReachedDictionary ? ShaderCacheOutcome.Dictionary : ShaderCacheOutcome.Compiled;
|
|
}
|
|
finally
|
|
{
|
|
SilkMarshal.Free((nint)entryPoint);
|
|
DestroyWarmModule(module);
|
|
_vk.DestroyPipelineLayout(_device, layout, null);
|
|
_vk.DestroyDescriptorSetLayout(_device, setLayout, null);
|
|
}
|
|
}
|
|
|
|
private ShaderCacheOutcome CompileGraphicsItem(
|
|
GraphicsPipelineRecord record,
|
|
ShaderCodeCapture vertexCode,
|
|
ShaderCodeCapture? pixelCode,
|
|
bool deriveVertexCursor)
|
|
{
|
|
CompiledStageRecord? pixel = null;
|
|
if (record.Pixel is { } pixelRecord && (pixel = GetCompiledStage(pixelRecord, pixelCode!)) is null)
|
|
{
|
|
return ShaderCacheOutcome.Failed;
|
|
}
|
|
|
|
var vertexRecord = deriveVertexCursor && pixel is not null ? record.Vertex.WithPushDataCursor(pixel.PushDataEnd) : record.Vertex;
|
|
if (GetCompiledStage(vertexRecord, vertexCode) is not { } vertex || record.Rendering.ColorCount > _maxColorAttachments)
|
|
{
|
|
return ShaderCacheOutcome.Failed;
|
|
}
|
|
|
|
var bindings = ToLayoutBindings(vertex, pixel);
|
|
var pushStages = ShaderStageFlags.VertexBit | ShaderStageFlags.FragmentBit;
|
|
var setLayout = CreateDescriptorSetLayout(bindings, out var usesPushDescriptors, out _);
|
|
var layout = CreatePipelineLayout(setLayout, pushStages);
|
|
var vertexModule = CreateWarmModule(vertex.Spirv);
|
|
var pixelModule = pixel is null ? default : CreateWarmModule(pixel.Spirv);
|
|
try
|
|
{
|
|
var attributes = new VertexAttributeResource[record.Attributes.Length];
|
|
for (var index = 0; index < attributes.Length; index++)
|
|
{
|
|
var input = record.VertexInput.Attributes[index];
|
|
attributes[index] = new VertexAttributeResource(
|
|
record.Attributes[index].Descriptor, 0, record.Attributes[index].RegisterCount, index, 0, input.Binding, input.Offset);
|
|
}
|
|
|
|
var description = new GraphicsPipelineDescription
|
|
{
|
|
Rendering = record.Rendering,
|
|
VertexInput = record.VertexInput,
|
|
VertexInfo = new VertexInputInfo { Attributes = attributes },
|
|
VertexProgram = new ShaderProgram(0, vertexModule.Handle),
|
|
VertexStage = new ShaderProgramInfo
|
|
{
|
|
Stage = ShaderStageKind.Vertex,
|
|
Hash = record.Vertex.Hash,
|
|
VertexFetchComponents = vertex.VertexFetchComponents,
|
|
},
|
|
PixelInfo = pixel is null ? null : new PixelInputInfo(),
|
|
PixelProgram = new ShaderProgram(0, pixelModule.Handle),
|
|
PixelStage = pixel is null ? null : new ShaderProgramInfo { Stage = ShaderStageKind.Pixel, Hash = record.Pixel!.Hash },
|
|
StaticParameters = record.StaticParameters,
|
|
};
|
|
var outcome = ShaderCacheOutcome.FromStore;
|
|
foreach (var (topology, polygonMode) in PrecompileTopologies(record.StaticParameters.Topology))
|
|
{
|
|
_lastCaptureReachedDictionary = false;
|
|
var pipeline = CreateRenderPipeline(description, topology, layout, polygonMode, precompile: true);
|
|
if (pipeline.Handle == 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
_vk.DestroyPipeline(_device, pipeline, null);
|
|
if (_lastCaptureReachedDictionary)
|
|
{
|
|
outcome = ShaderCacheOutcome.Dictionary;
|
|
}
|
|
else if (outcome == ShaderCacheOutcome.FromStore)
|
|
{
|
|
outcome = ShaderCacheOutcome.Compiled;
|
|
}
|
|
}
|
|
|
|
return outcome;
|
|
}
|
|
finally
|
|
{
|
|
DestroyWarmModule(vertexModule);
|
|
DestroyWarmModule(pixelModule);
|
|
_vk.DestroyPipelineLayout(_device, layout, null);
|
|
_vk.DestroyDescriptorSetLayout(_device, setLayout, null);
|
|
}
|
|
}
|
|
|
|
private IEnumerable<(PrimitiveTopology Topology, PolygonMode PolygonMode)> PrecompileTopologies(PrimitiveTopology topology)
|
|
{
|
|
if (topology != PrimitiveTopology.PatchList)
|
|
{
|
|
yield return (topology, PolygonMode.Fill);
|
|
yield break;
|
|
}
|
|
|
|
yield return (PrimitiveTopology.TriangleStrip, PolygonMode.Fill);
|
|
yield return (PrimitiveTopology.TriangleList, PolygonMode.Fill);
|
|
if (_supportsFillRectangle)
|
|
{
|
|
yield return (PrimitiveTopology.TriangleList, PolygonMode.FillRectangleNV);
|
|
}
|
|
}
|
|
|
|
private static PipelineShaderStageRequiredSubgroupSizeCreateInfo RequiredSubgroupSize() => new()
|
|
{
|
|
SType = StructureType.PipelineShaderStageRequiredSubgroupSizeCreateInfo,
|
|
RequiredSubgroupSize = RdnaSubgroupSize,
|
|
};
|
|
|
|
private static ComputePipelineCreateInfo ComputeCreateInfo(
|
|
ShaderModule module,
|
|
PipelineLayout layout,
|
|
byte* entryPoint,
|
|
PipelineShaderStageRequiredSubgroupSizeCreateInfo* subgroup) => new()
|
|
{
|
|
SType = StructureType.ComputePipelineCreateInfo,
|
|
Stage = new PipelineShaderStageCreateInfo
|
|
{
|
|
SType = StructureType.PipelineShaderStageCreateInfo,
|
|
PNext = subgroup,
|
|
Stage = ShaderStageFlags.ComputeBit,
|
|
Module = module,
|
|
PName = entryPoint,
|
|
},
|
|
Layout = layout,
|
|
};
|
|
|
|
private void NoteShaderCacheFailure(string kind, ulong identity, string error)
|
|
{
|
|
if (Interlocked.Increment(ref _shaderCacheFailed) <= MaxLoggedShaderCacheFailures)
|
|
{
|
|
Console.Error.WriteLine($"[SHADER CACHE] {kind} 0x{identity:X16} is skipped, the translator cannot compile it: {error}");
|
|
}
|
|
}
|
|
|
|
private static string FormatCacheBytes(long bytes) =>
|
|
string.Create(CultureInfo.InvariantCulture, $"{bytes / (1024.0 * 1024.0):F1} MB");
|
|
|
|
private void StopShaderCache()
|
|
{
|
|
Interlocked.Exchange(ref _shaderCacheSessionTimer, null)?.Dispose();
|
|
_shaderCacheCancel.Cancel();
|
|
KillShaderCacheWorkers();
|
|
_shaderCacheThread?.Join(TimeSpan.FromSeconds(30));
|
|
SetShaderCacheState(false);
|
|
if (Interlocked.Exchange(ref _shaderCache, null) is not { } file)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ShaderInventory.Detach(file);
|
|
UseShaderCacheStores(null);
|
|
Console.Error.WriteLine(
|
|
$"[SHADER CACHE] Session: pipelines from the store {Volatile.Read(ref _shaderCacheFromBinaries)}, " +
|
|
$"compiled {Volatile.Read(ref _shaderCacheCompiled)}, rejected binaries {Volatile.Read(ref _shaderCacheBinaryRejected)}.");
|
|
SaveShaderSeed(file, VideoOutExports.GetApplicationDisplayName());
|
|
CompactShaderCache(file);
|
|
file.Dispose();
|
|
}
|
|
|
|
private void CompactShaderCache(ShaderCacheFile file, bool dropUnreferencedBinaries = false)
|
|
{
|
|
if (file.Compact(_shaderCacheStamp, _pipelineBinaries?.DriverKey ?? [], _shaderCacheDriverStamp,
|
|
dropUnreferencedBinaries: dropUnreferencedBinaries) is { } result)
|
|
{
|
|
Console.Error.WriteLine(
|
|
$"[SHADER CACHE] Dropped {result.RecordsDropped} stale records, " +
|
|
$"{FormatCacheBytes(result.BytesBefore)} -> {FormatCacheBytes(result.BytesAfter)}.");
|
|
}
|
|
}
|
|
}
|
|
}
|