2026-04-21 05:22:19
諏訪子
gamedev
graphics
vulkan
c++
glsl

【Vulkan】ゲームエンジン 1: 基本的なレンダラーセットアップ

Nintendo Switch向けのNVN及びVulkanプログラミングの完全なドキュメントを、076スタジオのイントラネットに書き終えました。
将来的に新入社員が学ぶ為の資料です。
近いうちにPC向けのVulkanガイドを此のブログで書き直す予定ですが、先ずはゲームエンジンに必要な基本的な内容を高レベルで概説したいと思います。
自作ゲームエンジンを作る事は、多くの人が思っている程怖い物ではありません。
一番難しいのはレンダリングエンジンを動かす事ですが、其処を乗り越えれば後は比較的簡単です。
今回はレンダリングエンジンに焦点を当てます。

詳細は今後のフルチュートリアルで解説します。
此の記事は公式仕様に沿った基本的なレンダラーのセットアップ方法を教えることを目的としています。

ゲームエンジンに必要な物

先ずはゲームエンジンの高レベルな内訳を見てみましょう。
「ゲームエンジン」と言うと、初心者の多くはスクリプトをアタッチできる大きなシーンエディタを想像します。
然し本当の定義は、ゲームを動かす為に必要な機能の集合体です。
優れたゲームエンジンは、意図したゲームデザインに沿った物になります。
UnityやGodotの様に極めて汎用的なエンジンを作る事も、Unreal Engineのように専門特化したエンジンを汎用的に拡張する事も技術的には可能ですが、最も高いパフォーマンスとクオリティを得られるのは、自分が作るゲームの種類に特化したエンジンです。

勿論、一部の部品は将来のプロジェクトで再利用出来るので、毎回ゼロから全部書く必要はありません。

典型的なエンジンは次の様な順序で作られます。

  1. レンダラー(Vulkan、DirectX、Metal、NVN、GNM等)
  2. 基本的なプリミティブの生成(キューブ、カプセル、球等)
  3. デバッグインターフェース(Dear ImGui)
  4. アセットローディング(3Dモデル、2Dテクスチャ、オーディオファイル等)
  5. 物理エンジン
  6. GUIとパーティクルシステム
  7. ゲームプレイコード

プラットフォーム依存のコードはレンダラー開発の段階で済ませてしまいます。
最初に最も違いの大きい部分から実装する為です。
Vulkanはクロスプラットフォームですが、ネイティブサポートされているのはWindows、Linux、BSD、Android、Switch、Switch 2です。
macOSとiOSではMoltenVKという翻訳レイヤーを通して動作します。
DirectXはMicrosoftプラットフォーム専用です。
NVNはNintendoプラットフォーム専用です。
GNMはPlayStationプラットフォーム専用です。
MetalはAppleプラットフォーム専用です。

Vulkanはマルチプラットフォーム対応という点で最も魅力的ですが、歴史的にゲーム会社はDirectXや各ゲーム機固有のAPIを使ってきました。
当時はOpenGLが主流で、高パフォーマンスなゲームにはあんま適していませんでした。
Vulkanは其れに非常に適していますが、登場した頃には既に多くのゲーム会社がUnrealやUnityに依存する様になっており、Vulkanを使うのはチェックボックスをオンにするだけの作業になっていました。

又、ほとんどの開発者は自前で物理エンジンを実装せず、市販のライブラリを使います。
然しあたし達はコンピュータをいじるのが好きなので、自作します。

DirectXやゲーム機APIの方が一般的によく使われているなら、何故DirectXを学ばないのか?と思うかもしれません。
答えは以下の通りです:

  1. Vulkanは抽象化が少ない為、GPUの内部動作についてより深く学べる。
  2. Linuxがターゲットとして成長しており、Windowsのシェアは縮小傾向にあり、ゲーマー達はWindowsゲームがLinuxでも動く事を強く求めている。
  3. VulkanはNintendoやSonyのAPIと命名規則が似ている為、Vulkanで得た知識はDirectXよりも移行し易い。
  4. DirectXの歴史的な人気にも関わらず、DirectX 12の対応を探すよりVulkanのサポートの方が簡単に見つかる。DirectXのサポートの多くは11以前の物で、OpenGLに近い。

原則として、此の大部分は難しくありません。
最も難しいのはレンダラーのセットアップと、コマンドバッファにGPUで処理させる呼び出しを記録する事です。
今は何を言っているかわからないかもしれませんが、公開版のVulkanチュートリアルが出れば明確になります。
出来け詳細に踏み込み過ぎない様にように説明するつもりです。

3Dレンダラーが行う事

画面に三角形を描画する事は、グラフィックスプログラミングの「こんにちは、世界」です。
但し、レンダラーが正常に機能している事を確認する為、毎回これを行う必要があります。

レンダラーのセットアップ方法はAPIごとに異なり、DirectXから来た人は命名規則で混乱するかもしん。
何故かMicrosoftだけがパイプラインの名前付けを他のAPIと違う様にしています。
一部のAPI(特にMetal)は多くの部分を抽象化し、ゲーム機固有のAPIは手動メモリ管理をより重視します。
然し基本的な部分は、殆どのAPIでほぼ同じです。

Vulkanで三角形を描画するには、次の手順を行います:

ステップ1: インスタンスの作成と物理デバイスの選択

此れはAPIによって異なりますが、Vulkanの場合はまずインスタンスを作成します。
ゲーム機固有のAPIでは最初にデバイス+APIをブートストラップし、デバイス選択肢は1つだけです。
DirectXとMetalではインスタンス化をスキップして直接デバイス作成に進みます。

物理デバイスが存在する理由は、Vulkanが同時に複数のGPUを使えるようにしているためです。ただしすべてのGPUが同じではありません。
VRAM容量が大きい物や、レイキャスティングをサポートしていない物等があります。
主な用途は、利用可能なグラフィックスカードを全てループして、最も適した物を選ぶ事です。

ステップ2: 論理デバイスとキューファミリーの作成

此れはVulkan固有の機能です。
Vulkanだけが物理デバイスと論理デバイスを明確に区別しています。
論理デバイスでは、使用したい機能をより具体的に記述します。

キューファミリーもVulkan固有です。
全てのAPIで、レンダーコマンドをキューに入れてGPUで並列処理出来る様にします。
Vulkanでは其れらを別々のファミリーに分割出来、其々特定の操作セットを容易に対応します。
例えば、あるファミリーはグラフィックス、もう一つはコンピュート、もう一つはメモリ転送を担当します。

但し、現代のグラフィックスカードは全て必要なキュー操作を対応しています。

ステップ3: ウィンドウサーフェスとスワップチェーンの作成

