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

Vulkan API with Kotlin Native - Swapchain, Pipeline

0.00/5 (No votes)
16 Apr 2019GPL34 min read 5.6K  
Vulkan pipeline with Kotlin Native

Introduction

In the previous part, we created a surface where to draw. Now, we will prepare what to draw. But before this, we should make some preparations. We will need vectors, matrices, buffers and so on. It would be possible to use glm libraries for our purposes but as we want to check Kotlin Native performance itself, I will implement something like a little math library by myself. At first, we'll create a base vector class with add, subtract, multiply, dot product and so on operations. Also, we will need a companion object to cast our base vector to a vector with right dimensions. For easy of use with Vulkan API, we will define coordinates as a byte array.

Kotlin
open class Vec(vararg coordinates: Float) {

    protected var buffer: FloatArray = FloatArray(coordinates.size).apply {
        coordinates.copyInto(this)
    }

    val len by lazy {
        sqrtf(this dot this)
    }

    val size by lazy {
        coordinates.size
    }

    fun normalize() = len.let {
        if (it != 0f)
            for (i in 0 until buffer.size)
                buffer[i] /= it
    }

    open fun normalized() = len.run {

        val copyed = buffer.copyOf()

        if (this != 0f)
            for (i in 0 until copyed.size)
                copyed[i] /= this

        when (buffer.size) {
            2 -> Vec.fromBuffer(*copyed) as Vec2
            3 -> Vec.fromBuffer(*copyed) as Vec3
            4 -> Vec.fromBuffer(*copyed) as Vec4
            else -> throw IllegalArgumentException("not supported vector size")
        }
    }

    infix fun dot(vec: Vec): Float = buffer.foldIndexed(0f) { index, sum, element ->
        sum + element * vec.buffer[index]
    }

    open operator fun plus(scalar: Float): Vec = Vec.fromBuffer(*buffer.map {
        it + scalar
    }.toFloatArray())
    
    ....
  
    companion object {

        fun fromBuffer(vararg buffer: Float): Vec {
            when (buffer.size) {
                2 -> return Vec2(buffer[0], buffer[1])
                3 -> return Vec3(buffer[0], buffer[1], buffer[2])
                4 -> return Vec4(buffer[0], buffer[1], buffer[2], buffer[3])
                else -> throw IllegalArgumentException("not supported vector size")
            }
        }
    }
    ...
}

And as an example, I will show an inheritance of the 4-dimensional vector from the base class:

Kotlin
class Vec4(x: Float = 0f, y: Float = 0f, z: Float = 0f, w: Float = 0f) : Vec(x, y, z, w) {

    var x: Float
        get() = buffer[0]
        set(value) {
            buffer[0] = value
        }
    ....

    var w: Float
        get() = buffer[3]
        set(value) {
            buffer[3] = value
        }

    override operator fun inc(): Vec4 = super.inc() as Vec4
    ...
    override fun normalized(): Vec4 = super.normalized() as Vec4

    companion object {

        val Zero = Vec4()
    }
}

Also, we will need matrices. They are implemented in the same way - first, we'll create a base class and then will inherit from it:

The base class:

Kotlin
open class Mat(vararg columns: Vec) {

    private var _size = columns.size

    protected var buffer = FloatArray(_size * _size).apply {
        columns.forEachIndexed { index, vec ->
            vec.toArray().forEachIndexed { idx, fl ->
                this[index * columns.size + idx] = fl
            }
        }
    }

    ... 

    open operator fun plus(v: Float): Mat {

        val arr = this.toBuffer()
        for (i in 0 until arr.size)
            arr[i] += v

        return when (size) {
            2 -> Mat2.from(*arr)
            3 -> Mat3.from(*arr)
            4 -> Mat4.from(*arr)
            else -> throw IllegalArgumentException("invalid dimensions")
        }
    }

    ...

    fun toBuffer(): FloatArray = buffer.copyOfRange(0, buffer.size)
}

And Mat4:

Kotlin
class Mat4(x: Vec4 = Vec4(x = 1f), y: Vec4 = Vec4(y = 1f), 
           z: Vec4 = Vec4(z = 1f), w: Vec4 = Vec4(w = 1f)) : Mat(x, y, z, w) {

    var X: Vec4
        get() = Vec4(buffer[0], buffer[1], buffer[2], buffer[3])
        set(value) {
            buffer[0] = value[0]
            buffer[1] = value[1]
            buffer[2] = value[2]
            buffer[3] = value[3]
        }
   ...

    var W: Vec4
        get() = Vec4(buffer[12], buffer[13], buffer[14], buffer[15])
        set(value) {
            buffer[12] = value[0]
            buffer[13] = value[1]
            buffer[14] = value[2]
            buffer[15] = value[3]
        }

    override operator fun inc(): Mat4 = super.inc() as Mat4
    ...
    override operator fun minus(m: Mat): Mat4 = super.minus(m) as Mat4

    companion object {

        fun from(vararg a: Float): Mat4 {
            assert(a.size == 16)
            return Mat4(Vec4(a[0], a[1], a[2], a[3]), Vec4(a[4], a[5], a[6], a[7]), 
                        Vec4(a[8], a[9], a[10], a[11]), Vec4(a[12], a[13], a[14], a[15]))
        }
        ...
    }
}

Now we have the little math library, but it would be good to have some helpers to work with byte arrays as we will use them much while working with Vulkan API. Let's create a helper class for it that allows us to use iterator, forEach, etc.

Kotlin
@ExperimentalUnsignedTypes
internal class VulkanArray<T : CVariable>
private constructor(internal val _size: UInt) : DisposableContainer() {

    internal lateinit var _array: CArrayPointer<T>
        private set

    companion object {

        inline fun <reified K : CVariable> Make(size: UInt): VulkanArray<K> {
            val array = VulkanArray<K>(size)
            array._array = with(array.arena) { allocArray(size.toInt()) }
            return array
        }
    }
}

@ExperimentalUnsignedTypes
internal inline operator fun <reified T : CVariable> VulkanArray<T>.iterator(): Iterator<T> {

    return object : Iterator<T> {

        var cursor = 0
        override fun hasNext() = cursor < _size.toInt()
        override fun next(): T = _array.get(cursor++)
    }
}

@ExperimentalUnsignedTypes
internal inline fun <reified T : CVariable> VulkanArray<T>
    .forEach(callback: (it: T) -> Unit) {

    for (i in 0 until _size.toInt()) {
        callback(_array[i])
    }
}

...

Now we're ready to continue.

SwapChain. RenderPass.

As you know, only one image is presented to the surface at a time. But we can create a queue and render more images while one of them is being presented to a screen. So the swapchain is an array of such presentable images in a queue and it allows to show them on a screen. To create the swapchain, we first will get supported formats, check if we can use VK_PRESENT_MODE_IMMEDIATE_KHR or VK_PRESENT_MODE_MAILBOX_KHR, check if we can use composite alpha, create swapchain itself and create a buffer with images.
Let's get supported formats:

Kotlin
val formatsCount = alloc<UIntVar>()
var result: VkResult

var buffer: CArrayPointer<VkSurfaceFormatKHR>? = null

do {

    result = vkGetPhysicalDeviceSurfaceFormatsKHR(pDevice.device,
              surface, formatsCount.ptr, null)
    if (!VK_CHECK(result)) {
        throw RuntimeException("Could not get surface formats.")
    }

    if (formatsCount.value == 0u) break

    buffer?.let {
        nativeHeap.free(it)
        buffer = null
    }

    buffer = nativeHeap.allocArray(formatsCount.value.toInt())
    result =
        vkGetPhysicalDeviceSurfaceFormatsKHR(
            pDevice.device,
            surface,
            formatsCount.ptr,
            buffer!!.getPointer(memScope)
        )
    if (!VK_CHECK(result)) {
        throw RuntimeException("Could not get surface formats.")
    }


} while (result == VK_INCOMPLETE)

