Thursday, 31 March 2016

Vulkan, first steps 03: devices

This post is the third in series of presenting API samples from Lunar's Vulkan SDK recoded to use nVidia's Open-Source Vulkan C++ API. In the first post Vulkan Instance was created. The second post presented Visual Studio project properties, and had little in common with Vulkan but helped organize samples solution. Now it is time to get back to Vulkan!

List of all Vulkan tutorial posts is here.

Second thing to do, in order to use Vulkan, is to create device. Before that, one can enumerate all Vulkan devices, which are present in system running application. The code is shown below.
#include <iostream>
#include <iomanip>
#include <vector>
#include <string>

#include <madvk/instance.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;

        cout << "Vulkan devices:" << endl;
        std::vector<::vk::PhysicalDevice> devices;
        instance.enumeratePhysicalDevices(devices);
        for(const auto& dev : devices) {
            const auto& props = dev.getProperties();
            cout
                << std::showbase << std::internal << std::setfill('0')
                << std::hex
                << "\tID: " << props.deviceID()
                << std::noshowbase << std::right << std::setfill(' ')
                << std::dec
                << "\tname: " << props.deviceName()
                << "\ttype: " << to_string(props.deviceType())
                << "\tdriver version: " << props.driverVersion()
                << "\tapi version: "
                << ((props.apiVersion() >> 22) & 0xfff) << '.' // Major.
                << ((props.apiVersion() >> 12) & 0x3ff) << '.' // Minor.
                << (props.apiVersion() & 0xfff)                // Patch.
                << endl;
        }
    }
    catch(const std::system_error& err) {
        cerr << "[ERROR] " << err.what() << endl;
        return 1;
    }
}

There is difference in creating Vulkan instance between this post and the first one. From this post onward, I will use my RAII wrapper around vk::Instance object and not the bare object itself, to make sure that memory will be freed correctly.

Creating display device object requires two steps:
  1. Enumerating all devices and finding one, which is capable of graphical output.
  2. Creating device object with proper parameters and proper queues.
The first step is almost done in the example above and only minor corrections have to be made. The second one is similar to creating Vulkan instance, but requires vk::PhysicalDevice object from step one. Whole code is presented below. In later examples, vk::Device will be hidden in RAII wrapper.
#include <iostream>
#include <vector>
#include <algorithm>

#include <madvk/instance.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;

        // Get all Vulkan devices
        std::vector<::vk::PhysicalDevice> devices;
        instance.enumeratePhysicalDevices(devices);

        // Find Vulkan GPU
        const ::vk::PhysicalDevice* gpu = nullptr;
        for(const auto& dev : devices) {
            const auto& qprops = dev.getQueueFamilyProperties();
            const auto it = find_if(
                qprops.cbegin(), qprops.cend(),
                [](auto& qpr) { return qpr.queueFlags() & ::vk::QueueFlagBits::eGraphics; });
            if(it != qprops.cend()) {
                gpu = &dev;
                break;
            }
        }
        if(!gpu)
            throw runtime_error("Unable to find GPU");

        // Create device object
        float priorities[] = {1.0f};
        auto queue_info = ::vk::DeviceQueueCreateInfo().queueCount(1).pQueuePriorities(priorities);
        auto device_info = ::vk::DeviceCreateInfo().queueCreateInfoCount(1).pQueueCreateInfos(&queue_info);
        auto device = gpu->createDevice(device_info, nullptr);
        cout << "Vulkan device created" << endl;

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

If one would use Vulkan device for doing GPU calculations, then she or he should use vk::QueueFlagBits::eCompute flag in searching for proper device.

Vulkan, first steps 02: Visual Studio Project Properties

I have decided to create a Visual Studio solution with API samples from Lunar's Vulkan SDK recoded to use nVidia's Open-Source Vulkan C++ API. But for each project in the solution, I have to add: paths for Vulkan SDK, tools and libraries from it. Doing it by hand would be tedious. It is a place, where project properties come in handy! The solution presented below is inspired by "Sharing project properties in Visual C++" post.

List of all Vulkan tutorial posts is here.

