やっていくVulkan入門

4-2. スワップチェーン

サーフェスが取得出来たら、次は「スワップチェーン」というオブジェクトを作成する必要があります。

これは何なのかというと、少し説明が難しいオブジェクトなのですが、一言で言えば「画面に表示されようとしている画像の連なり」を表すオブジェクトです。

コンピュータのアニメーションというのは高速で絵が切り替わる、いわゆる「パラパラ漫画」の仕組みで実現されているというのは皆さんご存じですね。このパラパラ漫画の1コマ1コマは一般に「フレーム」と呼ばれます。ゲームをやっている人は良く聞くかも知れません。

そして(今どきの)一般的なコンピュータは、アニメーションを描画・表示する際に「描いている途中」が見えないようにするため、2枚以上のキャンバスを用意して、1枚を使って現在のフレームを表示させている裏で別の1枚に次のフレームを描画する、という仕組みを採用しています。

この「2枚以上のキャンバス」を管理し、そしてサーフェスと連携して表示するところまでやってくれるのがスワップチェーンというオブジェクトです。

Vulkanにおける表示処理はこのスワップチェーンを介して行うことになります。サーフェスを直接いじることはありません。


準備作業

さっそくスワップチェーンを作成していきたいところですが、下準備が必要です。スワップチェーンもサーフェスと同様に拡張機能なので、機能の有効化をする必要があります。前回までのコードに処理を追加していきます。

まずはサーフェス作成処理の後の部分に、物理デバイスの選定~論理デバイスの作成までの処理を追加してください。復習ですね。

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

問題なく動けばOKです。

さて、スワップチェーンは論理デバイスのcreateSwapchainKHRメソッドで作成できるのですが、このままだと拡張機能が有効化されていないためスワップチェーンを作成しようとしてもエラーで落ちます。

機能の有効化のため、これからここに2か所ほど手を加えます。


デバイスによるスワップチェーンのサポートの確認

まず、物理デバイスの選択処理に少し条件を加えます。処理を以下のように書き換えてください。

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

    // 処理を追加
    std::vector<vk::ExtensionProperties> extProps = physicalDevices[i].enumerateDeviceExtensionProperties();
    bool supportsSwapchainExtension = false;

    for (size_t j = 0; j < extProps.size(); j++) {
        if (std::string_view(extProps[j].extensionName.data()) == VK_KHR_SWAPCHAIN_EXTENSION_NAME) {
            supportsSwapchainExtension = true;
            break;
        }
    }

    // 条件を追加
    if (existsGraphicsQueue && supportsSwapchainExtension) {
        physicalDevice = physicalDevices[i];
        existsSuitablePhysicalDevice = true;
        break;
    }
}

これは何をしているかというと、enumerateDeviceExtensionPropertiesメソッドでその物理デバイスがサポートしている拡張機能の一覧を取得しています。その中にスワップチェーンの拡張機能の名前が含まれていなければそのデバイスは使わない、という風にしています。

先ほども述べたようにスワップチェーンは標準の機能ではなく拡張機能であり、全ての物理デバイスがスワップチェーンの機能をサポートしているわけではありません。 つまり「あっちのGPUではスワップチェーンが使えるけれどこっちのGPUでは使えない」ということがあり得ます。なので対応した物理デバイス(GPU)を選ぶ必要があったんですね。

VK_KHR_SWAPCHAIN_EXTENSION_NAMEというのは、拡張機能の名前を表す定数です。中身としては文字列"VK_KHR_swapchain"です。名前の文字列をそのまま書いても良いですが、上記のように書いた方がタイプミスしたときにコンパイラがエラーを出してくれて安心です。


スワップチェーンの機能の有効化

次は論理デバイスの作成部を書き換えます。

再三言うようにスワップチェーンは拡張機能なので、機能を有効化する必要があります。サーフェスのときと同じですね。しかし、ひとつだけサーフェスのときと違う点があります。それはスワップチェーンは「デバイスレベル」の拡張機能だということです。

実は拡張機能には、インスタンス単位でオン・オフするものとデバイス単位でオン・オフするものがあります。 サーフェスはインスタンスレベルの拡張機能なのでインスタンス作成時に有効化しましたが、スワップチェーンはデバイス単位の拡張機能なので、論理デバイス作成時に有効化する必要があります。