if (formatsCount.value == 1u) {
    displayFormat = buffer!![0].format
    _colorSpace = buffer!![0].colorSpace
} else {

    var chosenFormat: UInt? = null

    for (i in 0u until formatsCount.value) {
        if (buffer!![i.toInt()].format == VK_FORMAT_R8G8B8A8_UNORM) {
            chosenFormat = i
            break
        }
    }

    chosenFormat?.let {
        displayFormat = buffer!![it.toInt()].format
        _colorSpace = buffer!![it.toInt()].colorSpace
    } ?: kotlin.run {
        displayFormat = buffer!![0].format
        _colorSpace = buffer!![0].colorSpace
    }

}

nativeHeap.free(buffer!!)

Next, we'll define present mode. Earlier, in the PhysicalDevice class, we added surfacePresentModes property. In case we won't use vsync, we just check if it contains VK_PRESENT_MODE_MAILBOX_KHR otherwise if contains VK_PRESENT_MODE_IMMEDIATE_KHR, use it. And by default, it's VK_PRESENT_MODE_FIFO_KHR. Then in surface capabilities also, we should check max images count and if they contain any composite alpha bits. After all preparations, we can create swapchain itself and image:

Kotlin
val swapchainCreateInfo: VkSwapchainCreateInfoKHR =
     alloc<VkSwapchainCreateInfoKHR>().apply {
     sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR
     ...
 }
...

if (!VK_CHECK(vkCreateSwapchainKHR(lDevice,
      swapchainCreateInfo.ptr, null, _swapchain.ptr)))
     throw RuntimeException("Failed to create swapchain")

 val imagesCount: UIntVar = alloc()

 if (!VK_CHECK(vkGetSwapchainImagesKHR(lDevice, _swapchain.value,
   imagesCount.ptr, null)))
     throw RuntimeException("Failed to initialize vulkan. No images")

 _imagesBuffer = arena.allocArray(imagesCount.value.toInt())

 if (!VK_CHECK(vkGetSwapchainImagesKHR(lDevice, _swapchain.value,
    imagesCount.ptr, _imagesBuffer)))
     throw RuntimeException("Failed to initialize vulkan. No images")

 for (i in 0u until imagesCount.value) {

     val imageViewCreateInfo: VkImageViewCreateInfo =
         alloc<VkImageViewCreateInfo>().apply {
         sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO
         ...
         image = _imagesBuffer[i.toInt()]!!
     }

     val imageView = with(arena) { alloc<VkImageViewVar>() }
     if (!VK_CHECK(vkCreateImageView(lDevice,
         imageViewCreateInfo.ptr, null, imageView.ptr))) {
         throw RuntimeException("Failed to create image views")
     }

     imageBuffers.add(Pair(_imagesBuffer[i.toInt()]!!, imageView))

 }
 ...

The RenderPass implementation is quite easy to do. It's the standard way with use of structures and structures. So I leave it to the reader to check the source code.

Uniform Buffers

Uniform buffers are read-only memory areas allocated in the memory of the video card that can be used by shader programs. So we should prepare those areas in the host memory, then pass them to the video card memory. For this, yes, again some preparatory steps. First of all, let's create a Vertex class:

Kotlin
@ExperimentalUnsignedTypes
class Vertex(position: Vec3, color: Vec3) {
    var buffer: FloatArray = FloatArray(6) { 0f }
        private set

    var position: Vec3 ...

    var color: Vec3 ...
    
    ...

