Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Mobile / Android

Vulkan API with Kotlin Native - Instance

0.00/5 (No votes)
26 Apr 2019GPL33 min read 6.8K  
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:

Java
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:

Kotlin
 /**
 * 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:

Kotlin
...

// 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:

Kotlin
 ...
 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:

Kotlin
@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:

Kotlin
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

License

This article, along with any associated source code and files, is licensed under The GNU General Public License (GPLv3)