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

Vulkan API with Kotlin Native - Project Setup

5.00/5 (1 vote)
27 Mar 2019GPL37 min read 17.6K  
Using Vulkan API with Kotlin Native example

Introduction

With this article, I want to start a series telling how to work with Vulkan API using Kotlin Native. What is Kotlin Native? It is supposed to compile your code to binaries not requiring Java VM or any other VM. So, it is supposed to be platform independent code (except parts that are really platform dependant). Kotlin Native can produce arm32, arm64, x86_64, wasm32, etc. We're interested in arm and x86 binaries. Kotlin Native can work with C and C++ libraries via so-called cinterop. It's still in the development stage so there are some difficulties with it to work. First of all, there are no IDEs that have a good support for it. What I tried: IntelliJ Clion trial version, I could create a project, but I could not get a working multiplatform one. Android Studio - works fine for Android but not for other platforms. IntelliJ IDEA community edition - the only one that worked with some limitations. And none of them could debug a project. So it would be nice to have some help with Vulkan debug layers and RenderDoc. The compile process is not quite fast for now. And some other issues... I'll tell about their workarounds during this article. Anyways, it seems to be a good choice to use alongside with C++ to develop graphics, math, science and so on libraries when it becomes mature. At first, we will work with Windows and Linux platforms and at the end of the series, we'll add Android support. As for Vulkan API, I would recommend good examples in C++ by Sascha Willems or for beginners the set from Kronos:

At first, a couple of quotes:

 

If you are interested in Kotlin/Native because of performance, you really should choose something else. Kotlin/Native is native not for performance. It is for situations, where you can’t afford a JVM (like an iOS app) and want to share logic between platforms, or just write clean readable code. Kotlin/JVM is faster than K/N, and Rust is faster than both.

ark1JetBrains Team

 

If we compare development time and time to market performance I would say that Kotlin/Native shall be way faster :). Jokes aside, it really depends on workloads, for truly heavy computational workloads like image processing and heavy crypto I would suggest to use neither language, and rely on C. For tasks Kotlin/Native was intended for, performance shall be acceptable or even brilliant, especially if measure not raw throughput, but perceived performance.

Nikolay_Igotti Kotlin team

As you will see in this project, Kotlin is a good language. It's easy to learn. It's easy to use. And I hope both Jetbrain team and Kotlin team will change their minds concerning Kotlin Native performance and Kotlin Native will evolve this way.

So, what can we do with Kotlin/Native:

  • Compile to arm and x86 platform. All variants are available from presets
  • Produce executable
  • Produce static or dynamic library
  • Produce Apple framework
  • Use external static/dynamic libraries or Apple framework

And what we can't:

  • Debug project
  • Use macros from C/C++ part
  • I didn't find a way to check if we're in debug mode or at runtime
  • No intrinsic functions or inline assembly - only in C/C++ part
  • No coroutines yet

Let's begin...

Tools

Setting Up the Project