此れは最もプラットフォーム依存の部分です。
PCではOS提供のウィンドウAPI(WinAPI、Xlib/XCB、Cocoa、Wayland、HaikuのAPI、Plan9のAPI等)を使ってウィンドウをインスタンス化します。
又はGLFWやSDL等のクロスプラットフォームライブラリを使えば、もう考える必要がなくなります。
チュートリアルシリーズでは自前のウィンドウライブラリを作りますが、此の記事ではGLFWを使います。
スマートフォンやゲームゲーム機では、各プラットフォームSDKのビジュアルインターフェースとレイヤリングシステムを使います。

只ウィンドウを開くだけでは不十分です。
其のウィンドウにグラフィックスを描画する必要があります。
其れがウィンドウサーフェスの役割です。

スワップチェーンはレンダーターゲットの集合です。
現在描画中の画像と画面に表示されている画像を区別し、完成した画像だけを表示する為の物です。
フレームを描画するたびに、スワップチェーンが描画対象の画像を提供します。

ステップ4: イメージビューとダイナミックレンダリング

従来はフレームバッファとレンダーパスを作成する必要がありました。
Vulkan 1.3以降はダイナミックレンダリングにより、事前に定義されたレンダーパスとフレームバッファが不要になり、コマンド記録時に直接レンダリングアタッチメントを指定できる様になりました。

ステップ5: グラフィックスパイプラインのセットアップ

グラフィックスパイプラインは、ビューポートサイズ、デプスバッファ操作、シェーダーモジュール、シザー等、グラフィックスカードの設定可能な状態を記述します。
Vulkanの特徴として、グラフィックスパイプラインのほぼ全ての設定を事前に決めておく必要があります。
シェーダーを変えたり頂点レイアウトを少し変更したりするだけで、パイプライン全体を再作成しなければなりません。
其の為、様々なレンダリング操作の組み合わせを全てカバーするパイプラインオブジェクトを事前に多数作成する事になります。
ビューポートサイズやクリアカラー等の基本的な設定は動的に変更可能です。

ステップ6: コマンドプールとコマンドバッファ

此れが全てのグラフィックスAPIで最も面倒な中心部分です。
全てのレンダーコマンドで、記録開始、グラフィックスパイプラインのバインド、頂点描画、記録終了、キューへのコマンド投入が必要です。
Vulkan 1.3以降は此の操作が簡素化され、レンダリングの開始と終了だけで済む様になりました。
但し他のAPIでは未だ手動制御が必要な場合があります。

ステップ7: メインループ

メインループでは、画像の取得、正しい描画コマンドバッファのサブミット、スワップチェーンへの画像の返却を行ってフレームを描画します。

何故こんなにステップが多いのか?

OdinやRustのプログラミング言語レビューで見た様に、OpenGLは非常に少ないステップで済む事に気づいたかもしん。
此れはOpenGLが1990年代のAPIで、当時はGPUが出来たばかりで殆ど何も出来なかったからです。
GPUが新し過ぎて、当時の殆どのコンピュータには未だ搭載されていませんでした。
Nintendo 64にはRSP(リアリティー・コプロセッサ)という非常に独自性の高いものが搭載されていて、正規開発者でさえ其の仕組みを知る事を許されませんでした。
Nintendo DSはGPUを持たない最後のゲーム機で、代わりに3つのレンダリングエンジン(其のうち1つが3D用)を持っていました。
今コンピュータ、ゲーム機、スマホを買えば、専用か統合かを問わず100% GPUが搭載されています。

OpenGLは年月と共に、よりプログラマブルになった新しいGPUに対応して進化してきました。
然し追加出来る量には限界があり、其処でVulkanはゼロから設計されました。
AppleはOpenGLを完全に非推奨とし、代わりに独自の「Metal」というAPIを作りました。
Nintendo 3DSとWii Uは未だOpenGLに似たAPIを使っていましたが、独自の修正が加えられていました。
然しPlayStation 5とNintendo Switchの登場により、NintendoとSonyはどちらも現代のGPUに適した新しいAPIに移行しました。
そしてMicrosoftだけが、同じ名前で後方互換性を保ちつつ現代的なAPI書き換えを行いました。

Vulkanは難しいのか?

此処までの説明でVulkanはとても難しいと思われたかもしん。
然し実際の所、Vulkanは難しくありません。
只冗長なだけです。
1980年代のCPUが同じ進化過程をたどったと想像して下さい。
当初は手書きのアセンブリ命令数個で完全なプログラムが書け、時代と共にCPUが柔軟になるのに合わせてCが出来あがり、其の後完全な制御の為にC++が登場したでしょう。
勿論そんな事は起きませんでしたが、GPUは正に其の様な進化をしました。
プログラマビリティの欠如からほぼ完全なプログラマビリティへ。
未だ固定された機能がいくつか残っていますが、其れらも孰れプログラマブルになるでしょう。

グラフィックスプログラミングの経験量にもよりますが、最小限のVulkanレンダラーをセットアップするのにかかる時間は1日仕事から丸1ヶ月程度です。
あたしが初めてセットアップしたときは23日かかりました。
2回目は10日、3回目は3日だけでした。
其の後Nintendo Switchに移植するのに更に2週間かかりました。

なので、Vulkanのセットアップに丸2週間費やしても未だ三角形が見えない事に驚かないで下さい。

始めよう

話は十分です。
実際にコードを書いていきましょう。
仕様書は此方にあります:Vulkan仕様書
英語が読めない場合は、GrokやChatGPT等のLLMを使ってページを翻訳して下さい。
仕様書は常に更新される文書なので、此のブログに完全な翻訳を載せても数ヶ月後には古くなってしまう為、載せません。

Vulkanコードを書く最も一般的な方法はC++でC APIを使う事です。
然しあたし達は既にC++で書いているので、C++ APIを使いましょう。
事前にGLFW、GLM、Vulkan SDK、及びテキストエディタとC++20コンパイラをダウンロード・インストールしておいて下さい。

多くの人は最初からコードをクラスに分割して複雑にしがちです。
然しあたしは先ず手続き型でVulkanを実装し、其の後でクラスに分割する事をお勧めします。
初めてVulkanを扱う場合、未だ何をし様としているか分からないので、最初は出来るだけシンプルに保ち、完成してから分割するのがベストです。

完全なソースコードはこちらにあります:Microslop Github

Visual Studioを使っている場合は、#pragma regionを使って部分を分割する事をお勧めします。
そうする事でコードが増えても折りたたんで見易く保てます。例:

#pragma region check
  bool isTrue = false;
#pragma endregion

ウィンドウの作成