論理デバイス作成時のvk::DeviceCreateInfo構造体の初期化処理に、以下の内容を追加してください。

auto devRequiredExtensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME };

devCreateInfo.enabledExtensionCount = devRequiredExtensions.size();
devCreateInfo.ppEnabledExtensionNames = devRequiredExtensions.begin();

上記の記述を追加し、エラーなく実行出来たら成功です。これでスワップチェーンの機能を有効化した論理デバイスが得られました。


スワップチェーンの作成

ではようやくスワップチェーンの作成です。論理デバイス作成後の処理に以下のコードを追加してください。

vk::SurfaceCapabilitiesKHR surfaceCapabilities = physicalDevice.getSurfaceCapabilitiesKHR(surface.get());
std::vector<vk::SurfaceFormatKHR> surfaceFormats = physicalDevice.getSurfaceFormatsKHR(surface.get());
std::vector<vk::PresentModeKHR> surfacePresentModes = physicalDevice.getSurfacePresentModesKHR(surface.get());

vk::SurfaceFormatKHR swapchainFormat = surfaceFormats[0];
vk::PresentModeKHR swapchainPresentMode = surfacePresentModes[0];

vk::SwapchainCreateInfoKHR swapchainCreateInfo;
swapchainCreateInfo.surface = surface.get();
swapchainCreateInfo.minImageCount = surfaceCapabilities.minImageCount + 1;
swapchainCreateInfo.imageFormat = swapchainFormat.format;
swapchainCreateInfo.imageColorSpace = swapchainFormat.colorSpace;
swapchainCreateInfo.imageExtent = surfaceCapabilities.currentExtent;
swapchainCreateInfo.imageArrayLayers = 1;
swapchainCreateInfo.imageUsage = vk::ImageUsageFlagBits::eColorAttachment;
swapchainCreateInfo.imageSharingMode = vk::SharingMode::eExclusive;
swapchainCreateInfo.preTransform = surfaceCapabilities.currentTransform;
swapchainCreateInfo.presentMode = swapchainPresentMode;
swapchainCreateInfo.clipped = VK_TRUE;

vk::UniqueSwapchainKHR swapchain = device->createSwapchainKHRUnique(swapchainCreateInfo);

結構面倒ですね。初期化用構造体に情報を指定する→論理デバイスのメソッドで作成、という流れは見慣れたものですが、初期化用構造体の中身を決めるところが少しややこしいです。

各メンバについて解説しましょう。

surface には画像の表示先となるサーフェスを指定します。これは分かりやすいですね。

swapchainCreateInfo.surface = surface.get();

この後が問題です。

imageFormatimageColorSpaceなどには、スワップチェーンが取り扱う画像の形式などを指定します。しかしこれらに指定できる値はサーフェスとデバイスの事情によって制限されるものであり、自由に決めることができるものではないのです。そのためきっちりと情報を集める必要があります。

vk::SurfaceCapabilitiesKHR surfaceCapabilities = physicalDevice.getSurfaceCapabilitiesKHR(surface.get());
std::vector<vk::SurfaceFormatKHR> surfaceFormats = physicalDevice.getSurfaceFormatsKHR(surface.get());
std::vector<vk::PresentModeKHR> surfacePresentModes = physicalDevice.getSurfacePresentModesKHR(surface.get());

新たに3つのAPIが出てきました。 この3つのAPIは、「サーフェスの情報」および「物理デバイスが対象のサーフェスを扱う能力」の情報を取得するAPIです。ここで手に入れた情報を用いて種々の値を決めます。

まずは minImageCount から。

swapchainCreateInfo.minImageCount = surfaceCapabilities.minImageCount + 1;

minImageCountはスワップチェーンが扱うイメージの数です。上で「2枚以上のキャンバスを用いて」と説明しましたが、これがその数ですね。getSurfaceCapabilitiesKHRで得られたminImageCount(最小値)とmaxImageCount(最大値)の間の数なら何枚でも問題ないのですが、vulkan-tutorials.comの記述に従って最小値+1を指定しています。

