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

Vulkan API with Kotlin Native - Platform's Windows

5.00/5 (2 votes)
1 Apr 2019GPL32 min read 10.4K  
Native windows with Kotlin Native for Linux and Windows platforms

Introduction

In the previous part, Vulkan API with Kotlin Native - Project Setup, we created the project that works on Windows and Linux platforms — it determines on which platform it is and shows the corresponding message. In this part, we will create native windows for Linux and Windows. We will add a possibility to switch to fullscreen mode, for now real switch will be only for Windows platform, for Linux for now we'll just show it maximized and without decorations, later, I'll add real switch for the Linux window.

Windows

Let's start with Windows as it's a little simpler to implement. I'll add two different threads — the first one for the system message loop and the second one for the Vulkan rendering. First of all, we need a shared data to pass between threads. For this, we added global.def file to the native interop in the previous part. Now we'll define the data we need:

Java
/**
 * Data shared between threads
 */
internal class CommonData @ExperimentalUnsignedTypes constructor(
    val semaphore: sem_tVar,
    var hInstance: HINSTANCE? = null,
    var hwnd: HWND? = null,
    var showWindowCentered: Boolean = true,
    var showWindowFullscreen: Boolean = false,
    var onTheRun: Boolean = true,
    var windowsSurface: WindowsSurface? = null
)

internal data class SharedCommonData(val userdata: COpaquePointer?)

Here, we added all needed data to pass to the system message loop — the most important are: the semaphore to synchronize threads, "onTheRun" variable to stop all processing and reference to the new window class. We also added the class to get a pointer to our shared data.

Now some changes in common code:

Java
internal expect class Platform {

    fun Initialize()

    companion object {
        val type: PlatformEnum
        val VK_KHR_PLATFORM_SURFACE_EXTENSION_NAME: String
    }
}

Here, we'll expect from each native platform the "Initialize" that will create the window and start rendering, also added constant that will define the name of the surface extension. Now the main function will look like this:

Java
@ExperimentalUnsignedTypes
fun main(args: Array<String>) {

    val platform = Platform()
    platform.Initialize()

}

Now, it's time to create the window. Mostly, it's done the same way as in C++ — using standard WinAPI methods and passed the class reference to WndProc to call class methods. First of all, let's get the shared data in class initialization:

Java
init {
        val kotlinObject = DetachedObjectGraph<SharedCommonData>(sharedData.kotlinObject).attach()
        val sharedData = kotlinObject.userdata!!.asStableRef<CommonData>().get()
        sharedData.windowsSurface = this
    }

Then, let's create the native window itself and run it:

Java
fun initialize() {

        memScoped {

            val hInstance = GetModuleHandleW(null)
            val hBrush = CreateSolidBrush(0x00000000)

            val wc: WNDCLASSEXW = alloc()
            wc.cbSize = sizeOf<WNDCLASSEX>().convert()
            wc.style = (CS_HREDRAW or CS_VREDRAW or CS_OWNDC).convert()

            wc.lpfnWndProc = staticCFunction { hwnd, msg, wParam, lParam ->

                when (msg.toInt()) {
                    WM_CLOSE -> {
                        val kotlinObject = DetachedObjectGraph<SharedCommonData>
                                           (sharedData.kotlinObject).attach()
                        val sharedData = kotlinObject.userdata!!.asStableRef<CommonData>().get()
                        sharedData.onTheRun = false
                        DestroyWindow(hwnd)
                    }
                    WM_DESTROY -> {
                        PostQuitMessage(0)
                    }

                    ....

                    WM_KEYDOWN -> {
                        if (GetAsyncKeyState(VK_ESCAPE) != 0.toShort()) {
                            val kotlinObject = DetachedObjectGraph<SharedCommonData>
                                               (sharedData.kotlinObject).attach()
                            val sharedData = kotlinObject.userdata!!.asStableRef<CommonData>().get()
                            if(sharedData.showWindowFullscreen){
                                sharedData.windowsSurface?.changeFullscreen(false)
                            }
                            PostMessageW(hwnd, WM_CLOSE, 0, 0)
                        }
                    }
                    WM_SYSKEYDOWN -> {
                        if (wParam == VK_F4.toULong()) {
                            return@staticCFunction 1
                        }
                    }
                    else -> {
                        return@staticCFunction DefWindowProcW(hwnd, msg, wParam, lParam)
                    }
                }                
            }

            wc.hInstance = hInstance 

            ...

            val failure: ATOM = 0u
            if (RegisterClassExW(wc.ptr) == failure) {
                throw RuntimeException("Failed to create native window!")
            }

            hwnd = CreateWindowExW(
                WS_EX_CLIENTEDGE,
                "kvarc",
                "kvarc",
                WS_OVERLAPPEDWINDOW,
                CW_USEDEFAULT,
                CW_USEDEFAULT,
                _width,
                _height,
                null,
                null,
                hInstance,
                null
            )

            if (hwnd == null) {
                MessageBoxW(
                    null, "Failed to create native window!",
                    "kvarc", (MB_OK).convert()
                )
                throw RuntimeException("Failed to create native window!")
            }
            
            ...

            ShowWindow(hwnd, SW_SHOWNORMAL)
            UpdateWindow(hwnd)
        }
    }

