Vulkan Tutorial (Drawing a triangle/Presentation/Window surface) 日本語訳

この記事は [Window surface - Vulkan Tutorial](https://vulkan-tutorial.com/Drawing_a_triangle/Presentation/Window_surface) の日本語訳です。

# Window surface

Vulkanはプラットフォームに依存しないAPIなので、ウィンドウシステムと直接やりとりすることはできません。結果を画面に表示するために、Vulkanとウィンドウシステムの間の接続を確立するには、WSI(Window System Integration)拡張機能を使う必要があります。この章では、最初の一つである、`VK_KHR_surface`について説明します。これは、レンダリングした画像を表示するサーフェスの抽象型である、`VkSurfaceKHR`オブジェクトを使えるようにします。私達のプログラムでのサーフェスは、GLFWで既に開いているウィンドウに裏打ちされています。

`VK_KHR_surface`拡張はインスタンスレベルの拡張機能で、`glfwGetRequiredInstanceExtensions`で返されるリストに含まれているため、私達は既に有効にしています。このリストには、次の数章で使用する、他のWSI拡張も含まれています。

物理デバイスの選択に影響するので、ウィンドウサーフェスはインスタンス作成の直後に作成する必要があります。私達がこれを後回しにしたのは、ウィンドウサーフェスはレンダーターゲットと画面表示という大きなトピックの一部であり、説明していると基本的なセットアップがとっ散らかることになるからです。またVulkanでは、オフスクリーンレンダリングするだけであれば、ウィンドウサーフェスは完全にオプショナルなコンポーネントであることも注意が必要です。Vulkanでは、表示しないウィンドウを作成するといったようなハックは必要ありません。(OpenGLでは必要です)

## Window surface creation

はじめに、デバッグコールバックのすぐ下に`surface`メンバ変数を追加します。

```cpp
VkSurfaceKHR surface;
```

`VkSurfaceKHR`オブジェクトとその使い方はプラットフォームに依存しませんが、作成に関してはウィンドウシステムの詳細に依存するため、その限りではありません。例えば、Windowsでは`HWND`と`HMODULE`を必要とします。そのため、プラットフォーム固有の追加の拡張があり、Windowsでは`VK_KHR_win32_surface`と呼ばれ、それは`glfwGetRequiredInstanceExtensions`からのリストに自動的に含まれています。

Windows上でサーフェスを作成するために、プラットフォーム固有の拡張機能がどう使われるか説明しますが、このチュートリアルで実際に使うことはありません。GLFWのようなライブラリを使っていながら、プラットフォーム固有のコードを使うのは意味がありません。実際には、GLFWはプラットフォームの違いに対処するために、`glfwCreateWindowSurface`関数を持っています。それでも、それに頼る前に、舞台裏で何が行われているかを知ることは良いことです。

プラットフォームネイティブの関数にアクセスするために、一番上のインクルード部分を更新する必要があります。

```cpp
#define VK_USE_PLATFORM_WIN32_KHR
#define GLFW_INCLUDE_VULKAN
#include <GLFW/glfw3.h>
#define GLFW_EXPOSE_NATIVE_WIN32
#include <GLFW/glfw3native.h>
```

ウィンドウサーフェスはVulkanオブジェクトなので、作成するためには`VkWin32SurfaceCreateInfoKHR`構造体を埋める必要があります。2つの重要なパラメータが、`hwnd`と`hinstance`です。これらはウィンドウとプロセスのハンドルです。

```cpp
VkWin32SurfaceCreateInfoKHR createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR;
createInfo.hwnd = glfwGetWin32Window(window);
createInfo.hinstance = GetModuleHandle(nullptr);
```

`glfwGetWin32Window`関数は、GLFWウィンドウオブジェクトから生の`HWND`を取得するために使われます。`GetModuleHandle`を呼び出すと、現在のプロセスの`HINSTANCE`ハンドルを返します。

その後、引数にインスタンス、サーフェス作成の詳細、カスタムアロケータ、サーフェスのハンドルを格納する変数を渡して`vkCreateWin32SurfaceKHR`を呼び出すことにより、サーフェスを作ることができます。これは技術的にはWSI拡張の関数ですが、一般的によく使われているため標準Vulkanローダに含まれており、他の拡張機能と違って明示的にロードする必要はありません。

```cpp
if (vkCreateWin32SurfaceKHR(instance, &createInfo, nullptr, &surface) != VK_SUCCESS) {
    throw std::runtime_error("failed to create window surface!");
}
```

Linuxのような他のプラットフォームでも手順は似ていて、XCB接続とX11で作られたウィンドウを渡して`vkCreateXcbSurfaceKHR`を呼び出します。

`glfwCreateWindowSurface`関数はそれぞれのプラットフォームで異なる実装でこの処理を行います。これを私達のプログラムに組み込みます。`createSurface`関数を追加して、`initVulkan`内のインスタンス作成と`setupDebugMessenger`の直後に呼び出します。

```cpp
void initVulkan() {
    createInstance();
    setupDebugMessenger();
    createSurface();
    pickPhysicalDevice();
    createLogicalDevice();
}

void createSurface() {

}
```

GLFWの呼び出しは、構造体の代わりにシンプルに引数に渡せば良いので、関数の実装はとてもわかりやすくなります。

```cpp
void createSurface() {
    if (glfwCreateWindowSurface(instance, window, nullptr, &surface) != VK_SUCCESS) {
        throw std::runtime_error("failed to create window surface!");
    }
}
```

引数は、[`VkInstance`](https://www.khronos.org/registry/vulkan/specs/1.0/man/html/VkInstance.html)、GLFWウィンドウのポインタ、カスタムアロケータ、`VkSurfaceKHR`変数へのポインタです。適切なプラットフォームの呼び出しから、[`VkResult`](https://www.khronos.org/registry/vulkan/specs/1.0/man/html/VkResult.html)がシンプルにそのまま返されます。GLFWはサーフェスを破棄する特別な関数を提供しませんが、標準のAPIを使って簡単にそれを行うことができます。

```cpp
void cleanup() {
        ...
        vkDestroySurfaceKHR(instance, surface, nullptr);
        vkDestroyInstance(instance, nullptr);
        ...
    }
```

インスタンスが破棄されるよりも前に、サーフェスが破棄されるようにします。

## Querying for presentation support

Vulkan実装がWSIをサポートしているからといって、システムの全てのデバイスがサポートしているとは限りません。そのため、作成したサーフェスに画像を表示することができるかどうか確認するように、`isDeviceSuitable`を拡張する必要があります。描画はキュー固有の機能なので、実際には作成したサーフェスへの描画をサポートしたキューファミリーを探すことが課題となります。

ドローコマンドをサポートしたキューファミリーと、プレゼンテーションをサポートしたキューファミリーが一致しないことは実際にありえます。そのため、`QueueFamilyIndices`構造体を変更して、プレゼンテーション用のキューが別になった場合も考慮する必要があります。

```cpp
struct QueueFamilyIndices {
    std::optional<uint32_t> graphicsFamily;
    std::optional<uint32_t> presentFamily;

    bool isComplete() {
        return graphicsFamily.has_value() && presentFamily.has_value();
    }
};
```

次に、ウィンドウサーフェスへの描画機能を持ったキューファミリーを探すため、`findQueueFamilies`関数を変更します。これをチェックするための関数が`vkGetPhysicalDeviceSurfaceSupportKHR`で、物理デバイス、キューファミリーのインデックス、サーフェスを引数に取ります。`VK_QUEUE_GRAPHICS_BIT`と同じループに、この関数の呼び出しを追加してください。

```cpp
VkBool32 presentSupport = false;
vkGetPhysicalDeviceSurfaceSupportKHR(device, i, surface, &presentSupport);
```

そして、単純にブール値をチェックして、プレゼンテーション用のキューファミリーのインデックスを格納します。

```cpp
if (presentSupport) {
    indices.presentFamily = i;
}
```

これらは結局、最終的に同じキューファミリーである可能性が非常に高いですが、統一的なアプローチのために、私達はプログラム全体を通して、これらが別々のキューであるものとして扱います。とはいえ、性能向上のため、ドローとプレゼンテーションを同じキューでサポートした物理デバイスを明示的に優先させるロジックを追加することもできます。

## Creating the presentation queue

あと一つ残っているのは、論理デバイス作成の手続きを変更して、プレゼンテーション用のキューを作成し、[`VkQueue`](https://www.khronos.org/registry/vulkan/specs/1.0/man/html/VkQueue.html)ハンドルを取得することです。ハンドル用のメンバ変数を追加してください。

```cpp
VkQueue presentQueue;
```

次に、両方のキューファミリーからキューを作成するために、複数の[`VkDeviceQueueCreateInfo`](https://www.khronos.org/registry/vulkan/specs/1.0/man/html/VkDeviceQueueCreateInfo.html)構造体を用意する必要があります。これをするための簡潔な方法は、要求されたキューに必要な全てのユニークなキューファミリーのセットを作成することです。

```cpp
#include <set>

...

QueueFamilyIndices indices = findQueueFamilies(physicalDevice);

std::vector<VkDeviceQueueCreateInfo> queueCreateInfos;
std::set<uint32_t> uniqueQueueFamilies = {indices.graphicsFamily.value(), indices.presentFamily.value()};

float queuePriority = 1.0f;
for (uint32_t queueFamily : uniqueQueueFamilies) {
    VkDeviceQueueCreateInfo queueCreateInfo{};
    queueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
    queueCreateInfo.queueFamilyIndex = queueFamily;
    queueCreateInfo.queueCount = 1;
    queueCreateInfo.pQueuePriorities = &queuePriority;
    queueCreateInfos.push_back(queueCreateInfo);
}
```

そして、vectorを指すように[`VkDeviceCreateInfo`](https://www.khronos.org/registry/vulkan/specs/1.0/man/html/VkDeviceCreateInfo.html)を変更します。

```cpp
createInfo.queueCreateInfoCount = static_cast<uint32_t>(queueCreateInfos.size());
createInfo.pQueueCreateInfos = queueCreateInfos.data();
```

もしキューファミリーが同じなら、そのインデックスを一度だけ渡す必要があります。最後に、キューハンドルを取得するための呼び出しを追加します。

```cpp
vkGetDeviceQueue(device, indices.presentFamily.value(), 0, &presentQueue);
```

キューファミリーが同じ場合、2つのハンドルは同じ値を持つ可能性が高いです。次の章では、スワップチェインと、それによってどのように画像をサーフェスに描画することができるか見ていきます。

[C++ code](https://vulkan-tutorial.com/code/05_window_surface.cpp)

[前の記事](https://qiita.com/shibainuudon/items/6cc88da4263e48cf1c07)
[次の記事](https://qiita.com/shibainuudon/items/abdb69230514fcfc955f)