# Inicializálás

Mielőtt nekilátnál a kódolásnak, hozz létre egy kiinduló projektet. A példák kiinduló kódja megtalálható [ezen a linken](https://git.sch.bme.hu/kszk/devteam/vulkan-workshop/-/tree/starter).

A példák Vulkan mellett [glm](https://github.com/g-truc/glm), [GLFW](https://www.glfw.org/) és [VulkanMemoryAllocator](https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator?tab=readme-ov-file) könyvtárakat használnak.

Érdemes még a [bevezető előadás](https://git.sch.bme.hu/kszk/devteam/vulkan-workshop/-/blob/master/docs/Vulkan%20Bevezet%C5%91.pptx?ref_type=heads) áttanulmányozása.

## Általános

A workshop folyamán a [Vulkan hivatalos C++ binding-ja](https://github.com/KhronosGroup/Vulkan-Hpp)it fogjuk használni, és a kód részletek is C++20-ban lettek megírva.

## Instance

Bármilyen Vulkan kód kiinduló eleme egy `vk::Instance` (C-ben `VkInstance`) objektum. Ez mondja meg, hogy milyen Vulkan verziót kívánunk használni, és milyen konfigurációkkal.

!!! info inline end ""

    A C++ binding-ban [RAII támogatás](https://github.com/KhronosGroup/Vulkan-Hpp/blob/main/vk_raii_ProgrammingGuide.md) is adott. De ez tapasztalat alapján nekünk inkább csak a fordítást fogja lassítani, minthogy a kódolásban könnyedséget okozna.

Mivel ez egy Vulkan handle, ezért használat után nekünk kell "felszabadítani". Habár nem best practice, de mi minden ilyen handle helyett egy C++ `unique_ptr`-hez hasonló struktúrával fogjuk használni ezeket, amit a Vulkan binding biztosít.

??? example "Hozzuk létre a `Renderer` osztályt!"

    ```cpp title="Renderer.hpp"
    #pragma once

    #include <vulkan/vulkan.hpp>

    class Renderer {
    public:
        Renderer();
        
    private:
        vk::UniqueInstance m_instance;
    };
    ```

    Mint minden, Vulkan-ban, az Instance létrehozása is explicit. Így ezt a kódot kiszervezzük egy külön függvénybe.

    ```cpp title="Renderer.cpp"
    #include "Renderer.hpp"

    [[nodiscard]] auto create_instance() -> vk::UniqueInstance {
        return {};
    }

    Renderer::Renderer() : m_instance{ create_instance() } {}
    ```

Bizonyos játékmotorokhoz külön hardveres támogatás van, amit a Vulkan-nak ilyenkor lehet jelezni. Ez, és a használni tervezett legnagyobb API verziót egy `vk::ApplicationInfo` struct-ba szerveződik. Mi az alapbeállításokat fogjuk használni, ami olyan, mintha az 1.0-s verziót kértük volna.

??? example "Hozzunk létre egy `vk::ApplicationInfo` típusú változót!"

    ```cpp
    constexpr vk::ApplicationInfo application_info{};
    ```

!!! info inline end "Vulkan layers"

    A mi kódunk és a Vulkan függvényeken keresztül hívott kód közé [**layer**](https://renderdoc.org/vulkan-layer-guide.html)-eket helyezhetünk el profiling-hoz, debug-oláshoz, stb...

Debug-oláshoz beállítünk [egy layer](https://vulkan-tutorial.com/Drawing_a_triangle/Setup/Validation_layers)-t, ami majd üzenet küld, ha elrontottunk valamit.

??? example "Ezt egy globális változóba szervezzük, mert még később is jól jöhet."

    ```cpp
    const std::vector<std::string> g_layers{
    #ifdef ENGINE_VULKAN_DEBUG
        "VK_LAYER_KHRONOS_validation"
    #endif
    };
    ```

!!! info ""

    A debug üzenetek megformázásához szokás egy úgynevezett [DebugUtilsMessenger](https://vulkan-tutorial.com/Drawing_a_triangle/Setup/Validation_layers#page_Message-callback) használata. Mi ezzel most nem foglalkozunk.

Ezeknék több mindent is beállíthatnánk még az Instance létrehozásához, de egyelőre megelékszünk ennyivel.

!!! example ""

    ```cpp
    const std::vector<const char*> g_layers{
    #ifdef ENGINE_VULKAN_DEBUG
        "VK_LAYER_KHRONOS_validation"
    #endif
    };

    [[nodiscard]] auto create_instance() -> vk::UniqueInstance
    {
        constexpr static vk::ApplicationInfo application_info{
            .apiVersion = VK_API_VERSION_1_1
        };

        const vk::InstanceCreateInfo create_info{
            .pApplicationInfo  = &application_info,
            .enabledLayerCount = static_cast<uint32_t>(g_layers.size()),
            .ppEnabledLayerNames = g_layers.data(),
        };

        return vk::createInstanceUnique(create_info);
    }
    ```

## Physical Device

Minden a gépen jelen levő Vulkan-képes processzort egy `vk::PhysicalDevice` reprezentál könyvtár. Ezeket a `vk::Instance::enumeratePhysicalDevices` függvénnyel le is kérhetjük.

Érdemes egy diszkrét GPU-val dolgozni, ha az jelen van, rosszabb esetben integrálttal. Így válasszuk ki a számunkra legmegfelelőbb egységet.

!!! example ""

    ```cpp
        [[nodiscard]] auto choose_physical_device(vk::Instance t_instance) -> vk::PhysicalDevice
        {
        const auto physical_devices{ t_instance.enumeratePhysicalDevices() };

        if (std::ranges::empty(physical_devices)) {
            throw std::runtime_error{ "No Vulkan physical device is available." };
        }

        auto ranked_devices_view{
            physical_devices
            | std::views::transform([](vk::PhysicalDevice t_physical_device) {
                switch (t_physical_device.getProperties().deviceType) {
                    case vk::PhysicalDeviceType::eDiscreteGpu:
                        return std::make_pair(t_physical_device, 2);
                    case vk::PhysicalDeviceType::eIntegratedGpu:
                        return std::make_pair(t_physical_device, 1);
                    default: return std::make_pair(t_physical_device, 0);
                }
            })
        };
        std::vector ranked_devices(
            ranked_devices_view.begin(), ranked_devices_view.end()
        );

        std::ranges::sort(
            ranked_devices,
            std::ranges::greater{},
            &std::pair<vk::PhysicalDevice, int>::second
        );

        return ranked_devices.front().first;
    }
    ```

!!! danger "*A PhysicalDevice-t már nem szabad felszabadítani!*"

## (Logical) Device

Mielőtt elkezdünk a GPU-n dolgozni, azelőtt elengedhetetlen a használni tervezett funkciók megadása. Habár, mi most ezek közül nem fogunk sokat használni, azért érdemes ezt is megemlíteni.

A (Logical) Device szintaktikailag nagyon hasonlít az Instance-hez. Itt is lesz olyan amit majd csak később állítunk be. A mostani alkalommal egyedül a `vk::Queue`-kra fogunk koncentrálni.

A grafikus kártya egy hihetetlenül parallelizált eszköz. Ám ennek a kihasználásához adatot kell neki küldeni, és megmondani hogy mit csináljon. Ez a parancs feldolgozás *Queue*-kon keresztül történik, amelyek képesek párhuzamosan több "command" végrehajtására.

!!! note ""

    Habár egy `vk::Queue` egyszerre több `vk::CommandBuffer` végrehajtására is képes, `vk::CommandBuffer`-eket feldolgozásra küldeni egy `vk::Queue`-nak továbbra is csak egy szálon lehetséges.

Egy `vk::Queue` többféle feladat végrehajtására is képes - legyen az grafikai, általános, adat-átvitel, vagy valami más. A GPU tervezők számunkra a hasonló tulajdonságokkal rendelkező *Queue*-kat úgynevezett *Queue family*-kben teszik elérhetővé.

Válasszunk ki egy grafikai munkát támogató családot (ezek megkötése, hogy adat-átvitelre is képesek legyenek), és abból is egy *Queue*-t. A mi céljainkhoz ez az egy elég lesz mindenre.

!!! example ""

    ```cpp
    [[nodiscard]] auto find_graphics_queue_family(
        vk::PhysicalDevice t_physical_device
    ) -> uint32_t
    {
        uint32_t index{};
        for (const auto& properties : t_physical_device.getQueueFamilyProperties())
        {
            if (properties.queueFlags & vk::QueueFlagBits::eGraphics) {
                return index;
            }
            index++;
        }
        throw std::runtime_error{ "Could not find graphics queue family" };
    }
    ```

Végül hozzuk létre a *Device*-t is.

!!! example ""

    ```cpp
    [[nodiscard]] auto create_device(const vk::PhysicalDevice t_physical_device)
        -> vk::UniqueDevice
    {
        const vk::DeviceQueueCreateInfo queue_create_info{
            .queueFamilyIndex = find_graphics_queue_family(t_physical_device),
            .queueCount       = 1,
        };

        vk::DeviceCreateInfo device_create_info{
            .queueCreateInfoCount = 1,
            .pQueueCreateInfos    = &queue_create_info,
        };

        return t_physical_device.createDeviceUnique(device_create_info);
    }
    ```

!!! tip ""

    A `Renderer`-hez adjuk hozzá az így létrejövő `vk::UniqueDevice`-t, és a kiválasztott `vk::Queue`-t is. (Utóbbi lentebb a végső kódban látható.)

## Elkészült kód

!!! example ""

    ```cpp title="Renderer.hpp"
    #pragma once

    #include <vulkan/vulkan.hpp>

    class Renderer {
    public:
        Renderer();

    private:
        vk::UniqueInstance m_instance;
        vk::UniqueDevice m_device;
        vk::Queue m_graphics_queue;
    };
    ```

    ```cpp title="Renderer.cpp"
    #include "Renderer.hpp"

    #include <algorithm>
    #include <ranges>
    #include <vector>

    const std::vector<const char*> g_layers{
    #ifdef ENGINE_VULKAN_DEBUG
        "VK_LAYER_KHRONOS_validation"
    #endif
    };

    [[nodiscard]] auto create_instance() -> vk::UniqueInstance
    {
        constexpr static vk::ApplicationInfo application_info{
            .apiVersion = VK_API_VERSION_1_1
        };

        const vk::InstanceCreateInfo create_info{
            .pApplicationInfo    = &application_info,
            .enabledLayerCount   = static_cast<uint32_t>(g_layers.size()),
            .ppEnabledLayerNames = g_layers.data(),
        };

        return vk::createInstanceUnique(create_info);
    }

    [[nodiscard]] auto choose_physical_device(const vk::Instance t_instance)
        -> vk::PhysicalDevice
    {
        const auto physical_devices{ t_instance.enumeratePhysicalDevices() };

        if (std::ranges::empty(physical_devices)) {
            throw std::runtime_error{ "No Vulkan physical device is available." };
        }

        auto ranked_devices_view{
            physical_devices
            | std::views::transform([](vk::PhysicalDevice t_physical_device) {
                switch (t_physical_device.getProperties().deviceType) {
                    case vk::PhysicalDeviceType::eDiscreteGpu:
                        return std::make_pair(t_physical_device, 2);
                    case vk::PhysicalDeviceType::eIntegratedGpu:
                        return std::make_pair(t_physical_device, 1);
                    default: return std::make_pair(t_physical_device, 0);
                }
            })
        };
        std::vector ranked_devices(
            ranked_devices_view.begin(), ranked_devices_view.end()
        );

        std::ranges::sort(
            ranked_devices,
            std::ranges::greater{},
            &std::pair<vk::PhysicalDevice, int>::second
        );

        return ranked_devices.front().first;
    }

    [[nodiscard]] auto find_graphics_queue_family(
        const vk::PhysicalDevice t_physical_device
    ) -> uint32_t
    {
        uint32_t index{};
        for (const auto& properties : t_physical_device.getQueueFamilyProperties())
        {
            if (properties.queueFlags & vk::QueueFlagBits::eGraphics) {
                return index;
            }
            index++;
        }
        throw std::runtime_error{ "Could not find graphics queue family" };
    }

    [[nodiscard]] auto create_device(const vk::PhysicalDevice t_physical_device)
        -> vk::UniqueDevice
    {
        const vk::DeviceQueueCreateInfo queue_create_info{
            .queueFamilyIndex = find_graphics_queue_family(t_physical_device),
            .queueCount       = 1,
        };

        vk::DeviceCreateInfo device_create_info{
            .queueCreateInfoCount = 1,
            .pQueueCreateInfos    = &queue_create_info,
        };

        return t_physical_device.createDeviceUnique(device_create_info);
    }

    Renderer::Renderer()
        : m_instance{ create_instance() },
        m_device{ create_device(choose_physical_device(*m_instance)) },
        m_graphics_queue{ m_device->getQueue(
            find_graphics_queue_family(choose_physical_device(*m_instance)),
            0
        ) }
    {}
    ```