And the window message loop:

Java
fun messageLoop() {

        memScoped {

            val msg: MSG = alloc()

            while (GetMessageW(msg.ptr, null, 0u, 0u) > 0) {
                TranslateMessage(msg.ptr)
                DispatchMessageW(msg.ptr)
            }
        }
}

Quite easy, so, since we have the window, it's time to add threads for the window and for the rendering.

Java
@ExperimentalUnsignedTypes
internal actual class Platform {

    actual fun Initialize() {

        try {

            val arena = Arena()

            val semaphore = arena.alloc<sem_tVar>()
            sem_init(semaphore.ptr, 0, 0)

            ...

            memScoped {

               val winThread = alloc<pthread_tVar>()

                // It lies about redundant lambda arrow
                pthread_create(winThread.ptr, null, staticCFunction { _: COpaquePointer? ->

                    initRuntimeIfNeeded()

                    val kotlinObject = DetachedObjectGraph<SharedCommonData>
                                       (sharedData.kotlinObject).attach()
                    val data = kotlinObject.userdata!!.asStableRef<CommonData>().get()

                    val win = WindowsSurface(data.showWindowFullscreen)
                    win.initialize()

                    ...

                    @Suppress("UNCHECKED_CAST")
                    data.hwnd = win.hwnd!!

                    sem_post(data.semaphore.ptr)

                    win.messageLoop()
                    win.dispose()

                    null as COpaquePointer? //keep it lies not needed
                }, null)
                    .ensureUnixCallResult("pthread_create")
            }

            val vkThread = alloc<pthread_tVar>()

            // It lies about redundant lambda arrow
                pthread_create(vkThread.ptr, null, staticCFunction { _: COpaquePointer? ->

                    initRuntimeIfNeeded()

                    val kotlinObject = DetachedObjectGraph<SharedCommonData>
                                       (sharedData.kotlinObject).attach()
                    val data = kotlinObject.userdata!!.asStableRef<CommonData>().get()
                    sem_wait(data.semaphore.ptr)

                    //TODO Vulkan loop
                    null as COpaquePointer? //keep it lies not needed

                }, null)
                    .ensureUnixCallResult("pthread_create")

                pthread_join(vkThread.value, null).ensureUnixCallResult("pthread_join")
                pthread_join(winThread.value, null).ensureUnixCallResult("pthread_join")

                sem_destroy(semaphore.ptr)
                commonDataStableRef.dispose()
                arena.clear()
        }
        catch (ex: Exception) {
            logError("Failed to start with exception: ${ex.message}")
        }
    }
}

Linux Platform

The Linux window creation and run is much the same as for Window. The differences are in using specific API calls to create the window itself, message loop processing, using specific libraries such as xcb, xkb, etc.

So in the next part, we're already ready to work with Vulkan API.

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

  1. Kotlin Native Samples
  2. WinAPI Documentation
  3. xcb library documentation
  4. xkbcommon documentation

License

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