65.9K
CodeProject is changing. Read more.
Home

Vulkan API with Kotlin Native - Instance

starIconstarIconemptyStarIconemptyStarIconemptyStarIcon

2.00/5 (4 votes)

Apr 3, 2019

GPL3

3 min read

viewsIcon

6761

Creating Vulkan instance with Kotlin Native

Introduction

In previous parts, we made all preparations, so now at last we can start using Vulkan API. Let's summarize what we have at this moment:

  • The project compiling to a native code for Windows and Linux platforms.
  • Interop with C libraries.
  • Native windows for both platforms which we can move, resize, switch to a fullscreen mode and so on.
  • We can check on which platforms we are and define corresponding Vulkan extensions to use.
  • Added links to all needed shared libraries including Vulkan loader, debug layers libraries, etc.
  • Added task to compile shaders from sources
  • Vulkan API is claimed to be a cross-platform graphics API (not just, it's used for computations also) with GPU direct control. So we also created common part of the project where we will work with it.

Vulkan API seems to be complicated at first glance. But immerse yourself into it step by step understanding becomes easier. What's most important — structures, structures and structures again... Structures are used to define the behavior that you want to get. A first thing to start with Vulkan API is Vulkan instance. So, as it's first attempt to use Kotlin Native with Vulkan API, let's look at the instance creation in details.

Vulkan Instance

As I mentioned above, the first thing to do — we need to create a Vulkan instance. But we should say to the API how it should be created. For example, we should say which platform surface we will use, it will be different for each platform. To say it, we must check supported by a driver extensions. Here, we'll get available extensions:

 private fun setupExtensions(scope: MemScope): MutableList<String> {

        val availableInstanceExtensions: MutableList<String> = ArrayList()

        with(scope) {

            val extensionsCount = alloc<UIntVar>()
            extensionsCount.value = 0u
            var result: VkResult

            // Enumerate _instance extsensions and check if they're available
            do {
                result = vkEnumerateInstanceExtensionProperties(null, extensionsCount.ptr, null)

                if (!VK_CHECK(result)) throw RuntimeException
                                 ("Could not enumerate _instance extensions.")

                if (extensionsCount.value == 0u) break

                val buffer = allocArray<VkExtensionProperties>(extensionsCount.value.toInt())
                result = vkEnumerateInstanceExtensionProperties(null, extensionsCount.ptr, buffer)

                for (i in 0 until extensionsCount.value.toInt()) {
                    val ext = buffer[i].extensionName.toKString()
                    if (!availableInstanceExtensions.contains(ext))
                        availableInstanceExtensions.add(ext)
                }

            } while (result == VK_INCOMPLETE)
        }

        return availableInstanceExtensions
    }

Here, we pass the memory scope created earlier in the initialization function. The memory scope defines a lifetime of a memory allocated in it. Then, we use vkEnumerateInstanceExtensionProperties API function to get available extensions and add them to the list.

Also, we need to get available debug layers:

     /**
     * Prepare debug layers
     */
    private fun prepareLayers(scope: MemScope, instanceCreateInfo: 
                              VkInstanceCreateInfo): MutableList<String> {

        logInfo("Preparing Debug Layers")
        val availableLayers = mutableListOf<String>()

        with(scope) {

            // Layers optimal order: 
            // <a href='https://vulkan.lunarg.com/doc/view/1.0.13.0/windows/layers.html'/>
            val layers = arrayOf(
                "VK_LAYER_GOOGLE_threading",
                "VK_LAYER_LUNARG_parameter_validation",
                "VK_LAYER_LUNARG_object_tracker",
                "VK_LAYER_LUNARG_core_validation",
                "VK_LAYER_GOOGLE_unique_objects",
                "VK_LAYER_LUNARG_standard_validation"
            )

            val layersCount = alloc<UIntVar>()

            var result: VkResult

            run failure@{

                do {

                    // Enumerate available layers
                    result = vkEnumerateInstanceLayerProperties(layersCount.ptr, null)
                    if (!VK_CHECK(result)) {
                        logError("Failed to enumerate debug layers")
                        availableLayers.clear()
                        return@failure // failed to get layers break the loop

                    } else {

                        val buffer = allocArray<VkLayerProperties>(layersCount.value.toInt())

                        result = vkEnumerateInstanceLayerProperties(layersCount.ptr, buffer)
                        if (!VK_CHECK(result)) {
                            logError("Filed to enumerate Debug Layers to buffer")
                            availableLayers.clear()
                            return@failure // failed to get layers break the loop

                        }

                        for (i in 0 until layersCount.value.toInt()) {

                            val layer = buffer[i].layerName.toKString()
                            logDebug("Found $layer layer")
                            if (!availableLayers.contains(layer) && layers.contains(layer)) {
                                availableLayers.add(layer)
                                logDebug("$layer added")
                            }
                        }
                    }

                } while (result == VK_INCOMPLETE)
            }

            // Setting debug layers it they're available
            if (availableLayers.size > 0) {

                if (availableLayers.contains("VK_LAYER_LUNARG_standard_validation"))
                    availableLayers.removeAll {
                        it != "VK_LAYER_LUNARG_standard_validation"
                    }
                else
                // sort available layers in accordance with recommended order
                    availableLayers.sortBy {
                        layers.indexOf(it)
                    }

                logInfo("Setting up Layers:")
                availableLayers.forEach {
                    logInfo(it)
                }

                instanceCreateInfo.enabledLayerCount = availableLayers.size.toUInt()
                instanceCreateInfo.ppEnabledLayerNames =
                        availableLayers.toCStringArray(scope)
            }
        }

        return availableLayers
    }

And again, with use of vkEnumerateInstanceLayerProperties, we created the list of available debug layers. I should explain the last part a little. In the layers, I defined standard debug layers - they are the same as VK_LAYER_LUNARG_standard_validation layer. But sometimes, it returns either this list or just VK_LAYER_LUNARG_standard_validation so as for now, we will use only standard layer we check if we'll leave only VK_LAYER_LUNARG_standard_validation or the list. And at the end. prepared layers we set to Instance Create Info structure, converted to a C strings array.

And now it's time to create the instance:

            ...
             
            // Application info
            val appInfo = alloc<VkApplicationInfo>().apply {
                sType = VK_STRUCTURE_TYPE_APPLICATION_INFO
                pNext = null
                apiVersion = VK_MAKE_VERSION(1u, 0u, 0u)
                applicationVersion = VK_MAKE_VERSION(1u, 0u, 0u)
                engineVersion = VK_MAKE_VERSION(1u, 0u, 0u)
                pApplicationName = "kvarc".cstr.ptr
                pEngineName = "kvarc".cstr.ptr
                apiVersion = VK_API_VERSION_1_0.toUInt()
            }

            var instanceExt: Array<String> =
                arrayOf(VK_KHR_SURFACE_EXTENSION_NAME, VK_KHR_PLATFORM_SURFACE_EXTENSION_NAME)

            val debugSupported = availableInstanceExtensions.contains("VK_EXT_debug_report")
            if (debug && debugSupported) instanceExt += "VK_EXT_debug_report"

            // Debug layers will be added a little later if needed
            val instanceCreateInfo = alloc<VkInstanceCreateInfo>().apply {
                sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO
                pNext = null
                pApplicationInfo = appInfo.ptr
                enabledExtensionCount = instanceExt.size.toUInt()
                ppEnabledExtensionNames = instanceExt.toCStringArray(memScope)
                enabledLayerCount = 0u
                ppEnabledLayerNames = null
            }

            logInfo("Debug: $debug, DebugSupported: $debugSupported")
            val availableLayers =
                if (debug && debugSupported) prepareLayers(this, instanceCreateInfo) else ArrayList()

            logInfo("Creating _instance")
            if (!VK_CHECK(vkCreateInstance(instanceCreateInfo.ptr, null, _instance.ptr)))
                throw RuntimeException("Failed to create _instance")

            ...

Here, we defined the application info structure — passed to it its type, needed API version, the application info, the application name converted to C string, etc... Likewise, we defined the instance create info. And at the end, created the instance with vkCreateInstance call.

Last thing to do — setup a debug callback. Most interesting part here - to set up the callback itself:

                ...
                pfnCallback = staticCFunction { flags, _, _, _, msgCode, pLayerPrefix, pMsg, _ ->

                    var prefix = "kvarc-"

                    when {

                        flags and VK_DEBUG_REPORT_ERROR_BIT_EXT > 0u -> prefix += "ERROR:"
                        flags and VK_DEBUG_REPORT_WARNING_BIT_EXT > 0u -> prefix += "WARNING:"
                        flags and VK_DEBUG_REPORT_PERFORMANCE_WARNING_BIT_EXT > 0u -> prefix += "PERFORMANCE:"
                        flags and VK_DEBUG_REPORT_INFORMATION_BIT_EXT > 0u -> prefix += "INFO:"
                        flags and VK_DEBUG_REPORT_DEBUG_BIT_EXT > 0u -> prefix += "DEBUG:"

                    }

                    val debugMessage =
                        "$prefix [${pLayerPrefix?.toKString() ?: ""}] Code $msgCode:${pMsg?.toKString() ?: ""}"

                    if (flags and VK_DEBUG_REPORT_ERROR_BIT_EXT > 0.toUInt()) {
                        logError(debugMessage)
                    } else {
                        logDebug(debugMessage)
                    }

                    // abort/not
                    VK_FALSE.toUInt()
               }
               ...

It can be done just with the staticCFunction assigning. And in its body, we just write callback parameters and a code needed.

So, we just created the first Vulkan instance with Kotlin Native. Quite easy, isn't it? Now is the time to use it. Let's create a renderer class:

@ExperimentalUnsignedTypes
internal class Renderer : DisposableContainer() {

    private var _instance: Instance? = null

    fun initialize() {
        _instance = Instance()
        _instance!!.initialize(true)
    }

    override fun dispose() {
        _instance?.dispose()
        super.dispose()
    }
}

Why not in init{}, but initialize? Because we'll have a bunch of resources needed to be disposed at the end — the instance, devices, etc... In case something goes wrong, we won't have the renderer object and won't be available to dispose it.

And now, we should add the renderer call to both platforms:

    var renderer = Renderer()
    try {        
        renderer.initialize()
    } catch (exc: Exception) {
        logError(exc.message ?: "Failed to create renderer")
    }
    renderer.dispose() 

A rendering loop will be added much later... That's all for now...

History

  1. Vulkan API with Kotlin Native - Project Setup
  2. Vulkan API with Kotlin Native - Platform's Windows
  3. Vulkan API with Kotlin Native - Instance
  4. Vulkan API with Kotlin Native - Surface, Devices
  5. Vulkan API with Kotlin Native - SwapChain, Pipeline
  6. Vulkan API with Kotlin Native - Draw