やっていくVulkan入門

2-3. キュー

Vulkanにおけるキューとは、GPUの実行するコマンドを保持する待ち行列です。有り体に言えばGPUの「やることリスト」です。GPUにコマンドを送るときは、このキューにコマンドを詰め込むことになります。

ところで、1つのGPUが持っているキューは1つだけとは限りません。いくつも持っている場合があります。また、それらキューによってサポートしている機能とサポートしていない機能があったりもします。キューにコマンドを送るときは、そのキューが何の機能をサポートしているかを事前に把握しておく必要があります。

ある物理デバイスの持っているキューの情報は getQueueFamilyProperties メソッドで取得できます。メソッド名にある「キューファミリ」というのは、同じ能力を持っているキューをひとまとめにしたものです。1つの物理デバイスには1個以上のキューファミリがあり、1つのキューファミリには1個以上の同等の機能を持ったキューが所属しています。

1つのキューファミリの情報は vk::QueueFamilyProperties 構造体で表されます。 getQueueFamilyPropertiesを呼ぶと、std::vectorに入ったvk::QueueFamilyPropertiesという形で物理デバイスの各キューファミリの情報を返してくれます。

std::vector<vk::QueueFamilyProperties> queueProps =
    physicalDevice.getQueueFamilyProperties();

試しに、ある物理デバイスの全てのキューファミリの情報を表示するプログラムを作ってみましょう。

