やっていくVulkan入門

4-1. 画面とサーフェス

Vulkanにおいて、表示する先の画面は「サーフェス」というオブジェクトで抽象化されます。ウィンドウやスクリーンなど、「表示する先として使える何か」は全てサーフェスという同じ種類のオブジェクトで表され、統一的に扱うことができます。

このような仕組みになっているため、「どこに表示しようとしているのか」ということをいちいち気にする必要がありません。どんな媒体に何を表示するのであろうと、サーフェスさえ取得すればあとは同じ処理で描画・表示できるのです。便利ですね。

ただし当たり前と言えば当たり前ですが、サーフェスを作成する部分の処理はプラットフォーム依存になります。サーフェスは「表示する先」を表すオブジェクトですが、具体的な「表示する先」が何者なのかはプラットフォーム次第だからです。Windowsであればウィンドウでしょうし、Androidであればアプリ画面などでしょう。

そのため、WindowsであればWindowsの専用のAPIでウィンドウ情報からサーフェスを作成する必要がありますし、AndroidであればAndroid専用のAPIでアプリ情報からサーフェスを作成しなければなりません。クソですね。

// Windows用のサーフェス作成API
VkResult vkCreateWin32SurfaceKHR(
    VkInstance                                  instance,
    const VkWin32SurfaceCreateInfoKHR*          pCreateInfo,
    const VkAllocationCallbacks*                pAllocator,
    VkSurfaceKHR*                               pSurface);
// Android用のサーフェス作成API
VkResult vkCreateAndroidSurfaceKHR(
    VkInstance                                  instance,
    const VkAndroidSurfaceCreateInfoKHR*        pCreateInfo,
    const VkAllocationCallbacks*                pAllocator,
    VkSurfaceKHR*                               pSurface);

(興味のある方はこちらの公式資料をご覧ください。ありとあらゆるプラットフォームに対して専用のAPIが用意されていることが分かり、Vulkanの素晴らしいマルチプラットフォーム性と世にはびこる数多のプラットフォームの混沌が実感できます)

そこで、そうしたプラットフォーム依存な部分を覆い隠してくれる便利な外部ライブラリを使います。こうしたライブラリはいくつかあるのですが、今回は「GLFW」というライブラリを使います(公式サイトはこちら)。ちなみにVulkan公式が配布しているサンプルで使われているのはSDLというライブラリです。ぶっちゃけサーフェスさえとれれば使うライブラリは本当にどうでもいいです。

GLFWのインストール

vcpkgを利用している場合は以下のコマンド一発でインストールできます。

vcpkg install glfw3

CMakeLists.txtに2行ほど追加します。

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) # 追加

これでビルドすればglfwが使えるはずです。

GLFWによるウィンドウの作成

まずはGLFWで初期化・ウィンドウ作成・終了処理をするプログラムを書いてみましょう。 ここはVulkan関係ありません。

#include <iostream>
#include <GLFW/glfw3.h>

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

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

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

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

    glfwTerminate();
    return 0;
}

これでウィンドウが表示できれば成功です。

whileでスリープのない無限ループになっているので、少しPCが熱くなるかもしれません。気になる場合は適当に毎ループ1ms休ませるなどすると良いでしょう。

VulkanとGLFWの連携

ではVulkan部分の処理を書いていきます。これまで書いてきたコードを写しましょう。

まずvulkan.hppのインクルードを追加します。GLFWの前にインクルードすることに注意してください。

#include <iostream>
#include <vulkan/vulkan.hpp> // vulkanのインクルードが先
#include <GLFW/glfw3.h>

以下はダメな例です。

#include <iostream>
#include <GLFW/glfw3.h> // これはダメ!
#include <vulkan/vulkan.hpp>

次にインスタンス作成の処理を、GLFWの初期化処理(glfwInit()の部分)とウィンドウの作成処理(glfwCreateWindow()の部分)の間に挿入します。

if (!glfwInit())
    return -1;

vk::InstanceCreateInfo createInfo;

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

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

これも特にエラーなく実行できればVulkanの初期化も成功です。

サーフェスの作成

では、ウィンドウのサーフェスを作成しましょう。ここで少し準備が必要です。

先に説明したように、Vulkanにおいて「画面に表示する」という機能は拡張機能です。なので拡張機能をオンにしないとサーフェスは作成できません。

2-1. インスタンスと物理デバイスの項で一応ちらっと説明したのですが、インスタンスの初期化処理のところで拡張機能をオンにすることができます。なのでこの部分のコードに手を加えていくことになります。

