Při startu aplikace potřebujeme často ověřit, zda je podporována požadovaná verze Vulkan, požadovaný formát textur, zda je k dispozici geometry shader, a kolik grafické paměti je k dispozici, atd. Podle těchto informací můžeme z více zařízení, pokud jsou k dispozici, vybrat to, které více odpovídá našim potřebám.
Pojďme tedy na věc. Můžeme si stáhnout zdrojové kódy a zkusit je zkompilovat. Po jejich spuštění bychom měli vidět výstup podobný tomuto:
Vulkan instance version: 1.1.0 Physical devices: GeForce GTX 1050 Vulkan version: 1.1.84 MaxTextureSize: 32768 Geometry shader: supported Memory heaps: 0: 1958MiB 1: 32168MiB Queue families: 0: gct (count: 16) 1: t (count: 1) 2: c (count: 8) R8G8B8A8Unorm format support for color attachment: Images with linear tiling: no Images with optimal tiling: yes Buffers: no Radeon(TM) RX 460 Graphics Vulkan version: 1.1.101 MaxTextureSize: 16384 Geometry shader: supported Memory heaps: 0: 1792MiB 1: 16084MiB 2: 256MiB Queue families: 0: gct (count: 1) 1: ct (count: 3) 2: t (count: 2) R8G8B8A8Unorm format support for color attachment: Images with linear tiling: yes Images with optimal tiling: yes Buffers: no Intel(R) HD Graphics 530 Vulkan version: 1.0.31 MaxTextureSize: 16384 Geometry shader: supported Memory heaps: 0: 1824MiB Queue families: 0: gct (count: 1) R8G8B8A8Unorm format support for color attachment: Images with linear tiling: yes Images with optimal tiling: yes Buffers: no
V tomto výpise vidíme, že verze instance je 1.1. Pak vidíme výpis informací o grafických kartách v daném počítači, na kterém byla aplikace spuštěna. Zde opět vidíme tři karty. V praxi však uvidíme nejčastěji asi jen jedinou grafickou kartu, či dvě tam, kde je integrovaný Intel i diskrétní grafika. Podíváme-li se dále, může nás zmást, že každá karta má svou verzi Vulkan. Opravdu je to tak.
V minulém díle našeho seriálu jsme hovořili o Vulkan loader a vulkanních ovladačích (ICD, Installable Client Driver). Verze instance je verzí Vulkan loader knihovny, kdežto verze driveru je verze daného ICD. Pro více informací odkazuji do dokumentace k Vulkanu.
Ve výpisu dále vidíme maximální velikost textury, zda je podporován geometry shader, kolik máme hald paměti a jejich velikosti, kolik máme typů front a co umí (g – graphics rendering, c – compute, t – transfer) a na závěr podpora různých formátů. Nicméně, začněme náš výklad od kódu.
Verze Vulkan instance
// Vulkan version auto vkEnumerateInstanceVersion = reinterpret_cast<PFN_vkEnumerateInstanceVersion>( vkGetInstanceProcAddr(nullptr, "vkEnumerateInstanceVersion")); if(vkEnumerateInstanceVersion) { uint32_t version; vkEnumerateInstanceVersion(&version); cout << "Vulkan instance version: " << VK_VERSION_MAJOR(version) << "." << VK_VERSION_MINOR(version) << "." << VK_VERSION_PATCH(version) << endl; } else cout << "Vulkan version: 1.0" << endl;
Pro zjištění verze instance používáme funkci vkEnumerateInstanceVersion
. Tato funkce je přítomná až od Vulkanu verze 1.1, proto ji nevoláme staticky, neboť nemusí být přítomna. Místo toho si na ni vezmeme funkční pointer. Je-li null, jedná se o verzi 1.0. Není-li null, zavoláme ji. Vrácenou hodnotu v uint32_t pak dekódujeme za použití maker VK_VERSION_MAJOR
, VK_VERSION_MINOR
a VK_VERSION_PATCH
.
Dovolím si zmínit jeden podstatný detail týkající se vulkanní verze instance. Když vytváříme instanci, zadáváme požadovanou verzi Vulkanu, kterou bude aplikace používat. Daný řádek jsem zvýraznil tučně v následujícím kódu:
// Vulkan instance vk::UniqueInstance instance( vk::createInstanceUnique( vk::InstanceCreateInfo{ vk::InstanceCreateFlags(), // flags &(const vk::ApplicationInfo&)vk::ApplicationInfo{ "04-deviceInfo", // 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 0, nullptr, // no extensions }));
Pokud zadáme verzi 1.0, nevzniká žádný problém. Ale nesmíme používat funkce Vulkan 1.1 a novější. Můžeme používat různá vulkanní rozšíření (extensions), kterými zpravidla dosáhneme všeho, co umí nejnovější verze Vulkanu, ale samotné API spadající pod Vulkan 1.1 a novější používat nesmíme. Nepožádali jsme o ně, neměli bychom je tedy používat.
Pokud ale zadáme vyšší verzi, mohou nastat dvě situace. Pokud je podporována pouze verze 1.0, dojde k chybě při vytváření instance. Pokud je ale podporována verze alespoň 1.1, dochází ke změně chování, pro které se lidé z Khronosu rozhodli od této verze. Vulkan loader již nikdy nevyhodí chybu, ať požádáme o libovolnou verzi Vulkanu, která je větší nebo rovna 1.1. Můžeme požádat i o verzi z daleké budoucnosti. Nicméně je naší povinností si ověřit, jakou verzi a jakou funkcionalitu podporuje vk::Instance
a jakou vk::Device
a nezkoušet použít nic, co nepodporují.
Pokud tedy požádáme o Vulkan 1.2 a vk::Device
podporuje pouze verzi 1.0, neobdržíme chybu. Je to jako bychom řekli: „Naše aplikace umí Vulkan 1.2.“ Ze strany Vulkanu jsme si zjistili například: „Já instance umím verzi 1.1 a já device umím verzi 1.0.“ Zbývá poslední věc: nechtít po instanci nic nad verzi 1.1 a po device nic nad 1.0. Případné zbylé nejasnosti vysvětluje dokumentace v části pojednávající o struktuře VkApplicationInfo
a případně o funkci vkCreateInstance
.
V tomto tutoriálu si nějakou dobu vystačíme pouze s Vulkanem 1.0 a použitím extensions. Nicméně všem, kteří chtějí používat novější verze Vulkanu a zároveň chtějí, aby jejich aplikace fungovala i na Vulkanu 1.0, doporučuji, aby si nejprve zjistili přítomnost funkce vkEnumerateInstanceVersion
a zavolali si ji, aby si ověřili, zda jsou nebo nejsou na Vulkanu 1.0.
Další kód prochází jednotlivá fyzická zařízení. Pro každé zařízení pak volá několik funkcí, které si nyní popíšeme.
Device properties (vlastnosti zařízení)
// device properties vk::PhysicalDeviceProperties p = pd.getProperties(); cout << " " << p.deviceName << endl; // device Vulkan version cout << " Vulkan version: " << VK_VERSION_MAJOR(p.apiVersion) << "." << VK_VERSION_MINOR(p.apiVersion) << "." << VK_VERSION_PATCH(p.apiVersion) << endl;
Metoda getProperties nám pro dané fyzické zařízení vrátí strukturu vk::PhysicalDeviceProperties
. Podíváme-li se do dokumentace, tato struktura nese množství zajímavých proměnných. Nám již notoricky známá je šestá položka deviceName
, kterou si vypisujeme na obrazovku jako název zařízení.
Další zajímavou položkou je apiVersion
. Toto je verze Vulkan API, kterou dané zařízení poskytuje. Na obrazovku si jej vypíšeme za použití stejných maker jako u Vulkan verze instance.
Další proměnné jsou například driverVersion
, vendorId
, deviceId
a deviceType
. V případě zájmu odkazuji na jejich význam do dokumentace.
Device limits (limity zařízení)
Základní limity zařízení jsou uloženy ve struktuře vk::PhysicalDeviceLimits
, která je součástí vk::PhysicalDeviceProperties
. Můžeme si tedy velmi snadno vypsat například maximální rozlišení 2D textury:
// device limits cout << " MaxTextureSize: " << p.limits.maxImageDimension2D << endl;
Limitů je obrovské množství. Samotná dokumentace se na ně z různých míst odkazuje. I my se některými z nich budeme zabývat, ale až je budeme potřebovat.
Device features (funkcionality zařízení)
Jsou určité funkcionality, které u daného zařízení mohou nebo nemusí být podporovány. Například geometry shader nemusí být přítomen na některém tabletu, mobilním zařízení nebo Raspberry Pi. Soubor podporovaných funkcionalit Vulkanu 1.0 zjistíme zavoláním metody getFeatures
nad daným fyzickým zařízením.
// device features vk::PhysicalDeviceFeatures f = pd.getFeatures(); cout << " Geometry shader: "; if(f.geometryShader) cout << "supported" << endl; else cout << "not supported" << endl;
Vrácená struktura vk::PhysicalDeviceFeatures
obsahuje 55 booleanů indikujících, co je podporováno a co ne. Mezi nimi je zmíněný geometry shader, dále třeba tesselační shader nebo anizotropní filtrování, různé komprese textur, podpora výpočtů s dvojnásobnou přesností (double precision) a mnoho dalších, které opět najdeme v dokumentaci.
Memory properties (paměť)
Další informace, které můžeme získat o fyzickém zařízení se týkají paměti. Ty získáme zavoláním metody getMemoryProperties
:
// memory properties vk::PhysicalDeviceMemoryProperties m = pd.getMemoryProperties(); cout << " Memory heaps:" << endl; for(uint32_t i=0,c=m.memoryHeapCount; i<c; i++) cout << " " << i << ": " << m.memoryHeaps[i].size/1024/1024 << "MiB" << endl;
Vrácená struktura vk::PhysicalDeviceMemoryProperties
obsahuje informace o dostupných typech paměti (memory types) a o haldách (memory heaps), které jsou k dispozici. Začněme u hald, které si vypisujeme na obrazovku. Haldy jsou často jedna, dvě nebo tři. Jednu můžeme najít například na některých integrovaných grafikách, protože tam je paměť jediná a sdílená s procesorem.
Dvě haldy jsou velmi častý případ a řekl bych nejtypičtější: jedna paměť na grafice a druhá klasická paměť počítače nebo její část. Paměť na grafice je obyčejně ta nejrychlejší pro grafický čip. Klasická paměť počítače bývá zpravidla pomalejší, ale zase je snadno přístupná procesoru, čehož budeme využívat.
Tři haldy vídávám v dnešní době například na grafikách AMD. Firma AMD se rozhodla vzít malou část paměti grafiky a zpřístupnit ji jak grafickému čipu tak procesoru. Tato třetí halda tedy umožňuje zvlášť efektivně sdílet data mezi grafikou a procesorem. S příchodem Resizable Bar technologie možná dojde k dalším změnám, které třeba na svém osobním výpisu budete časem vidět. Ale nechme se překvapit. Určitě ale obecně platí, že můžeme mít různé konfigurace hald, třeba i mnohem složitěji navržené. Záleží na výrobci, jak věci implementuje.
Struktura vk::PhysicalDeviceMemoryProperties
obsahuje i informace o typech pamětí (memory types), ale ty si necháme na později, až budeme alokovat paměť.
Queues (fronty)
K čemu jsou fronty (queues), které vidíme ve výpisu?
Queue families: 0: gct (count: 16) 1: t (count: 1) 2: c (count: 8)
Zjednodušeně můžeme říci, že práci zadáváme grafické kartě skrze příkazy. Tyto příkazy odesíláme do front, které pak grafická karta zpracovává. Fronty se dělí do skupin či tříd (families) podle toho, co umí. Můžeme mít třídu, která umí pouze počítat (compute operations support). V našem výpisu jsou uvedeny písmenem „c“. Dále můžeme mít třídu, která umí rendrovat grafiku (graphics operations support) – ve výpisu vidíme „g“. Další důležitá funkcionalita front jsou také přenosy dat (transfer operations) – ve výpisu „t“. Mnoho front umí více funkcionalit naráz. V každé třídě je pak uveden i počet front, které jsou k dispozici.
// queue family properties vector<vk::QueueFamilyProperties> queueFamilyList = pd.getQueueFamilyProperties(); cout << " Queue families:" << endl; for(uint32_t i=0,c=uint32_t(queueFamilyList.size()); i<c; i++) { cout << " " << i << ": "; if(queueFamilyList[i].queueFlags & vk::QueueFlagBits::eGraphics) cout << "g"; if(queueFamilyList[i].queueFlags & vk::QueueFlagBits::eCompute) cout << "c"; if(queueFamilyList[i].queueFlags & vk::QueueFlagBits::eTransfer) cout << "t"; cout << " (count: " << queueFamilyList[i].queueCount << ")" << endl; }
Náš kód je poměrně triviální. Nejprve zavoláme getQueueFamilyProperties
nad daným fyzickým zařízením. Ta nám vrátí ve vektoru tolik vk::QueueFamilyProperties
, kolik tříd front je přítomno. V našem případě u GeForce 1050 jsou to tři. Následně cyklíme přes tyto třídy a z queueFlags
zjišťujeme, které operace jsou nad danou frontou podporovány. Na závěr vypíšeme počet front.
V dokumentaci je skryta jedna podstatná informace: Pokud fronta podporuje grafické nebo výpočetní operace, pak vždycky automaticky podporuje i přenosy (transfer). A to dokonce i kdybychom u dané fronty transfer bit nenalezli. Uvedení transfer bitu je v tomto případě, dle dokumentace, nepovinné.
Format properties (vlastnosti formátů)
Poslední oblast, kterou si uvedeme, je podpora formátů. Vulkan podporuje přehršel různých formátů uložení dat. Stačí se podívat na VkFormat
v dokumentaci. Abychom si uvedli jeden příklad: R8G8B8A8Unorm
je typický formát ukládající červenou, zelenou a modrou složku a alfa kanál (RGBA). Každá z těchto složek má 8 bitů, tedy byte. Proto je za každým písmenem RGBA připojena osmička. „U“ pak stojí za unsigned, tedy bez znaménka. Osm bitů bez znaménka dává rozsah hodnot 0..255. Norm pak značí normalizaci, neboli konverzi na hodnotu s plovoucí desetinou čárkou v rozsahu 0.0 až 1.0. Nula se tedy zkonvertuje na 0.0 a 255 na 1.0. Více opět najdeme v dokumentaci, ať už v sekci Format Definition, kde se pojednává o VkFormat
, či v sekci Fixed-Point Data Conversions o konverzi numerických hodnot.
Co tedy vidíme na obrazovce o vlastnostech formátu R8G8B8A8Unorm?
R8G8B8A8Unorm format support for color attachment: Images with linear tiling: no Images with optimal tiling: yes Buffers: no
Vidíme ne, ano a ne. Tedy tento formát není podporován automaticky vždy. Také vidíme, že se dotazujeme na jeho podporu při specifickém použití, v našem případě jako „color attachment“. Color attachment znamená jinými slovy, zda do něj můžeme rendrovat.
V prvním případě vidíme, že na GeForce 1050 rendering do obrázků s lineárním uložením (linear tiling) v tomto formátu podporován není. Linear tiling znamená klasické uložení pixelů po řádcích. Naproti tomu je ale podporován rendering do obrázků s optimálním uložením (optimal tiling). Při optimal tiling není specifikováno, v jakém pořadí si grafika body ukládá, a může to být například z-order curve pro větší lokalitu přístupů do paměti. Renderování do bufferu rovněž podporováno není, což pokud vím, vůbec nejde.
Jak tedy zjišťujeme podporu jednotlivých formátů?
// color attachment R8G8B8A8Unorm format support vk::FormatProperties fp = pd.getFormatProperties(vk::Format::eR8G8B8A8Unorm); cout << " R8G8B8A8Unorm format support for color attachment:" << endl; cout << " Images with linear tiling: " << string(fp.linearTilingFeatures & vk::FormatFeatureFlagBits::eColorAttachment ? "yes" : "no") << endl; cout << " Images with optimal tiling: " << string(fp.optimalTilingFeatures & vk::FormatFeatureFlagBits::eColorAttachment ? "yes" : "no") << endl; cout << " Buffers: " << string(fp.bufferFeatures & vk::FormatFeatureFlagBits::eColorAttachment ? "yes" : "no") << endl;
Nahradíme-li vk::FormatFeatureFlagBits::eColorAttachment
za jiný flag, můžeme se ptát, zda je daný formát podporován pro čtení dat z textury a podobně.
Shrnutí
V tomto díle jsme prošli v podstatě veškeré základní funkce pro získání informací z fyzického zařízení ve Vulkanu 1.0. Zároveň jsme si představili mnoho ze základních pojmů jako fronty (queues), haldy (memory heaps), vlastnosti zařízení (device properties), funkcionality a limity zařízení (device features and limits), formáty a Vulkan verze instance a zařízení. Mnoho z toho nám poslouží jako základ v dalších dílech tutoriálu, ve kterých se pustíme vstříc prvním krůčkům k renderingu.