From eb0e558a4db82026a6697110c5219b7e701a7c72 Mon Sep 17 00:00:00 2001 From: Alexey Tarasov Date: Fri, 23 Sep 2022 15:11:32 +1000 Subject: [PATCH 1/3] Adds SimpleCocoaWindow to manage Cocoa NSWindow. There are two major parts in this commit: * basic Objective-C runtime layer; * `SimpleCocoaWindow` that makes use of it. The former one relies on Objective-C runtime (`libobjc`) methods to perform a few basic operations. The most important ones are `objc_msgSend` bindings that differ in number of input arguments and return value yet invoke the same function exported by `libobjc`. See more on runtime in Apple documentation: https://developer.apple.com/documentation/objectivec/objective-c_runtime `SimpleCocoaWindow` invokes `Cocoa` object methods to acquire Metal layer and show/hide window. Note that it uses `AppKit` types such as `NSWindow` or `NSView` and thus is macOS-specific. iOS devices use `UIKit` (`Cocoa Touch`) and thus initialisation should be done by sending messages to `UIView`. --- SPB/Platform/Cocoa/ObjectiveC.cs | 179 ++++++++++++++++++++++++ SPB/Platform/Cocoa/SimpleCocoaWindow.cs | 109 +++++++++++++++ 2 files changed, 288 insertions(+) create mode 100644 SPB/Platform/Cocoa/ObjectiveC.cs create mode 100644 SPB/Platform/Cocoa/SimpleCocoaWindow.cs diff --git a/SPB/Platform/Cocoa/ObjectiveC.cs b/SPB/Platform/Cocoa/ObjectiveC.cs new file mode 100644 index 0000000..d518161 --- /dev/null +++ b/SPB/Platform/Cocoa/ObjectiveC.cs @@ -0,0 +1,179 @@ +using System; +using System.Runtime.InteropServices; + +namespace SPB.Platform.Cocoa; + +/// +/// This class provides basic interaction with ObjectiveC runtime. +/// See: https://developer.apple.com/documentation/objectivec/objective-c_runtime +/// +internal class ObjectiveC +{ + // In NET6+ this should be easier with updated interop services + private const string LibraryName = "libobjc.dylib"; + + [DllImport(LibraryName, CharSet = CharSet.Ansi)] + private static extern IntPtr objc_getClass(string className); + + [DllImport(LibraryName, CharSet = CharSet.Ansi)] + private static extern IntPtr sel_getUid(string selector); + + [DllImport(LibraryName, CharSet = CharSet.Ansi)] + private static extern string object_getClass(IntPtr obj); + + [DllImport(LibraryName)] + private static extern IntPtr objc_msgSend(IntPtr self, IntPtr selector); + + [DllImport(LibraryName, EntryPoint = "objc_msgSend")] + private static extern bool bool_msgSend_IntPtr(IntPtr self, IntPtr selector, IntPtr arg); + + [DllImport(LibraryName, EntryPoint = "objc_msgSend")] + private static extern void void_msgSend_IntPtr(IntPtr self, IntPtr selector, IntPtr arg); + + [DllImport(LibraryName, EntryPoint = "objc_msgSend")] + private static extern void void_msgSend_void(IntPtr self, IntPtr selector); + + [DllImport(LibraryName, EntryPoint = "objc_msgSend")] + private static extern void void_msgSend_bool(IntPtr self, IntPtr selector, bool arg); + + /// + /// This method checks if a given object is of expected class and throws + /// ArgumentException if it is not. + /// + /// Object to check class + /// Class name, e.g. 'NSWindow' + /// Is thrown if obj is not an instance of class className + internal static void EnsureIsKindOfClass(IntPtr obj, string className) + { + if (IsKindOfClass(obj, className)) + { + return; + } + + var objClassName = GetObjectClassName(obj); + throw new ArgumentException( + $"Object is of type '{objClassName}' instead of expected '{className}'" + ); + } + + /// + /// This method checks if a given object is of expected class specified by its string name. + /// + /// Object to check class + /// Class name, e.g. 'NSWindow' + /// true if object type matches class, false otherwise + internal static bool IsKindOfClass(IntPtr obj, string className) + { + if (obj == IntPtr.Zero) + { + return false; + } + + var klass = GetClass(className, false); + + if (klass == IntPtr.Zero) + { + return false; + } + + var selector = GetSelector("isKindOfClass:"); + return bool_msgSend_IntPtr(obj, selector, klass); + } + + /// + /// This method returns class name for an object. + /// + /// Instance to get class name for + /// Name of class for a given object instance + private static string GetObjectClassName(IntPtr obj) + { + return object_getClass(obj); + } + + /// + /// Returns type for class specified by its name. + /// + /// Class name, e.g. 'NSWindow' + /// Indicates need to raise exception if class name is not known + /// Class type as IntPtr reference + /// This exception is raised if class name is not known + internal static IntPtr GetClass(string className, bool throwOnUnknownName = true) + { + var result = objc_getClass(className); + + if (throwOnUnknownName && result == IntPtr.Zero) + { + throw new ArgumentException($"No such class '{className}'"); + } + + return result; + } + + /// + /// This method returns selector by its name. + /// + /// Name of selector, e.g. 'layer:' + /// Selector as IntPtr reference + /// This exception is raised if selector name is not known + private static IntPtr GetSelector(string selectorName) + { + var selector = sel_getUid(selectorName); + + if (selector != IntPtr.Zero) + { + return selector; + } + + throw new ArgumentException( + $"Can not get selector '{selectorName}'" + ); + } + + /// + /// This method sends message that object responds with IntPtr. + /// + /// Object to send message to + /// Name of selector for the message + /// IntPtr value as responded by object + internal static IntPtr GetIntPtrValue(IntPtr obj, string selectorName) + { + var selector = GetSelector(selectorName); + return objc_msgSend(obj, selector); + } + + /// + /// This method sends message to set object property to IntPtr value. + /// + /// Object to send message to + /// Name of selector for the message + /// Value to set + internal static void SetIntPtrValue(IntPtr obj, string selectorName, IntPtr value) + { + var selector = GetSelector(selectorName); + void_msgSend_IntPtr(obj, selector, value); + } + + /// + /// This method sends message to set object property to boolean value. + /// + /// Object to send message to + /// Name of selector for the message + /// Value to set + internal static void SetBoolValue(IntPtr obj, string selectorName, bool value) + { + var selector = GetSelector(selectorName); + void_msgSend_bool(obj, selector, value); + } + + /// + /// This method sends message to invoke object method without any arguments + /// or return value. + /// + /// Object to send message to + /// Name of selector for the message + internal static void InvokeVoid(IntPtr obj, string selectorName) + { + var selector = GetSelector(selectorName); + void_msgSend_void(obj, selector); + } +} diff --git a/SPB/Platform/Cocoa/SimpleCocoaWindow.cs b/SPB/Platform/Cocoa/SimpleCocoaWindow.cs new file mode 100644 index 0000000..adcea79 --- /dev/null +++ b/SPB/Platform/Cocoa/SimpleCocoaWindow.cs @@ -0,0 +1,109 @@ +using System; +using System.Runtime.Versioning; +using SPB.Platform.Exceptions; +using SPB.Windowing; + +namespace SPB.Platform.Cocoa; + +/// +/// Provides implementation for bindings to NSWindow class on macOS. +/// +[SupportedOSPlatform("macos")] +public class SimpleCocoaWindow : NativeWindowBase +{ + // NSWindow pointer wrapper + public override NativeHandle WindowHandle { get; } + + // CAMetalLayer pointer wrapper + public NativeHandle MetalLayerHandle { get; private set; } + + public override NativeHandle DisplayHandle => throw new NotImplementedException(); + + /// + /// Constructs instance of this class. + /// + /// NSWindow native pointer + /// Flag indicating need to acquire Metal layer + public SimpleCocoaWindow(NativeHandle windowHandle, bool initMetalLayer = true) + { + WindowHandle = windowHandle; + + // Sanity check + ObjectiveC.EnsureIsKindOfClass(windowHandle.RawHandle, "NSWindow"); + + if (initMetalLayer) + { + InitMetalLayer(); + } + } + + private void InitMetalLayer() + { + // assuming that handles is NSWindow + // 1. get top-level NSView + // 2. get its layer + // 3. if it is CAMetalLayer, we have what we need + // 4. make it CAMetalLayer + + // 1. get top-level NSView + var contentView = ObjectiveC.GetIntPtrValue(WindowHandle.RawHandle, "contentView"); + + ObjectiveC.EnsureIsKindOfClass(contentView, "NSView"); + + // 2. get its layer + var layer = ObjectiveC.GetIntPtrValue(contentView, "layer"); + + // 3. if it is CAMetalLayer, we have what we need + if (ObjectiveC.IsKindOfClass(contentView, "CAMetalLayer")) + { + MetalLayerHandle = new NativeHandle(layer); + return; + } + + // 4. make it CAMetalLayer + // https://developer.apple.com/documentation/quartzcore/cametallayer?language=objc + Console.Out.WriteLine("Replacing original NSView layer with CAMetalLayer"); + + var layerClass = ObjectiveC.GetClass("CAMetalLayer"); + var metalLayer = ObjectiveC.GetIntPtrValue(layerClass, "layer"); + MetalLayerHandle = new NativeHandle(metalLayer); + + ObjectiveC.SetBoolValue(contentView, "wantsLayer", true); + ObjectiveC.SetIntPtrValue(contentView, "setLayer:", metalLayer); + } + + protected override void Dispose(bool disposing) + { + // Do nothing as no actual resource is owned + } + + public override void Show() + { + if (WindowHandle.RawHandle == IntPtr.Zero) + { + throw new PlatformException("Attempted to show not initialised window"); + } + + // [self makeKeyAndOrderFront:self]; + ObjectiveC.SetIntPtrValue( + WindowHandle.RawHandle, + "makeKeyAndOrderFront:", + WindowHandle.RawHandle + ); + } + + public override void Hide() + { + if (WindowHandle.RawHandle == IntPtr.Zero) + { + throw new PlatformException("Attempted to hide not initialised window"); + } + + // [self orderOut:self]; + ObjectiveC.SetIntPtrValue( + WindowHandle.RawHandle, + "orderOut:", + WindowHandle.RawHandle + ); + } +} From ebf01cf99dd9d980b54c2f928af4ba2ca9755f0d Mon Sep 17 00:00:00 2001 From: Alexey Tarasov Date: Fri, 23 Sep 2022 15:32:20 +1000 Subject: [PATCH 2/3] Updates VulkanHelper with macOS specific implementation. Uses MoltenVk implementation of Vulkan API, see: https://github.com/KhronosGroup/MoltenVK Supports both deprecated `VK_MVK_macos_surface` and actual `VK_EXT_metal_surface` extensions to create Vulkan surface. Also slightly refactors helper to allow specification of multiple dynamic libraries to probe. --- SPB/Graphics/Vulkan/VulkanHelper.cs | 495 +++++++++++++++++++++------- 1 file changed, 373 insertions(+), 122 deletions(-) diff --git a/SPB/Graphics/Vulkan/VulkanHelper.cs b/SPB/Graphics/Vulkan/VulkanHelper.cs index 3907aab..1d03894 100644 --- a/SPB/Graphics/Vulkan/VulkanHelper.cs +++ b/SPB/Graphics/Vulkan/VulkanHelper.cs @@ -6,8 +6,10 @@ using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; +using System.Runtime.Versioning; using System.Text; using System.Threading.Tasks; +using SPB.Platform.Cocoa; namespace SPB.Graphics.Vulkan { @@ -17,9 +19,26 @@ public static class VulkanHelper private static IntPtr _vulkanHandle; - private const string VulkanLibraryNameWindows = "vulkan-1.dll"; - private const string VulkanLibraryNameLinux = "libvulkan.so.1"; - private const string VulkanLibraryNameMacOS = "libvulkan.dylib"; + // Platform specific library hints + // Win32 + private static readonly string[] VulkanLibraryNameWindows = + { + "vulkan-1.dll" + }; + + // Linux + private static readonly string[] VulkanLibraryNameLinux = + { + "libvulkan.so.1" + }; + + // macOS: try MoltenVK first (see: https://github.com/KhronosGroup/MoltenVK), + // then try libvulkan as a last chance option + private static readonly string[] VulkanLibraryNameMacOS = + { + "libMoltenVk.dylib", + "libvulkan.dylib" + }; private static string[] _extensions; @@ -34,9 +53,13 @@ private unsafe struct VkExtensionProperty public uint SpecVersion; } + // Extensions related structure type IDs + // See: https://github.com/KhronosGroup/Vulkan-Headers/blob/main/include/vulkan/vulkan_core.h private const uint VK_STRUCTURE_TYPE_XLIB_SURFACE_CREATE_INFO_KHR = 1000004000; private const uint VK_STRUCTURE_TYPE_XCB_SURFACE_CREATE_INFO_KHR = 1000005000; private const uint VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR = 1000009000; + private const uint VK_STRUCTURE_TYPE_MACOS_SURFACE_CREATE_INFO_MVK = 1000123000; + private const uint VK_STRUCTURE_TYPE_METAL_SURFACE_CREATE_INFO_EXT = 1000217000; private unsafe struct VkWin32SurfaceCreateInfoKHR { @@ -65,14 +88,45 @@ private unsafe struct VkXcbSurfaceCreateInfoKHR public IntPtr Window; } - private unsafe delegate int vkEnumerateInstanceExtensionPropertiesDelegate(string layerName, out uint layerCount, VkExtensionProperty* properties); - private unsafe delegate int vkCreateWin32SurfaceKHRDelegate(IntPtr instance, ref VkWin32SurfaceCreateInfoKHR createInfo, IntPtr allocator, out IntPtr surface); - private unsafe delegate int vkCreateXlibSurfaceKHRDelegate(IntPtr instance, ref VkXlibSurfaceCreateInfoKHR createInfo, IntPtr allocator, out IntPtr surface); - private unsafe delegate int vkCreateXcbSurfaceKHRDelegate(IntPtr instance, ref VkXcbSurfaceCreateInfoKHR createInfo, IntPtr allocator, out IntPtr surface); + // See: https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkMacOSSurfaceCreateInfoMVK.html + private unsafe struct VkMacOSSurfaceCreateInfoMVK + { + public uint StructType; // type of this structure + public IntPtr Next; // IntPtr.Zero or pointer to extending structure + public uint Flags; // reserved + public IntPtr View; // pointer to CAMetalLayer or NSView + } + + // See: https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/vkCreateMetalSurfaceEXT.html + private unsafe struct VkMetalSurfaceCreateInfoEXT + { + public uint StructType; // type of this structure + public IntPtr Next; // IntPtr.Zero or pointer to extending structure + public uint Flags; // reserved + public IntPtr View; // pointer to CAMetalLayer + } + + private unsafe delegate int vkEnumerateInstanceExtensionPropertiesDelegate(string layerName, + out uint layerCount, VkExtensionProperty* properties); + + private unsafe delegate int vkCreateWin32SurfaceKHRDelegate(IntPtr instance, + ref VkWin32SurfaceCreateInfoKHR createInfo, IntPtr allocator, out IntPtr surface); + + private unsafe delegate int vkCreateXlibSurfaceKHRDelegate(IntPtr instance, + ref VkXlibSurfaceCreateInfoKHR createInfo, IntPtr allocator, out IntPtr surface); + + private unsafe delegate int vkCreateXcbSurfaceKHRDelegate(IntPtr instance, + ref VkXcbSurfaceCreateInfoKHR createInfo, IntPtr allocator, out IntPtr surface); + + private unsafe delegate int vkCreateMacOSSurfaceMVKDelegate(IntPtr instance, + ref VkMacOSSurfaceCreateInfoMVK createInfo, IntPtr allocator, out IntPtr surface); + + private unsafe delegate int vkCreateMetalSurfaceEXTDelegate(IntPtr instance, + ref VkMetalSurfaceCreateInfoEXT createInfo, IntPtr allocator, out IntPtr surface); private static vkEnumerateInstanceExtensionPropertiesDelegate _vkEnumerateInstanceExtensionProperties; - private static string GetLibraryName() + private static string[] GetLibraryNameHints() { if (OperatingSystem.IsWindows()) { @@ -90,18 +144,33 @@ private static string GetLibraryName() return null; } + /// + /// This method attempts to load any suitable Vulkan library. + /// If multiple options are available, + /// handle to the first successfully loaded library is returned. + /// + /// Library handle to set if any library is loaded + /// true if library was successfully loaded, false otherwise private static bool TryLoadLibrary(out IntPtr vulkanHandle) { - string libraryName = GetLibraryName(); + var libraryNameHints = GetLibraryNameHints(); + vulkanHandle = IntPtr.Zero; - if (libraryName == null) + if (libraryNameHints == null) { - vulkanHandle = IntPtr.Zero; - return false; } - return NativeLibrary.TryLoad(libraryName, out vulkanHandle); + // return on the first successfully loaded library + foreach (var libraryName in libraryNameHints) + { + if (NativeLibrary.TryLoad(libraryName, out vulkanHandle)) + { + return true; + } + } + + return false; } private static bool IsExtensionPresent(string extensionName) @@ -109,7 +178,7 @@ private static bool IsExtensionPresent(string extensionName) return _extensions.Contains(extensionName); } - private static unsafe string GetStringFromUtf8Byte(byte *start) + private static unsafe string GetStringFromUtf8Byte(byte* start) { byte* end = start; while (*end != 0) end++; @@ -119,80 +188,96 @@ private static unsafe string GetStringFromUtf8Byte(byte *start) private static void EnsureInit() { - if (!_isInit) + if (_isInit) { - if (!TryLoadLibrary(out _vulkanHandle) || !NativeLibrary.TryGetExport(_vulkanHandle, "vkGetInstanceProcAddr", out IntPtr vkGetInstanceProcAddrPtr)) - { - throw new NotSupportedException("Unsupported platform for Vulkan!"); - } + return; + } + if (!TryLoadLibrary(out _vulkanHandle) || + !NativeLibrary.TryGetExport( + _vulkanHandle, + "vkGetInstanceProcAddr", + out IntPtr vkGetInstanceProcAddrPtr) + ) + { + throw new NotSupportedException("Unsupported platform for Vulkan!"); + } - _vkGetInstanceProcAddr = Marshal.GetDelegateForFunctionPointer(vkGetInstanceProcAddrPtr); - _vkEnumerateInstanceExtensionProperties = Marshal.GetDelegateForFunctionPointer(_vkGetInstanceProcAddr(IntPtr.Zero, "vkEnumerateInstanceExtensionProperties")); - unsafe - { - int res = _vkEnumerateInstanceExtensionProperties(null, out uint layerCount, null); + _vkGetInstanceProcAddr = Marshal.GetDelegateForFunctionPointer( + vkGetInstanceProcAddrPtr + ); + _vkEnumerateInstanceExtensionProperties = + Marshal.GetDelegateForFunctionPointer( + _vkGetInstanceProcAddr(IntPtr.Zero, "vkEnumerateInstanceExtensionProperties") + ); - if (res != 0) - { - throw new PlatformException($"vkEnumerateInstanceExtensionProperties failed: {res}"); - } + unsafe + { + int res = _vkEnumerateInstanceExtensionProperties(null, out uint layerCount, null); - VkExtensionProperty[] extensions = new VkExtensionProperty[layerCount]; + if (res != 0) + { + throw new PlatformException($"vkEnumerateInstanceExtensionProperties failed: {res}"); + } - fixed (VkExtensionProperty *extensionsPtr = extensions) - { - res = _vkEnumerateInstanceExtensionProperties(null, out layerCount, extensionsPtr); - } + VkExtensionProperty[] extensions = new VkExtensionProperty[layerCount]; - if (res != 0) - { - throw new PlatformException($"vkEnumerateInstanceExtensionProperties failed: {res}"); - } + fixed (VkExtensionProperty* extensionsPtr = extensions) + { + res = _vkEnumerateInstanceExtensionProperties(null, out layerCount, extensionsPtr); + } + + if (res != 0) + { + throw new PlatformException($"vkEnumerateInstanceExtensionProperties failed: {res}"); + } - _extensions = new string[extensions.Length]; + _extensions = new string[extensions.Length]; - for (int i = 0; i < extensions.Length; i++) + for (int i = 0; i < extensions.Length; i++) + { + fixed (byte* extensionNamePtr = extensions[i].ExtensionName) { - fixed (byte* extensionNamePtr = extensions[i].ExtensionName) - { - _extensions[i] = GetStringFromUtf8Byte(extensionNamePtr); - } + _extensions[i] = GetStringFromUtf8Byte(extensionNamePtr); } } + } - // Ensure that all extensions that we are requiring are present. + // Ensure that all extensions that we are requiring are present. - bool isVulkanUsable = IsExtensionPresent("VK_KHR_surface"); + bool isVulkanUsable = IsExtensionPresent("VK_KHR_surface"); - if (OperatingSystem.IsWindows()) - { - isVulkanUsable &= IsExtensionPresent("VK_KHR_win32_surface"); - } - // FIXME: We assume Linux == X11 for now. - else if (OperatingSystem.IsLinux()) + if (OperatingSystem.IsWindows()) + { + isVulkanUsable &= IsExtensionPresent("VK_KHR_win32_surface"); + } + // FIXME: We assume Linux == X11 for now. + else if (OperatingSystem.IsLinux()) + { + if (!IsExtensionPresent("VK_KHR_xcb_surface") || !X11.IsXcbAvailable()) { - if (!IsExtensionPresent("VK_KHR_xcb_surface") || !X11.IsXcbAvailable()) + if (!IsExtensionPresent("VK_KHR_xlib_surface")) { - if (!IsExtensionPresent("VK_KHR_xlib_surface")) - { - isVulkanUsable = false; - } + isVulkanUsable = false; } } - else if (OperatingSystem.IsMacOS()) - { - isVulkanUsable &= IsExtensionPresent("VK_MVK_macos_surface"); - } - - if (!isVulkanUsable) - { - throw new NotSupportedException("No supported Vulkan surface found!"); - } + } + else if (OperatingSystem.IsMacOS()) + { + // See: + // https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VK_MVK_macos_surface.html + // https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VK_EXT_metal_surface.html + isVulkanUsable &= IsExtensionPresent("VK_MVK_macos_surface") | // deprecated + IsExtensionPresent("VK_EXT_metal_surface"); // newer extension + } - _isInit = true; + if (!isVulkanUsable) + { + throw new NotSupportedException("No supported Vulkan surface found!"); } + + _isInit = true; } public static IntPtr GetInstanceProcAddr(IntPtr instance, string name) @@ -228,88 +313,254 @@ public static string[] GetRequiredInstanceExtensions() if (OperatingSystem.IsMacOS()) { - extensions.Add("VK_MVK_macos_surface"); + // prefer a newer extension over deprecated + extensions.Add( + IsExtensionPresent("VK_EXT_metal_surface") + ? "VK_EXT_metal_surface" + : "VK_MVK_macos_surface" + ); } return extensions.ToArray(); } - public static IntPtr CreateWindowSurface(IntPtr vulkanInstance, NativeWindowBase window) + /// + /// This method creates Vulkan surface on Win32 platform. + /// + /// Instance of Vulkan library + /// Window native handle + /// Reference to surface + /// This exception is raised on surface creation error + private static IntPtr CreateWindowSurfaceWin32( + IntPtr vulkanInstance, + NativeWindowBase window + ) { - EnsureInit(); + vkCreateWin32SurfaceKHRDelegate vkCreateWin32SurfaceKHR = + Marshal.GetDelegateForFunctionPointer( + _vkGetInstanceProcAddr(vulkanInstance, "vkCreateWin32SurfaceKHR" + ) + ); - if (OperatingSystem.IsWindows() && IsExtensionPresent("VK_KHR_win32_surface")) + VkWin32SurfaceCreateInfoKHR creationInfo = new VkWin32SurfaceCreateInfoKHR { - vkCreateWin32SurfaceKHRDelegate vkCreateWin32SurfaceKHR = Marshal.GetDelegateForFunctionPointer(_vkGetInstanceProcAddr(vulkanInstance, "vkCreateWin32SurfaceKHR")); - - VkWin32SurfaceCreateInfoKHR creationInfo = new VkWin32SurfaceCreateInfoKHR - { - StructType = VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR, - Next = IntPtr.Zero, - Flags = 0, + StructType = VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR, + Next = IntPtr.Zero, + Flags = 0, // Broken warning here there is no platform issues here... #pragma warning disable CA1416 - HInstance = Win32.GetWindowLong(window.WindowHandle.RawHandle, Win32.GetWindowLongIndex.GWL_HINSTANCE), + HInstance = Win32.GetWindowLong( + window.WindowHandle.RawHandle, + Win32.GetWindowLongIndex.GWL_HINSTANCE + ), #pragma warning restore CA1416 - Hwnd = window.WindowHandle.RawHandle - }; + Hwnd = window.WindowHandle.RawHandle + }; - int res = vkCreateWin32SurfaceKHR(vulkanInstance, ref creationInfo, IntPtr.Zero, out IntPtr surface); + int res = vkCreateWin32SurfaceKHR( + vulkanInstance, + ref creationInfo, + IntPtr.Zero, + out IntPtr surface + ); - if (res != 0) - { - throw new PlatformException($"vkCreateWin32SurfaceKHR failed: {res}"); - } + if (res != 0) + { + throw new PlatformException($"vkCreateWin32SurfaceKHR failed: {res}"); + } + + return surface; + } + + /// + /// This method creates Vulkan surface on Linux (XCB) platform. + /// + /// Instance of Vulkan library + /// Window native handle + /// Reference to surface + /// This exception is raised on surface creation error + [SupportedOSPlatform("linux")] + private static IntPtr CreateWindowSurfaceXCB(IntPtr vulkanInstance, NativeWindowBase window) + { + vkCreateXcbSurfaceKHRDelegate vkCreateXcbSurfaceKHR = + Marshal.GetDelegateForFunctionPointer( + _vkGetInstanceProcAddr(vulkanInstance, "vkCreateXcbSurfaceKHR" + ) + ); - return surface; + VkXcbSurfaceCreateInfoKHR creationInfo = new VkXcbSurfaceCreateInfoKHR + { + StructType = VK_STRUCTURE_TYPE_XCB_SURFACE_CREATE_INFO_KHR, + Next = IntPtr.Zero, + Flags = 0, + Connection = X11.GetXCBConnection(window.DisplayHandle.RawHandle), + Window = window.WindowHandle.RawHandle + }; + + int res = vkCreateXcbSurfaceKHR( + vulkanInstance, + ref creationInfo, + IntPtr.Zero, + out IntPtr surface + ); + + if (res != 0) + { + throw new PlatformException($"vkCreateXcbSurfaceKHR failed: {res}"); } - if (OperatingSystem.IsLinux()) + return surface; + } + + /// + /// This method creates Vulkan surface on Linux (Xlib) platform. + /// + /// Instance of Vulkan library + /// Window native handle + /// Reference to surface + /// This exception is raised on surface creation error + private static IntPtr CreateWindowSurfaceXlib(IntPtr vulkanInstance, NativeWindowBase window) + { + vkCreateXlibSurfaceKHRDelegate vkCreateXlibSurfaceKHR = + Marshal.GetDelegateForFunctionPointer( + _vkGetInstanceProcAddr(vulkanInstance, "vkCreateXlibSurfaceKHR" + ) + ); + + VkXlibSurfaceCreateInfoKHR creationInfo = new VkXlibSurfaceCreateInfoKHR { - if (IsExtensionPresent("VK_KHR_xcb_surface") && X11.IsXcbAvailable()) - { - vkCreateXcbSurfaceKHRDelegate vkCreateXcbSurfaceKHR = Marshal.GetDelegateForFunctionPointer(_vkGetInstanceProcAddr(vulkanInstance, "vkCreateXcbSurfaceKHR")); + StructType = VK_STRUCTURE_TYPE_XLIB_SURFACE_CREATE_INFO_KHR, + Next = IntPtr.Zero, + Flags = 0, + Display = window.DisplayHandle.RawHandle, + Window = window.WindowHandle.RawHandle + }; + + int res = vkCreateXlibSurfaceKHR( + vulkanInstance, + ref creationInfo, + IntPtr.Zero, + out IntPtr surface + ); + + if (res != 0) + { + throw new PlatformException($"vkCreateXlibSurfaceKHR failed: {res}"); + } - VkXcbSurfaceCreateInfoKHR creationInfo = new VkXcbSurfaceCreateInfoKHR - { - StructType = VK_STRUCTURE_TYPE_XCB_SURFACE_CREATE_INFO_KHR, - Next = IntPtr.Zero, - Flags = 0, - Connection = X11.GetXCBConnection(window.DisplayHandle.RawHandle), - Window = window.WindowHandle.RawHandle - }; + return surface; + } - int res = vkCreateXcbSurfaceKHR(vulkanInstance, ref creationInfo, IntPtr.Zero, out IntPtr surface); + /// + /// This method creates Vulkan surface on macOS platform using deprecated MoltenVK extension. + /// + /// Instance of Vulkan library + /// Window native handle + /// Reference to surface + /// This exception is raised on surface creation error + [SupportedOSPlatform("macos")] + private static IntPtr CreateWindowSurfaceMVK(IntPtr vulkanInstance, SimpleCocoaWindow window) + { + vkCreateMacOSSurfaceMVKDelegate vkCreateMacOSSurfaceMVK = + Marshal.GetDelegateForFunctionPointer( + _vkGetInstanceProcAddr(vulkanInstance, "vkCreateMacOSSurfaceMVK" + ) + ); - if (res != 0) - { - throw new PlatformException($"vkCreateXcbSurfaceKHR failed: {res}"); - } + VkMacOSSurfaceCreateInfoMVK creationInfo = new VkMacOSSurfaceCreateInfoMVK + { + StructType = VK_STRUCTURE_TYPE_MACOS_SURFACE_CREATE_INFO_MVK, + Next = IntPtr.Zero, + Flags = 0, + View = window.MetalLayerHandle.RawHandle + }; + + int res = vkCreateMacOSSurfaceMVK( + vulkanInstance, + ref creationInfo, + IntPtr.Zero, + out IntPtr surface + ); + + if (res != 0) + { + throw new PlatformException($"vkCreateMacOSSurfaceMVK failed: {res}"); + } - return surface; - } - else - { - vkCreateXlibSurfaceKHRDelegate vkCreateXlibSurfaceKHR = Marshal.GetDelegateForFunctionPointer(_vkGetInstanceProcAddr(vulkanInstance, "vkCreateXlibSurfaceKHR")); + return surface; + } - VkXlibSurfaceCreateInfoKHR creationInfo = new VkXlibSurfaceCreateInfoKHR - { - StructType = VK_STRUCTURE_TYPE_XLIB_SURFACE_CREATE_INFO_KHR, - Next = IntPtr.Zero, - Flags = 0, - Display = window.DisplayHandle.RawHandle, - Window = window.WindowHandle.RawHandle - }; + /// + /// This method creates Vulkan surface on macOS platform using modern MoltenVK extension. + /// + /// Instance of Vulkan library + /// Window native handle + /// Reference to surface + /// This exception is raised on surface creation error + [SupportedOSPlatform("macos")] + private static IntPtr CreateWindowSurfaceMetal(IntPtr vulkanInstance, SimpleCocoaWindow window) + { + vkCreateMetalSurfaceEXTDelegate vkCreateMetalSurfaceEXT = + Marshal.GetDelegateForFunctionPointer( + _vkGetInstanceProcAddr(vulkanInstance, "vkCreateMetalSurfaceEXT" + ) + ); + + VkMetalSurfaceCreateInfoEXT creationInfo = new VkMetalSurfaceCreateInfoEXT + { + StructType = VK_STRUCTURE_TYPE_METAL_SURFACE_CREATE_INFO_EXT, + Next = IntPtr.Zero, + Flags = 0, + View = window.MetalLayerHandle.RawHandle + }; + + int res = vkCreateMetalSurfaceEXT( + vulkanInstance, + ref creationInfo, + IntPtr.Zero, + out IntPtr surface + ); + + if (res != 0) + { + throw new PlatformException($"vkCreateMetalSurfaceEXT failed: {res}"); + } - int res = vkCreateXlibSurfaceKHR(vulkanInstance, ref creationInfo, IntPtr.Zero, out IntPtr surface); + return surface; + } - if (res != 0) - { - throw new PlatformException($"vkCreateXlibSurfaceKHR failed: {res}"); - } + /// + /// This method creates Vulkan surface using platform dependent API. + /// + /// Instance of Vulkan library + /// Window native handle + /// Reference to surface + /// This exception is raised if platform is not supported + public static IntPtr CreateWindowSurface(IntPtr vulkanInstance, NativeWindowBase window) + { + EnsureInit(); + + if (OperatingSystem.IsWindows() && IsExtensionPresent("VK_KHR_win32_surface")) + { + return CreateWindowSurfaceWin32(vulkanInstance, window); + } - return surface; + if (OperatingSystem.IsLinux()) + { + if (IsExtensionPresent("VK_KHR_xcb_surface") && X11.IsXcbAvailable()) + { + return CreateWindowSurfaceXCB(vulkanInstance, window); } + + return CreateWindowSurfaceXlib(vulkanInstance, window); + } + + if (OperatingSystem.IsMacOS()) + { + var cocoaWindow = (SimpleCocoaWindow)window; + return IsExtensionPresent("VK_EXT_metal_surface") + ? CreateWindowSurfaceMetal(vulkanInstance, cocoaWindow) + : CreateWindowSurfaceMVK(vulkanInstance, cocoaWindow); } throw new NotImplementedException(); From 5f1cb7d73f498c115cfd4483f56983ce02b742c5 Mon Sep 17 00:00:00 2001 From: Alexey Tarasov Date: Wed, 12 Oct 2022 21:15:34 +1100 Subject: [PATCH 3/3] Addresses comments in SPB PR #3. - Changes comments punctuation to the suggested. - Allows to pass either `NSView` or `NSWindow` into SimpleCocoaWindow constructor. --- SPB/Graphics/Vulkan/VulkanHelper.cs | 14 ++--- SPB/Platform/Cocoa/SimpleCocoaWindow.cs | 78 ++++++++++++++++++------- 2 files changed, 65 insertions(+), 27 deletions(-) diff --git a/SPB/Graphics/Vulkan/VulkanHelper.cs b/SPB/Graphics/Vulkan/VulkanHelper.cs index 1d03894..75ea59b 100644 --- a/SPB/Graphics/Vulkan/VulkanHelper.cs +++ b/SPB/Graphics/Vulkan/VulkanHelper.cs @@ -19,21 +19,21 @@ public static class VulkanHelper private static IntPtr _vulkanHandle; - // Platform specific library hints - // Win32 + // Platform specific library hints. + // For Win32: private static readonly string[] VulkanLibraryNameWindows = { "vulkan-1.dll" }; - // Linux + // For Linux: private static readonly string[] VulkanLibraryNameLinux = { "libvulkan.so.1" }; - // macOS: try MoltenVK first (see: https://github.com/KhronosGroup/MoltenVK), - // then try libvulkan as a last chance option + // For macOS: try MoltenVK first (see: https://github.com/KhronosGroup/MoltenVK), + // then try libvulkan as a last chance option. private static readonly string[] VulkanLibraryNameMacOS = { "libMoltenVk.dylib", @@ -53,7 +53,7 @@ private unsafe struct VkExtensionProperty public uint SpecVersion; } - // Extensions related structure type IDs + // Extensions related structure type IDs. // See: https://github.com/KhronosGroup/Vulkan-Headers/blob/main/include/vulkan/vulkan_core.h private const uint VK_STRUCTURE_TYPE_XLIB_SURFACE_CREATE_INFO_KHR = 1000004000; private const uint VK_STRUCTURE_TYPE_XCB_SURFACE_CREATE_INFO_KHR = 1000005000; @@ -161,7 +161,7 @@ private static bool TryLoadLibrary(out IntPtr vulkanHandle) return false; } - // return on the first successfully loaded library + // Returns on the first successfully loaded library. foreach (var libraryName in libraryNameHints) { if (NativeLibrary.TryLoad(libraryName, out vulkanHandle)) diff --git a/SPB/Platform/Cocoa/SimpleCocoaWindow.cs b/SPB/Platform/Cocoa/SimpleCocoaWindow.cs index adcea79..5a6c280 100644 --- a/SPB/Platform/Cocoa/SimpleCocoaWindow.cs +++ b/SPB/Platform/Cocoa/SimpleCocoaWindow.cs @@ -11,56 +11,94 @@ namespace SPB.Platform.Cocoa; [SupportedOSPlatform("macos")] public class SimpleCocoaWindow : NativeWindowBase { - // NSWindow pointer wrapper + // NSWindow pointer wrapper. public override NativeHandle WindowHandle { get; } - // CAMetalLayer pointer wrapper + // CAMetalLayer pointer wrapper. public NativeHandle MetalLayerHandle { get; private set; } public override NativeHandle DisplayHandle => throw new NotImplementedException(); /// /// Constructs instance of this class. + /// + /// If NSWindow pointer is given its top-level content view is used to setup + /// Metal layer. + /// If NSView pointer is given, window it belongs is extracted from NSView.window + /// property, Metal layer is set up for the given NSView object. /// - /// NSWindow native pointer + /// Native pointer /// Flag indicating need to acquire Metal layer - public SimpleCocoaWindow(NativeHandle windowHandle, bool initMetalLayer = true) + public SimpleCocoaWindow(NativeHandle handle, bool initMetalLayer = true) { - WindowHandle = windowHandle; - - // Sanity check - ObjectiveC.EnsureIsKindOfClass(windowHandle.RawHandle, "NSWindow"); + WindowHandle = GetNsWindow(handle); if (initMetalLayer) { - InitMetalLayer(); + var view = GetNsView(handle); + InitMetalLayer(view.RawHandle); } } - private void InitMetalLayer() + /// + /// This method returns content view for a given object. + /// If object is of NSWindow type then its top level content view is returned. + /// If object is of NSView type then it is returned as is. + /// + /// UI object pointer to retrieve view from + /// NSView pointer wrapped into NativeHandle + private NativeHandle GetNsView(NativeHandle handle) { - // assuming that handles is NSWindow - // 1. get top-level NSView - // 2. get its layer - // 3. if it is CAMetalLayer, we have what we need - // 4. make it CAMetalLayer + if (ObjectiveC.IsKindOfClass(handle.RawHandle, "NSView")) + { + return handle; + } - // 1. get top-level NSView var contentView = ObjectiveC.GetIntPtrValue(WindowHandle.RawHandle, "contentView"); + ObjectiveC.EnsureIsKindOfClass(contentView, "NSView"); + return new NativeHandle(contentView); + } + + /// + /// This method returns window for a given object. + /// If object is of NSWindow type then it is returned as is. + /// If object is of NSView type then its view window object or null pointer, + /// if view is not installed into window. + /// + /// UI object pointer to retrieve window from + /// NSWindow pointer wrapped into NativeHandle + private NativeHandle GetNsWindow(NativeHandle handle) + { + if (ObjectiveC.IsKindOfClass(handle.RawHandle, "NSView")) + { + IntPtr window = ObjectiveC.GetIntPtrValue(handle.RawHandle, "window"); + return new NativeHandle(window); + } + + ObjectiveC.EnsureIsKindOfClass(handle.RawHandle, "NSWindow"); + return handle; + } + + /// + /// This method initialises Metal layer of view object if needed. + /// + /// NSView object to initialise Metal layer + private void InitMetalLayer(IntPtr contentView) + { ObjectiveC.EnsureIsKindOfClass(contentView, "NSView"); - // 2. get its layer + // 1. Get view layer. var layer = ObjectiveC.GetIntPtrValue(contentView, "layer"); - // 3. if it is CAMetalLayer, we have what we need + // 2. If it is CAMetalLayer, we have what we need. if (ObjectiveC.IsKindOfClass(contentView, "CAMetalLayer")) { MetalLayerHandle = new NativeHandle(layer); return; } - // 4. make it CAMetalLayer + // 3. Make it CAMetalLayer. // https://developer.apple.com/documentation/quartzcore/cametallayer?language=objc Console.Out.WriteLine("Replacing original NSView layer with CAMetalLayer"); @@ -74,7 +112,7 @@ private void InitMetalLayer() protected override void Dispose(bool disposing) { - // Do nothing as no actual resource is owned + // Do nothing as no actual resource is owned. } public override void Show()