Saturday, 2 April 2016

Vulkan, first steps 04: pipeline layout and uniform buffer creation

List of all Vulkan tutorial posts is here.

In versions 1 and 2 of the OpenGL GPU pipeline was the fixed one. You have to set matrices, lights' parameters, bind textures and send some vertices to GPU. From the third version onward, the fixed pipeline was deprecated and focus was placed on a programmable one. There were no more matrices and light parameters. All data was passed by uniform variables and buffers to shaders - programs which run on GPU. In Vulkan, GPU pipeline is also programmable. But, before one can send anything to shaders, she or he has to define which resources will be available to which stage of the pipeline. In terms of nVidia's Open-Source Vulkan C++ API, that definition is called vk::PipelineLayout and is created in two steps. First, an object of class vk::DescriptorSetLayout is created and contains information about resources bindings. Resources are the buffers, textures and so on. After creating a descriptor object, Vulkan can create pipeline layout on GPU by creating vk::PipelineLayout object. Example code, where there is only one resource (uniform buffer), is shown below.
#include <iostream>
#include <vector>
#include <algorithm>

#include <madvk/instance.h>
#include <madvk/device.h>


using namespace std;
using namespace mad::vk;

int main(int argc, char* argv[]) {
    try {
        auto instance = Instance("vulkan playground 01");
        cout << "Vulkan instance created" << endl;
        auto device = instance.createDevice(::vk::QueueFlagBits::eGraphics);
        cout << "Vulkan device created" << endl;

        // Only one binding - uniform buffer for projection matrix
        auto layout_binding = ::vk::DescriptorSetLayoutBinding()
            .descriptorType(::vk::DescriptorType::eUniformBuffer)
            .descriptorCount(1)
            .stageFlags(::vk::ShaderStageFlagBits::eVertex);
        auto descriptor_layout = ::vk::DescriptorSetLayoutCreateInfo()
            .bindingCount(1)
            .pBindings(&layout_binding);
        auto layout = device.createDescriptorSetLayout(descriptor_layout, nullptr);
        cout << "Vulkan layout created" << endl;

        auto pipeline_layout = ::vk::PipelineLayoutCreateInfo().pSetLayouts(&layout);
        auto pipeline = device.createPipelineLayout(pipeline_layout, nullptr);
        cout << "Vulkan pipeline created" << endl;

        device.destroyPipelineLayout(pipeline, nullptr);
        cout << "Vulkan pipeline destroyed" << endl;
        device.destroyDescriptorSetLayout(layout, nullptr);
        cout << "Vulkan layout destroyed" << endl;
    }
    catch(const exception& err) {
        cerr << "[ERROR] " << err.what() << endl;
        return 1;
    }
}


To fill buffer data, I need the model-view-projection (MVP) matrix (in OpenGL terms). To calculate one, I use OpenGL Mathematics (GLM) library. Using it is even easier than writing matrices by hand on a piece of paper! Snippet for creating MVP matrix is presented below.
// Calculate matrix (like in OpenGL)
const auto projection = ::glm::perspective(glm::radians(45.0f), 1.0f, 0.1f, 100.0f);
const auto look_at = ::glm::lookAt(
    ::glm::vec3(0, 3, 10), // Camera is at (0,3,10), in World Space
    ::glm::vec3(0, 0, 0),  // and looks at the origin
    ::glm::vec3(0, -1, 0)  // Head is up (set to 0,-1,0 to look upside-down)
);
const auto model = ::glm::mat4(1.0f);
const auto matrix = projection * look_at * model;
cout << "Matrix calculated" << endl;

Creating and filling buffer requires a few steps. First, the Vulkan buffer object (vk::Buffer) has to be created. Then, GPU memory has to be allocated. Handle to GPU memory will be stored in an object of type vk::DeviceMemory. After GPU memory has been allocated, data can be copied to it just like it has been copied between two RAM locations. But allocating GPU memory in Vulkan requires two more steps. First, one has to find an index of memory type, which is visible from CPU and can be mapped. Without that, we could not copy data to it. The second step is to let Vulkan calculate GPU's memory requirements for the created buffer. Full source code is presented below.
#include <iostream>
#include <vector>
#include <algorithm>

#define GLM_FORCE_RADIANS
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>

#include <madvk/instance.h>
#include <madvk/device.h>


using namespace std;
using namespace mad::vk;