In solution's directory, I have created vulkan.props file intended for storing all changes in project properties corresponding to using Vulkan SDK. Setting include directory and the additional link-time library was easy because these are shared among all configurations. But libraries directory is different for Win32 and x64 builds. There I had to use a choose construct with proper condition. The whole file is presented below.
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <ImportGroup Label="PropertySheets" />
  
  <Choose>
    <When Condition="'$(Platform)'!='x64'">
      <PropertyGroup Label="UserMacros">
        <VULKAN_BIN_DIR>$(VULKAN_SDK)\Bin32</VULKAN_BIN_DIR>
      </PropertyGroup>
    </When>
    <Otherwise>
      <PropertyGroup Label="UserMacros">
        <VULKAN_BIN_DIR>$(VULKAN_SDK)\Bin</VULKAN_BIN_DIR>
      </PropertyGroup>
    </Otherwise>
  </Choose>

  <PropertyGroup>
    <IncludePath>$(VULKAN_SDK)\Include;$(IncludePath)</IncludePath>
    <LibraryPath>$(VULKAN_BIN_DIR);$(LibraryPath)</LibraryPath>
  </PropertyGroup>
  <ItemDefinitionGroup>
    <Link>
      <AdditionalDependencies>vulkan-1.lib;%(AdditionalDependencies)</AdditionalDependencies>
    </Link>
  </ItemDefinitionGroup>
  <ItemGroup>
    <BuildMacro Include="VULKAN_BIN_DIR">
      <Value>$(VULKAN_BIN_DIR)</Value>
      <EnvironmentVariable>true</EnvironmentVariable>
    </BuildMacro>
  </ItemGroup>
</Project>


The $(VULKAN_SDK) is a system variable created by Vulkan SDK installer. Now, to reference Vulkan SDK I have only to add vulkan.props property file to each project once. Thanks to the choose construct my property file is working in all project configurations.

Wednesday, 30 March 2016

Vulkan, first steps 01

When I was at university, I was interested in games and algorithms of computer graphics. I have taken lectures on it and OpenGL. My work does not require graphics programming, and skills gained during the lectures are used mainly in photography. When The Khronos Group Inc. released Vulkan, I have decided to try it and refresh my graphics skills. Because I am c++ programmer, I have decided to use nVidia's Open-Source Vulkan C++ API instead of original c API. This post is an introduction to Open-Source Vulkan C++ API. List of all Vulkan tutorial posts is here.


The first thing, which is necessary to use Vulkan, is to create its instance. The instance is a representation of Vulkan itself. It is the main object, which allows us to interact with Vulkan and do something useful. The minimal example of creating an instance is shown below. There is only one custom param: app title Vulkan APP in line 13th. Most of the time, the default parameters from Open-Source Vulkan C++ API are used. There is one exception, which is the API version (set in line 14th). The default value is 0, which on my machine resulted in an error of missing compatible device. After setting the API version to VK_MAKE_VERSION(1, 0, 0), Vulkan instance is successfully created and destroyed.
#include <iostream>
#include <string>

// Use more c++ friendly version of Vulkan cpp
#define VKCPP_ENHANCED_MODE
#include <vulkan/vk_cpp.h>

using namespace std;

int main(int argc, char* argv[]) {
 try {
  auto app_info = vk::ApplicationInfo()
   .pApplicationName("Vulkan APP")
   .apiVersion(VK_MAKE_VERSION(1, 0, 0));
  auto instance_info = vk::InstanceCreateInfo().pApplicationInfo(&app_info);

  auto instance = vk::createInstance(instance_info, nullptr);
  cout << "Vulkan instance created" << endl;

  instance.destroy(nullptr);
  cout << "Vulkan instance destroyed" << endl;
 }
 catch(const std::system_error& err) {
  cerr << "[ERROR] " << err.what() << endl;
  return 1;
 }
}

Unfortunately, vk::Instance is not an RAII wrapper, and the instance has to be freed manually. After deciding on memory allocation policy (nullptr parameters for vk::createInstance and vk::Instance::destroy), one should create RAII wrapper around vk::Instance.