始める前に、ウィンドウを作成する必要があります。
此のプロジェクトにはWindows版のGLFWとGLMライブラリを予め含めています。
他のOSの場合は、Linuxディストリビューションのパッケージマネージャ、BSDのポーツツリー、又はmacOSのHomebrewでインストールして下さい。

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

int main() {
  int winWidth = 800;
  int winHeight = 600;
  std::string winName = reinterpret_cast<const char *>(u8"Vulkanレンダー");

  glfwInit();
  glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
  glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);
  GLFWwindow *window = glfwCreateWindow(winWidth, winHeight, winName.c_str(), nullptr, nullptr);

  while (!glfwWindowShouldClose(window)) {
    if (glfwGetKey(window, GLFW_KEY_Q) == GLFW_PRESS) {
      glfwSetWindowShouldClose(window, true);
    }

    glfwPollEvents();
  }

  glfwDestroyWindow(window);
  glfwTerminate();
}

結果

Vulkanコードをまだ1行も書いていない時点で、既にOpenGLコードとの違いが見えます。
特にウィンドウヒントの部分です。
デフォルトではGLFWは指定したバージョンのOpenGLコンテキストを作成します。
Vulkanの場合は、GLFWにコンテキストを作成させない様指示する必要があります。
又、Vulkanではスワップチェーンがリサイズを処理する為、ウィンドウをリサイズ不可にする様GLFWに指示します。

インスタンス

次にVulkanインスタンスを作成します。
インスタンスは1つだけ作成し、ゲームエンジンとGPUドライバの間の接続として機能します。

Vulkan仕様書によると、必要な物は以下の通りです:

typedef struct VkApplicationInfo {
    VkStructureType    sType;
    const void*        pNext;
    const char*        pApplicationName;
    uint32_t           applicationVersion;
    const char*        pEngineName;
    uint32_t           engineVersion;
    uint32_t           apiVersion;
} VkApplicationInfo;

typedef struct VkInstanceCreateInfo {
    VkStructureType             sType;
    const void*                 pNext;
    VkInstanceCreateFlags       flags;
    const VkApplicationInfo*    pApplicationInfo;
    uint32_t                    enabledLayerCount;
    const char* const*          ppEnabledLayerNames;
    uint32_t                    enabledExtensionCount;
    const char* const*          ppEnabledExtensionNames;
} VkInstanceCreateInfo;

最もよく使われるフィールドはsTypepNextです。
pNextは別のCreate Infoを現在の物に渡す方法です。
sTypeはC APIでのみ必要な構造体タイプです。
C++及びRAII APIでは自動的に設定されます。

Create Infoを定義したら、次の様にインスタンスを作成します:

VkResult vkCreateInstance(
    const VkInstanceCreateInfo*                 pCreateInfo,
    const VkAllocationCallbacks*                pAllocator,
    VkInstance*                                 pInstance);

macOSを使っている場合は、VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHRフラグを設定しないと動作しません。
他のデスクトップOSでは此れはオプションです。

...
#define VULKAN_HPP_DISPATCH_LOADER_DYNAMIC 1
#include <vulkan/vulkan.hpp>

VULKAN_HPP_DEFAULT_DISPATCH_LOADER_DYNAMIC_STORAGE

int main() {
  ...
  vk::ApplicationInfo applicationInfo = {};
  applicationInfo.pApplicationName = "Vulkan Render";
  applicationInfo.applicationVersion = vk::makeVersion(1, 0, 0);
  applicationInfo.pEngineName = "Small Engine";
  applicationInfo.engineVersion = vk::makeVersion(1, 0, 0);
  applicationInfo.apiVersion = vk::ApiVersion14;

  VULKAN_HPP_DEFAULT_DISPATCHER.init();

  vk::InstanceCreateInfo instanceCreateInfo = {};
  instanceCreateInfo.pApplicationInfo = &applicationInfo;
  ...
}

インスタンスを作成する前に、もう一つやっておく必要があります。
必要な拡張機能を取得する事です。
此の場合、GLFWに必要な拡張機能を問い合わせる事が出来ます。

...
  uint32_t glfwExtCount = 0;
  const char **glfwExt = glfwGetRequiredInstanceExtensions(&glfwExtCount);

  instanceCreateInfo.enabledExtensionCount = glfwExtCount;
  instanceCreateInfo.ppEnabledExtensionNames = glfwExt;

  vk::Instance instance = vk::createInstance(instanceCreateInfo);
  VULKAN_HPP_DEFAULT_DISPATCHER.init(instance);

  while (!glfwWindowShouldClose(window)) {
    ...
  }

  instance.destroy();
...

此れは今後のセットアップでも繰り返し出てくるテーマなので、慣れておきましょう。
今コンパイルして実行しても、何も変化がない様に見えるでしょう。
然し裏ではVulkanインスタンスが作成され、破棄されています。

バリデーションレイヤー

次にバリデーションレイヤーを追加します。
バリデーションレイヤーは描画がおかしい理由を調べる為の物で、デバッグモードでのみ有効にすべきです。
リリースモードでは無視して構いません。

#include <vector>
#include <cassert>
...
int main() {
  ...
  uint32_t glfwExtCount = 0;
  const char **glfwExt = glfwGetRequiredInstanceExtensions(&glfwExtCount);
  std::vector<const char *> ext(glfwExt, glfwExt + glfwExtCount);
  ext.push_back(*glfwExt);

#ifndef NDEBUG
  ext.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME);

  std::vector<const char *> reqLayers = { "VK_LAYER_KHRONOS_validation" };

  uint32_t layerCount;
  auto res1 = vk::enumerateInstanceLayerProperties(&layerCount, nullptr);

  std::vector<vk::LayerProperties> availLayers(layerCount);
  auto res2 = vk::enumerateInstanceLayerProperties(&layerCount, availLayers.data());

  bool layerFound = std::ranges::any_of(reqLayers, [&](const char *r) {
    return std::ranges::any_of(availLayers, [&](const auto &p) {
      return strcmp(r, p.layerName) == 0;
    });
  });

  if (!layerFound) {
    assert(false && "必要なレイヤーを見つけられませんでした。\n");
    return -1;
  }

  instanceCreateInfo.enabledLayerCount = static_cast<uint32_t>(reqLayers.size());
  instanceCreateInfo.ppEnabledLayerNames = reqLayers.data();
#else
  instanceCreateInfo.enabledLayerCount = 0;
#endif

  instanceCreateInfo.enabledExtensionCount = static_cast<uint32_t>(ext.size());
  instanceCreateInfo.ppEnabledExtensionNames = ext.data();

  vk::Instance instance = vk::createInstance(instanceCreateInfo);
  VULKAN_HPP_DEFAULT_DISPATCHER.init(instance);
  ...
}

