Vulkanの話 第1回

Vulkan始めました 16/07/16 up

タイトル通り、Vulkanを使い始めました。

これからも使っていくかどうかは未定なのですが、今のところDX12より使いそうな予感がします。

何かサンプルができたら解説付きでアップしようと思いますのでよろしくお願いします。

Vulkanが何者なのか?については、このページを見に来ている方には解説不要だと思います。

簡単に言えば、OpenGLのDX12です。つまり、Khronos Groupが策定している次世代のLow Level Graphics APIです。

DirectXが11から12へバージョンが上がった際にAPIを大きく変更し、いわゆるコンソールゲーム機のようなLow LevelなAPIになりました。

これと同じことがOpenGLにも起こり、これがVulkanとして新しいAPIとなったわけです。

OpenGL5.0にならなかったのは古い設計のOpenGLを踏襲したくなかったからだとは思いますが、共存する意味もあったのかと思います。

OpenGLはバージョンを重ねても古い設計を踏襲せざるをえず、情報も古いものから新しいものまで錯綜しており、Vulkanという完全に新しい名前になったことは個人的には喜ばしいことです。

しかし残念なことに、APIはいまだにC言語であり、モダンな感じにはなっていません。モダンにしたいなら自分でラッパライブラリを書けばいいじゃない、ということなのでしょう。

やってられるか!

と思っていたのですが、ここで朗報。

PC用GPUベンダーの2大巨頭の一角であるNVIDIA様がVulkan用C++ラッパを作ってくれました!

しかも使い方がわかってくると大変便利。ありがとうNVIDIA様!

というわけで、この記事はNVIDIA様のVulkanC++を使って作成していくことを前提とします。

また、Vulkanの詳しい内容についてはあまり触れません。

この辺りについてはPROJECT ASURAさんなどを参照していただくのが一番早いと思います。

また、Vulkanの根本的な思想はDirectX12とほぼ同じなので、認識はかなり被ります。

当サイトのDirectXの話 第143回以降を参照していただけると理解しやすくなるかもしれません。

まず最初に、LunarGからVulkanSDKをダウンロードしてくるのですが、ここで注意すべきはSDKのバージョンです。

現在(2016.7.16)の段階でSDKのLatestバージョンは1.0.17ですが、これをDLしてもVulkanC++は使用できません。

SDKバージョンは1.0.13を利用しましょう。現状ではこれを使うのが最適です。

SDKのインストールができたらサンプルのビルドと実行ができることを確認しましょう。

次に行うのはGitHubからVulkanC++をダウンロードすることです。

以下のリンクからDLすることが出来ます。

https://github.com/nvpro-pipeline/vkcpp

SDKが1.0.13であれば、DLしてきた中の "vulkan/vk_cpp.hpp" がそのまま使用できます。

SDKが違う場合は VkCppGenerator を使うことで生成することが出来るのですが、これには注意が必要です。

vkcppリポジトリが参照している Vulkan-Docs とSDKのバージョンが一致している必要があります。

Vulkan-Docs はKhronos Groupが管理しているVulkanの仕様書的なものと思われますが、これを元にvk_cpp.hppが生成されます。

このバージョンとSDKのバージョンが違うとビルドエラーとなるので注意してください。

SDK1.0.17を使用したい方はVulkan-Docsも1.0.17のものをDLして VkCppGenerator で生成してください。

まあ、試してないのでうまくいくかわかりませんがね!←生成までは試した。

VulkanC++の実体はこのヘッダファイルのみです。なのでこれをインクルードするだけで使用できます。

では、実際に使用したコードを見ていきましょう。

サンプルですが、GitHubにコミット済みです。

https://github.com/Monsho/VulkanSample

Sample003まですでに作成してありますが、各サンプルのコードは順次解説を加えていきます。

まずはSample001のウィンドウ作成までを見ていきましょう。

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR pCmdLine, int nCmdShow)

{

g_hInstance = hInstance;

InitializeContext();

InitializeWindow();

...

DestroyWindow();

DestroyContext();

return 0;

}

今回解説するのはこの部分だけです。サンプルは画面クリアまでは行っていますが、今回はここまでとします。

まずは InitializeContext() 命令の中身を見ていきましょう。これはVulkanの基本的なオブジェクトの初期化を行っています。

// Vulkanインスタンスを作成

{

vk::ApplicationInfo appInfo;

appInfo.pApplicationName = "VulkanSample";

appInfo.pEngineName = "VulkanSample";

appInfo.apiVersion = VK_API_VERSION_1_0;

// Extension

const char* extensions[] = {

VK_KHR_SURFACE_EXTENSION_NAME,

VK_KHR_WIN32_SURFACE_EXTENSION_NAME, // Windows用Extension

#if defined(_DEBUG)

VK_EXT_DEBUG_REPORT_EXTENSION_NAME, // デバッグレポート用Extension

#endif

};

// インスタンス生成情報

vk::InstanceCreateInfo createInfo;

createInfo.pApplicationInfo = &appInfo;

createInfo.enabledExtensionCount = ARRAYSIZE(extensions);

createInfo.ppEnabledExtensionNames = extensions;

#if defined(_DEBUG)

// デバッグ関連

createInfo.enabledLayerCount = ARRAYSIZE(kDebugLayerNames);

createInfo.ppEnabledLayerNames = kDebugLayerNames;

#endif

// インスタンス生成

g_VkInstance = vk::createInstance(createInfo);

if (!g_VkInstance)

{

return false;

}

}

一番最初に行うのがVulkanインスタンスの作成です。これはDXGIFactoryみたいなものと思っていただければ結構です。

インスタンスを生成する際にはExtensionとしてプラットフォーム固有のものが必要になります。