    companion object {

        // Single vertex input binding at binding point 0

        fun bindingDescription(scope: MemScope) = 
            scope.alloc<VkVertexInputBindingDescription>().apply {
            binding = 0u
            stride = Vertex.SIZE.toUInt()
            inputRate = VK_VERTEX_INPUT_RATE_VERTEX
        }

        fun inputAtributes(scope: MemScope) = 
            scope.allocArray<VkVertexInputAttributeDescription>(2).apply {

            // Input attribute bindings describe shader attribute locations and memory layouts

            // These match the following shader layout (see triangle.vert):
            //	layout (location = 0) in vec3 inPos;
            //	layout (location = 1) in vec3 inColor;

            // Attribute location 0: Position
            this[0].binding = 0u
            this[0].location = 0u
            // Position attribute is three 32 bit signed (SFLOAT) floats (R32 G32 B32)
            this[0].format = VK_FORMAT_R32G32B32_SFLOAT
            this[0].offset = 0u

            // Attribute location 1: Color


        }

        val SIZE = 6 * sizeOf<FloatVar>()
        val BUFFER_SIZE = 6
    }
}

Now we can create a vertex buffer. As we will use staging buffers, let's create classes for them. StagingBuffer itself will be just a couple of properties, a buffer and a device memory of VkBufferVar and VkDeviceMemoryVar types. And StagingBuffers class also contains a couple of variables for a vertex staging buffer and an indices staging buffer. And the initialization of the vertex buffer class will include the following steps:

  • create a mappable buffer visible to the host
  • copy data to it
  • create a buffer in video card memory with the same size as the host buffer
  • with use of a command buffer, copy data from the host to the device
  • delete the host buffer
  • use the device buffer inside shaders

Here is the implementation:

Kotlin
@ExperimentalUnsignedTypes
internal class VertexBuffer(
    private val pDevice: PhysicalDevice,
    private val lDevice: LogicalDevice,
    vertices: Array<Vertex>,
    indices: Array<UInt>
) : DisposableContainer() {

    ...
    init {

        var voffset = 0

        vertices.forEach { v ->
            v.buffer.copyInto(vertexBuffer, voffset)
            voffset += Vertex.BUFFER_SIZE
        }

        indices.forEachIndexed { index, uInt ->
            indexBuffer[index] = uInt
        }

        memScoped {

            val memoryAllocateInfo = alloc<VkMemoryAllocateInfo>().apply {
                sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO
            }

            val memReqs: VkMemoryRequirements = alloc()

            // Vertex buffer
            val vertexBufferInfo = alloc<VkBufferCreateInfo>().apply {
                sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO
                size = vertexBufferSize.toULong()
                usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT //copy source
            }

            val stagingBuffers = StagingBuffers()

            if (!VK_CHECK(
                    vkCreateBuffer(
                        lDevice.device,
                        vertexBufferInfo.ptr,
                        null,
                        stagingBuffers.vertices.buffer.ptr
                    )
                )
            )
            throw RuntimeException("Failed to create buffer")

            vkGetBufferMemoryRequirements(lDevice.device, 
                 stagingBuffers.vertices.buffer.value, memReqs.ptr)

            memoryAllocateInfo.allocationSize = memReqs.size

            // host visible memory and coherent
            memoryAllocateInfo.memoryTypeIndex = pDevice.getMemoryType(
                memReqs.memoryTypeBits,
                VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT or 
                VK_MEMORY_PROPERTY_HOST_COHERENT_BIT
            )

            val mapped = alloc<COpaquePointerVar>()

            if (!VK_CHECK(
                    vkAllocateMemory(
                        lDevice.device,
                        memoryAllocateInfo.ptr,
                        null,
                        stagingBuffers.vertices.memory.ptr
                    )
                )
            )
                throw RuntimeException("Faild allocate memory")

            if (!VK_CHECK(
                    vkMapMemory(
                        lDevice.device,
                        stagingBuffers.vertices.memory.value,
                        0u,
                        memoryAllocateInfo.allocationSize,
                        0u,
                        mapped.ptr
                    )
                )
            )
                throw RuntimeException("Faild map memory")

            vertexBuffer.usePinned { buffer ->
                platform.posix.memcpy(mapped.value, buffer.addressOf(0), 
                    vertexBufferSize.toULong())
            }

            vkUnmapMemory(lDevice.device, stagingBuffers.vertices.memory.value)

            if (!VK_CHECK(
                    vkBindBufferMemory(
                        lDevice.device,
                        stagingBuffers.vertices.buffer.value,
                        stagingBuffers.vertices.memory.value,
                        0u
                    )
                )
            )
                throw RuntimeException("failed bind memory")

            // Create a _device local buffer to accept data
            vertexBufferInfo.usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT or 
               VK_BUFFER_USAGE_TRANSFER_DST_BIT

            if (!VK_CHECK(vkCreateBuffer(lDevice.device, vertexBufferInfo.ptr, 
                null, _vertexBuffer.ptr)))
                throw RuntimeException("Failed to create buffer")
            vkGetBufferMemoryRequirements(lDevice.device, _vertexBuffer.value, 
                  memReqs.ptr)

            memoryAllocateInfo.allocationSize = memReqs.size
            memoryAllocateInfo.memoryTypeIndex = pDevice.getMemoryType(
                memReqs.memoryTypeBits,
                VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT
            )

            if (!VK_CHECK(vkAllocateMemory(lDevice.device, memoryAllocateInfo.ptr, 
                null, _vertexMemory.ptr)))
                throw RuntimeException("Faild allocate memory")

            if (!VK_CHECK(vkBindBufferMemory(lDevice.device, _vertexBuffer.value, 
               _vertexMemory.value, 0u)))
                throw RuntimeException("failed bind memory")

            ...
            /* the same for index buffer */
            ...
            
            // copy buffer
            val copyCmd = lDevice.createCommandBuffers(VK_COMMAND_BUFFER_LEVEL_PRIMARY, 
                 1u, true)
            
            val copyRegion = alloc<VkBufferCopy>()

            copyRegion.size = vertexBufferSize.toULong()
            vkCmdCopyBuffer(
                copyCmd._array[0],
                stagingBuffers.vertices.buffer.value,
                _vertexBuffer.value,
                1u,
                copyRegion.ptr
            )

            ...

            val cc = alloc<VkCommandBufferVar>()
            cc.value = copyCmd._array[0]
            lDevice.flushCommandBuffer(cc, false)

            vkDestroyBuffer(lDevice.device, stagingBuffers.vertices.buffer.value, null)
            vkFreeMemory(lDevice.device, stagingBuffers.vertices.memory.value, null)

            ...
        }
    }
}