int main(int argc, char* argv[]) {
    try {
        auto instance = Instance("vulkan playground 01");
        cout << "Vulkan instance created" << endl;
        auto device = instance.createDevice(::vk::QueueFlagBits::eGraphics);
        cout << "Vulkan device created" << endl;

        // Only one binding - uniform buffer for projection matrix
        const auto layout_binding = ::vk::DescriptorSetLayoutBinding()
            .descriptorType(::vk::DescriptorType::eUniformBuffer)
            .descriptorCount(1)
            .stageFlags(::vk::ShaderStageFlagBits::eVertex);
        const auto descriptor_layout = ::vk::DescriptorSetLayoutCreateInfo()
            .bindingCount(1)
            .pBindings(&layout_binding);
        auto layout = make_raii(
            device.createDescriptorSetLayout(descriptor_layout, nullptr),
            [&device](auto& o) {device.destroyDescriptorSetLayout(o, nullptr); });
        cout << "Vulkan layout created" << endl;

        const auto pipeline_layout = ::vk::PipelineLayoutCreateInfo()
            .pSetLayouts(&layout);
        auto pipeline = make_raii(
            device.createPipelineLayout(pipeline_layout, nullptr),
            [&device](auto& o) {device.destroyPipelineLayout(o, nullptr); });
        cout << "Vulkan pipeline created" << endl;

        // Calculate matrix (like in OpenGL)
        const auto projection = ::glm::perspective(glm::radians(45.0f), 1.0f, 0.1f, 100.0f);
        const auto look_at = ::glm::lookAt(
            ::glm::vec3(0, 3, 10), // Camera is at (0,3,10), in World Space
            ::glm::vec3(0, 0, 0),  // and looks at the origin
            ::glm::vec3(0, -1, 0)  // Head is up (set to 0,-1,0 to look upside-down)
        );
        const auto model = ::glm::mat4(1.0f);
        const auto matrix = projection * look_at * model;
        cout << "Matrix calculated" << endl;

        // Create uniform buffer object for matrix
        const auto buffer_info = ::vk::BufferCreateInfo()
            .usage(::vk::BufferUsageFlagBits::eUniformBuffer)
            .sharingMode(::vk::SharingMode::eExclusive)
            .size(sizeof(matrix));
        auto buff = make_raii(
            device.createBuffer(buffer_info, nullptr),
            [&device](auto& o) { device.destroyBuffer(o, nullptr); });
        cout << "Buffer created" << endl;

        // Find memory block index, which is visible from CPU and can be mapped
        const auto memory_properties = device.getPhysicalDevice().getMemoryProperties();
        const auto mem_types_begin = memory_properties.memoryTypes();
        const auto mem_types_end = mem_types_begin + memory_properties.memoryTypeCount();
        const uint32_t mem_idx = static_cast<uint32_t>(find_if(
            mem_types_begin, mem_types_end,
            [](const auto& mt) {
            return mt.propertyFlags() & ::vk::MemoryPropertyFlagBits::eHostVisible;
        }) - mem_types_begin);
        if(mem_idx >= memory_properties.memoryTypeCount())
            throw runtime_error("Unable to find memory block visible from CPU");

        // Obtain buffer memory requirements on GPU
        const auto memory_requirements = device.getBufferMemoryRequirements(buff);
        cout << "Buffer memory requirements obtained" << endl;

        // Allocate memory on GPU, which can be mapped to RAM.
        const auto memory_info = ::vk::MemoryAllocateInfo()
            .allocationSize(memory_requirements.size())
            .memoryTypeIndex(mem_idx);
        auto memory = make_raii(
            device.allocateMemory(memory_info, nullptr),
            [&device](auto& o) { device.freeMemory(o, nullptr); });
        cout << "Memory on GPU allocated" << endl;

        // Copy data from CPU to GPU
        auto data = reinterpret_cast<uint8_t*>(
            device.mapMemory(memory, 0, memory_requirements.size(), ::vk::MemoryMapFlags()));
        auto begin = reinterpret_cast<const uint8_t*>(&matrix);
        copy(begin, begin + sizeof(matrix), data);
        device.unmapMemory(memory);
        cout << "Data copied" << endl;

        // Bind buffer to pipeline
        device.bindBufferMemory(buff, memory, 0);
        cout << "Buffer bound" << endl;
    }
    catch(const exception& err) {
        cerr << "[ERROR] " << err.what() << endl;
        return 1;
    }
}

One more note. As a lazy person, I have created template function mad::vk::make_raii, which creates RAII wrapper around an object passed as a first parameter. The second parameter of this function is a functor, which will be called in RAII wrapper destructor. In examples, I have used lambdas.