此れで未だコンパイル・実行出来るはずです。

次にmain関数の上にデバッグコールバック関数を追加します。

static VKAPI_ATTR vk::Bool32 VKAPI_CALL debugCallback(
  vk::DebugUtilsMessageSeverityFlagBitsEXT severity,
  vk::DebugUtilsMessageTypeFlagsEXT type,
  const vk::DebugUtilsMessengerCallbackDataEXT *pCallbackData,
  void *pUserData) {
  if (severity >= vk::DebugUtilsMessageSeverityFlagBitsEXT::eWarning) {
    std::cerr << "validation layer: type " << to_string(type)
      << " msg: " << pCallbackData->pMessage << std::endl;
  }

  return vk::False;
}

詳細はフルチュートリアルで説明します。
今回は1つの記事に収める為簡略化しておきます。

#ifndef NDEBUG
  vk::DebugUtilsMessageSeverityFlagsEXT severityFlags(vk::DebugUtilsMessageSeverityFlagBitsEXT::eWarning
                                                    | vk::DebugUtilsMessageSeverityFlagBitsEXT::eError);
  vk::DebugUtilsMessageTypeFlagsEXT messageTypeFlags(vk::DebugUtilsMessageTypeFlagBitsEXT::eGeneral
                                                   | vk::DebugUtilsMessageTypeFlagBitsEXT::ePerformance
                                                   | vk::DebugUtilsMessageTypeFlagBitsEXT::eValidation);
  vk::DebugUtilsMessengerCreateInfoEXT debugUtilsInfo = {};
  debugUtilsInfo.messageSeverity = severityFlags;
  debugUtilsInfo.messageType = messageTypeFlags;
  debugUtilsInfo.pfnUserCallback = &debugCallback;
  vk::DebugUtilsMessengerEXT debugMessenger = instance.createDebugUtilsMessengerEXT(debugUtilsInfo);
#endif

...

#ifndef NDEBUG
  instance.destroyDebugUtilsMessengerEXT(debugMessenger);
#endif

物理デバイスとキューファミリー

次に物理デバイスを実装します。

...
#include <map>
...

int main() {
  ...
    auto physDevs = instance.enumeratePhysicalDevices();
  if (physDevs.empty()) {
    assert(false && "Vulkanを対応しているGPUを見つけられませんでした。");
    return -1;
  }

  std::multimap<int, vk::PhysicalDevice> physDevices;
  for (const auto &dev : physDevs) {
    auto devProp = dev.getProperties();
    auto devFeat = dev.getFeatures();
    uint32_t score = 0;

    if (devProp.deviceType == vk::PhysicalDeviceType::eDiscreteGpu) score += 1000;
    score += devProp.limits.maxImageDimension2D;
    if (!devFeat.geometryShader) continue;
    if (devProp.apiVersion < vk::ApiVersion13) continue;
    physDevices.insert(std::make_pair(score, dev));
  }

  if (physDevices.empty()) {
    assert(false && "良いGPUを見つけられませんでした。");
    return -1;
  }

  vk::PhysicalDevice physicalDevice = physDevices.rbegin()->second;
  ...
}

先ず利用可能な全ての物理GPU(専用GPUと統合GPUの両方)を走査します。
其の後其れらを全て調べて、専用GPUと最大イメージ次元が大きいGPUを優先します。
ジオメトリシェーダーを対応していないGPUや、Vulkan 1.3を完全に対応していない古過ぎるGPUは除外します。

此処は自分のニーズに合わせて調整して下さい。
此れはセットアップ方法のイメージを与える為の物です。

利用可能なデバイスプロパティの完全なリストは以下の通りです:

typedef struct VkPhysicalDeviceProperties {
    uint32_t                            apiVersion;
    uint32_t                            driverVersion;
    uint32_t                            vendorID;
    uint32_t                            deviceID;
    VkPhysicalDeviceType                deviceType;
    char                                deviceName[VK_MAX_PHYSICAL_DEVICE_NAME_SIZE];
    uint8_t                             pipelineCacheUUID[VK_UUID_SIZE];
    VkPhysicalDeviceLimits              limits;
    VkPhysicalDeviceSparseProperties    sparseProperties;
} VkPhysicalDeviceProperties;

機能リストはもっと長いので、自分で調べて下さい。
全てはvulkan_core.hを開けば確認出来ます。
Windowsでは通常 C:\Vulkan\(現在のバージョン)\Include\vulkan\vulkan_core.h
Linuxでは /usr/include/vulkan/vulkan_core.h
macOSとBSDでは /usr/local/include/vulkan/vulkan_core.h にあります。

次にキューファミリーのチェックです。
此れで適したGPUを更に検証出来ます。

...
#include <ranges>
...
int main() {
  ...
  auto queueFamilies = physicalDevice.getQueueFamilyProperties();
  bool supportsGfx = std::ranges::any_of(queueFamilies, [](auto const &q) {
    return !!(q.queueFlags & vk::QueueFlagBits::eGraphics);
  });

  std::vector<const char *> reqDevExt = { vk::KHRSwapchainExtensionName };
  auto availDevExt = physicalDevice.enumerateDeviceExtensionProperties();
  bool supportAllExt = std::ranges::all_of(reqDevExt, [&availDevExt](auto const &reqDevExt) {
    return std::ranges::any_of(availDevExt, [reqDevExt](auto const &e) {
      return strcmp(e.extensionName, reqDevExt) == 0;;
    });
  });

  auto features = physicalDevice.template getFeatures2<vk::PhysicalDeviceFeatures2,
                                                       vk::PhysicalDeviceVulkan13Features,
                                                       vk::PhysicalDeviceExtendedDynamicStateFeaturesEXT>();
  bool supportReqFeature = features.template get<vk::PhysicalDeviceVulkan13Features>().dynamicRendering
                        && features.template get<vk::PhysicalDeviceExtendedDynamicStateFeaturesEXT>().extendedDynamicState;

  if (!supportAllExt || !supportsGfx || !supportReqFeature) {
    assert(false && "利用可能なGPUを見つけられませんでした。");
    return -1;
  }
  ...
}

論理デバイスとキュー

次に論理デバイスを作成します。
論理デバイスは私たちがインターフェースする為の物です。

