diff --git a/Directory.Packages.props b/Directory.Packages.props index 3df7e6d..e03cb42 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -2,7 +2,6 @@ Copyright (C) 2026 SharpEmu Emulator Project SPDX-License-Identifier: GPL-2.0-or-later --> - true @@ -10,7 +9,8 @@ SPDX-License-Identifier: GPL-2.0-or-later + - + \ No newline at end of file diff --git a/src/SharpEmu.CLI/packages.lock.json b/src/SharpEmu.CLI/packages.lock.json index 24f5e4c..4f4620a 100644 --- a/src/SharpEmu.CLI/packages.lock.json +++ b/src/SharpEmu.CLI/packages.lock.json @@ -81,6 +81,7 @@ "dependencies": { "SharpEmu.HLE": "[1.0.0, )", "Silk.NET.Vulkan": "[2.23.0, )", + "Silk.NET.Vulkan.Extensions.EXT": "[2.23.0, )", "Silk.NET.Vulkan.Extensions.KHR": "[2.23.0, )", "Silk.NET.Windowing": "[2.23.0, )" } @@ -103,6 +104,16 @@ "Silk.NET.Core": "2.23.0" } }, + "Silk.NET.Vulkan.Extensions.EXT": { + "type": "CentralTransitive", + "requested": "[2.23.0, )", + "resolved": "2.23.0", + "contentHash": "+Oth189ksRiL6HvGCwIdnsYHawqrbO8y49u1H61z3wsfcHhQZeVDYe/wF5LD7fk3NcdgDvwFD3mLm1QWhdZySw==", + "dependencies": { + "Silk.NET.Core": "2.23.0", + "Silk.NET.Vulkan": "2.23.0" + } + }, "Silk.NET.Vulkan.Extensions.KHR": { "type": "CentralTransitive", "requested": "[2.23.0, )", diff --git a/src/SharpEmu.Core/packages.lock.json b/src/SharpEmu.Core/packages.lock.json index d70a0d5..c3f7ecb 100644 --- a/src/SharpEmu.Core/packages.lock.json +++ b/src/SharpEmu.Core/packages.lock.json @@ -72,6 +72,7 @@ "dependencies": { "SharpEmu.HLE": "[1.0.0, )", "Silk.NET.Vulkan": "[2.23.0, )", + "Silk.NET.Vulkan.Extensions.EXT": "[2.23.0, )", "Silk.NET.Vulkan.Extensions.KHR": "[2.23.0, )", "Silk.NET.Windowing": "[2.23.0, )" } @@ -88,6 +89,16 @@ "Silk.NET.Core": "2.23.0" } }, + "Silk.NET.Vulkan.Extensions.EXT": { + "type": "CentralTransitive", + "requested": "[2.23.0, )", + "resolved": "2.23.0", + "contentHash": "+Oth189ksRiL6HvGCwIdnsYHawqrbO8y49u1H61z3wsfcHhQZeVDYe/wF5LD7fk3NcdgDvwFD3mLm1QWhdZySw==", + "dependencies": { + "Silk.NET.Core": "2.23.0", + "Silk.NET.Vulkan": "2.23.0" + } + }, "Silk.NET.Vulkan.Extensions.KHR": { "type": "CentralTransitive", "requested": "[2.23.0, )", diff --git a/src/SharpEmu.Libs/SharpEmu.Libs.csproj b/src/SharpEmu.Libs/SharpEmu.Libs.csproj index 13893e6..773ba5c 100644 --- a/src/SharpEmu.Libs/SharpEmu.Libs.csproj +++ b/src/SharpEmu.Libs/SharpEmu.Libs.csproj @@ -10,6 +10,7 @@ SPDX-License-Identifier: GPL-2.0-or-later + diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs index 4ae1177..47d5ceb 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs @@ -7,6 +7,7 @@ using Silk.NET.Maths; using SharpEmu.Libs.Agc; using Silk.NET.Vulkan; using Silk.NET.Vulkan.Extensions.KHR; +using Silk.NET.Vulkan.Extensions.EXT; using Silk.NET.Windowing; using System.Numerics; using System.Runtime.CompilerServices; @@ -846,6 +847,8 @@ internal static unsafe class VulkanVideoPresenter private delegate* unmanaged _cmdEndDebugUtilsLabel; private Instance _instance; private SurfaceKHR _surface; + private DebugUtilsMessengerEXT _debugMessenger; + private ExtDebugUtils? _debugUtils; private PhysicalDevice _physicalDevice; private Device _device; private Queue _queue; @@ -1241,6 +1244,9 @@ internal static unsafe class VulkanVideoPresenter private void CreateInstance() { var applicationName = (byte*)SilkMarshal.StringToPtr("SharpEmu"); + var enableValidation = Environment.GetEnvironmentVariable("SHARPEMU_VK_VALIDATION") == "1"; + byte* validationLayerName = null; + try { var applicationInfo = new ApplicationInfo @@ -1268,12 +1274,29 @@ internal static unsafe class VulkanVideoPresenter enabledExtensions[enabledExtensionCount++] = debugUtilsExtension; } + if (enableValidation && IsInstanceLayerAvailable("VK_LAYER_KHRONOS_validation")) + { + validationLayerName = (byte*)SilkMarshal.StringToPtr("VK_LAYER_KHRONOS_validation"); + } + else if (enableValidation) + { + 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, PApplicationInfo = &applicationInfo, EnabledExtensionCount = (uint)enabledExtensionCount, PpEnabledExtensionNames = enabledExtensions, + EnabledLayerCount = validationLayerName is not null ? 1u : 0u, + PpEnabledLayerNames = validationLayerName is not null ? layers : null, }; try @@ -1283,6 +1306,13 @@ internal static unsafe class VulkanVideoPresenter { 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 { @@ -1295,9 +1325,74 @@ internal static unsafe class VulkanVideoPresenter finally { SilkMarshal.Free((nint)applicationName); + if (validationLayerName is not null) + { + SilkMarshal.Free((nint)validationLayerName); + } } } + 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}"); + return Vk.False; + } private void CreateSurface() { var instanceHandle = new VkHandle(_instance.Handle); @@ -1358,7 +1453,28 @@ internal static unsafe class VulkanVideoPresenter QueueCount = 1, PQueuePriorities = &priority, }; + _vk.GetPhysicalDeviceFeatures(_physicalDevice, out var supportedFeatures); + var enabledFeatures = new PhysicalDeviceFeatures + { + VertexPipelineStoresAndAtomics = supportedFeatures.VertexPipelineStoresAndAtomics, + FragmentStoresAndAtomics = supportedFeatures.FragmentStoresAndAtomics, + ShaderInt64 = supportedFeatures.ShaderInt64, + }; + if (!supportedFeatures.ShaderInt64) + { + Console.Error.WriteLine( + "[LOADER][WARN] GPU does not support shaderInt64 " + + "translated shaders using 64-bit integers 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."); + } + var swapchainExtension = (byte*)SilkMarshal.StringToPtr("VK_KHR_swapchain"); try { @@ -1369,6 +1485,7 @@ internal static unsafe class VulkanVideoPresenter PQueueCreateInfos = &queueInfo, EnabledExtensionCount = 1, PpEnabledExtensionNames = &swapchainExtension, + PEnabledFeatures = &enabledFeatures, }; Check(_vk.CreateDevice(_physicalDevice, &createInfo, null, out _device), "vkCreateDevice"); @@ -5956,6 +6073,10 @@ internal static unsafe class VulkanVideoPresenter return; } + if (_debugUtils is not null && _debugMessenger.Handle != 0) + { + _debugUtils.DestroyDebugUtilsMessenger(_instance, _debugMessenger, null); + } _vulkanReady = false; _vk.DeviceWaitIdle(_device); CollectCompletedGuestSubmissions(waitForOldest: false); diff --git a/src/SharpEmu.Libs/packages.lock.json b/src/SharpEmu.Libs/packages.lock.json index 9a2e14b..49c33ed 100644 --- a/src/SharpEmu.Libs/packages.lock.json +++ b/src/SharpEmu.Libs/packages.lock.json @@ -11,6 +11,16 @@ "Silk.NET.Core": "2.23.0" } }, + "Silk.NET.Vulkan.Extensions.EXT": { + "type": "Direct", + "requested": "[2.23.0, )", + "resolved": "2.23.0", + "contentHash": "+Oth189ksRiL6HvGCwIdnsYHawqrbO8y49u1H61z3wsfcHhQZeVDYe/wF5LD7fk3NcdgDvwFD3mLm1QWhdZySw==", + "dependencies": { + "Silk.NET.Core": "2.23.0", + "Silk.NET.Vulkan": "2.23.0" + } + }, "Silk.NET.Vulkan.Extensions.KHR": { "type": "Direct", "requested": "[2.23.0, )",