526 lines
19 KiB
C#
526 lines
19 KiB
C#
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
using SharpEmu.Libs.Gpu.Buffers;
|
|
using SharpEmu.Libs.Gpu.Scheduling;
|
|
using SharpEmu.Libs.Gpu.Vulkan;
|
|
using Silk.NET.Core.Native;
|
|
using Silk.NET.Vulkan;
|
|
using Silk.NET.Vulkan.Extensions.EXT;
|
|
using Xunit;
|
|
using VkSemaphore = Silk.NET.Vulkan.Semaphore;
|
|
|
|
namespace SharpEmu.Libs.Tests.Gpu.Vulkan;
|
|
|
|
// A window-less Vulkan 1.2 device with timeline semaphores and buffer device addresses; null when the host has none.
|
|
internal sealed unsafe class HeadlessVulkan : IDisposable
|
|
{
|
|
// Set SHARPEMU_TEST_VK_VALIDATION=1 to load the validation layer and print its messages.
|
|
private const string ValidationVariable = "SHARPEMU_TEST_VK_VALIDATION";
|
|
private static readonly PfnDebugUtilsMessengerCallbackEXT DebugCallbackPointer = new(DebugCallback);
|
|
private static readonly List<string> ValidationMessages = new();
|
|
|
|
private GpuDeviceInfo? _deviceInfo;
|
|
private ExtDebugUtils? _debugUtils;
|
|
private DebugUtilsMessengerEXT _debugMessenger;
|
|
|
|
private HeadlessVulkan(Vk vk, Instance instance, PhysicalDevice physical, Device device, Queue queue, uint queueFamily, uint apiVersion, in PhysicalDeviceFeatures features, bool dynamicRendering)
|
|
{
|
|
Vk = vk;
|
|
Instance = instance;
|
|
Physical = physical;
|
|
Device = device;
|
|
Queue = queue;
|
|
QueueFamily = queueFamily;
|
|
ApiVersion = apiVersion;
|
|
SampleRateShading = features.SampleRateShading;
|
|
SamplerAnisotropy = features.SamplerAnisotropy;
|
|
StorageImageExtendedFormats = features.ShaderStorageImageExtendedFormats;
|
|
ShaderInt64 = features.ShaderInt64;
|
|
SupportsDynamicRendering = dynamicRendering;
|
|
}
|
|
|
|
// Dynamic rendering with the two extended dynamic state extensions, as the presenter's render host needs.
|
|
public bool SupportsDynamicRendering { get; }
|
|
public bool SupportsFragmentShaderBarycentric { get; private init; }
|
|
|
|
private static readonly string[] RenderingExtensionNames =
|
|
[
|
|
"VK_KHR_dynamic_rendering",
|
|
"VK_EXT_extended_dynamic_state",
|
|
"VK_EXT_extended_dynamic_state2",
|
|
"VK_KHR_push_descriptor",
|
|
];
|
|
|
|
// The push descriptor limit of the device; the render host pushes sets that fit it.
|
|
public uint MaxPushDescriptors
|
|
{
|
|
get
|
|
{
|
|
var pushDescriptors = new PhysicalDevicePushDescriptorPropertiesKHR { SType = StructureType.PhysicalDevicePushDescriptorPropertiesKhr };
|
|
var properties = new PhysicalDeviceProperties2 { SType = StructureType.PhysicalDeviceProperties2, PNext = &pushDescriptors };
|
|
Vk.GetPhysicalDeviceProperties2(Physical, &properties);
|
|
return pushDescriptors.MaxPushDescriptors;
|
|
}
|
|
}
|
|
|
|
public Vk Vk { get; }
|
|
|
|
public Instance Instance { get; }
|
|
|
|
public PhysicalDevice Physical { get; }
|
|
|
|
public Device Device { get; }
|
|
|
|
public Queue Queue { get; }
|
|
|
|
public uint QueueFamily { get; }
|
|
|
|
public object QueueGate { get; } = new();
|
|
|
|
// The device API version: 1.3 when the loader and the device support it, else 1.2.
|
|
public uint ApiVersion { get; }
|
|
|
|
// SPIR-V 1.6 modules (the compiled reference blobs) need a Vulkan 1.3 device.
|
|
public bool SupportsSpirv16 => ApiVersion >= Vk.Version13;
|
|
|
|
// True when the device was created with the sample-rate-shading feature the blit needs.
|
|
public bool SampleRateShading { get; }
|
|
|
|
public bool SamplerAnisotropy { get; }
|
|
|
|
public bool StorageImageExtendedFormats { get; }
|
|
|
|
public bool ShaderInt64 { get; }
|
|
|
|
public bool ValidationEnabled => _debugUtils is not null;
|
|
|
|
// The validation messages collected since the last call; empty when the layer is off.
|
|
public string[] TakeValidationMessages()
|
|
{
|
|
lock (ValidationMessages)
|
|
{
|
|
var messages = ValidationMessages.ToArray();
|
|
ValidationMessages.Clear();
|
|
return messages;
|
|
}
|
|
}
|
|
|
|
public void AssertNoValidationMessages()
|
|
{
|
|
var messages = TakeValidationMessages();
|
|
Assert.True(messages.Length == 0, "Validation layer messages:" + Environment.NewLine + string.Join(Environment.NewLine, messages));
|
|
}
|
|
|
|
// One shared instance so the live allocation counter spans every buffer and image of a test.
|
|
public GpuDeviceInfo DeviceInfo => _deviceInfo ??= new GpuDeviceInfo(Vk, Physical, Device);
|
|
|
|
public string DeviceName
|
|
{
|
|
get
|
|
{
|
|
Vk.GetPhysicalDeviceProperties(Physical, out var properties);
|
|
return Silk.NET.Core.Native.SilkMarshal.PtrToString((nint)properties.DeviceName) ?? "unknown";
|
|
}
|
|
}
|
|
|
|
public static HeadlessVulkan? TryCreate()
|
|
{
|
|
try
|
|
{
|
|
return Create();
|
|
}
|
|
catch (Exception)
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public VulkanTickDevice NewTickDevice() => new(Vk, Device, Queue, QueueFamily, QueueGate);
|
|
|
|
public ulong CreateTimelineSemaphore()
|
|
{
|
|
var typeInfo = new SemaphoreTypeCreateInfo
|
|
{
|
|
SType = StructureType.SemaphoreTypeCreateInfo,
|
|
SemaphoreType = SemaphoreType.Timeline,
|
|
InitialValue = 0,
|
|
};
|
|
var createInfo = new SemaphoreCreateInfo { SType = StructureType.SemaphoreCreateInfo, PNext = &typeInfo };
|
|
Require(Vk.CreateSemaphore(Device, &createInfo, null, out VkSemaphore semaphore), "vkCreateSemaphore");
|
|
return semaphore.Handle;
|
|
}
|
|
|
|
public ulong ReadSemaphore(ulong handle)
|
|
{
|
|
ulong value;
|
|
Require(Vk.GetSemaphoreCounterValue(Device, new VkSemaphore(handle), &value), "vkGetSemaphoreCounterValue");
|
|
return value;
|
|
}
|
|
|
|
public void DestroySemaphore(ulong handle) => Vk.DestroySemaphore(Device, new VkSemaphore(handle), null);
|
|
|
|
public void Dispose()
|
|
{
|
|
Vk.DeviceWaitIdle(Device);
|
|
Vk.DestroyDevice(Device, null);
|
|
if (_debugUtils is { } debugUtils)
|
|
{
|
|
debugUtils.DestroyDebugUtilsMessenger(Instance, _debugMessenger, null);
|
|
}
|
|
|
|
Vk.DestroyInstance(Instance, null);
|
|
}
|
|
|
|
private static uint DebugCallback(DebugUtilsMessageSeverityFlagsEXT severity, DebugUtilsMessageTypeFlagsEXT type, DebugUtilsMessengerCallbackDataEXT* data, void* userData)
|
|
{
|
|
var message = $"[VULKAN][{severity}] {SilkMarshal.PtrToString((nint)data->PMessage)}";
|
|
Console.Error.WriteLine(message);
|
|
lock (ValidationMessages)
|
|
{
|
|
ValidationMessages.Add(message);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
private void RegisterDebugMessenger()
|
|
{
|
|
if (!Vk.TryGetInstanceExtension(Instance, out ExtDebugUtils debugUtils))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var info = new DebugUtilsMessengerCreateInfoEXT
|
|
{
|
|
SType = StructureType.DebugUtilsMessengerCreateInfoExt,
|
|
MessageSeverity = DebugUtilsMessageSeverityFlagsEXT.WarningBitExt | DebugUtilsMessageSeverityFlagsEXT.ErrorBitExt,
|
|
MessageType = DebugUtilsMessageTypeFlagsEXT.ValidationBitExt | DebugUtilsMessageTypeFlagsEXT.GeneralBitExt,
|
|
PfnUserCallback = DebugCallbackPointer,
|
|
};
|
|
if (debugUtils.CreateDebugUtilsMessenger(Instance, &info, null, out _debugMessenger) == Result.Success)
|
|
{
|
|
_debugUtils = debugUtils;
|
|
}
|
|
}
|
|
|
|
private static HeadlessVulkan? Create()
|
|
{
|
|
var vk = Vk.GetApi();
|
|
uint instanceVersion = Vk.Version10;
|
|
vk.EnumerateInstanceVersion(ref instanceVersion);
|
|
var apiVersion = instanceVersion >= Vk.Version13 ? Vk.Version13 : Vk.Version12;
|
|
var appInfo = new ApplicationInfo { SType = StructureType.ApplicationInfo, ApiVersion = apiVersion };
|
|
var validation = Environment.GetEnvironmentVariable(ValidationVariable) == "1";
|
|
var layers = validation ? SilkMarshal.StringArrayToPtr(new[] { "VK_LAYER_KHRONOS_validation" }) : 0;
|
|
var extensions = validation ? SilkMarshal.StringArrayToPtr(new[] { ExtDebugUtils.ExtensionName }) : 0;
|
|
var instanceInfo = new InstanceCreateInfo
|
|
{
|
|
SType = StructureType.InstanceCreateInfo,
|
|
PApplicationInfo = &appInfo,
|
|
EnabledLayerCount = validation ? 1u : 0u,
|
|
PpEnabledLayerNames = (byte**)layers,
|
|
EnabledExtensionCount = validation ? 1u : 0u,
|
|
PpEnabledExtensionNames = (byte**)extensions,
|
|
};
|
|
var created = vk.CreateInstance(&instanceInfo, null, out var instance);
|
|
if (validation)
|
|
{
|
|
SilkMarshal.Free(layers);
|
|
SilkMarshal.Free(extensions);
|
|
}
|
|
|
|
if (created != Result.Success)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
uint deviceCount = 0;
|
|
vk.EnumeratePhysicalDevices(instance, &deviceCount, null);
|
|
if (deviceCount == 0)
|
|
{
|
|
vk.DestroyInstance(instance, null);
|
|
return null;
|
|
}
|
|
|
|
var devices = new PhysicalDevice[deviceCount];
|
|
fixed (PhysicalDevice* pointer = devices)
|
|
{
|
|
vk.EnumeratePhysicalDevices(instance, &deviceCount, pointer);
|
|
}
|
|
|
|
var physical = devices[0];
|
|
foreach (var candidate in devices)
|
|
{
|
|
vk.GetPhysicalDeviceProperties(candidate, out var properties);
|
|
if (properties.DeviceType == PhysicalDeviceType.DiscreteGpu)
|
|
{
|
|
physical = candidate;
|
|
break;
|
|
}
|
|
}
|
|
|
|
vk.GetPhysicalDeviceProperties(physical, out var physicalProperties);
|
|
if (physicalProperties.ApiVersion < apiVersion)
|
|
{
|
|
apiVersion = Vk.Version12;
|
|
}
|
|
|
|
uint familyCount = 0;
|
|
vk.GetPhysicalDeviceQueueFamilyProperties(physical, &familyCount, null);
|
|
var families = new QueueFamilyProperties[familyCount];
|
|
fixed (QueueFamilyProperties* pointer = families)
|
|
{
|
|
vk.GetPhysicalDeviceQueueFamilyProperties(physical, &familyCount, pointer);
|
|
}
|
|
|
|
var family = uint.MaxValue;
|
|
for (uint index = 0; index < familyCount; index++)
|
|
{
|
|
if ((families[index].QueueFlags & (QueueFlags.GraphicsBit | QueueFlags.ComputeBit)) ==
|
|
(QueueFlags.GraphicsBit | QueueFlags.ComputeBit))
|
|
{
|
|
family = index;
|
|
break;
|
|
}
|
|
}
|
|
|
|
const string barycentricExtension = "VK_KHR_fragment_shader_barycentric";
|
|
var barycentricFeatures = new PhysicalDeviceFragmentShaderBarycentricFeaturesKHR
|
|
{
|
|
SType = StructureType.PhysicalDeviceFragmentShaderBarycentricFeaturesKhr,
|
|
};
|
|
if (HasDeviceExtensions(vk, physical, [barycentricExtension]))
|
|
{
|
|
var query = new PhysicalDeviceFeatures2
|
|
{
|
|
SType = StructureType.PhysicalDeviceFeatures2,
|
|
PNext = &barycentricFeatures,
|
|
};
|
|
vk.GetPhysicalDeviceFeatures2(physical, &query);
|
|
}
|
|
var barycentric = (bool)barycentricFeatures.FragmentShaderBarycentric;
|
|
|
|
var extendedDynamicState2Features = new PhysicalDeviceExtendedDynamicState2FeaturesEXT
|
|
{
|
|
SType = StructureType.PhysicalDeviceExtendedDynamicState2FeaturesExt,
|
|
};
|
|
var extendedDynamicStateFeatures = new PhysicalDeviceExtendedDynamicStateFeaturesEXT
|
|
{
|
|
SType = StructureType.PhysicalDeviceExtendedDynamicStateFeaturesExt,
|
|
PNext = &extendedDynamicState2Features,
|
|
};
|
|
var dynamicRenderingFeatures = new PhysicalDeviceDynamicRenderingFeaturesKHR
|
|
{
|
|
SType = StructureType.PhysicalDeviceDynamicRenderingFeaturesKhr,
|
|
PNext = &extendedDynamicStateFeatures,
|
|
};
|
|
var addressFeatures = new PhysicalDeviceBufferDeviceAddressFeatures
|
|
{
|
|
SType = StructureType.PhysicalDeviceBufferDeviceAddressFeatures,
|
|
PNext = &dynamicRenderingFeatures,
|
|
};
|
|
var timelineFeatures = new PhysicalDeviceTimelineSemaphoreFeatures
|
|
{
|
|
SType = StructureType.PhysicalDeviceTimelineSemaphoreFeatures,
|
|
PNext = &addressFeatures,
|
|
};
|
|
var features = new PhysicalDeviceFeatures2 { SType = StructureType.PhysicalDeviceFeatures2, PNext = &timelineFeatures };
|
|
vk.GetPhysicalDeviceFeatures2(physical, &features);
|
|
var dynamicRendering = dynamicRenderingFeatures.DynamicRendering && extendedDynamicStateFeatures.ExtendedDynamicState &&
|
|
extendedDynamicState2Features.ExtendedDynamicState2 && HasDeviceExtensions(vk, physical, RenderingExtensionNames);
|
|
if (family == uint.MaxValue || !timelineFeatures.TimelineSemaphore || !addressFeatures.BufferDeviceAddress)
|
|
{
|
|
vk.DestroyInstance(instance, null);
|
|
return null;
|
|
}
|
|
|
|
vk.GetPhysicalDeviceFeatures(physical, out var baseFeatures);
|
|
var enabledFeatures = new PhysicalDeviceFeatures
|
|
{
|
|
SampleRateShading = baseFeatures.SampleRateShading,
|
|
SamplerAnisotropy = baseFeatures.SamplerAnisotropy,
|
|
ShaderStorageImageExtendedFormats = baseFeatures.ShaderStorageImageExtendedFormats,
|
|
ShaderInt64 = baseFeatures.ShaderInt64,
|
|
};
|
|
var priority = 1f;
|
|
var queueInfo = new DeviceQueueCreateInfo
|
|
{
|
|
SType = StructureType.DeviceQueueCreateInfo,
|
|
QueueFamilyIndex = family,
|
|
QueueCount = 1,
|
|
PQueuePriorities = &priority,
|
|
};
|
|
timelineFeatures.TimelineSemaphore = true;
|
|
extendedDynamicState2Features = new PhysicalDeviceExtendedDynamicState2FeaturesEXT
|
|
{
|
|
SType = StructureType.PhysicalDeviceExtendedDynamicState2FeaturesExt,
|
|
ExtendedDynamicState2 = true,
|
|
};
|
|
extendedDynamicStateFeatures = new PhysicalDeviceExtendedDynamicStateFeaturesEXT
|
|
{
|
|
SType = StructureType.PhysicalDeviceExtendedDynamicStateFeaturesExt,
|
|
ExtendedDynamicState = true,
|
|
PNext = &extendedDynamicState2Features,
|
|
};
|
|
dynamicRenderingFeatures = new PhysicalDeviceDynamicRenderingFeaturesKHR
|
|
{
|
|
SType = StructureType.PhysicalDeviceDynamicRenderingFeaturesKhr,
|
|
DynamicRendering = true,
|
|
PNext = &extendedDynamicStateFeatures,
|
|
};
|
|
addressFeatures = new PhysicalDeviceBufferDeviceAddressFeatures
|
|
{
|
|
SType = StructureType.PhysicalDeviceBufferDeviceAddressFeatures,
|
|
BufferDeviceAddress = true,
|
|
PNext = dynamicRendering ? &dynamicRenderingFeatures : null,
|
|
};
|
|
timelineFeatures.PNext = &addressFeatures;
|
|
if (barycentric)
|
|
{
|
|
barycentricFeatures.PNext = addressFeatures.PNext;
|
|
addressFeatures.PNext = &barycentricFeatures;
|
|
}
|
|
var extensionNames = new List<string>();
|
|
if (dynamicRendering) extensionNames.AddRange(RenderingExtensionNames);
|
|
if (barycentric) extensionNames.Add(barycentricExtension);
|
|
var deviceExtensions = extensionNames.Count > 0 ? SilkMarshal.StringArrayToPtr(extensionNames.ToArray()) : 0;
|
|
var deviceInfo = new DeviceCreateInfo
|
|
{
|
|
SType = StructureType.DeviceCreateInfo,
|
|
PNext = &timelineFeatures,
|
|
QueueCreateInfoCount = 1,
|
|
PQueueCreateInfos = &queueInfo,
|
|
PEnabledFeatures = &enabledFeatures,
|
|
EnabledExtensionCount = (uint)extensionNames.Count,
|
|
PpEnabledExtensionNames = (byte**)deviceExtensions,
|
|
};
|
|
var deviceCreated = vk.CreateDevice(physical, &deviceInfo, null, out var device);
|
|
if (deviceExtensions != 0)
|
|
{
|
|
SilkMarshal.Free(deviceExtensions);
|
|
}
|
|
|
|
if (deviceCreated != Result.Success)
|
|
{
|
|
vk.DestroyInstance(instance, null);
|
|
return null;
|
|
}
|
|
|
|
vk.GetDeviceQueue(device, family, 0, out var queue);
|
|
var result = new HeadlessVulkan(vk, instance, physical, device, queue, family, apiVersion, enabledFeatures, dynamicRendering)
|
|
{
|
|
SupportsFragmentShaderBarycentric = barycentric,
|
|
};
|
|
if (validation)
|
|
{
|
|
result.RegisterDebugMessenger();
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private static bool HasDeviceExtensions(Vk vk, PhysicalDevice physical, IReadOnlyList<string> names)
|
|
{
|
|
uint count = 0;
|
|
if (vk.EnumerateDeviceExtensionProperties(physical, (byte*)null, &count, null) != Result.Success || count == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var properties = new ExtensionProperties[count];
|
|
fixed (ExtensionProperties* pointer = properties)
|
|
{
|
|
if (vk.EnumerateDeviceExtensionProperties(physical, (byte*)null, &count, pointer) != Result.Success)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
var available = new HashSet<string>();
|
|
for (var index = 0; index < count; index++)
|
|
{
|
|
var property = properties[index];
|
|
available.Add(SilkMarshal.PtrToString((nint)property.ExtensionName) ?? string.Empty);
|
|
}
|
|
|
|
return names.All(available.Contains);
|
|
}
|
|
|
|
private static void Require(Result result, string operation)
|
|
{
|
|
if (result != Result.Success)
|
|
{
|
|
throw new InvalidOperationException($"{operation} failed with {result}");
|
|
}
|
|
}
|
|
}
|
|
|
|
public sealed class HeadlessVulkanFixture : IDisposable
|
|
{
|
|
internal HeadlessVulkan? Vulkan { get; } = HeadlessVulkan.TryCreate();
|
|
|
|
public void Dispose() => Vulkan?.Dispose();
|
|
}
|
|
|
|
// Records the scheduler's device calls around a real device so tests can see waits and submits.
|
|
internal sealed class LoggingTickDevice : IGpuTickDevice
|
|
{
|
|
private readonly IGpuTickDevice _inner;
|
|
private readonly List<string> _log = new();
|
|
|
|
public LoggingTickDevice(IGpuTickDevice inner) => _inner = inner;
|
|
|
|
public object QueueGate => _inner.QueueGate;
|
|
|
|
public ulong TimelineHandle => _inner.TimelineHandle;
|
|
|
|
public string[] Log
|
|
{
|
|
get
|
|
{
|
|
lock (_log)
|
|
{
|
|
return _log.ToArray();
|
|
}
|
|
}
|
|
}
|
|
|
|
public ulong ReadTimeline() => _inner.ReadTimeline();
|
|
|
|
public bool TryWaitTimeline(ulong tick, out string failure)
|
|
{
|
|
Note($"wait {tick}");
|
|
return _inner.TryWaitTimeline(tick, out failure);
|
|
}
|
|
|
|
public nint[] AllocateBuffers(int count) => _inner.AllocateBuffers(count);
|
|
|
|
public void BeginBuffer(nint buffer) => _inner.BeginBuffer(buffer);
|
|
|
|
public void EndBuffer(nint buffer) => _inner.EndBuffer(buffer);
|
|
|
|
public bool TrySubmit(nint buffer, SubmitBundle bundle, out string failure)
|
|
{
|
|
for (var i = 0; i < bundle.SignalCount; i++)
|
|
{
|
|
if (bundle.SignalSemaphores[i] == TimelineHandle)
|
|
{
|
|
Note($"submit {bundle.SignalTicks[i]}");
|
|
}
|
|
}
|
|
|
|
return _inner.TrySubmit(buffer, bundle, out failure);
|
|
}
|
|
|
|
public void Dispose() => _inner.Dispose();
|
|
|
|
private void Note(string entry)
|
|
{
|
|
lock (_log)
|
|
{
|
|
_log.Add(entry);
|
|
}
|
|
}
|
|
}
|