此の記事が長くなってきたので、未だやるべき事が沢山ある為、説明は此処で一旦止め、後のチュートリアルシリーズに回します。

  std::vector<vk::QueueFamilyProperties> queueFamProps = physicalDevice.getQueueFamilyProperties();
  auto gfxQueueFamProp = std::ranges::find_if(queueFamProps, [](auto const &q) {
    return (q.queueFlags & vk::QueueFlagBits::eGraphics) != static_cast<vk::QueueFlags>(0);
  });
  auto gfxIdx = static_cast<uint32_t>(std::distance(queueFamProps.begin(), gfxQueueFamProp));
  float queuePriority = .5f;

  vk::PhysicalDeviceFeatures2 baseFeatures{};
  vk::PhysicalDeviceVulkan13Features features13{};
  features13.dynamicRendering = true;
  features13.synchronization2 = true;

  vk::PhysicalDeviceExtendedDynamicStateFeaturesEXT extDynState{};
  extDynState.extendedDynamicState = true;

  vk::StructureChain<vk::PhysicalDeviceFeatures2,
                     vk::PhysicalDeviceVulkan13Features,
                     vk::PhysicalDeviceExtendedDynamicStateFeaturesEXT> featureChain = {
    baseFeatures,
    features13,
    extDynState,
  };

  vk::DeviceQueueCreateInfo devQueueCreateInfo = {};
  devQueueCreateInfo.queueFamilyIndex = gfxIdx;
  devQueueCreateInfo.queueCount = 1;
  devQueueCreateInfo.pQueuePriorities = &queuePriority;

  vk::DeviceCreateInfo devCreateInfo = {};
  devCreateInfo.pNext = &featureChain.get<vk::PhysicalDeviceFeatures2>();
  devCreateInfo.queueCreateInfoCount = 1;
  devCreateInfo.pQueueCreateInfos = &devQueueCreateInfo;
  devCreateInfo.enabledExtensionCount = static_cast<uint32_t>(reqDevExt.size());
  devCreateInfo.ppEnabledExtensionNames = reqDevExt.data();

  vk::Device device = physicalDevice.createDevice(devCreateInfo);
  VULKAN_HPP_DEFAULT_DISPATCHER.init(device);
  vk::Queue gfxQueue = device.getQueue(gfxIdx, 0);
  ...
  device.destroy();
  ...

ウィンドウサーフェスの作成

...
#if defined(_WIN64)
#define VK_USE_PLATFORM_WIN32_KHR
#elif defined(__APPLE__)
#define VK_USE_PLATFORM_METAL_EXT
#else
#define VK_USE_PLATFORM_XCB_KHR
#endif
#define GLFW_INCLUDE_VULKAN
#include <GLFW/glfw3.h>

#if defined(_WIN64)
#define GLFW_EXPOSE_NATIVE_WIN32
#elif defined(__APPLE__)
#define GLFW_EXPOSE_NATIVE_COCOA
#else
#define GLFW_EXPOSE_NATIVE_X11
#endif
#include <GLFW/glfw3native.h>
...
int main() {
  ...
  // インスタンス

#if defined(_WIN64)
  vk::Win32SurfaceCreateInfoKHR surfaceCreateInfo = {};
  surfaceCreateInfo.hinstance = GetModuleHandle(nullptr);
  surfaceCreateInfo.hwnd = glfwGetWin32Window(window);

  vk::SurfaceKHR surface = instance.createWin32SurfaceKHR(surfaceCreateInfo);
#elif defined(__APPLE__)
  vk::MetalSurfaceCreateInfoEXT surfaceCreateInfo = {};
  surfaceCreateInfo.pLayer = glfwGetCocoaWindow(window);

  vk::SurfaceKHR surface = instance.createMetalSurfaceEXT(surfaceCreateInfo);
#else
  vk::XcbSurfaceCreateInfoKHR surfaceCreateInfo = {};
  surfaceCreateInfo.connection = glfwGetX11Display();
  surfaceCreateInfo.window = glfwGetX11Window(window);

  vk::SurfaceKHR surface = instance.createXcbSurfaceKHR(surfaceCreateInfo);
#endif

  // 物理デバイス
  ...
  instance.destroySurfaceKHR(surface);
}

プラットフォームを分離している理由は、GLFWがC APIでしか問い合わせ出来ない為です。
他の人はみんなC APIでサーフェスを取得してからC++ APIに変換している為、上記の様な情報は見つけ憎いです。
其の為OSごとに分離して書く事にしました。

論理デバイス内でプレゼンテーションキューも作成します。

  ...
  std::vector<vk::QueueFamilyProperties> queueFamProps = physicalDevice.getQueueFamilyProperties();
  auto gfxQueueFamProp = std::ranges::find_if(queueFamProps, [](auto const &q) {
    return (q.queueFlags & vk::QueueFlagBits::eGraphics) != static_cast<vk::QueueFlags>(0);
  });

  uint32_t queueIdx = ~0;
  for (uint32_t q = 0; q < queueFamProps.size(); ++q) {
    if ((queueFamProps[q].queueFlags & vk::QueueFlagBits::eGraphics)
        && physicalDevice.getSurfaceSupportKHR(q, surface)) {
      queueIdx = q;
      break;
    }
  }

  if (queueIdx == ~0) {
    assert(false && "グラフィックと表示塞翁しているキューを見つけられませんでした。");
    return -1;
  }
  ...

スワップチェーン