次は imageFormatimageColorSpace です。

std::vector<vk::SurfaceFormatKHR> surfaceFormats = physicalDevice.getSurfaceFormatsKHR(surface.get());

// (中略)

vk::SurfaceFormatKHR swapchainFormat = surfaceFormats[0];

// (中略)

swapchainCreateInfo.imageFormat = swapchainFormat.format;
swapchainCreateInfo.imageColorSpace = swapchainFormat.colorSpace;

imageFormatimageColorSpaceはスワップチェーンの画像フォーマットを表します。ここに指定する値は、必ずgetSurfaceFormatsKHRが返した配列に含まれる組み合わせでなければなりません。ここでは簡単のため、配列の一番最初のものを採用しています。

なお、ここで選択した値は後で使うので、変数swapchainFormatに保持しています。

次は imageExtent

swapchainCreateInfo.imageExtent = surfaceCapabilities.currentExtent;

imageExtentはスワップチェーンの画像サイズを表します。getSurfaceCapabilitiesKHRで得られたminImageExtent(最小値)とmaxImageExtent(最大値)の間でなければなりません。currentExtentで現在のサイズが得られるため、それを指定しています。

このシリーズではウィンドウを640×480で初期化しているため、currentExtentの中身はほぼ間違いなく640×480になっていることでしょう。

次は preTransform

swapchainCreateInfo.preTransform = surfaceCapabilities.currentTransform;

preTransformは表示時の画面反転・画面回転などのオプションを表します。これもgetSurfaceCapabilitiesKHRの戻り値に依存します。supportedTransformsでサポートされている中に含まれるものでなければなりません。currentTransformを設定すれば現在の画面設定が反映されるので無難です。

次は presentMode

std::vector<vk::PresentModeKHR> surfacePresentModes = physicalDevice.getSurfacePresentModesKHR(surface.get());

// (中略)

vk::PresentModeKHR swapchainPresentMode = surfacePresentModes[0];

// (中略)

swapchainCreateInfo.presentMode = swapchainPresentMode;

presentModeは表示処理のモードを示すものです。ここまで良く読んだ方は大体お察しかもしれませんが、getSurfacePresentModesKHRの戻り値の配列に含まれる値でないといけません。ここでは簡単のため、最初の要素を参照しています。

最後、この4つについては固定で構いません。

swapchainCreateInfo.imageArrayLayers = 1;
swapchainCreateInfo.imageUsage = vk::ImageUsageFlagBits::eColorAttachment;
swapchainCreateInfo.imageSharingMode = vk::SharingMode::eExclusive;
swapchainCreateInfo.clipped = VK_TRUE;

これについてはここでは詳しい説明は避けます。気になる人は公式ドキュメントを参照してください。


これにてようやくスワップチェーンが作成できました。ここまでのコードを実行してみましょう。

問題なく動いた方もいるかもしれませんが、環境によってはエラーが出る可能性があります。なぜでしょう。

デバイスとサーフェスの事情が問題になってくるという話は上で触れましたね。デバイスとサーフェスの相性次第では、getSurfacePresentModesKHRgetSurfaceFormatsKHR戻り値が空ということがあり得ます。この場合は使えるモードが1つもないということになり、エラーを起こして死んでしまいます。

物理デバイスを選択する部分の処理で、デバイスがサーフェスを間違いなくサポートしていることを確かめましょう。

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

    std::vector<vk::ExtensionProperties> extProps = physicalDevices[i].enumerateDeviceExtensionProperties();
    bool supportsSwapchainExtension = false;

    for (size_t j = 0; j < extProps.size(); j++) {
        if (std::string_view(extProps[j].extensionName.data()) == std::string(VK_KHR_SWAPCHAIN_EXTENSION_NAME)) {
            supportsSwapchainExtension = true;
            break;
        }
    }

    bool supportsSurface =
        !physicalDevices[i].getSurfaceFormatsKHR(surface.get()).empty() ||
        !physicalDevices[i].getSurfacePresentModesKHR(surface.get()).empty();

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

これでサーフェスと相性の悪いGPUを事前に弾くことができました。


今回はスワップチェーンを作成しました。次回はスワップチェーンのイメージへレンダリングを行います。3章までの内容の復習と応用になります。