Windowsで作成する場合は VK_KHR_WIN32_SURFACE_EXTENSION_NAME を指定しましょう。

また、デバッグ時にはデバッグレイヤーを有効にするべきです。

ソースコード中の kDebugLayerNames には現在は1つの文字列だけが設定されています。

"VK_LAYER_LUNARG_standard_validation"

Windows以外のプラットフォームではこの文字列でデバッグレイヤーが使えるようになるかどうかは怪しいです。

アプリの情報とExtensionの情報などを vk::InstanceCreateInfo に登録し、 vk::createInstance() 命令でインスタンスを作成します。

インスタンスを作成後、デバイスの作成を行います。

// 物理デバイス

g_VkPhysicalDevice = g_VkInstance.enumeratePhysicalDevices()[0];

// Vulkan device

uint32_t graphicsQueueIndex = 0;

{

// グラフィクス用のキューを検索する

graphicsQueueIndex = FindQueue(vk::QueueFlagBits::eGraphics);

float queuePriorities[] = { 0.0f };

vk::DeviceQueueCreateInfo queueCreateInfo;

queueCreateInfo.queueFamilyIndex = graphicsQueueIndex;

queueCreateInfo.queueCount = 1;

queueCreateInfo.pQueuePriorities = queuePriorities;

vk::PhysicalDeviceFeatures deviceFeatures = g_VkPhysicalDevice.getFeatures();

const char* enabledExtensions[] = {

VK_KHR_SWAPCHAIN_EXTENSION_NAME,

};

vk::DeviceCreateInfo deviceCreateInfo;

deviceCreateInfo.queueCreateInfoCount = 1;

deviceCreateInfo.pQueueCreateInfos = &queueCreateInfo;

deviceCreateInfo.pEnabledFeatures = &deviceFeatures;

deviceCreateInfo.enabledExtensionCount = (uint32_t)ARRAYSIZE(enabledExtensions);

deviceCreateInfo.ppEnabledExtensionNames = enabledExtensions;

deviceCreateInfo.enabledLayerCount = ARRAYSIZE(kDebugLayerNames);

deviceCreateInfo.ppEnabledLayerNames = kDebugLayerNames;

g_VkDevice = g_VkPhysicalDevice.createDevice(deviceCreateInfo);

}

最初に取得している物理デバイスはGPUそのものです。

複数のGPUが存在しているハードの場合は目的に合ったGPUを選択しましょう。

今回は必ず0番のものを利用するようにしています。

この物理デバイスを元にしてVulkanデバイスを作成します。D3DDeviceと同じようなものと考えていただければよいでしょう。

SLIやCrossFireの場合はどうするのか?という点については調べてないのでわからないです。

まあ、たとえ調べたとしてもそれを実装して試せる環境を持っていないのでサンプルの作りようもないわけですが…

デバイス作成後にデバッグレイヤーでエラーや警告が出た場合のメッセージコールバックを登録したりするのですが、これはソースコードを読んでください。解説は割愛します。

g_VkPipelineCache = g_VkDevice.createPipelineCache(vk::PipelineCacheCreateInfo());

g_VkQueue = g_VkDevice.getQueue(graphicsQueueIndex, 0);

// コマンドプール作成

vk::CommandPoolCreateInfo cmdPoolInfo;

cmdPoolInfo.queueFamilyIndex = graphicsQueueIndex;

cmdPoolInfo.flags = vk::CommandPoolCreateFlagBits::eResetCommandBuffer;

g_VkCmdPool = g_VkDevice.createCommandPool(cmdPoolInfo);

最後にパイプラインキャッシュ、キュー、コマンドプールを作成してVulkanの初期化は一旦終了です。

もちろんこれだけでは何も出来ませんが、とりあえずエラーがないことは確かめておきましょう。

コマンドプールやデバイス作成時に graphicsQueueIndex というものを設定していますが、こちらについては少し解説を行います。

FindQueue() 命令で取得しているこのインデックスはコマンドキューを実行するパイプラインを意味しています。

DirectXの話 第143回でも解説していますが、Vulkanでもグラフィクスを描画するパイプラインと並列に非同期コンピュートとメモリコピーのパイプラインが実行可能です。

このインデックスは物理デバイスから取得できる vk::QueueFamilyProperties の配列のインデックスを指定します。

このプロパティのフラグに指定のパイプラインがあるものが使用できるのですが、通常であればそのチェックだけでOKです。

ただ、サーフェイスによってはサポートされていない場合もあるらしく、その場合はVulkanデバイス生成に失敗する恐れがあります。

まあ、Windows上で実行する分には問題ないはずですが。

さて、Vulkanの初期化の後は普通に CreateWindowEx() 命令でウィンドウを生成するだけです。

ウィンドウが閉じられた場合は初期化時に生成した各種Vulkanオブジェクトを解放する必要があります。

これらはほとんどが destroy~() という命令になっているので、対応する命令を使って順番に解放していきましょう。

デバッグメッセージが表示されるようになっているのであれば、正常終了時にエラーや警告が発生していないか確認しておきましょう。

アプリケーションが巨大になった状態で解放漏れが見つかるととても大変なので、時々解放漏れがないか確認するようにすると泣きを見ずにすみます。

とりあえず第1回めはここまで。

行数的には生のVulkanAPIを叩いてもあまり変わらないと思いますが、各オブジェクト生成はわかりやすくなっていると思います。

例えば、オブジェクト生成時に ~CreateInfo というものが使用されていますが、これらは生APIの場合はsTypeという変数にそのInfoに対応するタイプを指定する必要があります。

このような微妙に面倒くさい部分をvkcppは肩代わりしてくれるので、ちょっとだけ、でも確実に使いやすくなっています。

次回は画面クリアまでやっていきます。

できるだけ早めにテクスチャ描画までは解説したいと思います。