Note: It's critical to use usePinned otherwise you'll get a trash. It temporarily pins the native memory address of the byte array.

Next, we need UboVS class. It will just contain model, view and projection matrices in the byte array, so it would be simple to copy it to the buffer. UniformBufferVars with memory, buffer and descriptor properties.

And UniformBuffers class:

Kotlin
//
@ExperimentalUnsignedTypes
internal class UniformBuffers(
    private val pDevice: PhysicalDevice,
    private val lDevice: LogicalDevice,
    private val swapchain: SwapChain

) : DisposableContainer() {

    val uniformBufferVS: UniformBufferVars = UniformBufferVars()
    val uboVS: UboVS = UboVS(Mat4.ZERO, Mat4.ZERO, Mat4.ZERO)

    init {

        memScoped {

            val memReqs = alloc<VkMemoryRequirements>()

            uniformBufferVS.buffer = with(arena) { alloc() }
            uniformBufferVS.memory = with(arena) { alloc() }

            val bufferCreateInfo = alloc<VkBufferCreateInfo>().apply {
                sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO
                usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT
                size = UboVS.SIZE.toULong()
            }

            if (!VK_CHECK(vkCreateBuffer(lDevice.device, bufferCreateInfo.ptr, 
                null, uniformBufferVS.buffer!!.ptr)))
                throw RuntimeException("failed to create buffer")

            val memoryAllocateInfo = alloc<VkMemoryAllocateInfo>().apply {
                sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO
                pNext = null
                allocationSize = 0u
                memoryTypeIndex = 0u
            }

            vkGetBufferMemoryRequirements(lDevice.device, 
                             uniformBufferVS.buffer!!.value, memReqs.ptr)
            memoryAllocateInfo.allocationSize = memReqs.size

            memoryAllocateInfo.memoryTypeIndex = pDevice.getMemoryType(
                memReqs.memoryTypeBits,
                VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT or 
                VK_MEMORY_PROPERTY_HOST_COHERENT_BIT
            )

            if (!VK_CHECK(vkAllocateMemory(lDevice.device, 
                   memoryAllocateInfo.ptr, null, uniformBufferVS.memory!!.ptr)))
                throw RuntimeException("failed allocate memory")


            if (!VK_CHECK(
                    vkBindBufferMemory(
                        lDevice.device,
                        uniformBufferVS.buffer!!.value,
                        uniformBufferVS.memory!!.value,
                        0u
                    )
                )
            )
                throw  RuntimeException("faied bind memory")


            // Store information in the uniform's descriptor 
            // that is used by the descriptor set
            uniformBufferVS.descriptor = with(arena) { alloc() }
            uniformBufferVS.descriptor!!.offset = 0u
            uniformBufferVS.descriptor!!.buffer = uniformBufferVS.buffer!!.value
            uniformBufferVS.descriptor!!.range = UboVS.SIZE.toULong()
        }

        uboVS.modelMatrix = Mat4.identity

        update()
    }

    fun update() {

        uboVS.projectionMatrix = Mat4.perspective(
            radians(60f),
            swapchain.width.toInt().toFloat() / swapchain.height.toInt().toFloat(),
            0.1f, 256f
        )

        uboVS.viewMatrix = Mat4.translate(Mat4.identity, Vec3(0f, 0f, -5f))
        uboVS.modelMatrix = Mat4.rotate(uboVS.modelMatrix, radians(1f), Vec3(z = 1f))

        memScoped {

            val data = alloc<COpaquePointerVar>()

            if (!VK_CHECK(
                    vkMapMemory(
                        lDevice.device!!,
                        uniformBufferVS.memory!!.value,
                        0u,
                        UboVS.SIZE.toULong(),
                        0u,
                        data.ptr
                    )
                )
            )
                throw RuntimeException("failed bind memory")

            uboVS.buffer.usePinned { buffer ->
                platform.posix.memcpy(data.value, buffer.addressOf(0), 
                    UboVS.SIZE.toULong())
            }

            // Note: Since we requested a host coherent memory type for 
            // the uniform buffer, the write is instantly visible to the GPU
            vkUnmapMemory(lDevice.device, uniformBufferVS.memory!!.value)
        }
    }

 ...

}

