やっていくVulkan入門

2-4. コマンドバッファ

コマンドバッファとは、コマンドをため込んでおくバッファです。2-3. キューにおいて、VulkanでGPUに仕事をさせる際は「コマンドをキューに送る」として説明しましたが、より正確には「コマンドバッファの中にコマンドを記録し、そのコマンドバッファをキューに送る」という手続きをとります。なのでコマンドを送信する際には必ずコマンドバッファが必要になります。

一連の命令を書類にまとめて送りつける、みたいなイメージで考えると分かりやすいかもしれません。

コマンドバッファを作るには、その前段階として「コマンドプール」というまた別のオブジェクトを作る必要があります。コマンドバッファをコマンドの記録に使うオブジェクトとすれば、コマンドプールというのはコマンドを記録するためのメモリ実体のようなものです。コマンドバッファを作る際には必ず必要になります。

コマンドプールは論理デバイス(vk::Device)の createCommandPool メソッド、コマンドバッファは論理デバイス(vk::Device)の allocateCommandBuffers メソッドで作成することができます。コマンドプールの作成が「create」なのに対し、コマンドバッファの作成は「allocate」であるあたりから「コマンドバッファの記録能力は既に用意したコマンドプールから割り当てる」という気持ちが読み取れますね。

まずはコマンドプールを作成します。

vk::CommandPoolCreateInfo cmdPoolCreateInfo;
cmdPoolCreateInfo.queueFamilyIndex = graphicsQueueFamilyIndex;

vk::UniqueCommandPool cmdPool =
    device->createCommandPoolUnique(cmdPoolCreateInfo);

CommandPoolCreateInfoのqueueFamilyIndexには、後でそのコマンドバッファを送信するときに対象とするキューを指定します。

結局送信するときにも「このコマンドバッファをこのキューに送信する」というのは指定するのですが、そこはあまり深く突っ込まないでください。こんな二度手間が盛り沢山なのがVulkanです。

次はコマンドバッファを作成します。allocateCommandBufferではなくallocateCommandBuffers である名前から分かる通り、実は一度にいくつも作れる仕様になっています。逆に1個しか要らないよという場合でも要素数1の配列で返ってきます。恐らくどうせいくつも作るだろうみたいな想定でこういう仕様になっているものと思われます。

vk::CommandBufferAllocateInfo cmdBufAllocInfo;
cmdBufAllocInfo.commandPool = cmdPool.get();
cmdBufAllocInfo.commandBufferCount = 1;
cmdBufAllocInfo.level = vk::CommandBufferLevel::ePrimary;

std::vector<vk::UniqueCommandBuffer> cmdBufs =
    device->allocateCommandBuffersUnique(cmdBufAllocInfo);

Vulkan-Hppではstd::vectorで返してくれるようになっています。

作るコマンドバッファの数はCommandBufferAllocateInfocommandBufferCount で指定します。commandPoolにはコマンドバッファの作成に使うコマンドプールを指定します。このコードではUniqueCommandPoolを使っているので.get()を呼び出して生のCommandPoolを取得しています。

コマンドバッファが無事作成出来たら、今度はコマンドバッファにコマンドを記録してキューに送信してみましょう。といっても、今回やりたいのは「どうすればコマンドを記録・送信できるか」であり具体的にどんなコマンドが使えるのかはこの章の主題ではないので、とりあえず空のコマンドバッファを送信することにします。

コマンドの記録は次のようにして行います。

vk::CommandBufferBeginInfo cmdBeginInfo;
cmdBufs[0]->begin(cmdBeginInfo);

// コマンドを記録

cmdBufs[0]->end();

beginend の間で描画だのなんだの色々なコマンドを記録します。分かりやすいですね。

こうしてコマンドの記録が終わったら今度はそれをキューに送信しましょう。

vk::CommandBuffer submitCmdBuf[1] = { cmdBufs[0].get() };
vk::SubmitInfo submitInfo;
submitInfo.commandBufferCount = 1;
submitInfo.pCommandBuffers = submitCmdBuf;

graphicsQueue.submit({ submitInfo }, nullptr);

vk::SubmitInfo 構造体にコマンドバッファの送信に関わる情報を指定します。commandBufferCountには送信するコマンドバッファの数、pCommandBuffersには送信するコマンドバッファの配列へのポインタを指定します。

送信はvk::Queuesubmit メソッドで行います。第1引数には初期化子リストの形でvk::SubmitInfoを指定します。このコードでは一つしか指定していませんが、複数飛ばすことができます。

第2引数は現在nullptrにしていますが、本来はフェンスというものを指定することができます。詳細についてはまた今度にします。

こうしてsubmitでGPUに命令を送ることが出来るのですが、あくまで「送る」だけであることには留意が必要です。どういうことかというと、キューに仕事を詰め込むだけであるため、submitから処理が返ってきた段階で送った命令が完了されているとは限らないのです。

実用的なプログラムを作るためには、「依頼した処理が終わるまで待つ」方法を知る必要があります。これに必要なのが「セマフォ」や先ほど名前だけ出した「フェンス」という機構です。これらの詳細は別の章で書きたいと思います。とりあえずここでは、単純に「キューが空になるまで待つ」方法を書きます。

graphicsQueue.waitIdle();

キューの waitIdle メソッドを呼ぶと、その段階でキューに入っているコマンドが全て実行完了してキューが空になるまで待つことができます。分かりやすいですね。セマフォなどを使うと送った個別のコマンドに関する待ち処理などが出来るのですが、それに関する解説は別の記事とします。(補講2. セマフォとフェンスによる同期処理に詳述しました。)


この節ではコマンドバッファの作成とコマンドの記録、送信についてやりました。 2章の内容は以上で終わりです。これでGPUにコマンドを送信する方法を覚えたことになります。

次章は三角形の描画に入ります。

この節のコード
#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;

    float queuePriorities[1] = { 1.0 };

    queueCreateInfo[0].pQueuePriorities = queuePriorities;

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

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

    vk::CommandPoolCreateInfo cmdPoolCreateInfo;
    cmdPoolCreateInfo.queueFamilyIndex = graphicsQueueFamilyIndex;
    vk::UniqueCommandPool cmdPool = device->createCommandPoolUnique(cmdPoolCreateInfo);

    vk::CommandBufferAllocateInfo cmdBufAllocInfo;
    cmdBufAllocInfo.commandPool = cmdPool.get();
    cmdBufAllocInfo.commandBufferCount = 1;
    cmdBufAllocInfo.level = vk::CommandBufferLevel::ePrimary;
    std::vector<vk::UniqueCommandBuffer> cmdBufs =
        device->allocateCommandBuffersUnique(cmdBufAllocInfo);

    vk::CommandBufferBeginInfo cmdBeginInfo;
    cmdBufs[0]->begin(cmdBeginInfo);

    // コマンドを記録

    cmdBufs[0]->end();

    vk::CommandBuffer submitCmdBuf[1] = { cmdBufs[0].get() };
    vk::SubmitInfo submitInfo;
    submitInfo.commandBufferCount = 1;
    submitInfo.pCommandBuffers = submitCmdBuf;

    graphicsQueue.submit({ submitInfo }, nullptr);

    graphicsQueue.waitIdle();

    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})