この節のコード
#include <iostream>
#include <vulkan/vulkan.hpp>
#include <GLFW/glfw3.h>

const uint32_t screenWidth = 640;
const uint32_t screenHeight = 480;

int main() {
    if (!glfwInit())
        return -1;

    uint32_t requiredExtensionsCount;
    const char** requiredExtensions = glfwGetRequiredInstanceExtensions(&requiredExtensionsCount);

    vk::InstanceCreateInfo createInfo;
    createInfo.enabledExtensionCount = requiredExtensionsCount;
    createInfo.ppEnabledExtensionNames = requiredExtensions;

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

    glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
    GLFWwindow* window;
    window = glfwCreateWindow(screenWidth, screenHeight, "GLFW Test Window", NULL, NULL);
    if (!window) {
        const char* err;
        glfwGetError(&err);
        std::cout << err << std::endl;
        glfwTerminate();
        return -1;
    }

    VkSurfaceKHR c_surface;
    auto result = glfwCreateWindowSurface(instance.get(), window, nullptr, &c_surface);
    if (result != VK_SUCCESS) {
        const char* err;
        glfwGetError(&err);
        std::cout << err << std::endl;
        glfwTerminate();
        return -1;
    }
    vk::UniqueSurfaceKHR surface{ c_surface, instance.get() };

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

        std::vector<vk::ExtensionProperties> extProps = physicalDevices[i].enumerateDeviceExtensionProperties();
        bool supportsSwapchainExtension = false;

        for (size_t j = 0; j < extProps.size(); j++) {
            if (std::string_view(extProps[j].extensionName.data()) == VK_KHR_SWAPCHAIN_EXTENSION_NAME) {
                supportsSwapchainExtension = true;
                break;
            }
        }

        bool supportsSurface =
            !physicalDevices[i].getSurfaceFormatsKHR(surface.get()).empty() ||
            !physicalDevices[i].getSurfacePresentModesKHR(surface.get()).empty();

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

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

    vk::DeviceCreateInfo devCreateInfo;

    auto devRequiredExtensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME };

    devCreateInfo.enabledExtensionCount = devRequiredExtensions.size();
    devCreateInfo.ppEnabledExtensionNames = devRequiredExtensions.begin();

    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::SurfaceCapabilitiesKHR surfaceCapabilities = physicalDevice.getSurfaceCapabilitiesKHR(surface.get());
    std::vector<vk::SurfaceFormatKHR> surfaceFormats = physicalDevice.getSurfaceFormatsKHR(surface.get());
    std::vector<vk::PresentModeKHR> surfacePresentModes = physicalDevice.getSurfacePresentModesKHR(surface.get());

    vk::SurfaceFormatKHR swapchainFormat = surfaceFormats[0];
    vk::PresentModeKHR swapchainPresentMode = surfacePresentModes[0];

    vk::SwapchainCreateInfoKHR swapchainCreateInfo;
    swapchainCreateInfo.surface = surface.get();
    swapchainCreateInfo.minImageCount = surfaceCapabilities.minImageCount + 1;
    swapchainCreateInfo.imageFormat = swapchainFormat.format;
    swapchainCreateInfo.imageColorSpace = swapchainFormat.colorSpace;
    swapchainCreateInfo.imageExtent = surfaceCapabilities.currentExtent;
    swapchainCreateInfo.imageArrayLayers = 1;
    swapchainCreateInfo.imageUsage = vk::ImageUsageFlagBits::eColorAttachment;
    swapchainCreateInfo.imageSharingMode = vk::SharingMode::eExclusive;
    swapchainCreateInfo.preTransform = surfaceCapabilities.currentTransform;
    swapchainCreateInfo.presentMode = swapchainPresentMode;
    swapchainCreateInfo.clipped = VK_TRUE;

    vk::UniqueSwapchainKHR swapchain = device->createSwapchainKHRUnique(swapchainCreateInfo);

    while (!glfwWindowShouldClose(window)) {
        glfwPollEvents();
    }

    glfwTerminate();
    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})

find_package(glfw3 CONFIG REQUIRED)
target_link_libraries(app PRIVATE glfw)