Hlavní navigace

Vulkan: třída zapouzdřující okno

15. 7. 2022
Doba čtení: 17 minut

Sdílet

 Autor: Depositphotos, Vulkan
Minule jsme si ukázali, jak otevřít okno na třech plaformách. Dnes vytvoříme třídu zapouzdřující okno Vulkanu a ošetříme některé záludnosti správy prostředků v kontextu možné přítomnosti výjimek.

Tento díl bude mít dva cíle. Nejprve sjednotíme všechny naše zdrojáky z minulého dílu do jediné třídy VulkanWindow, přičemž si dáme pozor na správu prostředků této třídy s ohledem na možné výjimky. Druhý cíl pak bude jednoduchý algoritmus pro výběr nejvhodnější grafické karty, na které budeme chtít rendrovat. Význam tohoto algoritmu vynikne například na noteboocích se dvěma grafikami, kde můžeme preferovat, aby se aplikace nespustila na integrované grafice, ale aby běžela na dedikované grafice a využila tak plného grafického potenciálu, který daný notebook skrývá.

Můžeme si stáhnout zdrojáky a zkusit je zkompilovat.

Zapouzdření do VulkanWindow

V minulém díle jsme vytvářeli okno ve třech různých souborech pro tři různé platformy. V tomto díle vše sjednotíme do jediné třídy VulkanWindow. Můžeme si otevřít soubor VulkanWindow.h a začít hned od začátku souboru, kde jinak většinou najdeme klauzule include:

#if defined(USE_PLATFORM_WIN32)
  typedef struct HWND__* HWND;
  typedef struct HINSTANCE__* HINSTANCE;
  typedef unsigned short ATOM;
#elif defined(USE_PLATFORM_XLIB)
  typedef struct _XDisplay Display;
  typedef unsigned long Window;
  typedef unsigned long Atom;
#elif defined(USE_PLATFORM_WAYLAND)
  #include "xdg-shell-client-protocol.h"
  #include "xdg-decoration-client-protocol.h"
#endif

První pohled na tento kód nás překvapí definicemi typů pro jednotlivé platformy. Pouze Wayland má poctivé klauzule include. Proč to? Důvod je prostý: Minimalizujeme množství hlavičkových souborů, které se includují v případě, že někdo použije náš VulkanWindow.h. Méně hlavičkových souborů pak znamená rychlejší kompilaci a občas i podstatně méně problémů.

Windows.h je typickým reprezentantem hlavičkových souborů, které opravdu umějí programátorovi zkomplikovat život. Tento hlavičkový soubor includuje obrovské množství dalších souborů a doslova znečistí kompilační prostředí obrovským množstvím maker a definicí typů v globálním namespace. Ty pak kolidují s názvy proměnných a funkcí v našem vlastním kódu.

Navíc, includovat windows.h znamená na mém i7–10875@2.3GHz (boost 5.1GHz) prodloužit kompilaci o 350ms v každém souboru, kde je includován. To samozřejmě platí pro jednovláknovou kompilaci. Ale i při paralelní kompilaci je každé vlákno zpožděno přibližně o tento čas. Definovat si typy HWND, HINSTANCE a ATOM sami nám ušetří všechny tyto problémy. Pro platformu Xlib provedeme totéž a definujeme typy Display, Window a Atom. Tato API jsou na těchto platformách natolik binárně stabilní, že si to můžeme dovolit udělat.

Následuje začátek definice třídy:

class VulkanWindow {
public:

   typedef void FrameCallback();
   typedef void RecreateSwapchainCallback(const vk::SurfaceCapabilitiesKHR& surfaceCapabilities, vk::Extent2D newSurfaceExtent);

protected:

Ve třídě budeme vždy nejprve uvádět její data, neboť ta tvoří srdce třídy, nikoliv desítky metod, která nad těmito daty pracují. Data budeme dávat nejčastěji jako protected. Výjimečně se však vyskytnou typy, které potřebujeme definovat úplně hned na začátku třídy. Zde je to FrameCallback a RecreateSwapchainCallback. Callbacky těchto typů pak budeme volat z této třídy.

Následují proměnné, jak bylo avizováno, na začátku třídy. Začneme proměnnými pro Win32 a Xlib platformu:

#if defined(USE_PLATFORM_WIN32)

