2
\$\begingroup\$

I'm trying to follow this Vulkan API tutorial and have come up with an implementation that I don't completely loathe. The VkUtf8StringArray class is my least favorite thing and would like to know if there are better ways I structure things and/or make them safer...

Note: Depends on a helper file to load the dll at run-time.

Full Code:

using ByteTerrace.Windows.Api;
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;

namespace ByteTerrace.Vulkan.Api
{
    public sealed class VkAccessor : IDisposable
    {
        private static bool TryGetProcAddrDelegate<TDelegate>(IntPtr procAddress, out TDelegate @delegate) {
            if (IntPtr.Zero == procAddress) {
                @delegate = default;

                return false;
            }
            else {
                @delegate = Marshal.GetDelegateForFunctionPointer<TDelegate>(procAddress);
            }

            return true;
        }

        public static int MakeVersion(int major, int minor, int patch) => (((major) << 22) | ((minor) << 12) | (patch));
        public static VkAccessor New(string dllName) => new VkAccessor(dllName);

        private delegate IntPtr vkGetInstanceProcAddr(IntPtr instance, IntPtr pName);

        private readonly vkGetInstanceProcAddr m_vkGetInstanceProcAddr;
        private readonly IntPtr m_vkHandle;

        private bool m_isDisposed;

        private VkAccessor(string dllName) {
            m_isDisposed = false;

            if (!Module.TryGetModule(dllName, out m_vkHandle)) {
                throw new Exception(message: "unable to load the specified Vulkan DLL module");
            }

            if (!Module.TryGetDelegateForFunctionPointer(m_vkHandle, nameof(vkGetInstanceProcAddr), out m_vkGetInstanceProcAddr)) {
                throw new Exception(message: "unable to find an entry point for the vkGetInstanceProcAddr procedure");
            }
        }
        ~VkAccessor() => Dispose(false);

        private void Dispose(bool disposing) {
            if (m_isDisposed) {
                return;
            }
            else {
                if (disposing) {
                    // no managed resources to free...
                }

                if (!Module.FreeLibrary(m_vkHandle)) {
                    throw new Exception(message: "unable to free the Vulkan DLL module handle");
                }

                m_isDisposed = true;
            }
        }
        private IntPtr GetInstanceProcAddr(IntPtr instance, string procedureName) {
            if (string.IsNullOrEmpty(procedureName)) {
                throw new ArgumentException(message: "name cannot be null or empty", paramName: nameof(procedureName));
            }

            using (var vkProcedureName = VkUtf8String.New(procedureName)) {
                return m_vkGetInstanceProcAddr(instance, vkProcedureName.Handle);
            }
        }

        public void Dispose() {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
        public bool TryGetGlobalProcDelegate<TDelegate>(string procedureName, out TDelegate @delegate) => TryGetProcAddrDelegate(GetInstanceProcAddr(IntPtr.Zero, procedureName), out @delegate);
        public bool TryGetInstanceProcDelegate<TDelegate>(IntPtr instance, string procedureName, out TDelegate @delegate) => TryGetProcAddrDelegate(GetInstanceProcAddr(instance, procedureName), out @delegate);
    }
    public sealed class VkInstance : IDisposable
    {
        public static VkInstance New(VkAccessor vkAccessor, ref VkInstanceCreateInfo vkInstanceCreateInfo) => new VkInstance(vkAccessor, ref vkInstanceCreateInfo);

        private delegate VkResult vkCreateInstance(ref VkInstanceCreateInfo pCreateInfo, IntPtr pAllocator, out IntPtr pInstance);
        private delegate void vkDestroyInstance(IntPtr instance, IntPtr pAllocator);

        private readonly vkDestroyInstance m_vkDestroyInstanceDelegate;
        private readonly IntPtr m_vkInstanceHandle;

        private bool m_isDisposed;