Ok, we're almost at the end. Next time, we'll add frame buffers, command buffers and the drawing loop.

Pipeline

Before we will create the pipeline, we need for it the pipeline cache, the descriptor set layout, the pipeline layout, the descriptor pool and the descriptor set. They are also as the renderpass implemented in the standard way and you can find them in the source code. The only thing I would mention - loading of shaders. If you remember, we compiled them and copied to the assets folder. Here the one thing that should be kept in mind - usePinned when loading them from a file system to a buffer.

And now, we'll define all fixed states in the rendering pipeline:

Kotlin
@ExperimentalUnsignedTypes
internal class Pipeline(
    private val _device: LogicalDevice,
    private val _pipelineLayout: PipelineLayout,
    private val _renderPass: RenderPass,
    private val _pipelineCache: PipelineCache,
    private val _swapchain: SwapChain
) : DisposableContainer() {

    ...

    init {

        memScoped {

            // Shaders

            val shaderStages = allocArray<VkPipelineShaderStageCreateInfo>(2)

            val cwd = ByteArray(1024)
            cwd.usePinned {
                getcwd(it.addressOf(0), 1024)
            }

            var shaderFile = "${cwd.stringFromUtf8()}/assets/shaders/triangle.vert.spv"
            if(access(shaderFile, F_OK) == -1){
                shaderFile = "${cwd.stringFromUtf8()}
                  /build/bin/mingw/mainDebugExecutable/assets/shaders/triangle.vert.spv"
                if(access(shaderFile, F_OK) == -1){
                    shaderFile = "${cwd.stringFromUtf8()}/
                      build/bin/linux/mainDebugExecutable/assets/shaders/triangle.vert.spv"
                }
            }

            // Vertex shader
            shaderStages[0].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO
            shaderStages[0].stage = VK_SHADER_STAGE_VERTEX_BIT
            shaderStages[0].module = _pipelineCache.loadShader(_device.device!!, shaderFile)
            shaderStages[0].pName = "main".cstr.ptr
            assert(shaderStages[0].module != null)

            shaderFile = "${cwd.stringFromUtf8()}/assets/shaders/triangle.frag.spv"
            if(access(shaderFile, F_OK) == -1){
                shaderFile = "${cwd.stringFromUtf8()}/build/
                    bin/mingw/mainDebugExecutable/assets/shaders/triangle.frag.spv"
                if(access(shaderFile, F_OK) == -1){
                    shaderFile = "${cwd.stringFromUtf8()}/build/bin/linux/
                                   mainDebugExecutable/assets/shaders/triangle.frag.spv"
                }
            }

            ...

            // Vertex input state 

            val pipelineVertexInputStateCreateInfo = 
                    alloc<VkPipelineVertexInputStateCreateInfo>().apply {
                sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO
                vertexBindingDescriptionCount = 1u
                pVertexBindingDescriptions = vertexInputBindingDescription.ptr
                vertexAttributeDescriptionCount = 2u
                pVertexAttributeDescriptions = vertexInputAttributs
            }

            // Input assembly state 

            val pipelineInputAssemblyStateCreateInfo = 
                      alloc<VkPipelineInputAssemblyStateCreateInfo>().apply {
                sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO
                topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST
                primitiveRestartEnable = 0u
            }

            // Viewport state 
            val viewport = alloc<VkViewport>().apply {
                x = 0.0f
                y = 0.0f
                width = _swapchain.width.toInt().toFloat()
                height = _swapchain.height.toInt().toFloat()
                minDepth = 0f
                maxDepth = 1f
            }

            val scissor = alloc<VkRect2D>().apply {
                offset.x = 0
                offset.y = 0
                extent.width = _swapchain.width
                extent.height = _swapchain.height
            }

            val pipelineViewportStateCreateInfo = 
                     alloc<VkPipelineViewportStateCreateInfo>().apply {
                sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO
                viewportCount = 1u
                scissorCount = 1u
                pViewports = viewport.ptr
                pScissors = scissor.ptr
            }

            // Rasterization state

            val pipelineRasterizationStateCreateInfo = 
                   alloc<VkPipelineRasterizationStateCreateInfo>().apply {
                sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO
                depthClampEnable = VK_FALSE.toUInt()
                rasterizerDiscardEnable = VK_FALSE.toUInt()
                polygonMode = VK_POLYGON_MODE_FILL
                cullMode = VK_CULL_MODE_BACK_BIT //VK_CULL_MODE_NONE
                lineWidth = 1.0f
                frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE
                depthBiasEnable = VK_FALSE.toUInt()
            }

            // Multi sampling state

            val pipelineMultisampleStateCreateInfo = 
                  alloc<VkPipelineMultisampleStateCreateInfo>().apply {
                sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO
                rasterizationSamples = VK_SAMPLE_COUNT_1_BIT
                pSampleMask = null
                sampleShadingEnable = 0u
            }

            // Color blend state 

            val blendAttachmentState = allocArray<VkPipelineColorBlendAttachmentState>(1)
            blendAttachmentState[0].apply {
                colorWriteMask = VK_COLOR_COMPONENT_R_BIT or 
                                 VK_COLOR_COMPONENT_G_BIT or VK_COLOR_COMPONENT_B_BIT or
                        VK_COLOR_COMPONENT_A_BIT //0xfu
                blendEnable = VK_FALSE.toUInt()
            }

            val pipelineColorBlendStateCreateInfo = 
                   alloc<VkPipelineColorBlendStateCreateInfo>().apply {
                sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO
                attachmentCount = 1u
                pAttachments = blendAttachmentState
                logicOpEnable = 0u
                logicOp = VK_LOGIC_OP_COPY
                blendConstants[0] = 0.0f
                blendConstants[1] = 0.0f
                blendConstants[2] = 0.0f
                blendConstants[3] = 0.0f
            }

            val pipelineCreateInfo = alloc<VkGraphicsPipelineCreateInfo>().apply {
                sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO
                // The layout used for this pipeline 
                // (can be shared among multiple pipelines using the same layout)
                layout = _pipelineLayout._pipelineLayout.value
                //Renderpass this pipeline is attached to
                renderPass = _renderPass.renderPass.value
            }

            // Enable dynamic states

            val dynamicStateEnables = allocArray<VkDynamicStateVar>(2)
            dynamicStateEnables[0] = VK_DYNAMIC_STATE_VIEWPORT
            dynamicStateEnables[1] = VK_DYNAMIC_STATE_SCISSOR

            val pipelineDynamicStateCreateInfo = 
                    alloc<VkPipelineDynamicStateCreateInfo>().apply {
                sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO
                pDynamicStates = dynamicStateEnables
                dynamicStateCount = 2u
            }

            // Depth and stencil state 

            val pipelineDepthStencilStateCreateInfo = 
                      alloc<VkPipelineDepthStencilStateCreateInfo>().apply {
                sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO
                depthTestEnable = VK_TRUE.toUInt()
                depthWriteEnable = VK_TRUE.toUInt()
                depthCompareOp = VK_COMPARE_OP_LESS_OR_EQUAL
                depthBoundsTestEnable = VK_FALSE.toUInt()
                back.failOp = VK_STENCIL_OP_KEEP
                back.passOp = VK_STENCIL_OP_KEEP
                back.compareOp = VK_COMPARE_OP_ALWAYS
                stencilTestEnable = VK_FALSE.toUInt()
                front.failOp = VK_STENCIL_OP_KEEP
                front.passOp = VK_STENCIL_OP_KEEP
                front.compareOp = VK_COMPARE_OP_ALWAYS
            }

            pipelineCreateInfo.stageCount = 2u
            pipelineCreateInfo.pStages = shaderStages

            pipelineCreateInfo.pVertexInputState = pipelineVertexInputStateCreateInfo.ptr
            pipelineCreateInfo.pInputAssemblyState = pipelineInputAssemblyStateCreateInfo.ptr
            pipelineCreateInfo.pRasterizationState = pipelineRasterizationStateCreateInfo.ptr
            pipelineCreateInfo.pColorBlendState = pipelineColorBlendStateCreateInfo.ptr
            pipelineCreateInfo.pMultisampleState = pipelineMultisampleStateCreateInfo.ptr
            pipelineCreateInfo.pViewportState = pipelineViewportStateCreateInfo.ptr
            pipelineCreateInfo.pDepthStencilState = pipelineDepthStencilStateCreateInfo.ptr
            pipelineCreateInfo.renderPass = _renderPass.renderPass.value
            pipelineCreateInfo.pDynamicState = pipelineDynamicStateCreateInfo.ptr

            if (!VK_CHECK(
                    vkCreateGraphicsPipelines(
                        _device.device,
                        _pipelineCache.value,
                        1u,
                        pipelineCreateInfo.ptr,
                        null,
                        _pipeline.ptr
                    )
                )
            )
                throw RuntimeException("failed create pipeline")

            vkDestroyShaderModule(_device.device, shaderStages[0].module, null)
            vkDestroyShaderModule(_device.device, shaderStages[1].module, null)
        }
    }

...

}

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)