// 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 _setDebugUtilsObjectName; private delegate* unmanaged _cmdBeginDebugUtilsLabel; private delegate* unmanaged _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 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? _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 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) setObjectName.Handle; _cmdBeginDebugUtilsLabel = (delegate* unmanaged) beginLabel.Handle; _cmdEndDebugUtilsLabel = (delegate* unmanaged) 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 subgroupSizeControl = new PhysicalDeviceSubgroupSizeControlProperties { SType = StructureType.PhysicalDeviceSubgroupSizeControlProperties, PNext = &pushDescriptorProperties, }; var subgroup = new PhysicalDeviceSubgroupProperties { SType = StructureType.PhysicalDeviceSubgroupProperties, PNext = &subgroupSizeControl, }; var properties2 = new PhysicalDeviceProperties2 { SType = StructureType.PhysicalDeviceProperties2, PNext = &subgroup, }; _vk.GetPhysicalDeviceProperties2(_physicalDevice, &properties2); SetNativeSubgroupCapabilities(subgroup.SubgroupSize, subgroup.SupportedStages); _canRequireComputeSubgroup32 = subgroup.SubgroupSize != RdnaSubgroupSize && subgroupSizeControl.MinSubgroupSize <= RdnaSubgroupSize && subgroupSizeControl.MaxSubgroupSize >= RdnaSubgroupSize && (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 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; uint? general = 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; } // MoltenVK exposes only general families, one queue each, every one its own // Metal command queue: a second one still runs readbacks beside the main queue. if ((flags & (QueueFlags.TransferBit | QueueFlags.GraphicsBit | QueueFlags.ComputeBit)) != 0) { general ??= index; } } return compute ?? general; } 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; 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, 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(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 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(); } } }