        private VkInstance(VkAccessor vkAccessor, ref VkInstanceCreateInfo vkInstanceCreateInfo) {
            if (!vkAccessor.TryGetGlobalProcDelegate(nameof(vkCreateInstance), out vkCreateInstance vkCreateInstanceDelegate)) {
                throw new Exception(message: "unable to create a global Vulkan delegate for the vkCreateInstance procedure"); // TODO: Refactor to use a custom exception.
            }

            if (VkResult.VK_SUCCESS != vkCreateInstanceDelegate(ref vkInstanceCreateInfo, IntPtr.Zero, out IntPtr vkInstanceHandle)) {
                throw new Exception(message: "unable to create a Vulkan intance using the specified information"); // TODO: Refactor to use a custom exception.
            }

            if (!vkAccessor.TryGetInstanceProcDelegate(vkInstanceHandle, nameof(vkDestroyInstance), out vkDestroyInstance vkDestroyInstanceDelegate)) {
                throw new Exception(message: "unable to create an instance Vulkan delegate for the vkDestroyInstance procedure"); // TODO: Refactor to use a custom exception.
            }

            m_vkDestroyInstanceDelegate = vkDestroyInstanceDelegate;
            m_vkInstanceHandle = vkInstanceHandle;
        }
        ~VkInstance() => Dispose(false);

        private void Dispose(bool disposing) {
            if (m_isDisposed) {
                return;
            }
            else {
                if (disposing) {
                    // no managed resources to free...
                }

                m_vkDestroyInstanceDelegate(m_vkInstanceHandle, IntPtr.Zero);

                m_isDisposed = true;
            }
        }

        public void Dispose() {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
    }
    public sealed class VkUtf8String : IDisposable
    {
        public static VkUtf8String New(string value) => new VkUtf8String(value);

        private readonly IntPtr m_handle;
#if (DEBUG)
        private readonly string m_value;
#endif

        private bool m_isDisposed;

        public IntPtr Handle {
            get {
                if (m_isDisposed) {
                    return IntPtr.Zero;
                }
                else {
                    return m_handle;
                }
            }
        }
#if (DEBUG)
        public string Value {
            get {
                if (m_isDisposed) {
                    return null;
                }
                else {
                    return m_value;
                }
            }
        }
#endif
        private unsafe VkUtf8String(string value) {
            if (value == null) {
                m_handle = IntPtr.Zero;
            }
            else {
                value = (value + char.MinValue);

                var length = value.Length;
                var maxByteCount = Encoding.UTF8.GetMaxByteCount(length);
                var valueCopyHandle = Marshal.AllocHGlobal(maxByteCount);
                var valueCopySpan = new Span<byte>(valueCopyHandle.ToPointer(), maxByteCount);

                Encoding.UTF8.GetBytes(value, valueCopySpan);

                m_handle = valueCopyHandle;
            }

            m_isDisposed = false;
#if (DEBUG)
            m_value = value;
#endif
        }
        ~VkUtf8String() => Dispose(false);

        private void Dispose(bool disposing) {
            if (m_isDisposed) {
                return;
            }
            else {
                if (disposing) {
                    // no managed resources to free...
                }

                Marshal.FreeHGlobal(m_handle);

                m_isDisposed = true;
            }
        }

        public void Dispose() {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
#if (DEBUG)
        public override string ToString() => Value?.Substring(0, (Value.Length - 1));
#endif
    }
    public sealed class VkUtf8StringArray : IDisposable
    {
        public static VkUtf8StringArray New(params string[] values) => new VkUtf8StringArray(values);

        private readonly int m_count;
        private unsafe readonly IntPtr* m_handle;

        private bool m_isDisposed;

        public int Count => m_count;
        public unsafe IntPtr* Handle {
            get {
                if (m_isDisposed) {
                    return null;
                }
                else {
                    return m_handle;
                }
            }
        }

        private unsafe VkUtf8StringArray(string[] values) {
            if ((null == values) || (0 == values.Length)) {
                m_count = 0;
                m_handle = null;
            }
            else {
                var count = values.Length;
                var handle = ((IntPtr*)Marshal.AllocHGlobal(Unsafe.SizeOf<IntPtr>() * count));

                for (var i = 0; (i < count); i++) {
                    handle[i] = VkUtf8String.New(values[i]).Handle;
                }

                m_count = count;
                m_handle = handle;
            }

            m_isDisposed = false;
        }
        ~VkUtf8StringArray() => Dispose(false);