   HWND _hwnd = nullptr;
   std::exception_ptr _wndProcException;
   HINSTANCE _hInstance;
   ATOM _windowClass = 0;

   static inline const std::vector<const char*> _requiredInstanceExtensions =
      { "VK_KHR_surface", "VK_KHR_win32_surface" };

#elif defined(USE_PLATFORM_XLIB)

   Display* _display = nullptr;
   Window _window = 0;
   Atom _wmDeleteMessage;

   static inline const std::vector<const char*> _requiredInstanceExtensions =
      { "VK_KHR_surface", "VK_KHR_xlib_surface" };

Význam jednotlivých proměnných si vysvětlíme až v kódu, nicméně z velké části jejich význam můžeme tušit z minulého dílu. Mnohé může zarazit použití podtržítka před názvy proměnných. Objevila se totiž praxe převzatá z jazyka Python začínat privátní (či protected) proměnné ve třídách podtržítkem. Objevily se dokonce názory, že je to proti standardu C++, ale není. C a C++ nedovoluje dvě podtržítka na začátku a nedovoluje jedno podtržítko následované velkým písmenem. Ale podtržítko a malé písmeno neodporuje standardu. Toto platí pro proměnné tříd. V globálním namespace jsou však jména začínající podtržítkem rezervovaná. Globální proměnné tedy nikdy začínat podtržítkem nebudeme.

Druhá věc, která nás může překvapit, je nepoužití unique objektů pro jednotlivé proměnné, které by velmi elegantně zajišťovaly korektní uvolňování prostředků při výjimkách. Místo toho budeme všechny alokované prostředky uvolňovat v destruktoru VulkanWindow. Proč takováto změna?

Argumenty pro toto řešení jsou dva: Jednak to odstraní nutnost struktur UniqueWindow a jí podobným z minulého dílu, na Waylandu nutnost definovat Deleter struktury pro unique_ptr a podobně. Druhý důvod je drobně vyšší spotřeba paměti u unique objektů z Vulkan.hpp. Ve Vulkan.hpp unique objekty obyčejně nesou kromě handlu daného resource i ukazatel na device, neboť device je potřeba pro likvidaci handlu. Navíc potřebují i allocator, který se používá vyjímečně, nicméně tam musí být. Místo jednoho pointeru (8 bajtů) tedy máme obyčejně tři (24 bajtů). Naše řešení je tedy zapouzdření do VulkanWindow třídy, která se postará o likvidaci všech prostředků, které vlastní. Ušetříme tak trochu paměti a zbavíme se škaredého kódu Deleterů, UniqueWindow a jím podobným.

Vraťme se však ke kódu. Následují proměnné, které použijeme na platformě Wayland:

#elif defined(USE_PLATFORM_WAYLAND)

   // globals
   wl_display* _display = nullptr;
   wl_registry* _registry;
   wl_compositor* _compositor;
   xdg_wm_base* _xdgWmBase = nullptr;
   zxdg_decoration_manager_v1* _zxdgDecorationManagerV1;

   // objects
   wl_surface* _wlSurface = nullptr;
   xdg_surface* _xdgSurface = nullptr;
   xdg_toplevel* _xdgTopLevel = nullptr;
   zxdg_toplevel_decoration_v1* _decoration = nullptr;

   // state
   bool _running = true;

   // listeners
   wl_registry_listener _registryListener;
   xdg_wm_base_listener _xdgWmBaseListener;
   xdg_surface_listener _xdgSurfaceListener;
   xdg_toplevel_listener _xdgToplevelListener;

   static inline const std::vector<const char*> _requiredInstanceExtensions =
      { "VK_KHR_surface", "VK_KHR_wayland_surface" };

#endif

Kód je lehčí o množství Deleter objektů a že, tak jako na předchozích dvou platformách, opět nepoužívá unique_ptr. O likvidaci objektů se totiž postará destruktor VulkanWindow.

Následuje několik proměnných, které budeme potřebovat napříč platformami:

   std::function<FrameCallback> _frameCallback;
   vk::Instance _instance;
   vk::PhysicalDevice _physicalDevice;
   vk::Device _device;
   vk::SurfaceKHR _surface;