#define NOMINMAX
...
#include <algorithm>
#include <limits>
...
  auto surfaceCapabilities = physicalDevice.getSurfaceCapabilitiesKHR(surface);
  std::vector<vk::SurfaceFormatKHR> availFormats = physicalDevice.getSurfaceFormatsKHR(surface);
  std::vector<vk::PresentModeKHR> availPresentModes = physicalDevice.getSurfacePresentModesKHR(surface);
  assert(!availFormats.empty());

  const auto formatIt = std::ranges::find_if(availFormats, [](const auto &format) {
    return format.format == vk::Format::eB8G8R8A8Srgb && format.colorSpace == vk::ColorSpaceKHR::eSrgbNonlinear;
  });

  vk::SurfaceFormatKHR swapChainFormat = formatIt != availFormats.end() ? *formatIt : availFormats[0];

  assert(std::ranges::any_of(availPresentModes, [](auto present) { return present == vk::PresentModeKHR::eFifo; }));
  vk::PresentModeKHR presentMode = std::ranges::any_of(availPresentModes, [](const vk::PresentModeKHR val) {
    return vk::PresentModeKHR::eMailbox == val;
  }) ? vk::PresentModeKHR::eMailbox : vk::PresentModeKHR::eFifo;

  if (surfaceCapabilities.currentExtent.width != std::numeric_limits<uint32_t>::max()) {
    swapChainExtent = surfaceCapabilities.currentExtent;
  } else {
    int width, height;
    glfwGetFramebufferSize(window, &width, &height);
    swapChainExtent.width = std::clamp<uint32_t>(width, surfaceCapabilities.minImageExtent.width, surfaceCapabilities.maxImageExtent.width);
    swapChainExtent.height = std::clamp<uint32_t>(height, surfaceCapabilities.minImageExtent.height, surfaceCapabilities.maxImageExtent.height);
  }

  uint32_t minImageCount = std::max(3u, surfaceCapabilities.minImageCount);
  if ((0 < surfaceCapabilities.maxImageCount) && (surfaceCapabilities.maxImageCount < minImageCount)) {
    minImageCount = surfaceCapabilities.maxImageCount;
  }

  vk::SwapchainCreateInfoKHR swapChainCreateInfo = {};
  swapChainCreateInfo.surface = surface;
  swapChainCreateInfo.minImageCount = minImageCount;
  swapChainCreateInfo.imageFormat = swapChainFormat.format;
  swapChainCreateInfo.imageColorSpace = swapChainFormat.colorSpace;
  swapChainCreateInfo.imageExtent = swapChainExtent;
  swapChainCreateInfo.imageArrayLayers = 1;
  swapChainCreateInfo.imageUsage = vk::ImageUsageFlagBits::eColorAttachment;
  swapChainCreateInfo.imageSharingMode = vk::SharingMode::eExclusive;
  swapChainCreateInfo.preTransform = surfaceCapabilities.currentTransform;
  swapChainCreateInfo.compositeAlpha = vk::CompositeAlphaFlagBitsKHR::eOpaque;
  swapChainCreateInfo.presentMode = presentMode;
  swapChainCreateInfo.clipped = true;

  vk::SwapchainKHR swapChain = device.createSwapchainKHR(swapChainCreateInfo);
  std::vector<vk::Image> swapChainImages = device.getSwapchainImagesKHR(swapChain);
...
  if (swapChain) device.destroySwapchainKHR(swapChain);
...

イメージビュー

次にイメージビューです。

...
  assert(swapChainImageViews.empty());

  vk::ImageSubresourceRange subrange = {};
  subrange.aspectMask = vk::ImageAspectFlagBits::eColor;
  subrange.levelCount = 1;
  subrange.layerCount = 1;

  vk::ImageViewCreateInfo imgViewCreateInfo = {};
  imgViewCreateInfo.viewType = vk::ImageViewType::e2D;
  imgViewCreateInfo.format = swapChainFormat.format;
  imgViewCreateInfo.subresourceRange = subrange;
  imgViewCreateInfo.components = {
    vk::ComponentSwizzle::eIdentity,
    vk::ComponentSwizzle::eIdentity,
    vk::ComponentSwizzle::eIdentity,
    vk::ComponentSwizzle::eIdentity,
  };

  for (auto &img : swapChainImages) {
    imgViewCreateInfo.image = img;
    swapChainImageViews.push_back(device.createImageView(imgViewCreateInfo));
  }
...
  for (auto &view : swapChainImageViews) device.destroyImageView(view);
  swapChainImageViews.clear();
...

グラフィックスパイプライン

shader.vert

#version 450

layout(location = 0) out vec3 fragColor;

vec2 pos[3] = vec2[](
  vec2(0.0, -0.5),
  vec2(0.5, 0.5),
  vec2(-0.5, 0.5)
);

vec3 col[3] = vec3[](
  vec3(1.0, 0.0, 0.0),
  vec3(0.0, 1.0, 0.0),
  vec3(0.0, 0.0, 1.0)
);

void main() {
  gl_Position = vec4(pos[gl_VertexIndex], 0.0, 1.0);
  fragColor = col[gl_VertexIndex];
}

shader.frag

#version 450

layout(location = 0) in vec3 fragColor;
layout(location = 0) out vec4 outColor;

void main() {
  outColor = vec4(fragColor, 1.0);
}

Windows

> C:\\Vulkan\\(現在のバージョン)\\Bin\\glslc.exe shader.vert -o vert.spv
> C:\\Vulkan\\(現在のバージョン)\\Bin\\glslc.exe shader.frag -o frag.spv

Linux

$ /usr/share/vulkan/bin/glslc shader.vert -o vert.spv
$ /usr/share/vulkan/bin/glslc shader.frag -o frag.spv

BSD + macOS

$ /usr/local/share/vulkan/bin/glslc shader.vert -o vert.spv
$ /usr/local/share/vulkan/bin/glslc shader.frag -o frag.spv

main.cc

...
#include <fstream>
...
static std::vector<char> readFile(const std::string &filename) {
  std::ifstream file(filename, std::ios::ate | std::ios::binary);
  if (!file.is_open()) {
    assert(false && "ファイルを開くに失敗。");
    return {};
  }

  size_t fileSize = (size_t)file.tellg();
  std::vector<char> buf(fileSize);
  file.seekg(0);
  file.read(buf.data(), fileSize);
  file.close();
  return buf;
}
...
  auto vertShader = readFile("vert.spv");
  if (vertShader.empty()) return -1;
  auto fragShader = readFile("frag.spv");
  if (fragShader.empty()) return -1;

  vk::ShaderModuleCreateInfo vertCreateInfo = {};
  vertCreateInfo.codeSize = vertShader.size() * sizeof(char);
  vertCreateInfo.pCode = reinterpret_cast<const uint32_t *>(vertShader.data());
  vk::ShaderModule vertShaderModule = device.createShaderModule(vertCreateInfo);

  vk::ShaderModuleCreateInfo fragCreateInfo = {};
  fragCreateInfo.codeSize = fragShader.size() * sizeof(char);
  fragCreateInfo.pCode = reinterpret_cast<const uint32_t *>(fragShader.data());
  vk::ShaderModule fragShaderModule = device.createShaderModule(fragCreateInfo);

  vk::PipelineShaderStageCreateInfo vertShaderStageInfo = {};
  vertShaderStageInfo.stage = vk::ShaderStageFlagBits::eVertex;
  vertShaderStageInfo.module = vertShaderModule;
  vertShaderStageInfo.pName = "main";

  vk::PipelineShaderStageCreateInfo fragShaderStageInfo = {};
  fragShaderStageInfo.stage = vk::ShaderStageFlagBits::eFragment;
  fragShaderStageInfo.module = fragShaderModule;
  fragShaderStageInfo.pName = "main";

  vk::PipelineShaderStageCreateInfo shaderStages[] = {
    vertShaderStageInfo,
    fragShaderStageInfo,
  };
...
  if (fragShaderModule) device.destroyShaderModule(fragShaderModule);
  if (vertShaderModule) device.destroyShaderModule(vertShaderModule);
...

