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

1598 lines
71 KiB
C#

// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.Libs.VideoOut;
using System.Text;
using SharpEmu.Libs.Gpu.Buffers;
using SharpEmu.Libs.Gpu.Pipelines;
using SharpEmu.Libs.Gpu.Rendering;
using SharpEmu.Libs.Gpu.Scheduling;
using Silk.NET.Core;
using Silk.NET.Core.Native;
using Silk.NET.Vulkan;
using Silk.NET.Vulkan.Extensions.EXT;
using Silk.NET.Vulkan.Extensions.KHR;
using VkSemaphore = Silk.NET.Vulkan.Semaphore;
internal static unsafe partial class VulkanVideoPresenter
{
private sealed partial class Presenter
{
// This partial initializes Vulkan host infrastructure.
private readonly SdlHostWindow _window;
private Vk _vk = null!;
private KhrSurface _surfaceApi = null!;
private KhrSwapchain _swapchainApi = null!;
private delegate* unmanaged<Device, DebugUtilsObjectNameInfoEXT*, Result> _setDebugUtilsObjectName;
private delegate* unmanaged<CommandBuffer, DebugUtilsLabelEXT*, void> _cmdBeginDebugUtilsLabel;
private delegate* unmanaged<CommandBuffer, void> _cmdEndDebugUtilsLabel;
private Instance _instance;
private SurfaceKHR _surface;
private DebugUtilsMessengerEXT _debugMessenger;
private ExtDebugUtils? _debugUtils;
private PhysicalDevice _physicalDevice;
private uint _physicalDeviceVendorId;
private uint _maxComputeWorkGroupCountX;
private uint _maxComputeWorkGroupCountY;
private uint _maxComputeWorkGroupCountZ;
private uint _maxComputeWorkGroupSizeX;
private uint _maxComputeWorkGroupSizeY;
private uint _maxComputeWorkGroupSizeZ;
private uint _maxComputeWorkGroupInvocations;
private ulong _minStorageBufferOffsetAlignment = 1;
private bool _supportsIndependentBlend;
private bool _supportsDepthBiasClamp;
private bool _supportsShaderSignedZeroInfNanPreserveFloat32;
private uint _maxColorAttachments;
private Device _device;
private PipelineCache _pipelineCache;
private string? _pipelineCachePath;
private bool _pipelineCacheDirty;
private long _lastPipelineCacheSaveTick;
// Reading a mature driver cache can itself take several seconds. Keep the
// render thread focused on pipeline warm-up; teardown still saves eagerly.
private const long PipelineCacheCheckpointIntervalMs = 300_000;
private const long PipelineCacheRetryMs = 1_000;
private long _pipelineCacheRetryTick;
private const ulong MaxPipelineCacheBytes = 256UL * 1024 * 1024;
private HashSet<string>? _resetPipelineCachePaths;
private nuint _pipelineCacheSavedBytes;
private Queue _queue;
private uint _queueFamilyIndex;
private CommandPool _commandPool;
private CommandBuffer _commandBuffer;
// Scratch buffer of the synchronous readback path; each use waits for the queue.
private CommandBuffer _syncCommandBuffer;
private void Initialize()
{
_relay.BindCurrentThread();
WaitForRenderDocAttachIfRequested();
_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; loader exposes {loaderVersion >> 22}.{(loaderVersion >> 12) & 0x3ff}.");
}
CreateInstance();
CreateSurface();
SelectPhysicalDevice();
CreateDevice();
CreatePipelineCache();
CreateSwapchain();
CreateCommandResources();
CreateGuestDrawResources();
_vulkanReady = true;
AttachGuestGpuMemory();
Console.Error.WriteLine(
$"[LOADER][INFO] Vulkan VideoOut ready: {_extent.Width}x{_extent.Height}, format={_swapchainFormat}");
StartShaderPrewarm();
}
private static void WaitForRenderDocAttachIfRequested()
{
var value = Environment.GetEnvironmentVariable("SHARPEMU_RENDERDOC_WAIT");
if (string.IsNullOrWhiteSpace(value))
{
return;
}
if (string.Equals(value, "enter", StringComparison.OrdinalIgnoreCase))
{
Console.Error.WriteLine(
$"[LOADER][INFO] Waiting for RenderDoc attach before Vulkan init. pid={Environment.ProcessId}. Press Enter to continue.");
_ = Console.ReadLine();
return;
}
var seconds = 15;
if (int.TryParse(value, out var parsedSeconds))
{
seconds = Math.Clamp(parsedSeconds, 1, 300);
}
Console.Error.WriteLine(
$"[LOADER][INFO] Waiting {seconds}s for RenderDoc attach before Vulkan init. pid={Environment.ProcessId}");
Thread.Sleep(TimeSpan.FromSeconds(seconds));
}
private bool IsInstanceExtensionAvailable(string extensionName)
{
uint extensionCount = 0;
if (_vk.EnumerateInstanceExtensionProperties((byte*)null, &extensionCount, null) != Result.Success ||
extensionCount == 0)
{
return false;
}
var properties = new ExtensionProperties[extensionCount];
fixed (ExtensionProperties* propertyPointer = properties)
{
if (_vk.EnumerateInstanceExtensionProperties(
(byte*)null,
&extensionCount,
propertyPointer) != Result.Success)
{
return false;
}
var expected = Encoding.UTF8.GetBytes(extensionName);
for (var index = 0; index < extensionCount; index++)
{
if (Utf8NullTerminatedEquals(propertyPointer[index].ExtensionName, expected))
{
return true;
}
}
}
return false;
}
private static bool Utf8NullTerminatedEquals(byte* actual, ReadOnlySpan<byte> expected)
{
for (var index = 0; index < expected.Length; index++)
{
if (actual[index] != expected[index])
{
return false;
}
}
return actual[expected.Length] == 0;
}
private void LoadDebugUtilsCommands()
{
if (!_vulkanDebugUtilsEnabled)
{
return;
}
var setObjectName = _vk.GetDeviceProcAddr(_device, "vkSetDebugUtilsObjectNameEXT");
var beginLabel = _vk.GetDeviceProcAddr(_device, "vkCmdBeginDebugUtilsLabelEXT");
var endLabel = _vk.GetDeviceProcAddr(_device, "vkCmdEndDebugUtilsLabelEXT");
_setDebugUtilsObjectName =
(delegate* unmanaged<Device, DebugUtilsObjectNameInfoEXT*, Result>)
setObjectName.Handle;
_cmdBeginDebugUtilsLabel =
(delegate* unmanaged<CommandBuffer, DebugUtilsLabelEXT*, void>)
beginLabel.Handle;
_cmdEndDebugUtilsLabel =
(delegate* unmanaged<CommandBuffer, void>)
endLabel.Handle;
if (_setDebugUtilsObjectName is not null)
{
Console.Error.WriteLine("[LOADER][INFO] Vulkan debug labels enabled.");
}
}
private void SetDebugName(ObjectType objectType, ulong objectHandle, string name)
{
if (_setDebugUtilsObjectName is null ||
_device.Handle == 0 ||
objectHandle == 0)
{
return;
}
var bytes = NullTerminatedUtf8(name);
fixed (byte* namePointer = bytes)
{
var info = new DebugUtilsObjectNameInfoEXT
{
SType = StructureType.DebugUtilsObjectNameInfoExt,
ObjectType = objectType,
ObjectHandle = objectHandle,
PObjectName = namePointer,
};
_ = _setDebugUtilsObjectName(_device, &info);
}
}
private void BeginDebugLabel(CommandBuffer commandBuffer, string name)
{
if (_cmdBeginDebugUtilsLabel is null ||
commandBuffer.Handle == 0)
{
return;
}
var bytes = NullTerminatedUtf8(name);
fixed (byte* namePointer = bytes)
{
var label = new DebugUtilsLabelEXT
{
SType = StructureType.DebugUtilsLabelExt,
PLabelName = namePointer,
};
label.Color[0] = 0.20f;
label.Color[1] = 0.60f;
label.Color[2] = 1.00f;
label.Color[3] = 1.00f;
_cmdBeginDebugUtilsLabel(commandBuffer, &label);
}
}
private void EndDebugLabel(CommandBuffer commandBuffer)
{
if (_cmdEndDebugUtilsLabel is not null &&
commandBuffer.Handle != 0)
{
_cmdEndDebugUtilsLabel(commandBuffer);
}
}
private static byte[] NullTerminatedUtf8(string value)
{
var bytes = Encoding.UTF8.GetBytes(value);
Array.Resize(ref bytes, bytes.Length + 1);
return bytes;
}
private void CreateInstance()
{
var applicationName = (byte*)SilkMarshal.StringToPtr("SharpEmu");
byte* validationLayerName = null;
try
{
var applicationInfo = new ApplicationInfo
{
SType = StructureType.ApplicationInfo,
PApplicationName = applicationName,
ApplicationVersion = Vk.MakeVersion(0, 0, 1),
PEngineName = applicationName,
EngineVersion = Vk.MakeVersion(0, 0, 1),
ApiVersion = Vk.Version13,
};
var extensions = _window.GetRequiredVulkanInstanceExtensions(out var extensionCount);
byte* debugUtilsExtension = null;
byte* portabilityExtension = null;
byte* swapchainColorspaceExtension = null;
var instanceCreateFlags = InstanceCreateFlags.None;
var enabledExtensionCount = (int)extensionCount;
var enabledExtensions = stackalloc byte*[(int)extensionCount + 3];
for (var index = 0; index < (int)extensionCount; index++)
{
enabledExtensions[index] = extensions[index];
}
if (_vulkanDebugUtilsEnabled &&
IsInstanceExtensionAvailable(DebugUtilsExtensionName))
{
debugUtilsExtension = (byte*)SilkMarshal.StringToPtr(DebugUtilsExtensionName);
enabledExtensions[enabledExtensionCount++] = debugUtilsExtension;
}
if (IsInstanceExtensionAvailable(PortabilityEnumerationExtensionName))
{
// MoltenVK is a portability (non-conformant) implementation;
// without this flag + extension the loader hides it.
portabilityExtension = (byte*)SilkMarshal.StringToPtr(PortabilityEnumerationExtensionName);
enabledExtensions[enabledExtensionCount++] = portabilityExtension;
instanceCreateFlags |= InstanceCreateFlags.EnumeratePortabilityBitKhr;
}
if (IsInstanceExtensionAvailable(SwapchainColorspaceExtensionName))
{
swapchainColorspaceExtension =
(byte*)SilkMarshal.StringToPtr(SwapchainColorspaceExtensionName);
enabledExtensions[enabledExtensionCount++] = swapchainColorspaceExtension;
}
if (_vulkanValidationEnabled &&
IsInstanceLayerAvailable("VK_LAYER_KHRONOS_validation"))
{
validationLayerName = (byte*)SilkMarshal.StringToPtr("VK_LAYER_KHRONOS_validation");
}
else if (_vulkanValidationEnabled)
{
Console.Error.WriteLine("[LOADER][WARN] SHARPEMU_VK_VALIDATION=1 but VK_LAYER_KHRONOS_validation not found (Vulkan SDK installed?).");
}
var layers = stackalloc byte*[1];
if (validationLayerName is not null)
{
layers[0] = validationLayerName;
}
var createInfo = new InstanceCreateInfo
{
SType = StructureType.InstanceCreateInfo,
Flags = instanceCreateFlags,
PApplicationInfo = &applicationInfo,
EnabledExtensionCount = (uint)enabledExtensionCount,
PpEnabledExtensionNames = enabledExtensions,
EnabledLayerCount = validationLayerName is not null ? 1u : 0u,
PpEnabledLayerNames = validationLayerName is not null ? layers : null,
};
try
{
Check(_vk.CreateInstance(&createInfo, null, out _instance), "vkCreateInstance");
if (!_vk.TryGetInstanceExtension(_instance, out _surfaceApi))
{
throw new InvalidOperationException("VK_KHR_surface is unavailable.");
}
if (validationLayerName is not null && _vk.TryGetInstanceExtension(_instance, out ExtDebugUtils debugUtils))
{
_debugUtils = debugUtils;
RegisterDebugMessenger(debugUtils);
Console.Error.WriteLine("[LOADER][INFO] Vulkan Validation Layers active (SHARPEMU_VK_VALIDATION=1).");
}
}
finally
{
if (debugUtilsExtension is not null)
{
SilkMarshal.Free((nint)debugUtilsExtension);
}
if (portabilityExtension is not null)
{
SilkMarshal.Free((nint)portabilityExtension);
}
if (swapchainColorspaceExtension is not null)
{
SilkMarshal.Free((nint)swapchainColorspaceExtension);
}
}
}
finally
{
SilkMarshal.Free((nint)applicationName);
if (validationLayerName is not null)
{
SilkMarshal.Free((nint)validationLayerName);
}
}
}
private bool IsDeviceExtensionAvailable(string extensionName)
{
uint extensionCount = 0;
if (_vk.EnumerateDeviceExtensionProperties(_physicalDevice, (byte*)null, &extensionCount, null) != Result.Success ||
extensionCount == 0)
{
return false;
}
var properties = new ExtensionProperties[extensionCount];
fixed (ExtensionProperties* propertyPointer = properties)
{
if (_vk.EnumerateDeviceExtensionProperties(
_physicalDevice,
(byte*)null,
&extensionCount,
propertyPointer) != Result.Success)
{
return false;
}
var expected = Encoding.UTF8.GetBytes(extensionName);
for (var index = 0; index < extensionCount; index++)
{
if (Utf8NullTerminatedEquals(propertyPointer[index].ExtensionName, expected))
{
return true;
}
}
}
return false;
}
private bool IsInstanceLayerAvailable(string layerName)
{
uint layerCount = 0;
if (_vk.EnumerateInstanceLayerProperties(&layerCount, null) != Result.Success || layerCount == 0)
{
return false;
}
var properties = new LayerProperties[layerCount];
fixed (LayerProperties* propertyPointer = properties)
{
if (_vk.EnumerateInstanceLayerProperties(&layerCount, propertyPointer) != Result.Success)
{
return false;
}
var expected = Encoding.UTF8.GetBytes(layerName);
for (var index = 0; index < layerCount; index++)
{
if (Utf8NullTerminatedEquals(propertyPointer[index].LayerName, expected))
{
return true;
}
}
}
return false;
}
private void RegisterDebugMessenger(ExtDebugUtils debugUtils)
{
var messengerInfo = new DebugUtilsMessengerCreateInfoEXT
{
SType = StructureType.DebugUtilsMessengerCreateInfoExt,
MessageSeverity = DebugUtilsMessageSeverityFlagsEXT.ErrorBitExt
| DebugUtilsMessageSeverityFlagsEXT.WarningBitExt,
MessageType = DebugUtilsMessageTypeFlagsEXT.ValidationBitExt
| DebugUtilsMessageTypeFlagsEXT.PerformanceBitExt
| DebugUtilsMessageTypeFlagsEXT.GeneralBitExt,
PfnUserCallback = new PfnDebugUtilsMessengerCallbackEXT(DebugCallback),
};
Check(debugUtils.CreateDebugUtilsMessenger(_instance, &messengerInfo, null, out _debugMessenger),
"vkCreateDebugUtilsMessengerEXT");
}
private static unsafe uint DebugCallback(
DebugUtilsMessageSeverityFlagsEXT severity,
DebugUtilsMessageTypeFlagsEXT type,
DebugUtilsMessengerCallbackDataEXT* callbackData,
void* userData)
{
var message = SilkMarshal.PtrToString((nint)callbackData->PMessage);
var prefix = severity switch
{
DebugUtilsMessageSeverityFlagsEXT.ErrorBitExt => "[VULKAN][ERROR]",
DebugUtilsMessageSeverityFlagsEXT.WarningBitExt => "[VULKAN][WARN]",
_ => "[VULKAN][INFO]",
};
Console.Error.WriteLine($"{prefix} {message}");
if (severity == DebugUtilsMessageSeverityFlagsEXT.ErrorBitExt &&
message is not null &&
message.Contains("vkCreateShaderModule", StringComparison.Ordinal))
{
var dumpPath = _pendingShaderModuleDumpPath;
var dumpHint = dumpPath is null
? "Set SHARPEMU_SHADER_SPIRV_DUMP_DIR to a directory to "
+ "capture every module's .spv bytes for offline "
+ "spirv-dis/spirv-val analysis."
: $"Dumped module for this failure: {dumpPath}";
Console.Error.WriteLine(
"[SHARPEMU][ERROR] A guest shader compiled to invalid SPIR-V."
+ $"The shader module was created without an API-level error. {dumpHint}");
}
return Vk.False;
}
private void CreateSurface()
{
_surface = _window.CreateVulkanSurface(_instance);
}
private void SelectPhysicalDevice()
{
uint deviceCount = 0;
Check(_vk.EnumeratePhysicalDevices(_instance, &deviceCount, null), "vkEnumeratePhysicalDevices");
if (deviceCount == 0)
{
throw new InvalidOperationException("No Vulkan physical device was found.");
}
var devices = new PhysicalDevice[deviceCount];
fixed (PhysicalDevice* devicePointer = devices)
{
Check(_vk.EnumeratePhysicalDevices(_instance, &deviceCount, devicePointer), "vkEnumeratePhysicalDevices");
}
var deviceOverride = Environment.GetEnvironmentVariable("SHARPEMU_VK_DEVICE");
var bestScore = int.MinValue;
var found = false;
foreach (var device in devices)
{
uint queueCount = 0;
_vk.GetPhysicalDeviceQueueFamilyProperties(device, &queueCount, null);
var queues = new QueueFamilyProperties[queueCount];
fixed (QueueFamilyProperties* queuePointer = queues)
{
_vk.GetPhysicalDeviceQueueFamilyProperties(device, &queueCount, queuePointer);
}
for (uint index = 0; index < queueCount; index++)
{
var supportsGraphics = (queues[index].QueueFlags & QueueFlags.GraphicsBit) != 0;
_surfaceApi.GetPhysicalDeviceSurfaceSupport(device, index, _surface, out var supportsPresent);
if (!supportsGraphics || !supportsPresent)
{
continue;
}
_vk.GetPhysicalDeviceProperties(device, out var properties);
if (properties.ApiVersion < Vk.Version13)
{
var unsupportedName = SilkMarshal.PtrToString((nint)properties.DeviceName) ?? string.Empty;
Console.Error.WriteLine(
$"[LOADER][INFO] Vulkan candidate skipped: {unsupportedName} api={properties.ApiVersion >> 22}.{(properties.ApiVersion >> 12) & 0x3ff} requires=1.3");
break;
}
var name = SilkMarshal.PtrToString((nint)properties.DeviceName) ?? string.Empty;
var score = ScorePhysicalDevice(properties, name, deviceOverride);
Console.Error.WriteLine(
$"[LOADER][INFO] Vulkan candidate: {name} ({properties.DeviceType}) score={score}");
if (score > bestScore)
{
bestScore = score;
_physicalDevice = device;
_queueFamilyIndex = index;
found = true;
}
break;
}
}
if (!found)
{
throw new InvalidOperationException("No Vulkan graphics/present queue was found.");
}
LoadComputeDeviceLimits();
_vk.GetPhysicalDeviceProperties(_physicalDevice, out var selected);
_physicalDeviceVendorId = selected.VendorID;
_maxColorAttachments = selected.Limits.MaxColorAttachments;
var selectedName = SilkMarshal.PtrToString((nint)selected.DeviceName) ?? "unknown";
Console.Error.WriteLine(
$"[LOADER][INFO] Vulkan device: {selectedName} ({selected.DeviceType})");
VideoOutExports.SetSelectedGpuName(selectedName);
if (_window is not null)
{
_window.SetTitle(VideoOutExports.GetWindowTitle());
}
}
private void LoadComputeDeviceLimits()
{
_vk.GetPhysicalDeviceProperties(_physicalDevice, out var properties);
var pushDescriptorProperties = new PhysicalDevicePushDescriptorPropertiesKHR
{
SType = StructureType.PhysicalDevicePushDescriptorPropertiesKhr,
};
var floatControls = new PhysicalDeviceFloatControlsProperties
{
SType = StructureType.PhysicalDeviceFloatControlsProperties,
PNext = &pushDescriptorProperties,
};
var subgroupSizeControl = new PhysicalDeviceSubgroupSizeControlProperties
{
SType = StructureType.PhysicalDeviceSubgroupSizeControlProperties,
PNext = &floatControls,
};
var subgroup = new PhysicalDeviceSubgroupProperties
{
SType = StructureType.PhysicalDeviceSubgroupProperties,
PNext = &subgroupSizeControl,
};
var properties2 = new PhysicalDeviceProperties2
{
SType = StructureType.PhysicalDeviceProperties2,
PNext = &subgroup,
};
_vk.GetPhysicalDeviceProperties2(_physicalDevice, &properties2);
_supportsShaderSignedZeroInfNanPreserveFloat32 =
floatControls.ShaderSignedZeroInfNanPreserveFloat32;
SetNativeSubgroupCapabilities(subgroup.SubgroupSize, subgroup.SupportedStages);
_canRequireComputeSubgroup32 =
subgroup.SubgroupSize != RdnaSubgroupSize &&
subgroupSizeControl.MinSubgroupSize <= RdnaSubgroupSize &&
subgroupSizeControl.MaxSubgroupSize >= RdnaSubgroupSize &&
(subgroupSizeControl.RequiredSubgroupSizeStages & ShaderStageFlags.ComputeBit) != 0;
_canRequireComputeSubgroup64 =
subgroupSizeControl.MinSubgroupSize <= Wave64SubgroupSize &&
subgroupSizeControl.MaxSubgroupSize >= Wave64SubgroupSize &&
(subgroupSizeControl.RequiredSubgroupSizeStages & ShaderStageFlags.ComputeBit) != 0;
_maxComputeWorkgroupSubgroups = subgroupSizeControl.MaxComputeWorkgroupSubgroups;
// MoltenVK reports a zero OpArrayLength for storage buffers bound through push
// descriptors, which turns every bounds-checked load into zero and drops every store.
_maxPushDescriptors = Gpu.Vulkan.VulkanPushDescriptorPolicy.UsableCount(_vk, _physicalDevice, pushDescriptorProperties.MaxPushDescriptors);
_noAttachmentSampleCounts = properties.Limits.FramebufferNoAttachmentsSampleCounts;
_maxComputeWorkGroupCountX = properties.Limits.MaxComputeWorkGroupCount[0];
_maxComputeWorkGroupCountY = properties.Limits.MaxComputeWorkGroupCount[1];
_maxComputeWorkGroupCountZ = properties.Limits.MaxComputeWorkGroupCount[2];
_maxComputeWorkGroupSizeX = properties.Limits.MaxComputeWorkGroupSize[0];
_maxComputeWorkGroupSizeY = properties.Limits.MaxComputeWorkGroupSize[1];
_maxComputeWorkGroupSizeZ = properties.Limits.MaxComputeWorkGroupSize[2];
_maxComputeWorkGroupInvocations = properties.Limits.MaxComputeWorkGroupInvocations;
_renderHostLimits = new RenderHostLimits(
properties.Limits.MaxFramebufferWidth,
properties.Limits.MaxFramebufferHeight,
properties.Limits.MaxViewportDimensions[0],
properties.Limits.MaxViewportDimensions[1]);
_minStorageBufferOffsetAlignment = Math.Max(
properties.Limits.MinStorageBufferOffsetAlignment,
1UL);
if (GuestStorageBufferOffsetAlignment %
_minStorageBufferOffsetAlignment != 0)
{
throw new InvalidOperationException(
$"Vulkan storage-buffer alignment " +
$"{_minStorageBufferOffsetAlignment} is not compatible with " +
$"the portable alias alignment " +
$"{GuestStorageBufferOffsetAlignment}");
}
Console.Error.WriteLine(
$"[LOADER][INFO] Vulkan compute limits groups=" +
$"{_maxComputeWorkGroupCountX}x{_maxComputeWorkGroupCountY}x{_maxComputeWorkGroupCountZ} " +
$"local={_maxComputeWorkGroupSizeX}x{_maxComputeWorkGroupSizeY}x" +
$"{_maxComputeWorkGroupSizeZ} invocations={_maxComputeWorkGroupInvocations} " +
$"storage_alignment={_minStorageBufferOffsetAlignment}");
Console.Error.WriteLine(
$"[LOADER][INFO] Vulkan subgroup default={subgroup.SubgroupSize} " +
$"stages={subgroup.SupportedStages} ops={subgroup.SupportedOperations} " +
$"size_control={subgroupSizeControl.MinSubgroupSize}-" +
$"{subgroupSizeControl.MaxSubgroupSize} " +
$"required_stages={subgroupSizeControl.RequiredSubgroupSizeStages} " +
$"max_compute_subgroups=" +
$"{subgroupSizeControl.MaxComputeWorkgroupSubgroups}");
var graphicsSubgroupOverride =
Environment.GetEnvironmentVariable("SHARPEMU_GRAPHICS_SUBGROUPS");
var graphicsSubgroupMode = graphicsSubgroupOverride switch
{
"0" => "forced-off",
"1" => "forced-on",
_ when GraphicsSubgroupOperationsEnabled => "auto-native-wave32",
_ => "auto-fallback",
};
Console.Error.WriteLine(
$"[LOADER][INFO] Vulkan shaderSignedZeroInfNanPreserveFloat32 " +
$"enabled={_supportsShaderSignedZeroInfNanPreserveFloat32}");
Console.Error.WriteLine(
$"[LOADER][INFO] Vulkan graphics subgroup operations " +
$"enabled={GraphicsSubgroupOperationsEnabled} " +
$"mode={graphicsSubgroupMode} compute_subgroups=unchanged");
}
// A second queue family for buffer readbacks (see VulkanAsyncReadback): a transfer-only
// family first, then a compute family. SHARPEMU_ASYNC_READBACK=0 keeps one queue.
private uint? _readbackQueueFamilyIndex;
private Queue _readbackQueue;
private uint? SelectReadbackQueueFamily()
{
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_ASYNC_READBACK"), "0", StringComparison.Ordinal))
{
return null;
}
uint count = 0;
_vk.GetPhysicalDeviceQueueFamilyProperties(_physicalDevice, &count, null);
var families = new QueueFamilyProperties[count];
fixed (QueueFamilyProperties* pointer = families)
{
_vk.GetPhysicalDeviceQueueFamilyProperties(_physicalDevice, &count, pointer);
}
uint? compute = null;
for (uint index = 0; index < count; index++)
{
var flags = families[index].QueueFlags;
if (index == _queueFamilyIndex || families[index].QueueCount == 0)
{
continue;
}
if ((flags & QueueFlags.TransferBit) != 0 && (flags & (QueueFlags.GraphicsBit | QueueFlags.ComputeBit)) == 0)
{
return index;
}
if ((flags & QueueFlags.ComputeBit) != 0 && (flags & QueueFlags.GraphicsBit) == 0)
{
compute ??= index;
}
}
return compute;
}
private bool _supportsFragmentShaderBarycentric;
private bool _supportsPerVertexPixelInputs;
private const string FragmentShaderBarycentricExtensionName = "VK_KHR_fragment_shader_barycentric";
private const string Maintenance5ExtensionName = "VK_KHR_maintenance5";
private const string ImageViewMinLodExtensionName = "VK_EXT_image_view_min_lod";
private const string FillRectangleExtensionName = "VK_NV_fill_rectangle";
private bool _supportsImageViewMinLod;
private void CreateDevice()
{
var priority = 1.0f;
var queueInfos = stackalloc DeviceQueueCreateInfo[2];
queueInfos[0] = new DeviceQueueCreateInfo
{
SType = StructureType.DeviceQueueCreateInfo,
QueueFamilyIndex = _queueFamilyIndex,
QueueCount = 1,
PQueuePriorities = &priority,
};
var queueInfoCount = 1u;
_readbackQueueFamilyIndex = SelectReadbackQueueFamily();
if (_readbackQueueFamilyIndex is { } readbackFamily)
{
queueInfos[1] = new DeviceQueueCreateInfo
{
SType = StructureType.DeviceQueueCreateInfo,
QueueFamilyIndex = readbackFamily,
QueueCount = 1,
PQueuePriorities = &priority,
};
queueInfoCount = 2;
}
_vk.GetPhysicalDeviceFeatures(_physicalDevice, out var supportedFeatures);
_supportsIndependentBlend = supportedFeatures.IndependentBlend;
_supportsDepthBiasClamp = supportedFeatures.DepthBiasClamp;
_supportsDepthBounds = supportedFeatures.DepthBounds;
_supportsShaderClipDistance = supportedFeatures.ShaderClipDistance;
_supportsFillRectangle = IsDeviceExtensionAvailable(FillRectangleExtensionName);
var enabledFeatures = new PhysicalDeviceFeatures
{
DepthBounds = supportedFeatures.DepthBounds,
IndependentBlend = supportedFeatures.IndependentBlend,
VertexPipelineStoresAndAtomics = supportedFeatures.VertexPipelineStoresAndAtomics,
FragmentStoresAndAtomics = supportedFeatures.FragmentStoresAndAtomics,
ShaderInt64 = supportedFeatures.ShaderInt64,
ShaderFloat64 = supportedFeatures.ShaderFloat64,
ShaderClipDistance = supportedFeatures.ShaderClipDistance,
ShaderCullDistance = supportedFeatures.ShaderCullDistance,
ShaderImageGatherExtended = supportedFeatures.ShaderImageGatherExtended,
ShaderStorageImageExtendedFormats = supportedFeatures.ShaderStorageImageExtendedFormats,
ShaderStorageImageReadWithoutFormat = supportedFeatures.ShaderStorageImageReadWithoutFormat,
ShaderStorageImageWriteWithoutFormat = supportedFeatures.ShaderStorageImageWriteWithoutFormat,
TextureCompressionBC = supportedFeatures.TextureCompressionBC,
RobustBufferAccess = supportedFeatures.RobustBufferAccess,
DepthBiasClamp = supportedFeatures.DepthBiasClamp,
SampleRateShading = supportedFeatures.SampleRateShading,
SamplerAnisotropy = supportedFeatures.SamplerAnisotropy,
};
if (!supportedFeatures.SampleRateShading)
{
throw new InvalidOperationException(
"The GPU does not support sampleRateShading, which the multisample depth conversion needs.");
}
if (!supportedFeatures.RobustBufferAccess)
{
Console.Error.WriteLine(
"[LOADER][WARN] GPU does not support robustBufferAccess " +
"translated shaders performing out-of-bounds buffer access may cause device loss.");
}
if (!supportedFeatures.ShaderInt64)
{
throw SubmissionScheduler.Fatal(
"The device lacks the shaderInt64 feature, which the device-address programs need.");
}
if (!supportedFeatures.ShaderFloat64)
{
Console.Error.WriteLine(
"[LOADER][WARN] GPU does not support shaderFloat64 " +
"translated shaders using double precision will fail.");
}
if (!supportedFeatures.VertexPipelineStoresAndAtomics || !supportedFeatures.FragmentStoresAndAtomics)
{
Console.Error.WriteLine(
"[LOADER][WARN] GPU does not support vertexPipelineStoresAndAtomics/fragmentStoresAndAtomics " +
"translated shaders using storage buffers in vertex/fragment stages may fail.");
}
if (!supportedFeatures.ShaderImageGatherExtended)
{
Console.Error.WriteLine(
"[LOADER][WARN] GPU does not support shaderImageGatherExtended " +
"translated shaders using image gather with offsets/LOD/bias will fail.");
}
if (!supportedFeatures.ShaderStorageImageReadWithoutFormat ||
!supportedFeatures.ShaderStorageImageWriteWithoutFormat)
{
Console.Error.WriteLine(
"[LOADER][WARN] GPU does not support shaderStorageImage(Read|Write)WithoutFormat " +
"translated shaders using unformatted storage image load/store will fail.");
}
if (!supportedFeatures.TextureCompressionBC)
{
Console.Error.WriteLine(
"[LOADER][WARN] GPU does not support textureCompressionBC " +
"guest BC1-BC7 textures cannot be sampled directly.");
}
var barycentricFeatures = new PhysicalDeviceFragmentShaderBarycentricFeaturesKHR
{
SType = StructureType.PhysicalDeviceFragmentShaderBarycentricFeaturesKhr,
};
if (IsDeviceExtensionAvailable(FragmentShaderBarycentricExtensionName))
{
var barycentricQuery = new PhysicalDeviceFeatures2
{
SType = StructureType.PhysicalDeviceFeatures2,
PNext = &barycentricFeatures,
};
_vk.GetPhysicalDeviceFeatures2(_physicalDevice, &barycentricQuery);
_supportsFragmentShaderBarycentric = barycentricFeatures.FragmentShaderBarycentric;
}
// With maintenance5 a vertex program that writes no PointSize draws 1-pixel points. The
// guest sizes points from PA_SU_POINT_SIZE, which is not modelled; without the feature a
// point-list pipeline whose program does not export a size is invalid.
var maintenance5Features = new PhysicalDeviceMaintenance5FeaturesKHR
{
SType = StructureType.PhysicalDeviceMaintenance5FeaturesKhr,
};
var supportsMaintenance5 = false;
if (IsDeviceExtensionAvailable(Maintenance5ExtensionName))
{
var maintenance5Query = new PhysicalDeviceFeatures2
{
SType = StructureType.PhysicalDeviceFeatures2,
PNext = &maintenance5Features,
};
_vk.GetPhysicalDeviceFeatures2(_physicalDevice, &maintenance5Query);
supportsMaintenance5 = maintenance5Features.Maintenance5;
}
var imageViewMinLodFeatures = new PhysicalDeviceImageViewMinLodFeaturesEXT
{
SType = StructureType.PhysicalDeviceImageViewMinLodFeaturesExt,
};
if (IsDeviceExtensionAvailable(ImageViewMinLodExtensionName))
{
var imageViewMinLodQuery = new PhysicalDeviceFeatures2
{
SType = StructureType.PhysicalDeviceFeatures2,
PNext = &imageViewMinLodFeatures,
};
_vk.GetPhysicalDeviceFeatures2(_physicalDevice, &imageViewMinLodQuery);
_supportsImageViewMinLod = imageViewMinLodFeatures.MinLod;
}
// MoltenVK exposes the barycentric builtins, but SPIRV-Cross rejects PerVertexKHR inputs.
_supportsPerVertexPixelInputs = _supportsFragmentShaderBarycentric &&
!IsDeviceExtensionAvailable(PortabilitySubsetExtensionName);
var depthClipEnableFeatures = new PhysicalDeviceDepthClipEnableFeaturesEXT
{
SType = StructureType.PhysicalDeviceDepthClipEnableFeaturesExt,
};
var depthClipControlFeatures = new PhysicalDeviceDepthClipControlFeaturesEXT
{
SType = StructureType.PhysicalDeviceDepthClipControlFeaturesExt,
PNext = &depthClipEnableFeatures,
};
var colorWriteEnableFeatures = new PhysicalDeviceColorWriteEnableFeaturesEXT
{
SType = StructureType.PhysicalDeviceColorWriteEnableFeaturesExt,
PNext = &depthClipControlFeatures,
};
var vulkan13Features = new PhysicalDeviceVulkan13Features
{
SType = StructureType.PhysicalDeviceVulkan13Features,
PNext = &colorWriteEnableFeatures,
};
var maintenance8Features = new PhysicalDeviceMaintenance8FeaturesKHR
{
SType = StructureType.PhysicalDeviceMaintenance8FeaturesKhr,
PNext = &vulkan13Features,
};
var robustness2Features = new PhysicalDeviceRobustness2FeaturesEXT
{
SType = StructureType.PhysicalDeviceRobustness2FeaturesExt,
PNext = &maintenance8Features,
};
var timelineSemaphoreFeatures = new PhysicalDeviceTimelineSemaphoreFeatures
{
SType = StructureType.PhysicalDeviceTimelineSemaphoreFeatures,
PNext = &robustness2Features,
};
var addressFeatures = new PhysicalDeviceBufferDeviceAddressFeatures
{
SType = StructureType.PhysicalDeviceBufferDeviceAddressFeatures,
PNext = &timelineSemaphoreFeatures,
};
var atomicInt64Features = new PhysicalDeviceShaderAtomicInt64Features
{
SType = StructureType.PhysicalDeviceShaderAtomicInt64Features,
PNext = &addressFeatures,
};
var featuresQuery = new PhysicalDeviceFeatures2
{
SType = StructureType.PhysicalDeviceFeatures2,
PNext = &atomicInt64Features,
};
_vk.GetPhysicalDeviceFeatures2(_physicalDevice, &featuresQuery);
var supportsTimelineSemaphore = timelineSemaphoreFeatures.TimelineSemaphore;
var supportsBufferDeviceAddress = addressFeatures.BufferDeviceAddress;
var supportsSharedInt64Atomics = atomicInt64Features.ShaderSharedInt64Atomics;
var supportsMaintenance8 = maintenance8Features.Maintenance8;
var supportsRobustBufferAccess2 = robustness2Features.RobustBufferAccess2;
var supportsRobustImageAccess2 = robustness2Features.RobustImageAccess2;
var supportsNullDescriptor = robustness2Features.NullDescriptor;
var supportsRobustness2 = supportsRobustImageAccess2 || supportsNullDescriptor;
_canRequireComputeSubgroup32 &= vulkan13Features.SubgroupSizeControl;
_canRequireComputeSubgroup64 &= vulkan13Features.SubgroupSizeControl;
SetSharedInt64AtomicsCapability(supportsSharedInt64Atomics);
if (!supportsSharedInt64Atomics)
{
Console.Error.WriteLine(
"[LOADER][WARN] GPU does not support shaderSharedInt64Atomics " +
"64-bit LDS atomics fall back to a non-atomic pair of 32-bit atomics.");
}
_vk.GetPhysicalDeviceProperties(_physicalDevice, out var deviceProperties);
var deviceName = SilkMarshal.PtrToString((nint)deviceProperties.DeviceName) ?? "unknown";
RequireRenderingFeature(vulkan13Features.DynamicRendering, "Vulkan 1.3 dynamicRendering", deviceName);
RequireRenderingFeature(vulkan13Features.Synchronization2, "Vulkan 1.3 synchronization2", deviceName);
var supportsColorWriteEnable = colorWriteEnableFeatures.ColorWriteEnable;
_supportsDepthClipControl = depthClipControlFeatures.DepthClipControl;
_supportsDepthClipEnable = depthClipEnableFeatures.DepthClipEnable;
if (!supportsMaintenance8)
{
Console.Error.WriteLine(
"[LOADER][WARN] GPU does not support VK_KHR_maintenance8 " +
"translated shaders using a dynamic texel offset on non-gather image samples will fail.");
}
if (!supportsRobustImageAccess2)
{
Console.Error.WriteLine(
"[LOADER][WARN] GPU does not support VK_EXT_robustness2 robustImageAccess2 " +
"translated shaders performing out-of-bounds image access may cause device loss.");
}
if (!IsDeviceExtensionAvailable(PushDescriptorExtensionName))
{
throw SubmissionScheduler.Fatal($"The device lacks a required rendering feature: device={deviceName} extension={PushDescriptorExtensionName}.");
}
var swapchainExtension = (byte*)SilkMarshal.StringToPtr("VK_KHR_swapchain");
var pushDescriptorExtension = (byte*)SilkMarshal.StringToPtr(PushDescriptorExtensionName);
var maintenance8Extension = (byte*)SilkMarshal.StringToPtr("VK_KHR_maintenance8");
var robustness2Extension = (byte*)SilkMarshal.StringToPtr("VK_EXT_robustness2");
var portabilitySubsetExtension = (byte*)SilkMarshal.StringToPtr(PortabilitySubsetExtensionName);
var colorWriteEnableExtension = (byte*)SilkMarshal.StringToPtr(ColorWriteEnableExtensionName);
var depthClipControlExtension = (byte*)SilkMarshal.StringToPtr(DepthClipControlExtensionName);
var depthClipEnableExtension = (byte*)SilkMarshal.StringToPtr(DepthClipEnableExtensionName);
var barycentricExtension = (byte*)SilkMarshal.StringToPtr(FragmentShaderBarycentricExtensionName);
var viewportIndexLayerExtension = (byte*)SilkMarshal.StringToPtr("VK_EXT_shader_viewport_index_layer");
var maintenance5Extension = (byte*)SilkMarshal.StringToPtr(Maintenance5ExtensionName);
var imageViewMinLodExtension = (byte*)SilkMarshal.StringToPtr(ImageViewMinLodExtensionName);
var fillRectangleExtension = (byte*)SilkMarshal.StringToPtr(FillRectangleExtensionName);
try
{
var extensions = stackalloc byte*[15];
var extensionCount = 0u;
extensions[extensionCount++] = swapchainExtension;
extensions[extensionCount++] = pushDescriptorExtension;
if (IsDeviceExtensionAvailable("VK_EXT_shader_viewport_index_layer"))
{
extensions[extensionCount++] = viewportIndexLayerExtension;
}
if (_supportsFragmentShaderBarycentric)
{
extensions[extensionCount++] = barycentricExtension;
}
if (supportsColorWriteEnable)
{
extensions[extensionCount++] = colorWriteEnableExtension;
}
if (_supportsDepthClipControl)
{
extensions[extensionCount++] = depthClipControlExtension;
}
if (_supportsDepthClipEnable)
{
extensions[extensionCount++] = depthClipEnableExtension;
}
if (supportsMaintenance8)
{
extensions[extensionCount++] = maintenance8Extension;
}
if (supportsMaintenance5)
{
extensions[extensionCount++] = maintenance5Extension;
}
if (_supportsImageViewMinLod)
{
extensions[extensionCount++] = imageViewMinLodExtension;
}
if (_supportsFillRectangle)
{
extensions[extensionCount++] = fillRectangleExtension;
}
if (supportsRobustness2)
{
extensions[extensionCount++] = robustness2Extension;
}
if (IsDeviceExtensionAvailable(PortabilitySubsetExtensionName))
{
// The spec requires enabling this when the (MoltenVK)
// device advertises it.
extensions[extensionCount++] = portabilitySubsetExtension;
}
maintenance8Features.Maintenance8 = supportsMaintenance8;
maintenance8Features.PNext = null;
robustness2Features.RobustBufferAccess2 =
supportsRobustBufferAccess2 && supportedFeatures.RobustBufferAccess;
robustness2Features.RobustImageAccess2 = supportsRobustImageAccess2;
robustness2Features.NullDescriptor = supportsNullDescriptor;
robustness2Features.PNext = supportsMaintenance8 ? &maintenance8Features : null;
if (!supportsTimelineSemaphore)
{
throw SubmissionScheduler.Fatal(
"the submission scheduler needs timeline semaphores, which this device lacks");
}
if (!supportsBufferDeviceAddress)
{
throw SubmissionScheduler.Fatal(
"the buffer store needs bufferDeviceAddress, which this device lacks");
}
timelineSemaphoreFeatures.TimelineSemaphore = true;
timelineSemaphoreFeatures.PNext = supportsRobustness2
? &robustness2Features
: (supportsMaintenance8 ? &maintenance8Features : null);
addressFeatures = new PhysicalDeviceBufferDeviceAddressFeatures
{
SType = StructureType.PhysicalDeviceBufferDeviceAddressFeatures,
BufferDeviceAddress = true,
PNext = &timelineSemaphoreFeatures,
};
void* renderingChain = &addressFeatures;
if (supportsSharedInt64Atomics)
{
atomicInt64Features = new PhysicalDeviceShaderAtomicInt64Features
{
SType = StructureType.PhysicalDeviceShaderAtomicInt64Features,
ShaderSharedInt64Atomics = true,
PNext = renderingChain,
};
renderingChain = &atomicInt64Features;
}
if (_supportsFragmentShaderBarycentric)
{
barycentricFeatures.PNext = renderingChain;
renderingChain = &barycentricFeatures;
}
if (supportsMaintenance5)
{
maintenance5Features = new PhysicalDeviceMaintenance5FeaturesKHR
{
SType = StructureType.PhysicalDeviceMaintenance5FeaturesKhr,
Maintenance5 = true,
PNext = renderingChain,
};
renderingChain = &maintenance5Features;
}
if (_supportsImageViewMinLod)
{
imageViewMinLodFeatures = new PhysicalDeviceImageViewMinLodFeaturesEXT
{
SType = StructureType.PhysicalDeviceImageViewMinLodFeaturesExt,
MinLod = true,
PNext = renderingChain,
};
renderingChain = &imageViewMinLodFeatures;
}
if (_supportsDepthClipEnable)
{
depthClipEnableFeatures = new PhysicalDeviceDepthClipEnableFeaturesEXT
{
SType = StructureType.PhysicalDeviceDepthClipEnableFeaturesExt,
DepthClipEnable = true,
PNext = renderingChain,
};
renderingChain = &depthClipEnableFeatures;
}
if (_supportsDepthClipControl)
{
depthClipControlFeatures = new PhysicalDeviceDepthClipControlFeaturesEXT
{
SType = StructureType.PhysicalDeviceDepthClipControlFeaturesExt,
DepthClipControl = true,
PNext = renderingChain,
};
renderingChain = &depthClipControlFeatures;
}
if (supportsColorWriteEnable)
{
colorWriteEnableFeatures = new PhysicalDeviceColorWriteEnableFeaturesEXT
{
SType = StructureType.PhysicalDeviceColorWriteEnableFeaturesExt,
ColorWriteEnable = true,
PNext = renderingChain,
};
renderingChain = &colorWriteEnableFeatures;
}
vulkan13Features = new PhysicalDeviceVulkan13Features
{
SType = StructureType.PhysicalDeviceVulkan13Features,
DynamicRendering = true,
Synchronization2 = true,
SubgroupSizeControl = _canRequireComputeSubgroup32 || _canRequireComputeSubgroup64,
PNext = renderingChain,
};
var features2 = new PhysicalDeviceFeatures2
{
SType = StructureType.PhysicalDeviceFeatures2,
PNext = &vulkan13Features,
Features = enabledFeatures,
};
var createInfo = new DeviceCreateInfo
{
SType = StructureType.DeviceCreateInfo,
PNext = &features2,
QueueCreateInfoCount = queueInfoCount,
PQueueCreateInfos = queueInfos,
EnabledExtensionCount = extensionCount,
PpEnabledExtensionNames = extensions,
};
Check(_vk.CreateDevice(_physicalDevice, &createInfo, null, out _device), "vkCreateDevice");
}
finally
{
SilkMarshal.Free((nint)swapchainExtension);
SilkMarshal.Free((nint)maintenance8Extension);
SilkMarshal.Free((nint)maintenance5Extension);
SilkMarshal.Free((nint)imageViewMinLodExtension);
SilkMarshal.Free((nint)fillRectangleExtension);
SilkMarshal.Free((nint)robustness2Extension);
SilkMarshal.Free((nint)portabilitySubsetExtension);
SilkMarshal.Free((nint)colorWriteEnableExtension);
SilkMarshal.Free((nint)depthClipControlExtension);
SilkMarshal.Free((nint)depthClipEnableExtension);
SilkMarshal.Free((nint)pushDescriptorExtension);
SilkMarshal.Free((nint)barycentricExtension);
SilkMarshal.Free((nint)viewportIndexLayerExtension);
}
_vk.GetDeviceQueue(_device, _queueFamilyIndex, 0, out _queue);
_deviceInfo = new GpuDeviceInfo(_vk, _physicalDevice, _device) { ImageViewMinLodSupported = _supportsImageViewMinLod };
if (_readbackQueueFamilyIndex is { } readbackQueueFamily)
{
_vk.GetDeviceQueue(_device, readbackQueueFamily, 0, out _readbackQueue);
_deviceInfo.SharedQueueFamilies = [_queueFamilyIndex, readbackQueueFamily];
Console.Error.WriteLine($"[LOADER][INFO] Vulkan async readback queue: family={readbackQueueFamily} (main family={_queueFamilyIndex})");
}
CreateScheduler();
CreateBufferCache();
CreateImageCache();
_descriptorHeap = new DescriptorHeap(_deviceInfo, _scheduler);
CreateCommandStream();
LoadDebugUtilsCommands();
if (!_vk.TryGetDeviceExtension(_instance, _device, out _swapchainApi))
{
throw new InvalidOperationException("VK_KHR_swapchain is unavailable.");
}
LoadRenderingCommands(supportsColorWriteEnable, deviceName);
}
private void CreatePipelineCache()
{
var cacheMode = Environment.GetEnvironmentVariable("SHARPEMU_VK_PIPELINE_CACHE");
// MoltenVK imports compile every cached MSL library. Load shader
// groups on demand on macOS so a mature cache cannot hold startup
// behind unrelated scenes. Keep explicit overrides for comparison.
var shardMode = Environment.GetEnvironmentVariable("SHARPEMU_VK_PIPELINE_CACHE_SHARDS");
var useShards = shardMode == "1" || (shardMode is null && OperatingSystem.IsMacOS());
// Vulkan cache blobs carry the implementation's compatibility
// header and are rejected/rebuilt below when the device or driver
// changes. MoltenVK compilation of a large translated shader can
// take ten seconds, so discarding a valid cache at every launch is
// much more harmful than using Vulkan's normal persistence path.
// Keep an explicit opt-out for diagnostics and read-only systems.
var persistentCacheEnabled =
!string.Equals(cacheMode, "0", StringComparison.Ordinal) &&
(useShards || !OperatingSystem.IsWindows() || cacheMode is "1" or "file");
_pipelineCachePath = persistentCacheEnabled ? GetPipelineCachePath() : null;
if (_pipelineCachePath is not null &&
useShards)
{
var singleFilePath = _pipelineCachePath;
_pipelineCacheShardDirectory = _pipelineCachePath + ".shards";
_pipelineCachePath = Path.Combine(_pipelineCacheShardDirectory, "present.bin");
if (string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("SHARPEMU_VK_PIPELINE_CACHE_PATH")))
{
RemoveSingleFilePipelineCache(singleFilePath);
}
}
byte[] initialData = [];
try
{
if (_pipelineCachePath is not null && File.Exists(_pipelineCachePath))
{
if (!PipelineCacheSignature.TryUnwrap(DriverCacheSignature(), File.ReadAllBytes(_pipelineCachePath), out initialData))
{
Console.Error.WriteLine(
$"[LOADER][INFO] Vulkan pipeline cache invalidated: path={_pipelineCachePath} reason=signature-or-hash-mismatch");
initialData = [];
}
}
}
catch (Exception exception)
{
Console.Error.WriteLine(
$"[LOADER][WARN] Vulkan pipeline cache read failed: {exception.Message}");
}
var result = TryCreatePipelineCache(initialData, out _pipelineCache);
if (result != Result.Success && initialData.Length != 0)
{
Console.Error.WriteLine(
$"[LOADER][WARN] Vulkan pipeline cache rejected ({result}); rebuilding it.");
result = TryCreatePipelineCache([], out _pipelineCache);
}
if (result != Result.Success)
{
_pipelineCache = default;
_pipelineCachePath = null;
Console.Error.WriteLine(
$"[LOADER][WARN] Vulkan pipeline cache unavailable: {result}");
return;
}
if (initialData.Length != 0 && QueryPipelineCacheSize(_pipelineCache) is { } loadedBytes)
{
_pipelineCacheSavedBytes = loadedBytes;
}
SetDebugName(
ObjectType.PipelineCache,
_pipelineCache.Handle,
_pipelineCachePath is null
? "SharpEmu in-memory pipeline cache"
: "SharpEmu persistent pipeline cache");
_lastPipelineCacheSaveTick = Environment.TickCount64;
if (_pipelineCachePath is null)
{
Console.Error.WriteLine(
string.Equals(cacheMode, "0", StringComparison.Ordinal)
? "[LOADER][INFO] Vulkan pipeline cache ready: memory-only " +
"(persistence disabled with SHARPEMU_VK_PIPELINE_CACHE=0)."
: "[LOADER][INFO] Vulkan pipeline cache ready: memory-only " +
"(the driver shader cache and the shader prewarm list replace the cache file; " +
"SHARPEMU_VK_PIPELINE_CACHE=file keeps one).");
}
else
{
Console.Error.WriteLine(
$"[LOADER][INFO] Vulkan pipeline cache ready: path={_pipelineCachePath} initial={initialData.Length} bytes");
}
}
// The build and device the saved cache belongs to; a different one invalidates the file.
private string DriverCacheSignature()
{
_vk.GetPhysicalDeviceProperties(_physicalDevice, out var properties);
var uuid = new byte[PipelineCacheSignature.UuidSize];
for (var index = 0; index < uuid.Length; index++)
{
uuid[index] = properties.PipelineCacheUuid[index];
}
return PipelineCacheSignature.Build(properties.VendorID, properties.DeviceID, properties.DriverVersion, uuid);
}
private Result TryCreatePipelineCache(byte[] initialData, out PipelineCache pipelineCache)
{
fixed (byte* initialDataPointer = initialData)
{
var createInfo = new PipelineCacheCreateInfo
{
SType = StructureType.PipelineCacheCreateInfo,
InitialDataSize = (nuint)initialData.Length,
PInitialData = initialData.Length == 0 ? null : initialDataPointer,
};
return _vk.CreatePipelineCache(
_device,
&createInfo,
null,
out pipelineCache);
}
}
private static string GetPipelineCachePath()
{
var configured = Environment.GetEnvironmentVariable("SHARPEMU_VK_PIPELINE_CACHE_PATH");
var cachePath = VulkanPipelineCacheStorage.ResolvePath(
VideoOutExports.GetApplicationTitleId(),
configured);
if (string.IsNullOrWhiteSpace(configured))
{
try
{
var legacyPath = VulkanPipelineCacheStorage.GetLegacyPath();
if (VulkanPipelineCacheStorage.ImportLegacyCache(legacyPath, cachePath))
{
Console.Error.WriteLine(
$"[LOADER][INFO] Imported legacy Vulkan pipeline cache: source={legacyPath} destination={cachePath}");
}
}
catch (Exception exception)
{
Console.Error.WriteLine(
$"[LOADER][WARN] Vulkan pipeline cache migration failed: {exception.Message}");
}
}
return cachePath;
}
private static void RemoveSingleFilePipelineCache(string path)
{
try
{
if (!File.Exists(path))
{
return;
}
var bytes = new FileInfo(path).Length;
File.Delete(path);
Console.Error.WriteLine(
$"[LOADER][INFO] Vulkan pipeline cache now keeps one file per shader; removed the single-file cache: path={path} bytes={bytes}");
}
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException)
{
Console.Error.WriteLine(
$"[LOADER][WARN] Vulkan single-file pipeline cache removal failed: {exception.Message}");
}
}
private void MarkPipelineCacheDirty()
{
if (_pipelineCache.Handle == 0)
{
return;
}
_pipelineCacheDirty = true;
// Exporting MoltenVK's cache can itself serialize the compiler.
// Gameplay may discover dozens of expensive pipelines in one
// frame, so saving after every slow creation compounds a warm-up
// hitch into a multi-minute stall. Coalesce all creations into one
// periodic snapshot; shutdown still forces a final save.
CheckpointPipelineCache();
}
private void CheckpointPipelineCache()
{
if (!_pipelineCacheDirty || _pipelineCachePath is null)
{
return;
}
var now = Environment.TickCount64;
if (now - _lastPipelineCacheSaveTick < PipelineCacheCheckpointIntervalMs || now < _pipelineCacheRetryTick)
{
return;
}
_pipelineCacheRetryTick = now + PipelineCacheRetryMs;
SavePipelineCache(force: false);
}
private void SavePipelineCache(bool force)
{
// Cache export must not race asynchronous pipeline creation.
if (_pendingComputePipelines.Values.Any(pending => !pending.Compile.IsCompleted)) return;
_lastPipelineCacheSaveTick = Environment.TickCount64;
SaveGuestPipelineCaches();
if (_pipelineCache.Handle == 0 || string.IsNullOrWhiteSpace(_pipelineCachePath))
{
return;
}
if (!force && !_pipelineCacheDirty)
{
return;
}
if (QueryPipelineCacheSize(_pipelineCache) is { } size && size == _pipelineCacheSavedBytes)
{
_pipelineCacheDirty = false;
return;
}
if (SaveDriverPipelineCache(_pipelineCache, _pipelineCachePath, MaxPipelineCacheBytes, out var savedBytes))
{
_pipelineCacheDirty = false;
_pipelineCacheSavedBytes = savedBytes;
Console.Error.WriteLine(
$"[LOADER][INFO] Vulkan pipeline cache saved: path={_pipelineCachePath} bytes={savedBytes}");
}
}
private nuint? QueryPipelineCacheSize(PipelineCache cache)
{
nuint size = 0;
return _vk.GetPipelineCacheData(_device, cache, &size, null) == Result.Success ? size : null;
}
private bool SaveDriverPipelineCache(PipelineCache cache, string path, ulong byteLimit, out nuint savedBytes)
{
savedBytes = 0;
try
{
nuint size = 0;
var result = _vk.GetPipelineCacheData(
_device,
cache,
&size,
null);
if (result != Result.Success || size == 0)
{
Console.Error.WriteLine(
$"[LOADER][WARN] Vulkan pipeline cache query failed: result={result} size={size}");
return false;
}
if (size > byteLimit)
{
if ((_resetPipelineCachePaths ??= new HashSet<string>(StringComparer.Ordinal)).Add(path))
{
WritePipelineCacheFile(path, []);
Console.Error.WriteLine(
$"[LOADER][INFO] Vulkan pipeline cache outgrew its limit; the next launch starts a fresh cache: path={path} bytes={size} limit={byteLimit}");
}
savedBytes = size;
return true;
}
var data = new byte[checked((int)size)];
fixed (byte* dataPointer = data)
{
result = _vk.GetPipelineCacheData(
_device,
cache,
&size,
dataPointer);
}
if (result != Result.Success)
{
Console.Error.WriteLine(
$"[LOADER][WARN] Vulkan pipeline cache export failed: {result}");
return false;
}
if (size != (nuint)data.Length)
{
Array.Resize(ref data, checked((int)size));
}
WritePipelineCacheFile(path, data);
savedBytes = size;
return true;
}
catch (Exception exception)
{
Console.Error.WriteLine(
$"[LOADER][WARN] Vulkan pipeline cache save failed: {exception.Message}");
return false;
}
}
private void WritePipelineCacheFile(string path, ReadOnlySpan<byte> data)
{
var directory = Path.GetDirectoryName(path);
if (!string.IsNullOrWhiteSpace(directory))
{
Directory.CreateDirectory(directory);
}
var temporaryPath = path + $".{Environment.ProcessId}.tmp";
File.WriteAllBytes(temporaryPath, PipelineCacheSignature.Wrap(DriverCacheSignature(), data));
File.Move(temporaryPath, path, overwrite: true);
}
private void CreateCommandResources()
{
var poolInfo = new CommandPoolCreateInfo
{
SType = StructureType.CommandPoolCreateInfo,
Flags = CommandPoolCreateFlags.ResetCommandBufferBit,
QueueFamilyIndex = _queueFamilyIndex,
};
Check(_vk.CreateCommandPool(_device, &poolInfo, null, out _commandPool), "vkCreateCommandPool");
var allocateInfo = new CommandBufferAllocateInfo
{
SType = StructureType.CommandBufferAllocateInfo,
CommandPool = _commandPool,
Level = CommandBufferLevel.Primary,
CommandBufferCount = 1,
};
Check(
_vk.AllocateCommandBuffers(_device, &allocateInfo, out _syncCommandBuffer),
"vkAllocateCommandBuffers(sync)");
var semaphoreInfo = new SemaphoreCreateInfo
{
SType = StructureType.SemaphoreCreateInfo,
};
_frameImageAvailable = new VkSemaphore[MaxFramesInFlight];
_frameInFlight = new bool[MaxFramesInFlight];
_frameTimelines = new ulong[MaxFramesInFlight];
_frameGuestImageVersions = new GuestImageResource?[MaxFramesInFlight];
for (var slot = 0; slot < MaxFramesInFlight; slot++)
{
Check(
_vk.CreateSemaphore(_device, &semaphoreInfo, null, out _frameImageAvailable[slot]),
"vkCreateSemaphore");
}
_renderFinishedPerImage = new VkSemaphore[_swapchainImages.Length];
for (var image = 0; image < _renderFinishedPerImage.Length; image++)
{
Check(
_vk.CreateSemaphore(_device, &semaphoreInfo, null, out _renderFinishedPerImage[image]),
"vkCreateSemaphore");
}
_currentFrameSlot = 0;
CreateStagingBuffer((ulong)_extent.Width * _extent.Height * 4);
CreateFrameUploadBuffers(_stagingSize);
CreateOverlayResources();
}
}
}