sharpemu/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Pipeli...

149 lines
6.3 KiB
C#

// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.Libs.VideoOut;
using System.Diagnostics;
using SharpEmu.Libs.Gpu.Pipelines;
using Silk.NET.Vulkan;
internal static unsafe partial class VulkanVideoPresenter
{
private sealed partial class Presenter
{
private sealed class DriverCacheShard
{
public Lazy<PipelineCache> Cache = null!;
public required string Path;
public bool Dirty;
public nuint SavedBytes;
}
private const double SlowShardLoadMilliseconds = 50;
private string? _pipelineCacheShardDirectory;
private readonly Dictionary<string, DriverCacheShard> _pipelineCacheShards = new();
private static readonly bool LegacyShaderGroups =
Environment.GetEnvironmentVariable("SHARPEMU_VK_PIPELINE_CACHE_LEGACY_SHARDS") == "1";
private string ComputeCacheKey(ulong guestHash, ulong module) =>
!LegacyShaderGroups && _shaderModuleCacheIdentities.TryGetValue(module, out var identity)
? $"c2-{identity}" : $"c-{guestHash:X16}";
private string GraphicsCacheKey(ulong vertexHash, ulong pixelHash, ulong vertexModule, ulong pixelModule)
{
if (!LegacyShaderGroups && _shaderModuleCacheIdentities.TryGetValue(vertexModule, out var vertex) &&
(pixelModule == 0 || _shaderModuleCacheIdentities.ContainsKey(pixelModule)))
return $"g2-{vertex}-{(pixelModule == 0 ? "none" : _shaderModuleCacheIdentities[pixelModule])}";
return $"g-{vertexHash:X16}-{pixelHash:X16}";
}
// MoltenVK recompiles cached MSL libraries during vkCreatePipelineCache.
// Loading only requested translated modules prevents a mature cache from
// compiling every previously visited scene before the first frame.
// Vulkan still validates the complete shader/layout key inside each blob;
// content hashes only select a storage bucket, never a pipeline to reuse.
private PipelineCache GetGuestPipelineCache(string key)
=> ResolveGuestPipelineCache(GetGuestPipelineCacheSource(key));
// The dictionary belongs to the render thread. Only the lazy native
// creation runs on compiler workers; variants share one initialization.
private Lazy<PipelineCache>? GetGuestPipelineCacheSource(string key)
{
if (_pipelineCacheShardDirectory is null) return null;
if (_pipelineCacheShards.TryGetValue(key, out var existing))
{
existing.Dirty = true;
return existing.Cache;
}
var shard = new DriverCacheShard { Path = Path.Combine(_pipelineCacheShardDirectory, key + ".bin"), Dirty = true };
shard.Cache = new Lazy<PipelineCache>(() => LoadGuestPipelineCache(key, shard));
_pipelineCacheShards.Add(key, shard);
return shard.Cache;
}
private PipelineCache ResolveGuestPipelineCache(Lazy<PipelineCache>? source)
{
var cache = source?.Value ?? default;
return cache.Handle != 0 ? cache : _pipelineCache;
}
private PipelineCache LoadGuestPipelineCache(string key, DriverCacheShard shard)
{
var path = shard.Path;
byte[] data = [];
try
{
if (File.Exists(path) &&
!PipelineCacheSignature.TryUnwrap(DriverCacheSignature(), File.ReadAllBytes(path), out data))
Console.Error.WriteLine($"[LOADER][INFO] Vulkan cache shard invalidated: path={path}");
}
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException)
{
Console.Error.WriteLine($"[LOADER][WARN] Vulkan cache shard read failed: {exception.Message}");
}
var started = Stopwatch.GetTimestamp();
var result = TryCreatePipelineCache(data, out var cache);
if (result != Result.Success && data.Length != 0)
result = TryCreatePipelineCache([], out cache);
if (result != Result.Success)
{
Console.Error.WriteLine($"[LOADER][WARN] Vulkan cache shard unavailable: key={key} result={result}");
return default;
}
if (data.Length == 0)
return cache;
var milliseconds = Stopwatch.GetElapsedTime(started).TotalMilliseconds;
if (QueryPipelineCacheSize(cache) is { } size)
shard.SavedBytes = size;
if (milliseconds >= SlowShardLoadMilliseconds)
Console.Error.WriteLine($"[LOADER][INFO] Vulkan cache shard loaded: key={key} bytes={data.Length} ms={milliseconds:F1}");
return cache;
}
private void SaveGuestPipelineCaches()
{
var savedShards = 0;
nuint savedBytes = 0;
foreach (var shard in _pipelineCacheShards.Values)
{
if (!shard.Dirty || !shard.Cache.IsValueCreated || shard.Cache.Value.Handle == 0)
continue;
var cache = shard.Cache.Value;
if (QueryPipelineCacheSize(cache) is { } size && size == shard.SavedBytes)
{
shard.Dirty = false;
continue;
}
if (SaveDriverPipelineCache(cache, shard.Path, MaxPipelineCacheBytes, out var bytes))
{
shard.Dirty = false;
shard.SavedBytes = bytes;
savedShards++;
savedBytes += bytes;
}
}
if (savedShards != 0)
Console.Error.WriteLine(
$"[LOADER][INFO] Vulkan cache shards saved: directory={_pipelineCacheShardDirectory} shards={savedShards} bytes={savedBytes}");
}
private void DestroyGuestPipelineCaches()
{
foreach (var shard in _pipelineCacheShards.Values)
{
if (shard.Cache.IsValueCreated && shard.Cache.Value.Handle != 0)
_vk.DestroyPipelineCache(_device, shard.Cache.Value, null);
}
_pipelineCacheShards.Clear();
}
}
}