オンにする必要のある拡張機能の名前もプラットフォーム依存です。クソですね。GLFWはこの辺も上手くやってくれます。神ですね。

glfwGetRequiredInstanceExtensions関数で必要な拡張機能の情報を取得することができます。

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

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

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

インスタンスの初期化処理を以上のように書き換えます。今は機能をオンにしただけなので何も起こらないと思いますが、これがエラーなく実行できれば成功です。

余談ですが、有効にする拡張機能は「拡張機能の名前の文字列(char*型)の配列」と「拡張機能の数」という形式で指定しています。試しにどんな拡張機能をオンにしているのか確認してみましょう。

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

std::cout << "Extensions:" << std::endl;
for (int i = 0; i < requiredExtensionsCount; i++) {
    std::cout << requiredExtensions[i] << std::endl;
}

筆者の環境(Windows10)では以下のような結果になりました。

Extensions:
VK_KHR_surface
VK_KHR_win32_surface

サーフェスの基本機能の有効化、そしてさらにWindowsプラットフォーム専用のサーフェス機能の有効化、といったところでしょうか。サーフェス自体はプラットフォームに依存せず統一的に扱えますが、中身の処理はプラットフォームに依存して動作するはずなので納得ですね。

さて、もう一つ準備が必要です。glfwCreateWindow前に以下のコードを追加してください。これはGLFW特有の事情によるものなので、おまじないだと思ってください。

glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);

これで準備ができました。ウィンドウ作成処理の後に以下のコードを追加しましょう。

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

// 破棄処理
vkDestroySurfaceKHR(instance.get(), surface, nullptr);

これでサーフェスを作成することが出来ました!目的達成です!

これからこのサーフェスに対して様々な処理を行うことで、ウィンドウに三角形を表示することになります。頑張っていきましょう。

Vulkan-Hppへの対応

ところで、VkSurfaceKHRとなっていますね。vk::SurfaceKHRではありません。そう、GLFWが返してくれるのは、Vulkan-Hppに包まれた形式ではなくC言語の生Vulkanの形式なのです。 GLFWはC++のライブラリではなくC言語のライブラリなので仕方ないですね。

C言語形式のVulkanオブジェクトは、キャストやコンストラクタ処理などによって簡単にVulkan-Hppの形式に変換できます。

VkSurfaceKHR c_surface;
VkResult 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;
}

// コンストラクタでVulkan-Hpp(C++)形式に変換
vk::SurfaceKHR surface{ c_surface };
// キャストによる変換も可
// vk::SurfaceKHR surface = (vk::SurfaceKHR)c_surface;

// 破棄処理
instance->destroySurfaceKHR(surface)

これまでさんざん見てきたように、破棄処理は自動化してしまいましょう。vk::UniqueSurfaceKHRがもちろん存在します。

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に変換
vk::UniqueSurfaceKHR surface{ c_surface };

いやー便利になりましたね。

さて、上記のコードを実行しようとするとエラーが出ます。 なぜでしょうか。(時間がない人は正解のコードのところまで読み飛ばして構いません。)

サーフェスはインスタンスに依存して作られるオブジェクトのため、破棄処理にはインスタンスの情報が必要になります。vkDestroySurfaceKHRの第一引数にはインスタンスを渡していました。子供は親が責任をとって殺すわけですね。物騒だな。

実は今まで扱ってきたようなvk::UniqueXXX系のオブジェクトは、本体だけでなく「親」の情報も保持しています。例えばレンダーパスは論理デバイスに依存して作られるオブジェクトなので、vk::UniqueRenderPassはレンダーパスそのものだけでなく元となった論理デバイスも保持しているのです。そのためデストラクタで破棄処理が行えます。

今までは例えば「device->CreateRenderPassUnique(...)」というように、そもそも親のメソッドでUniqueと名のついたスマートポインタを作成していました。今回はどうでしょう。サーフェス作成処理はGLFWのC言語の領域で行われ、本体(子供)だけが返ってきます。それをもとにスマートポインタを作成しても親の情報がないため、実行時エラーになるのです。

具体的には、デストラクタでの破棄処理のときに持っていない親の情報を参照してぬるぽで落ちます。

ということで、「親」の情報をしっかり含めてやると解決します。正解のコードはこちら。

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

これでようやくVulkan-Hppで安全なサーフェスの取得ができました。


今回はサーフェスの作成処理を行いました。次回はスワップチェーンを作成します。

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

    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)