固定機能

  std::vector<vk::DynamicState> dynStates = {
    vk::DynamicState::eViewport,
    vk::DynamicState::eScissor,
  };

  vk::PipelineVertexInputStateCreateInfo vertexInputInfo = {};

  vk::PipelineInputAssemblyStateCreateInfo inputAss = {};
  inputAss.topology = vk::PrimitiveTopology::eTriangleList;

  vk::Viewport viewport = {};
  viewport.x = 0.f;
  viewport.y = 0.f;
  viewport.width = static_cast<float>(swapChainExtent.width);
  viewport.height = static_cast<float>(swapChainExtent.height);
  viewport.minDepth = 0.f;
  viewport.maxDepth = 1.f;

  vk::Rect2D scissor = {};
  scissor.offset = vk::Offset2D{ 0, 0 };
  scissor.extent = swapChainExtent;

  vk::PipelineDynamicStateCreateInfo dynState = {};
  dynState.dynamicStateCount = static_cast<uint32_t>(dynStates.size());
  dynState.pDynamicStates = dynStates.data();

  vk::PipelineViewportStateCreateInfo viewportState = {};
  viewportState.viewportCount = 1;
  viewportState.pViewports = &viewport;
  viewportState.scissorCount = 1;
  viewportState.pScissors = &scissor;

  vk::PipelineRasterizationStateCreateInfo rasterizer = {};
  rasterizer.depthClampEnable = vk::False;
  rasterizer.rasterizerDiscardEnable = vk::False;
  rasterizer.polygonMode = vk::PolygonMode::eFill;
  rasterizer.cullMode = vk::CullModeFlagBits::eBack;
  rasterizer.frontFace = vk::FrontFace::eClockwise;
  rasterizer.depthBiasEnable = vk::False;
  rasterizer.lineWidth = 1.f;

  vk::PipelineMultisampleStateCreateInfo multisampling = {};
  multisampling.rasterizationSamples = vk::SampleCountFlagBits::e1;
  multisampling.sampleShadingEnable = vk::False;

  vk::PipelineColorBlendAttachmentState colorBlendAttachment = {};
  colorBlendAttachment.blendEnable = vk::True;
  colorBlendAttachment.srcColorBlendFactor = vk::BlendFactor::eSrcAlpha;
  colorBlendAttachment.dstColorBlendFactor = vk::BlendFactor::eOneMinusSrcAlpha;
  colorBlendAttachment.colorBlendOp = vk::BlendOp::eAdd;
  colorBlendAttachment.srcAlphaBlendFactor = vk::BlendFactor::eOne;
  colorBlendAttachment.dstAlphaBlendFactor = vk::BlendFactor::eZero;
  colorBlendAttachment.alphaBlendOp = vk::BlendOp::eAdd;
  colorBlendAttachment.colorWriteMask = vk::ColorComponentFlagBits::eR
                                      | vk::ColorComponentFlagBits::eG
                                      | vk::ColorComponentFlagBits::eB
                                      | vk::ColorComponentFlagBits::eA;

  vk::PipelineColorBlendStateCreateInfo colorBlending = {};
  colorBlending.logicOpEnable = vk::False;
  colorBlending.logicOp = vk::LogicOp::eCopy;
  colorBlending.attachmentCount = 1;
  colorBlending.pAttachments = &colorBlendAttachment;

  vk::PipelineLayoutCreateInfo pipelineLayoutInfo = {};
  pipelineLayoutInfo.setLayoutCount = 0;
  pipelineLayoutInfo.pushConstantRangeCount = 0;

  vk::PipelineLayout pipelineLayout = device.createPipelineLayout(pipelineLayoutInfo);

パイプラインレンダリング

  vk::GraphicsPipelineCreateInfo pipelineInfo = {};
  pipelineInfo.stageCount = 2;
  pipelineInfo.pStages = shaderStages;
  pipelineInfo.pVertexInputState = &vertexInputInfo;
  pipelineInfo.pInputAssemblyState = &inputAss;
  pipelineInfo.pViewportState = &viewportState;
  pipelineInfo.pRasterizationState = &rasterizer;
  pipelineInfo.pMultisampleState = &multisampling;
  pipelineInfo.pColorBlendState = &colorBlending;
  pipelineInfo.pDynamicState = &dynState;
  pipelineInfo.layout = pipelineLayout;
  pipelineInfo.pNext = &pipelineRenderingCreateInfo;
  pipelineInfo.renderPass = nullptr;

  auto [result, pipeline] = device.createGraphicsPipeline(nullptr, pipelineInfo);
  if (result != vk::Result::eSuccess) {
    assert(false && "グラフィックパイプラインの設置にしっぱい。");
    return -1;
  }

  gfxPipeline = pipeline;

レンダーパスはnullptrに設定しています。
旧風のレンダーパスの代わりにダイナミックレンダリングを使っている為です。

コマンドプール

  vk::CommandPoolCreateInfo poolInfo = {};
  poolInfo.flags = vk::CommandPoolCreateFlagBits::eResetCommandBuffer;
  poolInfo.queueFamilyIndex = queueIdx;

  vk::CommandPool cmdPool = device.createCommandPool(poolInfo);

コマンドバッファ

...
std::vector<vk::Image> swapChainImages;
vk::CommandBuffer cmdBuf;
vk::CommandBufferBeginInfo beginInfo;
std::vector<vk::ImageView> swapChainImageViews;
vk::Extent2D swapChainExtent;
vk::Pipeline gfxPipeline;
...
void transitionImgLayout(uint32_t imgIdx,
                         vk::ImageLayout oldLayout,
                         vk::ImageLayout newLayout,
                         vk::AccessFlags2 srcAccessMask,
                         vk::AccessFlags2 dstAccessMask,
                         vk::PipelineStageFlags2 srcStageMask,
                         vk::PipelineStageFlags2 dstStageMask) {
  vk::ImageSubresourceRange barrierSubres = {};
  barrierSubres.aspectMask = vk::ImageAspectFlagBits::eColor;
  barrierSubres.baseMipLevel = 0;
  barrierSubres.levelCount = 1;
  barrierSubres.baseArrayLayer = 0;
  barrierSubres.layerCount = 1;

  vk::ImageMemoryBarrier2 barrier = {};
  barrier.srcStageMask = srcStageMask;
  barrier.dstStageMask = dstStageMask;
  barrier.srcAccessMask = srcAccessMask;
  barrier.dstAccessMask = dstAccessMask;
  barrier.oldLayout = oldLayout;
  barrier.newLayout = newLayout;
  barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
  barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
  barrier.image = swapChainImages[imgIdx];
  barrier.subresourceRange = barrierSubres;

  vk::DependencyInfo depInfo = {};
  depInfo.dependencyFlags = {};
  depInfo.imageMemoryBarrierCount = 1;
  depInfo.pImageMemoryBarriers = &barrier;

  cmdBuf.pipelineBarrier2(depInfo);
}

