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:
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:
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:
@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:
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:
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:
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.
@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>()
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?
}, null)
.ensureUnixCallResult("pthread_create")
}
val vkThread = alloc<pthread_tVar>()
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)
null as COpaquePointer?
}, 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
- Vulkan API with Kotlin Native - Project Setup
- Vulkan API with Kotlin Native - Platform's Windows
- Vulkan API with Kotlin Native - Instance
- Vulkan API with Kotlin Native - Surface, Devices
- Vulkan API with Kotlin Native - SwapChain, Pipeline
- Vulkan API with Kotlin Native - Draw
- Kotlin Native Samples
- WinAPI Documentation
- xcb library documentation
- xkbcommon documentation