1590 lines
70 KiB
C#
1590 lines
70 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 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 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<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();
|
|
}
|
|
}
|
|
}
|