   vk::Extent2D _surfaceExtent = vk::Extent2D(0,0);
   std::function<RecreateSwapchainCallback> _recreateSwapchainCallback;

Metody VulkanWindow

Jádro rozhraní VulkanWindow, včetně budoucích dílů, pak bude množina metod, kterou začněme od inicializace a destrukce:

   VulkanWindow() = default;
   ~VulkanWindow();
   void destroy() noexcept;
   vk::SurfaceKHR init(vk::Instance instance, vk::Extent2D surfaceExtent, const char* title = "Vulkan window");

V prvé řadě oddělíme default konstruktor a metodu init. Toto oddělení dá uživateli naší třídy možnost deklarovat si VulkanWindow třeba jako globální objekt, aniž by v tu chvíli musel inicializovat celý objekt. K inicializaci by totiž potřeboval vk::Instance, a ta při konstrukci globálních proměnných typicky ještě neexistuje. Navíc můžeme volat init() metodu vícenásobně pro re-inicializaci našeho okna.

Druhé věci, které si můžeme všimnout, je oddělení destruktoru a metody destroy(). Destruktor volá destroy() a až destroy() teprve likviduje celý objekt. Takto můžeme využít destroy() i z ostatních metod. Například při vícenásobném zavolání init() vždy před novou inicializací VulkanWindow zavoláme destroy() a uvolníme tak všechny dříve alokované zdroje.

Třetí úvaha jsou pak výjimky. Opět se potřebujeme ujistit, že při vzniku jakékoliv výjimky v kódu budou uvolněny všechny alokované prostředky a to i kdyby ten, kdo vyhodil výjimku, byl samotný konstruktor. V případě výjimky v konstruktoru se totiž nevolá destruktor. Jinými slovy, destruktor je volán pouze pro objekty, pro něž doběhl konstruktor až do konce. Pokud tedy alokujeme něco v těle konstruktoru, měli bychom to buď uložit do proměnné, která má destruktor zodpovědný za případné uvolnění, např. unique_ptr, nebo bychom měli kód konstruktoru uzavřít do try-catch bloku a být připraveni uvolnit již alokované prostředky.

Před tělem konstruktoru pak může probíhat ještě „member initialization“ – inicializace členů třídy. Tam ale nemáme možnost žádné výjimky odchytávat. Proto členy třídy, které samy nemají destruktor, například inicializujeme pouze na nullptr a samotnou případnou alokaci provedeme až v těle konstruktoru, kde již máme možnost odchytat výjimky a případně po sobě korektně uklidit. My zmíněné problémy obejdeme už v návrhu tím, že v konstruktoru nic nealokujeme a přenecháme tuto činnost až do metody  init().

Tím máme navrženu inicializaci a destrukci třídy. Další část rozhraní tvoří metody, které budeme pravděpodobně potřebovat po zavolání VulkanWindow::init(). Jsou jimi:

   void setRecreateSwapchainCallback(std::function<RecreateSwapchainCallback>&& cb);
   void setRecreateSwapchainCallback(const std::function<RecreateSwapchainCallback>& cb);
   void setFrameCallback(std::function<FrameCallback>&& cb, vk::PhysicalDevice physicalDevice, vk::Device device);
   void setFrameCallback(const std::function<FrameCallback>& cb, vk::PhysicalDevice physicalDevice, vk::Device device);
   void mainLoop();

První dvojice metod nastavuje callback, který bude zavolán ve chvíli, kdy je potřeba nový swapchain. Co je swapchain se dozvíme v příštím díle. A kdy jej potřebujeme znova vytvořit je typicky při změně velikosti okna nebo při startu aplikace. Pro callbacky budeme často používat std::function pro její velkou flexibilitu. Někdo by mohl namítnout, že std::function není výkonnostně efektivní. Nicméně dle mého měření na i7–10875@2.3GHz (boost 5.1GHz) rychlost volání není nějaký problém: 1.9 a 2.3ns při porovnání C-čkového callbacku se std::function. Toto platí, pokud nepředáváme žádné parametry.

S počtem parametrů pak záleží na kompilátoru, jak efektivně se porve s jejich předáváním na zásobníku. Nicméně i pro čtyři int parametry není zpoždění velké: 2.0ns proti 3.0ns. Pokud se tedy nebude jednat o výkonnostně kritické rutiny, budeme často používat std::function pro její velkou flexibilitu. Doplňme ještě, že samotná std::function typicky na mnou testovaných kompilátorech zabírala 32 bajtů, tedy čtyři pointery.

Druhá dvojice metod nastaví frame callback. Tento callback bude zavolán vždy, když vznikne požadavek na vyrendrování nového snímku.

Pátá metoda mainLoop() pak rozjede smyčku zpráv a vrátí se teprve, až je zavřeno okno aplikace.

Poslední část metod tvoří gettery a statické funkce vracející seznam vyžadovaných extensions:

   vk::SurfaceKHR surface() const;
   vk::Extent2D surfaceExtent() const;

   // required Vulkan Instance extensions
   static const std::vector<const char*>& requiredExtensions();
   static void appendRequiredExtensions(std::vector<const char*>& v);
   static uint32_t requiredExtensionCount();
   static const char* const* requiredExtensionNames();

Vyžadované extensions se liší podle platformy. Už jsme si je uvedli v minulém díle a nyní si jejich seznam pro danou platformu snadno dohledáme v kódu.

Implementace všech inline metod je triviální a samovysvětlující, takže si je ani nebudeme uvádět. Místo toho se podíváme do VulkanWindow.cpp. Zmíníme jen to nejzajímavější, neboť používáme mnoho z kódu vysvětleného v minulém díle. První zajímavost je hned ve  VulkanWindow::destroy().

Ve VulkanWindow::destroy() máme kód, který uvolňuje všechny prostředky, které VulkanWindow vlastní. Kód je pak pro všechny podporované platformy. Některé použité funkce ale mohou vrátit chybový kód. V takových případech jsme vyhazovali výjimky. Zde si ale podobnou věc nedovolíme. Metoda destroy() je volána přímo z destruktoru a v rámci běhu destruktoru se opravdu nedoporučuje vyhazovat výjimky. Metodu destroy() tedy označíme jako noexcept a případné chyby budeme v release configuraci ignorovat a v debug je oznámíme programátorovi assertem. Dobře navržené API by totiž spíše nemělo vracet chyby při uvolňování prostředků, kromě výjimek jako je špatné použití API a podobně.

Do metody VulkanWindow::init() se přemístilo mnoho kódu ze všech třech platforem minulého dílu. Na platformě Win32 si můžeme všimnout, že jsme doplnili obsluhu dalších zpráv. Kromě WM_CLOSE nyní obsluhujeme i WM_PAINT  a WM_ERASEBKGND. Vyčistit pozadí okna před rendrováním nepotřebujeme, takže pouze vrátíme jedničku. Nicméně WM_PAINT je zajímavější. Za prvé musíme validovat obsah okna, aby nám zpráva WM_PAINT nechodila stále dokola. To provedeme funkcí ValidateRect(). Za druhé zavoláme náš frame callback, aby se vyrendroval obsah okna. A za třetí musíme uvážit, že v tomto kódu může vzniknout výjimka a Win32 API nemá rádo, když se šíří výjimky ven z funkce pro obsluhu zpráv okna. Všechny výjimky tedy odchytíme a uložíme si je do wndProcException. V hlavní smyčce aplikace je pak znova vyhodíme.

Pokud by se někdo podivoval nad #if _UNICODE, konverzí utf8 na wstring a _T makrem, je to pro přepínání mezi multi-byte a unicode kódováním textu na Windows. Tuto volbu přepínáme ve Visual C++ v nastavení projektu.

V kódu nám navíc přibyla funkce SetWindowLongPtr(), kterou si uložíme ukazatel na VulkanWindow přímo do okna. Ve wndProc si funkcí GetWindowLongPtr() tento ukazatel opět vyzvedneme. Vše funguje díky rezervaci 8 byte ve struktuře WNDCLASSEX v položce  cbWndExtra.

Kód pro platformu Xlib je prakticky stejný. Pro Wayland je jediný větší rozdíl v přidání podpory pro zxdg_decoration_manager_v1 Wayland interface, který vytvoří kolem okna dekorace. Bez dekorací by nám chybělo i tlačítko „x“ pro uzavření okna. Název zxdg_decoration_manager_v1 může vypadat zvláštně. Jedná se totiž ještě o nestabilní protokol ve verzi 1. Toto platí v létě 2022, kdy tento článek vychází. Nicméně, až bude protokol prohlášen za stabilní, změní se jeho název na xdg_decoration_manager, tedy bez počátečního „z“ a bez přípony v podobě verze protokolu. Tato změna pak bude pravděpodobně promítnuta na zdrojáky tohoto tutoriálu umístěného na GitHubu.

Soubor main.cpp

Při pohledu do souboru main.cpp si již nebudeme uvádět seznam globálních proměnných. Pouze připomínám, že jejich pořadí není náhodné, ale uvádí pořadí jejich destrukce při ukončování aplikace, tedy od poslední proměnné k první. Zvláště vk::Instance a okno a jeho alokované zdroje musí být uvolněny jako poslední. V případě nedodržení těchto pravidel může sice občas aplikace fungovat zdánlivě správně, nicméně může havarovat na driveru jiného výrobce. Proto existují validační vrstvy. Ty mohou pomoci s odhalením špatného použití API Vulkan. Případní zájemci je mohou aktivovat v utilitě  vkconfig.

V samotné funkci main() nejprve vytvoříme instanci tak, jak jsme zvyklí:

// Vulkan instance
instance =
   vk::createInstanceUnique(
      vk::InstanceCreateInfo{
         vk::InstanceCreateFlags(),  // flags
         &(const vk::ApplicationInfo&)vk::ApplicationInfo{
            appName,                 // application name
            VK_MAKE_VERSION(0,0,0),  // application version
            nullptr,                 // engine name
            VK_MAKE_VERSION(0,0,0),  // engine version
            VK_API_VERSION_1_0,      // api version
         },
         0, nullptr,  // no layers
         VulkanWindow::requiredExtensionCount(),  // enabled extension count
         VulkanWindow::requiredExtensionNames(),  // enabled extension names
      }
   );

Jediný větší rozdíl je ve specifikování vyžadovaných instance-level extensions. Na ty se nyní budeme dotazovat VulkanWindow tak, jak vidíme v tučně zvýrazněném kódu. VulkanWindow nám podle použité platformy vrátí vyžadované extensions.

Dalším krokem je vytvoření surface:

// create surface
vk::SurfaceKHR surface =
   window.init(instance.get(), {1024, 768}, appName);

VulkanWindow::init() nám zapouzdřilo celou konstrukci okna a vytvoření surface. Jako první parametr předáváme objekt vk::Instance, jako druhý rozměry okna a jako třetí titulek okna. Surface je nám vrácen, ale jeho vlastníkem je VulkanWindow, které se postará i o jeho uvolnění při ukončení aplikace. Proto si vracíme pouze vk::SurfaceKHR a nikoliv  vk::UniqueSurfaceKHR.

Dalším úkolem je výběr kompatibilního fyzického zařízení. Výběr je však složitější, než v minulém díle. Zařízení musí podporovat VK_KHR_swapchain extension, musí mít grafickou frontu a musí podporovat prezentaci. Zařízení, která toto splňují, si uložíme do  compatibleDevices:

// find compatible devices
vector<vk::PhysicalDevice> deviceList = instance->enumeratePhysicalDevices();
vector<tuple<vk::PhysicalDevice, uint32_t, uint32_t, vk::PhysicalDeviceProperties>> compatibleDevices;
for(vk::PhysicalDevice pd : deviceList) {

   // skip devices without VK_KHR_swapchain
   auto extensionList = pd.enumerateDeviceExtensionProperties();
   for(vk::ExtensionProperties& e : extensionList)
      if(strcmp(e.extensionName, "VK_KHR_swapchain") == 0)
         goto swapchainSupported;
   continue;
   swapchainSupported:

   // select queues for graphics rendering and for presentation
   uint32_t graphicsQueueFamily = UINT32_MAX;
   uint32_t presentationQueueFamily = UINT32_MAX;
   vector<vk::QueueFamilyProperties> queueFamilyList = pd.getQueueFamilyProperties();
   for(uint32_t i=0, c=uint32_t(queueFamilyList.size()); i<c; i++) {

      // test for presentation support
      if(pd.getSurfaceSupportKHR(i, surface)) {

         // test for graphics operations support
         if(queueFamilyList[i].queueFlags & vk::QueueFlagBits::eGraphics) {
            // if presentation and graphics operations are supported on the same queue,
            // we will use single queue
            compatibleDevices.emplace_back(pd, i, i, pd.getProperties());
            goto nextDevice;
         }
         else
            // if only presentation is supported, we store the first such queue
            if(presentationQueueFamily == UINT32_MAX)
               presentationQueueFamily = i;
      }
      else {
         if(queueFamilyList[i].queueFlags & vk::QueueFlagBits::eGraphics)
            // if only graphics operations are supported, we store the first such queue
            if(graphicsQueueFamily == UINT32_MAX)
               graphicsQueueFamily = i;
      }
   }

   if(graphicsQueueFamily != UINT32_MAX && presentationQueueFamily != UINT32_MAX)
      // presentation and graphics operations are supported on the different queues
      compatibleDevices.emplace_back(pd, graphicsQueueFamily, presentationQueueFamily, pd.getProperties());
   nextDevice:;
}

Procházíme tedy všechna fyzická zařízení a přeskočíme ty, které nepodporují VK_KHR_swapchain, což je device-level extension. Pro ty, které podporují tuto extension, procházíme seznam tříd front. U každé se zeptáme, zda podporuje prezentaci na náš surface a zda podporuje grafické operace. Pokud podporuje některá třída front obojí naráz, což bude naše preferenční volba, uložíme si ji do compatibleDevices se stejným indexem pro grafickou i prezentační třídu front. V opačném případě si uložíme index první fronty, která podporuje prezentaci a první fronty, která podporuje grafické operace.

Následně si seznam kompatibilních zařízení vypíšeme:

// print compatible devices
cout << "Compatible devices:" << endl;
for(auto& t : compatibleDevices)
   cout << "   " << get<3>(t).deviceName << " (graphics queue: " << get<1>(t)
         << ", presentation queue: " << get<2>(t)
         << ", type: " << to_string(get<3>(t).deviceType) << ")" << endl;

Výpis na obrazovce může vypadat například takto:

Compatible devices:
   Intel(R) HD Graphics 530 (SKL GT2) (graphics queue: 0, presentation queue: 0, type: IntegratedGpu)
   NVIDIA GeForce GTX 1050 (graphics queue: 0, presentation queue: 0, type: DiscreteGpu)
   AMD RADV POLARIS11 (graphics queue: 0, presentation queue: 0, type: DiscreteGpu)
   llvmpipe (LLVM 13.0.1, 256 bits) (graphics queue: 0, presentation queue: 0, type: Cpu)

Jak vidíme, výpis obsahuje čtyři vulkaní fyzická zařízení. Na každém řádku se nachází název zařízení, grafická fronta, prezentační fronta a typ zařízení. Co se týká typu zařízení, máme integrované GPU, dvě diskrétní grafiky a jednu cpu implementaci. Jak vybrat jediné zařízení, které je pro uživatele nejvhodnější?

Obyčejně bude nejlepší volbou jedna z diskrétních grafik. Pokud na daném počítači není diskrétní grafika, pak se jeví jako vhodná volba integrovaná grafika. V dalším sledu by to pak byla virtuální grafika typicky využívající virtualizaci, pak cpu implementace, a pak vše ostatní. Vše implementuje následující kód hledající bestDevice v našem seznamu compatibleDevices. Klíčovou roli zde hraje pole deviceTypeScore, které každému typu grafiky přiřazuje skóre. Toto skóre je ještě zvětšeno o jedničku, pokud zařízení používá stejnou frontu pro grafické operace i pro prezentaci:

// choose the best device
auto bestDevice = compatibleDevices.begin();
if(bestDevice == compatibleDevices.end())
   throw runtime_error("No compatible devices.");
constexpr const array deviceTypeScore = {
   10, // vk::PhysicalDeviceType::eOther         - lowest score
   40, // vk::PhysicalDeviceType::eIntegratedGpu - high score
   50, // vk::PhysicalDeviceType::eDiscreteGpu   - highest score
   30, // vk::PhysicalDeviceType::eVirtualGpu    - normal score
   20, // vk::PhysicalDeviceType::eCpu           - low score
   10, // unknown vk::PhysicalDeviceType
};
int bestScore = deviceTypeScore[clamp(int(get<3>(*bestDevice).deviceType), 0, int(deviceTypeScore.size())-1)];
if(get<1>(*bestDevice) == get<2>(*bestDevice))
   bestScore++;
for(auto it=compatibleDevices.begin()+1; it!=compatibleDevices.end(); it++) {
   int score = deviceTypeScore[clamp(int(get<3>(*it).deviceType), 0, int(deviceTypeScore.size())-1)];
   if(get<1>(*it) == get<2>(*it))
      score++;
   if(score > bestScore) {
      bestDevice = it;
      bestScore = score;
   }
}
cout << "Using device:\n"
        "   " << get<3>(*bestDevice).deviceName << endl;
physicalDevice = get<0>(*bestDevice);
graphicsQueueFamily = get<1>(*bestDevice);
presentationQueueFamily = get<2>(*bestDevice);

V našem případě máme dvě diskrétní grafiky, které získaly nejvyšší skóre. Dnes nebudeme vymýšlet žádné další složitosti, abychom mezi nimi rozhodli, a jednoduše vybereme první grafiku s nejvyšším skóre, tedy NVIDIA GeForce GTX 1050.

Následuje vytvoření logického zařízení. V kódu jsou zvýrazněny řádky, které jsou odlišné od toho, na co jsme zvyklí z minulých dílů:

// create device
device =
   physicalDevice.createDeviceUnique(
      vk::DeviceCreateInfo{
         vk::DeviceCreateFlags(),  // flags
         graphicsQueueFamily==presentationQueueFamily ? uint32_t(1) : uint32_t(2),  // queueCreateInfoCount
         array{  // pQueueCreateInfos
            vk::DeviceQueueCreateInfo{
               vk::DeviceQueueCreateFlags(),
               graphicsQueueFamily,
               1,
               &(const float&)1.f,
            },
            vk::DeviceQueueCreateInfo{
               vk::DeviceQueueCreateFlags(),
               presentationQueueFamily,
               1,
               &(const float&)1.f,
            },
         }.data(),
         0, nullptr,  // no layers
         1,           // number of enabled extensions
         array<const char*, 1>{ "VK_KHR_swapchain" }.data(),  // enabled extension names
         nullptr,    // enabled features
      }
   );

V kódu jsou tři změny. První se týká počtu front. Tu nastavujeme na hodnotu jedna, pokud je grafická a prezentační fronta jedna a ta samá. Jinak použijeme fronty dvě. V poli informací o frontách pak předáváme dvě struktury: Jedna pro grafickou a druhá pro prezentační frontu. Pokud používáme jedinou frontu, bude druhá struktura ignorována. A poslední změna nás čeká mezi požadovanými extensions, kde budeme vyžadovat podporu VK_KHR_swapchain, kterou budeme používat už v příštím díle pro zobrazování na obrazovku.

Kód v main.cpp  zakončíme získáním front, nastavením prázdných callbacků a zavoláním  VulkanWindow::mainLoop():

// get queues
graphicsQueue = device->getQueue(graphicsQueueFamily, 0);
presentationQueue = device->getQueue(presentationQueueFamily, 0);

// set window callbacks
window.setRecreateSwapchainCallback(
   [](const vk::SurfaceCapabilitiesKHR&, vk::Extent2D){}
);
window.setFrameCallback(
   [](){},
   physicalDevice,
   device.get()
);

// run main loop
window.mainLoop();

Kód callbacků si necháme na další díl. V tomto díle je pouze nastavíme na prázdné lambda funkce. Pak již jen zavoláme hlavní smyčku aplikace. Hlavní smyčka aplikace nyní nově volá zaregistrované callbacky. Detailně si ji však popíšeme až příště, kdy i callbacky vyplníme smysluplným kódem a začneme rendrovat na obrazovku.

Závěr

Dnes jsme sjednotili kód pro tři platformy pod jediné rozhraní třídy VulkanWindow a vyřešili jsme správu prostředků této třídy v prostředí s výjimkami. Následně jsme za pomoci této třídy vytvořili okno a mezi vhodnými fyzickými zařízeními vybrali to nejvhodnější podle jednoduchých kritérií. Na závěr jsme si zaregistrovali callbacky pro VulkanWindow a rozjeli hlavní smyčku aplikace. Příště si vytvoříme swapchain, se kterým si poprvé zobrazíme výsledky našeho rendrování na obrazovku.