この記事は Window surface - Vulkan Tutorial の日本語訳です。
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
メンバ変数を追加します。
VkSurfaceKHR surface;
VkSurfaceKHR
オブジェクトとその使い方はプラットフォームに依存しませんが、作成に関してはウィンドウシステムの詳細に依存するため、その限りではありません。例えば、WindowsではHWND
とHMODULE
を必要とします。そのため、プラットフォーム固有の追加の拡張があり、WindowsではVK_KHR_win32_surface
と呼ばれ、それはglfwGetRequiredInstanceExtensions
からのリストに自動的に含まれています。
Windows上でサーフェスを作成するために、プラットフォーム固有の拡張機能がどう使われるか説明しますが、このチュートリアルで実際に使うことはありません。GLFWのようなライブラリを使っていながら、プラットフォーム固有のコードを使うのは意味がありません。実際には、GLFWはプラットフォームの違いに対処するために、glfwCreateWindowSurface
関数を持っています。それでも、それに頼る前に、舞台裏で何が行われているかを知ることは良いことです。
プラットフォームネイティブの関数にアクセスするために、一番上のインクルード部分を更新する必要があります。
#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
です。これらはウィンドウとプロセスのハンドルです。
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ローダに含まれており、他の拡張機能と違って明示的にロードする必要はありません。
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
の直後に呼び出します。
void initVulkan() {
createInstance();
setupDebugMessenger();
createSurface();
pickPhysicalDevice();
createLogicalDevice();
}
void createSurface() {
}
GLFWの呼び出しは、構造体の代わりにシンプルに引数に渡せば良いので、関数の実装はとてもわかりやすくなります。
void createSurface() {
if (glfwCreateWindowSurface(instance, window, nullptr, &surface) != VK_SUCCESS) {
throw std::runtime_error("failed to create window surface!");
}
}
引数は、VkInstance
、GLFWウィンドウのポインタ、カスタムアロケータ、VkSurfaceKHR
変数へのポインタです。適切なプラットフォームの呼び出しから、VkResult
がシンプルにそのまま返されます。GLFWはサーフェスを破棄する特別な関数を提供しませんが、標準のAPIを使って簡単にそれを行うことができます。
void cleanup() {
...
vkDestroySurfaceKHR(instance, surface, nullptr);
vkDestroyInstance(instance, nullptr);
...
}
インスタンスが破棄されるよりも前に、サーフェスが破棄されるようにします。
Querying for presentation support
Vulkan実装がWSIをサポートしているからといって、システムの全てのデバイスがサポートしているとは限りません。そのため、作成したサーフェスに画像を表示することができるかどうか確認するように、isDeviceSuitable
を拡張する必要があります。描画はキュー固有の機能なので、実際には作成したサーフェスへの描画をサポートしたキューファミリーを探すことが課題となります。
ドローコマンドをサポートしたキューファミリーと、プレゼンテーションをサポートしたキューファミリーが一致しないことは実際にありえます。そのため、QueueFamilyIndices
構造体を変更して、プレゼンテーション用のキューが別になった場合も考慮する必要があります。
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
と同じループに、この関数の呼び出しを追加してください。
VkBool32 presentSupport = false;
vkGetPhysicalDeviceSurfaceSupportKHR(device, i, surface, &presentSupport);
そして、単純にブール値をチェックして、プレゼンテーション用のキューファミリーのインデックスを格納します。
if (presentSupport) {
indices.presentFamily = i;
}
これらは結局、最終的に同じキューファミリーである可能性が非常に高いですが、統一的なアプローチのために、私達はプログラム全体を通して、これらが別々のキューであるものとして扱います。とはいえ、性能向上のため、ドローとプレゼンテーションを同じキューでサポートした物理デバイスを明示的に優先させるロジックを追加することもできます。
Creating the presentation queue
あと一つ残っているのは、論理デバイス作成の手続きを変更して、プレゼンテーション用のキューを作成し、VkQueue
ハンドルを取得することです。ハンドル用のメンバ変数を追加してください。
VkQueue presentQueue;
次に、両方のキューファミリーからキューを作成するために、複数のVkDeviceQueueCreateInfo
構造体を用意する必要があります。これをするための簡潔な方法は、要求されたキューに必要な全てのユニークなキューファミリーのセットを作成することです。
#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
を変更します。
createInfo.queueCreateInfoCount = static_cast<uint32_t>(queueCreateInfos.size());
createInfo.pQueueCreateInfos = queueCreateInfos.data();
もしキューファミリーが同じなら、そのインデックスを一度だけ渡す必要があります。最後に、キューハンドルを取得するための呼び出しを追加します。
vkGetDeviceQueue(device, indices.presentFamily.value(), 0, &presentQueue);
キューファミリーが同じ場合、2つのハンドルは同じ値を持つ可能性が高いです。次の章では、スワップチェインと、それによってどのように画像をサーフェスに描画することができるか見ていきます。