Vulkan Tutorial (Drawing a triangle/Setup/Physical devices and queue families) 日本語訳

https://vulkan-tutorial.com/Drawing_a_triangle/Setup/Physical_devices_and_queue_families

# Physical devices and queue families

## Selecting a physical device

[`VkInstance`](https://www.khronos.org/registry/vulkan/specs/1.0/man/html/VkInstance.html)でVulkanライブラリを初期化したあと、私達が必要としている機能をサポートしたグラフィックスカードを、システムから探して選択なければなりません。実は、任意の数のグラフィックスカードを選択して同時に使うことも可能ですが、このチュートリアルでは、私達の要望を満たすグラフィックスカードのうち最初に見つかったものを使います。

`pickPhysicalDevice`関数を追加して、`initVulkan`関数の中でそれを呼び出します。

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

void pickPhysicalDevice() {

}
```

最終的に選択されたグラフィックスカードを格納する[`VkPhysicalDevice`](https://www.khronos.org/registry/vulkan/specs/1.0/man/html/VkPhysicalDevice.html)ハンドルを、新しいメンバ変数として追加します。このオブジェクトは[`VkInstance`](https://www.khronos.org/registry/vulkan/specs/1.0/man/html/VkInstance.html)が破棄されたときに暗黙のうちに破棄されるので、`cleanup`関数の中で何か新しいことをする必要はありません。

```cpp
VkPhysicalDevice physicalDevice = VK_NULL_HANDLE;
```

グラフィックスカードをリストアップするのは拡張機能をリストアップするのとよく似ていて、まずは要素数を問い合わせます。

```cpp
uint32_t deviceCount = 0;
vkEnumeratePhysicalDevices(instance, &deviceCount, nullptr);
```

もしVulkanをサポートしたデバイスが０個の場合、これ以上進めても意味がありません。

```cpp
if (deviceCount == 0) {
    throw std::runtime_error("failed to find GPUs with Vulkan support!");
}
```

そうでなければ、全ての[`VkPhysicalDevice`](https://www.khronos.org/registry/vulkan/specs/1.0/man/html/VkPhysicalDevice.html)ハンドルを保持する配列を確保できます。

```cpp
std::vector<VkPhysicalDevice> devices(deviceCount);
vkEnumeratePhysicalDevices(instance, &deviceCount, devices.data());
```

ここで、全てのグラフィックスカードが同じに作られているわけではないので、それぞれを評価して、実行したい操作に適合しているかどうかチェックする必要があります。このために、新しい関数を追加します。

```cpp
bool isDeviceSuitable(VkPhysicalDevice device) {
    return true;
}
```

そして、要求に合った物理デバイスがあるかどうかのチェックを、関数に追加します。

```cpp
for (const auto& device : devices) {
    if (isDeviceSuitable(device)) {
        physicalDevice = device;
        break;
    }
}

if (physicalDevice == VK_NULL_HANDLE) {
    throw std::runtime_error("failed to find a suitable GPU!");
}
```

次の節では、`isDeviceSuitable`関数でチェックする最初の要件を追加します。あとの章ではより多くのVulkanの機能を使いはじめるので、この関数もより多くのチェックを含むように拡張していきます。

## Base device suitability checks

デバイスの適合度を評価するために、いくつかの詳細を問い合わせるところから始めます。デバイスの名前、型、サポートされているVulkanのバージョン番号といったような基本的な属性は、[`vkGetPhysicalDeviceProperties`](https://www.khronos.org/registry/vulkan/specs/1.0/man/html/vkGetPhysicalDeviceProperties.html)を使って問い合わせることができます。

```cpp
VkPhysicalDeviceProperties deviceProperties;
vkGetPhysicalDeviceProperties(device, &deviceProperties);
```

圧縮テクスチャ、64ビットfloat、マルチビューポートレンダリング(VRで有用です)といったようなオプションの機能がサポートされているかどうかは、[`vkGetPhysicalDeviceFeatures`](https://www.khronos.org/registry/vulkan/specs/1.0/man/html/vkGetPhysicalDeviceFeatures.html)を使って問い合わせることができます。

```cpp
VkPhysicalDeviceFeatures deviceFeatures;
vkGetPhysicalDeviceFeatures(device, &deviceFeatures);
```

デバイスに問い合わせることができるさらに詳細な項目があり、デバイスメモリとキューファミリーについては後述します(次の節を参照)。

例えば、私達のアプリケーションが、ジオメトリシェーダをサポートしている専用グラフィックスカードのみ利用可能と決めたとしましょう。そうすると、`isDeviceSuitable`関数は次のようになります。

```cpp
bool isDeviceSuitable(VkPhysicalDevice device) {
    VkPhysicalDeviceProperties deviceProperties;
    VkPhysicalDeviceFeatures deviceFeatures;
    vkGetPhysicalDeviceProperties(device, &deviceProperties);
    vkGetPhysicalDeviceFeatures(device, &deviceFeatures);

    return deviceProperties.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU &&
           deviceFeatures.geometryShader;
}
```

デバイスが適合しているかどうかチェックして最初にみつかったものを使う代わりに、それぞれのデバイスに点数を与え、最も高いものを選択するということもできます。このやり方であれば、専用グラフィックスカードに高い点数を与えて優先しつつ、統合GPUしか利用可能なものがなければそれを使うといったことが可能です。これは以下のようにして実装できます。

```cpp
#include <map>

...

void pickPhysicalDevice() {
    ...

    // Use an ordered map to automatically sort candidates by increasing score
    std::multimap<int, VkPhysicalDevice> candidates;

    for (const auto& device : devices) {
        int score = rateDeviceSuitability(device);
        candidates.insert(std::make_pair(score, device));
    }

    // Check if the best candidate is suitable at all
    if (candidates.rbegin()->first > 0) {
        physicalDevice = candidates.rbegin()->second;
    } else {
        throw std::runtime_error("failed to find a suitable GPU!");
    }
}

int rateDeviceSuitability(VkPhysicalDevice device) {
    ...

    int score = 0;

    // Discrete GPUs have a significant performance advantage
    if (deviceProperties.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU) {
        score += 1000;
    }

    // Maximum possible size of textures affects graphics quality
    score += deviceProperties.limits.maxImageDimension2D;

    // Application can't function without geometry shaders
    if (!deviceFeatures.geometryShader) {
        return 0;
    }

    return score;
}
```

このチュートリアルの関数は、全て実装する必要はありませんが、デバイスを選択する方法についてどうやって設計するかのアイデアを与えます。もちろん、選択肢の名前を表示して、ユーザーが選択できるようにすることもできます。

私達はまだ始めたばかりなので、Vulkanがサポートされてさえいれば、どんなGPUでもそれに決定するようにします。

```cpp
bool isDeviceSuitable(VkPhysicalDevice device) {
    return true;
}
```

次の節では、最初にチェックすべき必須の機能について解説します。

## Queue families

以前にも手短に触れたように、描画からテクスチャのアップロードまで、ほとんど全てのVulkanの操作は、コマンドをキューにサブミットすることが必要です。異なるキューファミリーに由来する異なるタイプのキューが存在し、それぞれのキューファミリーはコマンドの一部だけを許可しています。例えば、コンピュートコマンドの処理だけを許可したキューファミリーや、メモリ転送に関連したコマンドだけを許可したキューファミリーがあるかもしれません。

どのキューファミリーがデバイスでサポートされているか、そして、そのうちのどれで私達が使いたいコマンドがサポートされているかをチェックする必要があります。この目的のために、必要な全てのキューファミリーを見つけ出す関数`findQueueFamilies`を新たに追加します。

今はグラフィックスコマンドをサポートしたキューを探したいだけなので、関数は次のようになります。

```cpp
uint32_t findQueueFamilies(VkPhysicalDevice device) {
    // Logic to find graphics queue family
}
```

しかし、次の章で、既に別のキューを探すことになるので、それに備えてインデックスを構造体にまとめておいたほうが良いでしょう。

```cpp
struct QueueFamilyIndices {
    uint32_t graphicsFamily;
};

QueueFamilyIndices findQueueFamilies(VkPhysicalDevice device) {
    QueueFamilyIndices indices;
    // Logic to find queue family indices to populate struct with
    return indices;
}
```

しかし、仮にキューファミリーが見つからなかったら？`findQueueFamilies`関数内で例外を投げることもできますが、この関数はデバイスの適合度について判断をするのに正しい場所ではありません。例えば、専用の転送キューファミリーを持つデバイスを希望するけれども必須ではないかもしれません。そのため、特定のキューファミリーが見つかったかどうかを示す、何らかの方法が必要です。

キューファミリーのインデックスの有効な値として、理論上は`0`を含む全ての`uint32_t`の値を取りうるので、キューファミリーが存在しなかったことを示すのにマジックナンバーを使うことは不可能です。幸運なことに、C++17から、値が存在しているかどうかを識別するためのデータ構造が導入されました。

```cpp
#include <optional>

...

std::optional<uint32_t> graphicsFamily;

std::cout << std::boolalpha << graphicsFamily.has_value() << std::endl; // false

graphicsFamily = 0;

std::cout << std::boolalpha << graphicsFamily.has_value() << std::endl; // true
```

`std::optional`は、何かを代入するまで値を格納しないようなラッパーです。`has_value()`メンバ関数を呼び出すことで、値を格納しているかどうか、いつでも問い合わせることができます。ということは、次のようにロジックを変更することができるというわけです。

```cpp
#include <optional>

...

struct QueueFamilyIndices {
    std::optional<uint32_t> graphicsFamily;
};

QueueFamilyIndices findQueueFamilies(VkPhysicalDevice device) {
    QueueFamilyIndices indices;
    // Assign index to queue families that could be found
    return indices;
}
```

ようやく、`findQueueFamilies`の実装を始めることができます。

```cpp
QueueFamilyIndices findQueueFamilies(VkPhysicalDevice device) {
    QueueFamilyIndices indices;

    ...

    return indices;
}
```

キューファミリーのリストを集める手順は、あなたが期待しているとおり、[`vkGetPhysicalDeviceQueueFamilyProperties`](https://www.khronos.org/registry/vulkan/specs/1.0/man/html/vkGetPhysicalDeviceQueueFamilyProperties.html)を使います。

```cpp
uint32_t queueFamilyCount = 0;
vkGetPhysicalDeviceQueueFamilyProperties(device, &queueFamilyCount, nullptr);

std::vector<VkQueueFamilyProperties> queueFamilies(queueFamilyCount);
vkGetPhysicalDeviceQueueFamilyProperties(device, &queueFamilyCount, queueFamilies.data());
```

[`VkQueueFamilyProperties`](https://www.khronos.org/registry/vulkan/specs/1.0/man/html/VkQueueFamilyProperties.html)構造体は、サポートされている操作の種類や、そのファミリーから作ることができるキューの数など、キューファミリーに関する詳細な情報が格納されています。私達は、`VK_QUEUE_GRAPHICS_BIT`をサポートするキューファミリーを、少なくとも1つは見つける必要があります。

```cpp
int i = 0;
for (const auto& queueFamily : queueFamilies) {
    if (queueFamily.queueFlags & VK_QUEUE_GRAPHICS_BIT) {
        indices.graphicsFamily = i;
    }

    i++;
}
```

このキューファミリーを探索するイケてる関数もできたので、`isDeviceSuitable`関数の中で、私達が使いたいコマンドを処理できるデバイスかどうかチェックするのに使うことができます。

```cpp
bool isDeviceSuitable(VkPhysicalDevice device) {
    QueueFamilyIndices indices = findQueueFamilies(device);

    return indices.graphicsFamily.has_value();
}
```

もう少し便利にするために、構造体自身にチェック関数を追加します。

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

    bool isComplete() {
        return graphicsFamily.has_value();
    }
};

...

bool isDeviceSuitable(VkPhysicalDevice device) {
    QueueFamilyIndices indices = findQueueFamilies(device);

    return indices.isComplete();
}
```

`findQueueFamilies`からの早期exitにも使うことができます。

```cpp
for (const auto& queueFamily : queueFamilies) {
    ...

    if (indices.isComplete()) {
        break;
    }

    i++;
}
```

グレイト！これで正しい物理デバイスを探すのに必要なものは全てそろいました。次のステップは、物理デバイスとのインターフェースとなる、[論理デバイス](https://vulkan-tutorial.com/Drawing_a_triangle/Setup/Logical_device_and_queues)の作成です。

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

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