In IntelliJ IDEA, create a new "Native" project (New project -> Kotlin -> Kotlin Native and check use "Use Auto-Import" so it won't ask every time a gradle file being changed). Don't use the "Multiplatform" one! With it, you won't be able to use common native code. IDE will create a project with a target depending on the platform you're in. Don't worry, we'll add all needed platforms manually using project gradle file. If you're on Windows platform, you will see sample package and SampleMingw file containing main function, refactor them to names you wish, for our project, I will use k(Kotlin)v(Vulkan)arc abriaviature for package and MainMingw for main Windows file. For other platforms, follow the same steps as for Windows just replacing "sample" and "platform" names.

Let's take a look at project gradle file. Change yellowed "args '' " in runProgram block to "args []" so it won't irritate us anymore and rename it to runReleaseLinuxProgram. Then, copy/paste this block, rename it to runDebugLinuxProgram and change builldType to "DEBUG". We will work with this.

Now if you will open "Gradle panel" at the left of IDE, you will see two tasks, that we can run:

  • runDebugLinuxProgram
  • runReleaseLinuxProgram

Image 1

Right click it and run. Now you can have a cup of coffee until cinerops are done. Luckily, it's just once, after that IDE will use already compiled binaries. As a result, you will see "Hello, Kotlin/Native!" as a result in "Run" tab at the bottom of IDE:

Image 2

To access Vulkan SDK include files and libraries, add "VULKAN_SDK_ROOT" environment variable and for Windows "MINGW_ROOT" environment variable also. Set their values to corresponding folders. Now we can use them in our gradle file:

Python
// MINGW and Vulkan SDK paths

def ENV = System.getenv()
project.ext.set("MINGW", ENV['MINGW_ROOT'])
project.ext.set("VULKAN", ENV['VULKAN_SDK_ROOT'])  

Now we can add interop with C libraries. For this, create nativeInterop/cinterop folder in the project source folder and add the following files:

Lvulkan.def (for Linux) - here, we'll add all needed include file and define xkb_event as we can't add union in Kotlin.

C++
package = vulkan
headers = vulkan/vulkan.h X11/Xlib.h X11/Xatom.h X11/keysym.h X11/extensions/xf86vmode.h X11/
XKBlib.h X11/extensions/XKBproto.h stdio.h stdlib.h locale.h unistd.h termios.h xcb/xkb.h 
xkbcommon/xkbcommon-x11.h xkbcommon/xkbcommon-compose.h

---

union xkb_event {

        struct {
            uint8_t response_type;
            uint8_t xkbType;
            uint16_t sequence;
            xcb_timestamp_t time;
            uint8_t deviceID;
        } any;

        xcb_xkb_new_keyboard_notify_event_t new_keyboard_notify;
        xcb_xkb_map_notify_event_t map_notify;
        xcb_xkb_state_notify_event_t state_notify;

};

vulkan.def (for Windows):

C++
package = vulkan
headers = vulkan/vulkan.h

And global.def (to pass data between threads, taken from Kotlin Native samples.):

C++
package = kvn.global

---

typedef struct {
  void* kotlinObject;
} SharedDataStruct;

SharedDataStruct sharedData;

Now to use them, change the gradle file as follows - we'll add kotlin targets sections:

For Linux:

Python
configure([linux]) {

   // Comment to generate Kotlin/Native library (KLIB) instead of executable file:
   compilations.main.outputKinds('EXECUTABLE')
   // fully qualified name of the application's entry point:
   compilations.main.entryPoint = 'kvarc.main'

   compilations.main.linkerOpts "-L${project.VULKAN}lib -L/usr/lib/x86_64-linux-gnu 
   -lVkLayer_threading -lVkLayer_core_validation -lVkLayer_object_tracker 
   -lVkLayer_parameter_validation -lVkLayer_unique_objects -lvulkan -lX11 -lxcb 
   -lxkbcommon -lxkbcommon-x11 -lxcb-xkb -lXxf86vm -lxcb"

   compilations.main.cinterops {
       lvulkan {
           includeDirs "/usr/include", "${project.VULKAN}/include"
           compilerOpts "-DVK_USE_PLATFORM_XCB_KHR"
       }
       global
   }
}

For Windows:

Python
configure([mingw]) {

   // Comment to generate Kotlin/Native library (KLIB) instead of executable file:
   compilations.main.outputKinds('EXECUTABLE')
   // Fully qualified name of the application's entry point:
   compilations.main.entryPoint = 'kvarc.main'

   compilations.main.linkerOpts "-L${project.VULKAN}lib -lvulkan-1 
   -lVkLayer_threading -lVkLayer_core_validation -lVkLayer_object_tracker 
   -lVkLayer_parameter_validation -lVkLayer_unique_objects"

   compilations.main.cinterops {
       vulkan {
           compilerOpts "-DVK_USE_PLATFORM_WIN32_KHR"
           includeDirs "${project.MINGW}\\include", "${project.VULKAN}\\include"
       }
       global
   }
}

What's just happened. In "outputKinds", we said that we need an executable file. The program entry point to "kvarc.main" function - "kvarc" is the package name like in Java. In "linkerOpts", we set a path to libraries and libraries needed.

Now if you try to run program, it will crash while loading libraries. On Windows, just add a path to them to the PATH variable and on Linux, add to LD_LIBRARY_PATH.
After that, let's do some more modifications to the gradle build file. But first, let's add shaders to the project. Add shaders folder to the project and add triangle vertex and fragment shaders there (thanks to Sascha Willems):

Vertex shader:

C++
#version 450

layout (location = 0) in vec3 inPos;
layout (location = 1) in vec3 inColor;

layout (binding = 0) uniform UBO
{
    mat4 projectionMatrix;
    mat4 modelMatrix;
    mat4 viewMatrix;
} ubo;

layout (location = 0) out vec3 outColor;

out gl_PerVertex
{
    vec4 gl_Position;
};

void main()
{
    outColor = inColor;
    gl_Position = ubo.projectionMatrix * ubo.viewMatrix * ubo.modelMatrix * vec4(inPos.xyz, 1.0);
}

Fragment shader:

C++
#version 450

layout (location = 0) in vec3 inColor;

layout (location = 0) out vec4 outFragColor;

void main()
{
  outFragColor = vec4(inColor, 1.0);
}

And now a big function to run platform dependent build:

Python
// Task create function to compile project, compile shaders and run program
def createRunTask(def buildType, def platform ) {

    return tasks.create("tmpRunProgram${buildType.capitalize()}${platform}") {

        // get link executable task name
        def depends = platform.substring(0,1).capitalize() + platform.substring(1)

        dependsOn "link${buildType.capitalize()}Executable$depends"

        doLast {

            def folder = new File
            ( "${projectDir}/build/bin/$platform/main/$buildType/executable/assets/shaders" )

            // check if a folder for assets exists and create if not
            if( !folder.exists() ) {
                folder.mkdirs()
            }

            def shaders = fileTree("${projectDir}/src/shaders").filter { it.isFile() }.files.name

            shaders.each { shader ->

                exec {
                    workingDir "."
                    commandLine "${project.VULKAN}/bin/glslangValidator", "-V", 
                         "$projectDir/src/shaders/$shader", "-o", "$folder/${shader}.spv"

                    //store the output instead of printing to the console:
                    standardOutput = new ByteArrayOutputStream()

                    //extension method to obtain the output:
                    ext.output = {
                        return standardOutput.toString()
                    }
                }
            }

            def programFile = kotlin.targets."${platform}".compilations.main.getBinary
                                           ('EXECUTABLE', buildType)
            exec {
                executable programFile
                args []
                environment 'LD_LIBRARY_PATH' : "${project.VULKAN}lib:/usr/lib:"
            }
        }
    }
}

What is it? It creates a task with a name depending on selected platform and selected build type. Then it goes through all shader files in source folder, compiles them and save to assets folder. After that, it runs the app with "LD_LIBRARY_PATH" set with Vulkan SDK libraries path.

And our tasks for different platforms and release/debug builds:

Python
// Linux debug build
task runLinuxDebugProgram {
   dependsOn createRunTask("debug", "linux")
}

// Linux release build
task runLinuxReleaseProgram {
   dependsOn createRunTask("release", "linux")
}

// Windows debug build
task runWindowsDebugProgram {
   dependsOn createRunTask("debug", "mingw")
}

// Windows release build
task runWindowsReleaseProgram {
   dependsOn createRunTask("release", "mingw")
}

And the last step - add common code, so we won't return to the gradle build file anymore - for now, we'll just create common/kvarc folder in the project source folder. Then modify the gradle build file again, in sourceSets:

Python
// Windows build
mingwMain {
    kotlin.srcDirs += file("src/common")
}

// Linux build
linuxMain {
    kotlin.srcDirs += file("src/common")
}

Note: Don't follow their note to enable common source sets, you won't be able to use native platform dependent libraries in this case.
The problem with common code we just added - every time you open the project or change the gradle build file, you will need to change the project structure - remove the common code from there for not platform you're working on and set "use project properties" check for every module. Go to Preferences->Build, Execution, Deployment->Compiler->Kotlin Compiler and append to Additional command line parameters the following key "-Xmulti-platform".

Now the common code part. We'll need to know on which platform we're in during runtime. For this, we will use enum class with platform list:

Java
package kvarc.utils

/**
 * Supported platforms
 */
internal enum class PlatformEnum {
    WINDOWS,
    LINUX,
    ANDROID //TODO
}

Now we'll add common platform class:

Java
package kvarc

import kvarc.utils.PlatformEnum

internal expect class Platform {

    companion object {
        val type: PlatformEnum
    }
}  

Here, "expect" keyword means that our class expects "actual" class to be implemented by each platform. And here they are, changing:

MainLinux.kt:

Java
package kvarc

import kvarc.utils.PlatformEnum

internal actual class Platform {

    actual companion object {
        actual val type = PlatformEnum.LINUX
    }
}  

MainMingw.kt:

Java
package kvarc

import kvarc.utils.PlatformEnum


internal actual class Platform {

    actual companion object {
        actual val type = PlatformEnum.WINDOWS
    }
} 

And the final step - common "main" function:

Java
package kvarc

import kvarc.utils.PlatformEnum

fun main(args: Array<String>) {

    when (Platform.type) {
        PlatformEnum.WINDOWS -> println("Windows platform")
        PlatformEnum.LINUX -> println("Linux platform")
        PlatformEnum.ANDROID -> println("Android platform")
    }
}

That's it. All our preparations are finished. Now, after we'll implement native windows for each platform, we will work only with common code.

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

Resources

License

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