1-4. プロジェクトの作成(Windows/VisualStudio2019に従って空のプロジェクトから作った方はコンソール画面が出ないと思います。プロジェクトのプロパティ→リンカー→システム→サブシステム→コンソールを選んでコンソールアプリケーションにして下さい。

#include <vulkan/vulkan.hpp>
#include <iostream>
#include <vector>

int main() {
    vk::InstanceCreateInfo createInfo;

    vk::UniqueInstance instance;
    instance = vk::createInstanceUnique(createInfo);

    std::vector<vk::PhysicalDevice> physicalDevices = instance->enumeratePhysicalDevices();

    vk::PhysicalDevice physicalDevice = physicalDevices[0];

    std::vector<vk::QueueFamilyProperties> queueProps = physicalDevice.getQueueFamilyProperties();

    std::cout << "queue family count: " << queueProps.size() << std::endl;
    std::cout << std::endl;
    for (size_t i = 0; i < queueProps.size(); i++) {
        std::cout << "queue family index: " << i << std::endl;
        std::cout << "  queue count: " << queueProps[i].queueCount << std::endl;
        std::cout << "  graphic support: " << (queueProps[i].queueFlags & vk::QueueFlagBits::eGraphics ? "True" : "False") << std::endl;
        std::cout << "  compute support: " << (queueProps[i].queueFlags & vk::QueueFlagBits::eCompute ? "True" : "False") << std::endl;
        std::cout << "  transfer support: " << (queueProps[i].queueFlags & vk::QueueFlagBits::eTransfer ? "True" : "False") << std::endl;
        std::cout << std::endl;
    }

    return 0;
}

どのような結果が出力されたでしょうか。手元の環境(外付けグラボなし・CPU intel core i7)では以下のようになりました。

queue family count: 1

queue family index: 0
  queue count: 1
  graphic support: True
  compute support: True
  transfer support: True

どうやらintel core i7内臓のGPUはキューファミリが1つだけ、キューも1つだけ、唯一のキューファミリは描画と計算とデータ移動を全てサポートということのようです。面白いですね。

2-1. インスタンスと物理デバイスの項において「本来であれば各物理デバイスを吟味する」と書いたのを覚えていますでしょうか。今までの知識を元に「グラフィックスをサポートするキューファミリを最低でも1つ持っている」物理デバイスを選択するようなプログラムを書いてみましょう。どんなGPUだってそれくらいの機能はサポートしているかも知れませんが、1つもGPUが存在しない状況はあり得るかもしれません。

std::vector<vk::PhysicalDevice> physicalDevices = instance->enumeratePhysicalDevices();

vk::PhysicalDevice physicalDevice;
bool existsSuitablePhysicalDevice = false;

for (size_t i = 0; i < physicalDevices.size(); i++) {
    std::vector<vk::QueueFamilyProperties> queueProps = physicalDevices[i].getQueueFamilyProperties();
    bool existsGraphicsQueue = false;

    for (size_t j = 0; j < queueProps.size(); j++) {
        if (queueProps[j].queueFlags & vk::QueueFlagBits::eGraphics) {
            existsGraphicsQueue = true;
            break;
        }
    }

    if (existsGraphicsQueue) {
        physicalDevice = physicalDevices[i];
        existsSuitablePhysicalDevice = true;
        break;
    }
}

if (!existsSuitablePhysicalDevice) {
    std::cerr << "使用可能な物理デバイスがありません。" << std::endl;
    return -1;
}

さて、これで使用に適した物理デバイスを選ぶことが出来るようになりました。グラフィック機能をサポートしたキューを間違いなく持っています。

ではそのキューを取得しましょう。キューは論理デバイス(vk::Device)の getQueue メソッドで取得できます。第1引数がキューファミリのインデックス、第2引数が取得したいキューのキューファミリ内でのインデックスです。

使用したいキューファミリのインデックスを指定するということは、グラフィック機能をサポートしているキューファミリを見つけるだけでなく、そのキューファミリのインデックスも覚えておく必要がありますね。そのようなコードを追加しましょう。

vk::PhysicalDevice physicalDevice;
bool existsSuitablePhysicalDevice = false;
uint32_t graphicsQueueFamilyIndex;

for (size_t i = 0; i < physicalDevices.size(); i++) {
    std::vector<vk::QueueFamilyProperties> queueProps = physicalDevices[i].getQueueFamilyProperties();
    bool existsGraphicsQueue = false;

    for (size_t j = 0; j < queueProps.size(); j++) {
        if (queueProps[j].queueFlags & vk::QueueFlagBits::eGraphics) {
            existsGraphicsQueue = true;
            graphicsQueueFamilyIndex = j;
            break;
        }
    }

    if (existsGraphicsQueue) {
        physicalDevice = physicalDevices[i];
        existsSuitablePhysicalDevice = true;
        break;
    }
}

このgraphicsQueueFamilyIndexを使用してgetQueueを呼びます。取得するキューはキューファミリ内のどのキューでも変わりないので最初のキュー(0番)にします。

vk::Queue graphicsQueue = device->getQueue(graphicsQueueFamilyIndex, 0);

これを実行してみましょう。すると実行時エラーが出たのではないでしょうか。一体なぜエラーが出たのでしょう。

#include <vulkan/vulkan.hpp>
#include <iostream>
#include <vector>

int main() {
    vk::InstanceCreateInfo createInfo;

    vk::UniqueInstance instance;
    instance = vk::createInstanceUnique(createInfo);

    std::vector<vk::PhysicalDevice> physicalDevices = instance->enumeratePhysicalDevices();

    vk::PhysicalDevice physicalDevice;
    bool existsSuitablePhysicalDevice = false;
    uint32_t graphicsQueueFamilyIndex;

    for (size_t i = 0; i < physicalDevices.size(); i++) {
        std::vector<vk::QueueFamilyProperties> queueProps = physicalDevices[i].getQueueFamilyProperties();
        bool existsGraphicsQueue = false;

        for (size_t j = 0; j < queueProps.size(); j++) {
            if (queueProps[j].queueFlags & vk::QueueFlagBits::eGraphics) {
                existsGraphicsQueue = true;
                graphicsQueueFamilyIndex = j;
                break;
            }
        }

        if (existsGraphicsQueue) {
            physicalDevice = physicalDevices[i];
            existsSuitablePhysicalDevice = true;
            break;
        }
    }

    if (!existsSuitablePhysicalDevice) {
        std::cerr << "使用可能な物理デバイスがありません。" << std::endl;
        return -1;
    }

    vk::DeviceCreateInfo devCreateInfo;
    vk::UniqueDevice device = physicalDevice.createDeviceUnique(devCreateInfo);

    vk::Queue graphicsQueue = device->getQueue(graphicsQueueFamilyIndex, 0); // エラー!

    return 0;
}

実はVulkanでは論理デバイスからキューを取得するとき、論理デバイスにあらかじめ「このキューを使うよ」ということを伝えておく必要があります。 論理デバイスの作成時、vk::DeviceCreateInfo構造体に「この論理デバイスを通じてこのキューファミリからいくつのキューを使う」という情報を指定する必要があるのです。その情報は vk::DeviceQueueCreateInfo 構造体の配列で表されます。

vk::DeviceCreateInfo devCreateInfo;

vk::DeviceQueueCreateInfo queueCreateInfo[1];
queueCreateInfo[0].queueFamilyIndex = graphicsQueueFamilyIndex;
queueCreateInfo[0].queueCount = 1;
float queuePriorities[1] = { 1.0f };
queueCreateInfo[0].pQueuePriorities = queuePriorities;

devCreateInfo.pQueueCreateInfos = queueCreateInfo;
devCreateInfo.queueCreateInfoCount = 1;

今のところ欲しいキューは1つだけなので要素数1の配列にします。

queueFamilyIndex はキューの欲しいキューファミリのインデックスを表し、 queueCount はそのキューファミリからいくつのキューが欲しいかを表します。今欲しいのはグラフィック機能をサポートするキューを1つだけです。 pQueuePriorities はキューのタスク実行の優先度を表すfloat値配列を指定します。1.0fを指定しておきます。優先度の値はキューごとに決められるため、この配列はそのキューファミリから欲しいキューの数だけの要素数を持ちます。

vk::DeviceCreateInfopQueueCreateInfosに先ほどのvk::DeviceQueueCreateInfo構造体配列の先頭アドレス、queueCreateInfoCountに配列の要素数を指定します。上記のコードを追加した上で再度実行してみましょう。今度はエラーもなく通ったのではないでしょうか。

これで無事キューを取得することが出来ました。ここにコマンドを詰め込んでいくので、その方法を次回解説します。

なお、vk::Queueはvk::PhysicalDeviceと同じく、後始末の必要はありません。


この節では物理デバイスの選択とキューの取得をやりました。次回はコマンドバッファの作成とキューへの送信をやります。

この節のコード
#include <vulkan/vulkan.hpp>
#include <iostream>
#include <vector>

int main() {
    vk::InstanceCreateInfo createInfo;

    vk::UniqueInstance instance;
    instance = vk::createInstanceUnique(createInfo);

    std::vector<vk::PhysicalDevice> physicalDevices = instance->enumeratePhysicalDevices();

    vk::PhysicalDevice physicalDevice;
    bool existsSuitablePhysicalDevice = false;
    uint32_t graphicsQueueFamilyIndex;

    for (size_t i = 0; i < physicalDevices.size(); i++) {
        std::vector<vk::QueueFamilyProperties> queueProps = physicalDevices[i].getQueueFamilyProperties();
        bool existsGraphicsQueue = false;

        for (size_t j = 0; j < queueProps.size(); j++) {
            if (queueProps[j].queueFlags & vk::QueueFlagBits::eGraphics) {
                existsGraphicsQueue = true;
                graphicsQueueFamilyIndex = j;
                break;
            }
        }

        if (existsGraphicsQueue) {
            physicalDevice = physicalDevices[i];
            existsSuitablePhysicalDevice = true;
            break;
        }
    }

    if (!existsSuitablePhysicalDevice) {
        std::cerr << "使用可能な物理デバイスがありません。" << std::endl;
        return -1;
    }

    vk::DeviceCreateInfo devCreateInfo;

    vk::DeviceQueueCreateInfo queueCreateInfo[1];
    queueCreateInfo[0].queueFamilyIndex = graphicsQueueFamilyIndex;
    queueCreateInfo[0].queueCount = 1;

    devCreateInfo.pQueueCreateInfos = queueCreateInfo;
    devCreateInfo.queueCreateInfoCount = 1;
    vk::UniqueDevice device = physicalDevice.createDeviceUnique(devCreateInfo);

    vk::Queue graphicsQueue = device->getQueue(graphicsQueueFamilyIndex, 0);

    return 0;
}
cmake_minimum_required(VERSION 3.22)

project(vulkan-test)

set(CMAKE_CXX_STANDARD 17)

add_executable(app main.cpp)

find_package(Vulkan REQUIRED)
target_include_directories(app PRIVATE ${Vulkan_INCLUDE_DIRS})
target_link_libraries(app PRIVATE ${Vulkan_LIBRARIES})