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, )",