        private unsafe void Dispose(bool disposing) {
            if (m_isDisposed) {
                return;
            }
            else {
                if (disposing) {
                    // no managed resources to free...
                }

                if (null != m_handle) {
                    for (int i = 0; (i < m_count); i++) {
                        Marshal.FreeHGlobal(m_handle[i]);
                    }

                    Marshal.FreeHGlobal(new IntPtr(m_handle));
                }

                m_isDisposed = true;
            }
        }

        public void Dispose() {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
    }

    public enum VkInstanceCreateFlags
    {
        VK_NONE = 0,
    }
    public enum VkResult
    {
        VK_SUCCESS = 0,
    }
    public enum VkStructureType
    {
        VK_STRUCTURE_TYPE_APPLICATION_INFO = 0,
        VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO = 1,
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct VkApplicationInfo
    {
        public static VkApplicationInfo New(VkUtf8String applicationName, VkUtf8String engineName) => new VkApplicationInfo(applicationName.Handle, engineName.Handle);

        public VkStructureType sType;
        public IntPtr pNext;
        public IntPtr pApplicationName;
        public int applicationVersion;
        public IntPtr pEngineName;
        public int engineVersion;
        public int apiVersion;

        private VkApplicationInfo(IntPtr applicationName, IntPtr engineName) {
            sType = VkStructureType.VK_STRUCTURE_TYPE_APPLICATION_INFO;
            pNext = IntPtr.Zero;
            pApplicationName = applicationName;
            applicationVersion = VkAccessor.MakeVersion(1, 1, 0);
            pEngineName = engineName;
            engineVersion = VkAccessor.MakeVersion(1, 1, 0);
            apiVersion = VkAccessor.MakeVersion(1, 1, 0);
        }
    }
    [StructLayout(LayoutKind.Sequential)]
    public struct VkInstanceCreateInfo
    {
        public unsafe static VkInstanceCreateInfo New(VkApplicationInfo* applicationInfo, VkUtf8StringArray enabledLayerNames, VkUtf8StringArray enabledExtensionNames) => new VkInstanceCreateInfo(
            applicationInfo,
            enabledLayerNames.Count,
            enabledLayerNames.Handle,
            enabledExtensionNames.Count,
            enabledExtensionNames.Handle
        );

        public VkStructureType sType;
        public IntPtr pNext;
        public VkInstanceCreateFlags flags;
        public unsafe VkApplicationInfo* pApplicationInfo;
        public int enabledLayerCount;
        public unsafe IntPtr* ppEnabledLayerNames;
        public int enabledExtensionCount;
        public unsafe IntPtr* ppEnabledExtensionNames;

        public unsafe VkInstanceCreateInfo(VkApplicationInfo* applicationInfo, int enabledLayerCount, IntPtr* enabledLayerNames, int enabledExtensionCount, IntPtr* enabledExtensionNames) {
            sType = VkStructureType.VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
            pNext = IntPtr.Zero;
            flags = VkInstanceCreateFlags.VK_NONE;
            pApplicationInfo = applicationInfo;
            this.enabledLayerCount = enabledLayerCount;
            ppEnabledLayerNames = enabledLayerNames;
            this.enabledExtensionCount = enabledExtensionCount;
            ppEnabledExtensionNames = enabledExtensionNames;
        }
    }
}

Example Usage:

using ByteTerrace.Vulkan.Api;
using System;

namespace TestBench
{
    unsafe class Program
    {
        static void Main(string[] args) {
            using (var applicationName = VkUtf8String.New("ByteTerrace Vulkan API"))
            using (var engineName = VkUtf8String.New("N/A"))
            using (var enabledLayers = VkUtf8StringArray.New(null))
            using (var enabledExtensions = VkUtf8StringArray.New("VK_KHR_surface", "VK_KHR_win32_surface")) {
                var applicationInfo = VkApplicationInfo.New(applicationName, engineName);
                var instanceCreateInfo = VkInstanceCreateInfo.New(&applicationInfo, enabledLayers, enabledExtensions);

                using (var vkAccessor = VkAccessor.New("vulkan-1.dll"))
                using (var vkInstance = VkInstance.New(vkAccessor, ref instanceCreateInfo)) {
                    // TODO: Something useful once I bind more structures and methods...
                }
            }

            Console.WriteLine("Done.");
        }
    }
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.