void recordCmdBuf(uint32_t imgIdx) {
  cmdBuf.reset();
  cmdBuf.begin(beginInfo);


  transitionImgLayout(imgIdx,
                      vk::ImageLayout::eUndefined,
                      vk::ImageLayout::eColorAttachmentOptimal,
                      {},
                      vk::AccessFlagBits2::eColorAttachmentWrite,
                      vk::PipelineStageFlagBits2::eColorAttachmentOutput,
                      vk::PipelineStageFlagBits2::eColorAttachmentOutput);

  vk::ClearValue clearColor = vk::ClearColorValue(0.f, 0.f, 0.f, 1.f);
  vk::RenderingAttachmentInfo attachInfo = {};
  attachInfo.imageView = swapChainImageViews[imgIdx];
  attachInfo.imageLayout = vk::ImageLayout::eColorAttachmentOptimal;
  attachInfo.loadOp = vk::AttachmentLoadOp::eClear;
  attachInfo.storeOp = vk::AttachmentStoreOp::eStore;
  attachInfo.clearValue = clearColor;

  vk::RenderingInfo renderingInfo = {};
  renderingInfo.renderArea = vk::Rect2D{ {0, 0}, swapChainExtent };
  renderingInfo.layerCount = 1;
  renderingInfo.colorAttachmentCount = 1;
  renderingInfo.pColorAttachments = &attachInfo;

  cmdBuf.beginRendering(renderingInfo);
  cmdBuf.bindPipeline(vk::PipelineBindPoint::eGraphics, gfxPipeline);
  cmdBuf.setViewport(0, vk::Viewport(0.f, 0.f,
                                     static_cast<float>(swapChainExtent.width),
                                     static_cast<float>(swapChainExtent.height),
                                     0.f, 1.f));
  cmdBuf.setScissor(0, vk::Rect2D(vk::Offset2D(0, 0), swapChainExtent));
  cmdBuf.draw(3, 1, 0, 0);
  cmdBuf.endRendering();

  transitionImgLayout(imgIdx,
                      vk::ImageLayout::eColorAttachmentOptimal,
                      vk::ImageLayout::ePresentSrcKHR,
                      vk::AccessFlagBits2::eColorAttachmentWrite,
                      {},
                      vk::PipelineStageFlagBits2::eColorAttachmentOutput,
                      vk::PipelineStageFlagBits2::eBottomOfPipe);

  cmdBuf.end();
}
...
  vk::CommandBufferAllocateInfo allocInfo = {};
  allocInfo.commandPool = cmdPool;
  allocInfo.level = vk::CommandBufferLevel::ePrimary;
  allocInfo.commandBufferCount = 1;

  std::vector<vk::CommandBuffer> cmdBufs = device.allocateCommandBuffers(allocInfo);
  cmdBuf = cmdBufs.front();

  beginInfo.flags = vk::CommandBufferUsageFlagBits::eOneTimeSubmit;
...

レンダリングとプレゼンテーション

  vk::Semaphore presentCompleteSemaphore = device.createSemaphore(vk::SemaphoreCreateInfo());
  vk::Semaphore renderFinishedSemaphore = device.createSemaphore(vk::SemaphoreCreateInfo());

  vk::FenceCreateInfo fenceInfo = {};
  fenceInfo.flags = vk::FenceCreateFlagBits::eSignaled;

  vk::Fence drawFence = device.createFence(fenceInfo);

  while (!glfwWindowShouldClose(window)) {
    if (glfwGetKey(window, GLFW_KEY_Q) == GLFW_PRESS) {
      glfwSetWindowShouldClose(window, true);
    }

    glfwPollEvents();

    auto fenceResult = device.waitForFences(drawFence, vk::True, UINT64_MAX);
    if (fenceResult != vk::Result::eSuccess) {
      assert(false && "フェンスまで待つに失敗。");
      break;
    }

    device.resetFences(drawFence);
    auto [acquireRes, imgIdx] = device.acquireNextImageKHR(swapChain, UINT64_MAX, presentCompleteSemaphore, nullptr);

    if (acquireRes == vk::Result::eErrorOutOfDateKHR || acquireRes == vk::Result::eSuboptimalKHR) continue;
    if (acquireRes != vk::Result::eSuccess) {
      assert(false && "次の画像の受け取りに失敗。");
      break;
    }

    recordCmdBuf(imgIdx);

    vk::PipelineStageFlags waitStage = vk::PipelineStageFlagBits::eColorAttachmentOutput;

    vk::SubmitInfo submitInfo = {};
    submitInfo.waitSemaphoreCount = 1;
    submitInfo.pWaitSemaphores = &presentCompleteSemaphore;
    submitInfo.pWaitDstStageMask = &waitStage;
    submitInfo.commandBufferCount = 1;
    submitInfo.pCommandBuffers = &cmdBuf;
    submitInfo.signalSemaphoreCount = 1;
    submitInfo.pSignalSemaphores = &renderFinishedSemaphore;

    gfxQueue.submit(submitInfo, drawFence);

    vk::SubpassDependency depend = {};
    depend.srcSubpass = vk::SubpassExternal;
    depend.dstSubpass = 0;
    depend.srcStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput;
    depend.dstStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput;
    depend.srcAccessMask = vk::AccessFlagBits::eNone;
    depend.dstAccessMask = vk::AccessFlagBits::eColorAttachmentWrite;

    vk::PresentInfoKHR presentInfo = {};
    presentInfo.waitSemaphoreCount = 1;
    presentInfo.pWaitSemaphores = &renderFinishedSemaphore;
    presentInfo.swapchainCount = 1;
    presentInfo.pSwapchains = &swapChain;
    presentInfo.pImageIndices = &imgIdx;

    vk::Result presentRes = gfxQueue.presentKHR(presentInfo);

    device.waitIdle();
  }
...
  if (device) device.waitIdle();
...
  if (presentCompleteSemaphore) device.destroySemaphore(presentCompleteSemaphore);
  if (renderFinishedSemaphore) device.destroySemaphore(renderFinishedSemaphore);
  if (drawFence) device.destroyFence(drawFence);
...

結果

遂に画面に三角形が表示されました!
此れを作るのに4時間しかかかりませんでした。
新記録です。

此れで終わりではありません。
実は此れはスタート地点に過ぎません。

次回はフレームインフライトとスワップチェーンの再作成を設定します。

以上