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
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:
private fun prepareLayers(scope: MemScope, instanceCreateInfo:
VkInstanceCreateInfo): MutableList<String> {
logInfo("Preparing Debug Layers")
val availableLayers = mutableListOf<String>()
with(scope) {
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 {
result = vkEnumerateInstanceLayerProperties(layersCount.ptr, null)
if (!VK_CHECK(result)) {
logError("Failed to enumerate debug layers")
availableLayers.clear()
return@failure
} 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
}
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)
}
if (availableLayers.size > 0) {
if (availableLayers.contains("VK_LAYER_LUNARG_standard_validation"))
availableLayers.removeAll {
it != "VK_LAYER_LUNARG_standard_validation"
}
else
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 string
s array.
And now it's time to create the instance:
...
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"
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)
}
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
- 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