Hlavní navigace

Vulkan: vytvoření okna

1. 7. 2022
Doba čtení: 20 minut

Sdílet

 Autor: Depositphotos, Vulkan
V letošní prázdninové sérii o API Vulkan si ukážeme, jak otevřít okno a jak do něj rendrovat. Naším cílem budou primárně platformy Win32, Xlib a Wayland. Dnes začneme otevřením okna a vytvořením surface.

Zdrojáky si můžete stáhnout. Pro zkompilování na Win32 nepotřebujeme nic instalovat. Pro platformu Xlib a Wayland ale můžeme potřebovat vývojové balíčky, tedy pokud je již nemáme nainstalované. Na Ubuntu jsou to obyčejně libx11-dev pro Xlib a libwayland-dev a wayland-protocols pro Wayland.

Ve zdrojácích máme tři cpp soubory, každý pro jednu platformu. Začneme od Win32, ať pak můžeme pokračovat knihovnou Xlib následovanou moderním Waylandem.

Win32

První verze Windows přišla na svět v roce 1985, až do Windows 3.11 se ovšem nejednalo o operační systém, ale spíše o grafickou nadstavbu MS-DOSu. Tyto starší Windows měly pouze 16bitové rozhraní nazývané Win16. Windows NT a následně Windows 95 přinesly Win32, tedy 32bitové rozhraní. Win64 je pak varianta pro 64bitové systémy. Nicméně pokud náš kód nepoužívá nějaké speciality a pointerová kouzla, měl by být přeložitelný jak pro Win32, tak pro Win64. Budeme-li tedy hovořit o platformě Win32, budeme implicitně myslet i na Win64, která je ze stejného kódu kompilovatelná.

Jak tedy vytvořit okno pro Vulkan? V prvé řadě potřebujeme mít kam uložit jeho handle. Win32 používá typ HWND. Ale protože my používáme výjimky, nepoužijeme přímo HWND, ale zapouzdříme si jej do struktury UniqueWindow, ve které implementujeme destruktor:

static struct UniqueWindow {
   HWND handle = nullptr;
   ~UniqueWindow()  { if(handle) DestroyWindow(handle); }
   operator HWND() const  { return handle; }
} window;

Vynecháme mazání copy konstruktorů a implementaci move konstruktorů, a všeho dalšího, na co jsme u unique objektů zvyklí, neboť nevytváříme veřejné API. To si necháme až na další díl tutoriálu. Možná by se mě čtenáři v budoucnu zeptali, proč jsem nepoužil std::unique_resource. Důvod je prostý: tato třída prozatím není ve standardu, ale těšme se na ni.

Další proměnné, které budeme potřebovat, jsou hInstancewindowClass:

static HINSTANCE hInstance = NULL;
static struct UniqueWindowClass {
   ATOM handle = 0;
   ~UniqueWindowClass()  { if(handle) UnregisterClass(MAKEINTATOM(handle), hInstance); }
   operator ATOM() const  { return handle; }
} windowClass; 

Proměnná hInstance bude obsahovat handle spustitelného souboru exe a budeme ji mimo jiné potřebovat při vytváření okna. Proměnná windowClass ponese třídu okna, přesněji řečeno win32 atom jednoznačně identifikující třídu okna. Opět používáme strukturu s destruktorem, která se postará o zlikvidování třídy okna ve chvíli, kdy už není potřeba.

Poslední dvě proměnné jsou wndProcException a surface:

static exception_ptr wndProcException = nullptr;

// Vulkan window surface
static vk::UniqueSurfaceKHR surface;

První z nich slouží na uložení výjimky vzniklé ve funkci obsluhy zpráv okna, k čemuž si řekneme později více. Druhá proměnná surface reprezentuje povrch okna, do kterého budeme v dalších dílech rendrovat.

Nyní můžeme zaměřit naše úsilí na vytvoření okna. Začneme vulkanní instancí:

// Vulkan instance
instance =
   vk::createInstanceUnique(
      vk::InstanceCreateInfo{
         vk::InstanceCreateFlags(),  // flags
         &(const vk::ApplicationInfo&)vk::ApplicationInfo{
            "09-helloWindow-Win32",  // 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
         2,           // enabled extension count
         array<const char*, 2>{  // enabled extension names
            "VK_KHR_surface",
            "VK_KHR_win32_surface",
         }.data(),
      });

Kód je skoro stejný, jako jsme zvyklí, až na jednu podstatnější změnu, která je zvýrazněna tučně. Abychom mohli rendrovat do okna, potřebujeme zapnout rozšíření VK_KHR_surface a VK_KHR_win32_surface. První z nich řeší všeobecnou práci se surface (tedy s „povrchem“ okna), druhá pak všechno, co je specifické pro Win32.

Dále potřebujeme funkci pro zpracování zpráv zaslaných oknu:

// window's message handling procedure
auto wndProc = [](HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) noexcept -> LRESULT {
   switch(msg)
   {
      case WM_CLOSE:
         if(!DestroyWindow(hwnd))
            wndProcException = make_exception_ptr(runtime_error("DestroyWindow(): The function failed."));
         window.handle = nullptr;
         return 0;

      case WM_DESTROY:
         PostQuitMessage(0);
         return 0;

      default:
         return DefWindowProc(hwnd, msg, wParam, lParam);
   }
};

My se v tuto chvíli spokojíme pouze s ošetřením dvou zpráv: WM_CLOSE a WM_DESTROY. Protože toto není tutoriál o Win32, najdou zájemci detaily opět například ve WinAPI dokumentaci na stránkách Microsoftu. Zadáním názvu WinAPI funkcí do Googlu tyto stránky obyčejně snadno najdeme.

Avšak poznámka k výjimkám: Z funkce wndProc pro obsluhu zpráv okna není rozumné vyhazovat výjimky. Běh programu se vrací do systémových knihoven, které byly napsány v nespecifikovaném jazyce, a je poněkud problematické hádat, co se tam stane s výjimkou. Proto funkci wndProc označíme jako noexcept a místo vyhození výjimek si ji jen vytvoříme skrz std::make_exception_ptr a uložíme si ji do proměnné wndProcException. Uloženou výjimku pak znova vyhodíme v hlavní smyčce aplikace. Až budeme v následujících dílech volat další funkce a metody z wndProc, které samy mohou vyhazovat výjimky, uzavřeme je do try-catch bloku a odchycenou výjimku opět uložíme do  wndProcException.

V dalším kroku zaregistrujeme třídu okna. K tomu použijeme funkci RegisterClassEx() a strukturu WNDCLASSEX, kterou inicializujeme mimo jiné i naší funkcí wndProc pro zpracování zpráv zaslaných oknu:

// register window class
hInstance = GetModuleHandle(NULL);
windowClass.handle =
   RegisterClassEx(
      &(const WNDCLASSEX&)WNDCLASSEX{
         sizeof(WNDCLASSEX),  // cbSize
         0,                   // style
         wndProc,             // lpfnWndProc
         0,                   // cbClsExtra
         0,                   // cbWndExtra
         hInstance,           // hInstance
         LoadIcon(NULL, IDI_APPLICATION),  // hIcon
         LoadCursor(NULL, IDC_ARROW),  // hCursor
         NULL,                // hbrBackground
         NULL,                // lpszMenuName
         "HelloWindow",       // lpszClassName
         LoadIcon(NULL, IDI_APPLICATION)  // hIconSm
      }
   );
if(!windowClass.handle)
   throw runtime_error("Cannot register window class.");

Po registraci třídy okna už můžeme vytvořit samotné okno:

// create window
window.handle =
   CreateWindowEx(
      WS_EX_CLIENTEDGE,  // dwExStyle
      MAKEINTATOM(windowClass.handle),  // lpClassName
      _T("Hello window!"),  // lpWindowName
      WS_OVERLAPPEDWINDOW,  // dwStyle
      CW_USEDEFAULT, CW_USEDEFAULT, 400, 300,  // X, Y, nWidth, nHeight
      NULL, NULL, hInstance, NULL  // hWndParent, hMenu, hInstance, lpParam
   );
if(window == NULL)
   throw runtime_error("Cannot create window.");

Pro detaily opět odkazuji na dokumentaci WinAPI. A máme-li okno, můžeme vytvořit i surface:

// create surface
vk::UniqueSurfaceKHR surface =
   instance->createWin32SurfaceKHRUnique(
      vk::Win32SurfaceCreateInfoKHR(
         vk::Win32SurfaceCreateFlagsKHR(),  // flags
         hInstance,  // hinstance
         window  // hwnd
      )
   );

Jak vidíme v kódu, pro vytvoření surface jsme potřebovali pouze HINSTANCE okna a handle samotného okna.

Nyní máme okno a vulkanní surface, na kterém máme úmysl zobrazovat, či přesněji řečeno, prezentovat výsledek našeho renderingu. Nicméně je zde drobný problém: Ne každé fyzické zařízení musí nutně umět prezentovat na náš surface. Například počítač se dvěma grafickými kartami nemusí podporovat rendering na jedné kartě a prezentaci výsledků na obrazovku, která je připojena skrz druhou grafickou kartu. Existují dokonce fyzická zařízení, které nazýváme výpočetní akcelerátory a které neumějí prezentovat vůbec a ani nemají konektor pro připojení obrazovky. Potřebujeme tedy zjistit, která fyzická zařízení jsou kompatibilní s naším surface:

// find compatible devices
vector<vk::PhysicalDevice> deviceList = instance->enumeratePhysicalDevices();
vector<string> compatibleDevices;
for(vk::PhysicalDevice pd : deviceList) {
   uint32_t numQueues;
   pd.getQueueFamilyProperties(&numQueues, nullptr);
   for(uint32_t i=0; i<numQueues; i++)
      if(pd.getSurfaceSupportKHR(i, surface.get())) {
         compatibleDevices.push_back(pd.getProperties().deviceName);
         break;
      }
}
cout << "Compatible devices:" << endl;
for(string& name : compatibleDevices)
   cout << "   " << name << endl;

V kódu postupně procházíme všechna fyzická zařízení, dotazujeme se na počet tříd front, a pak hledáme, zda je pro nějakou třídu front podporována prezentace na náš surface, což zjišťujeme zavoláním metody vk::PhysicalDevice::getSurfaceSupportKHR(), která je v kódu zdůrazněna tučně. Pokud je prezentace pro některou třídu front podporována, pak si fyzické zařízení uložíme do seznamu. Tento seznam pak vypíšeme do konzole.

Jsme téměř hotovi. Abychom dostali okno na obrazovku, zbývá již pouze zavolat funkci ShowWindow() a rozjet smyčku zpráv:

// show window
ShowWindow(window, SW_SHOWDEFAULT);

// run event loop
MSG msg;
BOOL r;
while((r = GetMessage(&msg, NULL, 0, 0)) != 0) {

   // handle GetMessage() errors
   if(r == -1)
      throw runtime_error("GetMessage(): The function failed.");

   // handle message
   TranslateMessage(&msg);
   DispatchMessage(&msg);

   // handle exceptions raised in window procedure
   if(wndProcException)
      rethrow_exception(wndProcException);
}

Smyčka zpráv vybírá zprávy z fronty funkcí GetMessage(). Samotná zpráva je pak doručena oknu ve funkci DispatchMessage(). V okně je obsloužena ve funkci wndProc, kterou jsme si již popsali. Pokud vznikla výjimka ve wndProc, budeme ji mít uloženu ve wndProcException. V tom případě výjimku znova vyhodíme.

Aplikaci můžeme zkusit spustit a měli bychom vidět prázdné okno.

Co se stane, když se pokusíme okno zavřít? Nejprve ve funkci zpráv okna dostaneme zprávu WM_CLOSE . V ní smažeme okno, což způsobí zaslání zprávy WM_DESTROY . Při obsluzeWM_DESTROY zavoláme funkci PostQuitMessage() , která způsobí ukončení smyčky zpráv, neboťGetMessage() vrátí nulu. Aplikace pak opustí funkci main() . DestuktorUniqueWindow neudělá nic, neboť okno je již uvolněno. Následuje destruktor UniqueWindowClass , který uvolní třídu okna. Posledním objektem je vk::UniqueInstance , jehož destruktor uvolní instanci Vulkan. Tím je aplikace korektně ukončena a my jsme hotovi s platformou Win32 pro tento díl tutoriálu.

Více pro tuto platformu příště. Nyní nás čeká vytvoření okna na Linuxu s využitím knihovny Xlib a po ní moderní přístup Waylandu.

Xlib

Knihovna Xlib přišla na svět někdy kolem roku 1985. Je tedy podobně stará, jako Windows. Dnes, pokud ještě musíme používat X Window System, používáme obyčejně rozhraní XCB. V každém případě, obojí je v čase psaní tohoto článku na ústupu a naděje se upírají k Waylandu.

Navzdory pomalému odchodu této platformy si ukážeme, jak vytvořit okno v Xlib ze třech důvodů: Jednak Xlib zde s námi bylo opravdu hodně let a hned tak magicky nezmizí. Programátoři rozsáhlých projektů mají mnohdy bolestnou zkušenost, jak mnoho práce dá nahradit jednu knihovnu v projektu za jinou. Některé knihovny jsou doslova „prorostlé“ skrz projekt a mnohdy je to práce na roky. Xlib tedy může přežít roky a roky v mnoha projektech. Druhý důvod je v lepším pochopení Waylandu, můžeme-li jej porovnat s jeho předchůdcem. A třetí: vytvoření okna s Xlib je podstatně jednodušší než s Wayland. Projdeme si jej tedy před Waylandem.

Jak tedy vytvořit okno pro vulkanní rendering v Xlib? Opět potřebujeme v první řadě mít kam uložit handle okna a dále i handle spojení na display server:

// Display and Window handle
static struct UniqueDisplay {
   Display* handle = nullptr;
   ~UniqueDisplay()  { if(handle) XCloseDisplay(handle); }
   operator Display*() const  { return handle; }
} display;
struct UniqueWindow {
   Window handle = 0;
   ~UniqueWindow()  { if(handle) XDestroyWindow(display, handle); }
   operator Window() const  { return handle; }
} window;

Náš kód opět začíná vytvořením vk::Instance:

// Vulkan instance
instance =
   vk::createInstanceUnique(
      vk::InstanceCreateInfo{
         vk::InstanceCreateFlags(),  // flags
         &(const vk::ApplicationInfo&)vk::ApplicationInfo{
            "09-helloWindow-Xlib",   // 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
         2,           // enabled extension count
         array<const char*, 2>{  // enabled extension names
            "VK_KHR_surface",
            "VK_KHR_xlib_surface",
         }.data(),
      });

Kód je opět stejný jako jsme zvyklí kromě toho, že zde žádáme o povolení extensions VK_KHR_surface a VK_KHR_xlib_surface. Oproti Windows je tedy VK_KHR_win32_surface nahrazena VK_KHR_xlib_surface. Tak budeme moci používat Xlib s vulkanním surface dohromady.

Dalším krokem je otevření X spojení, přes které budeme komunikovat s X serverem:

// open X connection
display.handle = XOpenDisplay(nullptr);
if(display == nullptr)
   throw runtime_error("Can not open display. No X-server running or wrong DISPLAY variable.");

Hned můžeme vytvořit okno:

// create window
XSetWindowAttributes attr;
attr.event_mask = ExposureMask | StructureNotifyMask | VisibilityChangeMask;
window.handle =
   XCreateWindow(
      display,  // display
      DefaultRootWindow(display.handle),  // parent
      0, 0,  // x, y
      400, 300,  // width, height
      0,  // border_width
      CopyFromParent,  // depth
      InputOutput,  // class
      CopyFromParent,  // visual
      CWEventMask,  // valuemask
      &attr  // attributes
   );

Pro detaily k jednotlivým parametrům opět odkazuji na internet. Samotné okno potřebuje ještě nastavit věci jako například titulek okna a WM_DELETE_WINDOW atom. Pak je také potřeba okno namapovat – tedy zobrazit na obrazovce:

XSetStandardProperties(display, window, "Hello window!", "Hello window!", None, NULL, 0, NULL);
Atom wmDeleteMessage = XInternAtom(display, "WM_DELETE_WINDOW", False);
XSetWMProtocols(display, window, &wmDeleteMessage, 1);

// show window
XMapWindow(display, window);

Následuje vytvoření surface:

// create surface
surface =
   instance->createXlibSurfaceKHRUnique(
      vk::XlibSurfaceCreateInfoKHR(
         vk::XlibSurfaceCreateFlagsKHR(),  // flags
         display,  // dpy
         window  // window
      )
   );

Surface máme vytvořen. Nyní, tak jako na Windows, potřebujeme zjistit, ze kterých fyzických zařízení na náš surface můžeme prezentovat:

// find compatible devices
vector<vk::PhysicalDevice> deviceList = instance->enumeratePhysicalDevices();
vector<string> compatibleDevices;
for(vk::PhysicalDevice pd : deviceList) {
   uint32_t numQueues;
   pd.getQueueFamilyProperties(&numQueues, nullptr);
   for(uint32_t i=0; i<numQueues; i++)
      if(pd.getSurfaceSupportKHR(i, surface.get())) {
         compatibleDevices.push_back(pd.getProperties().deviceName);
         break;
      }
}
cout << "Compatible devices:" << endl;
for(string& name : compatibleDevices)
   cout << "   " << name << endl;

Kód je stejný jak na platformě Win32. Pro detaily tedy odkazuji tam.

Zbývá poslední bod, aby naše aplikace fungovala, tedy rozjet smyčku zpráv:

// run event loop
XEvent e;
while(true) {
   XNextEvent(display, &e);
   if(e.type==ClientMessage && ulong(e.xclient.data.l[0])==wmDeleteMessage)
      break;
}

Smyčka zpráv pouze odebírá zprávy z fronty zpráv. Ve smyčce obsluhujeme jedinou zprávu a to žádost o smazání okna. Pokud tato zpráva přijde, opustíme smyčku zpráv i funkci main(). Následně je zavolán destruktor UniqueWindow a je smazáno okno. Po té je zavolán destruktor UniqueDisplay a je uzavřeno spojení s X-serverem. Na závěr je uvolněna instance Vulkan. Že vše funguje, ověříme spuštěním aplikace:

Wayland

Wayland je protokol pro komunikaci zobrazovacího serveru (Wayland compositor) s jeho klienty. Zároveň je to také název pro céčkovskou implementaci tohoto protokolu. K projektu Wayland patří i Weston, což je referenční implementace waylandového kompozitoru.

Wayland se zrodil v roce 2008 jako volnočasový projekt Kristiana Høgsberga. Už v roce 2012 byl vydán Wayland 1.0, což jinými slovy znamenalo, že zde byl stabilní protokol. Ovšem problém stabilního protokolu je v tom, že je stabilní – tedy obyčejně v něm nelze jen tak dělat zpětně nekompatibilní změny, i kdyby byly opravdu potřeba.

Wayland si však našel cestu vpřed. Začaly v rámci něj vznikat nová rozšíření protokolu, které v roce 2015 vyústily ve vznik nového balíku wayland-protocols. Ve Waylandu zůstal core protokol, který byl již stabilní a neumožňoval radikálnější změny. Naproti tomu balík wayland-protocols neobsahoval ve své verzi 1.0 jediný stabilní protokol. Obsahoval pouze nestabilní protokoly, které se dále dynamicky rozvíjely. Mezi nimi byl i protokol xdg-shell, který budeme používat i my. Protokol xdg-shell v podstatě nahradil wl_shell interface z core protokolu a jeho předpona xdg je zkratka z Cross-Desktop Group, kde x stojí za cross. V roce 2017 byl xdg-shell protokol v jeho šesté verzi prohlášen za stabilní. Ohledně dalších protokolů doporučuji stránku wayland.app/protocols/, kde najdeme jednotlivé protokoly, informaci o jejich stabilitě a popis jejich API.

Samotný Wayland si do našich počítačů hledal cestu dlouho. Úvahy o jeho nasazení v Ubuntu distribuci byly již v roce 2011 pro verzi 11.10 (zdroj). Obstojné podpory jsme se však dočkali až ve verzi 17.10. Nicméně, jedna věc je, že něco „nějak“ jede, a druhá věc je, že je to vychytané a spolehlivé. A tak po úvodním nadšení ve verzi 17.10 nebyl už dále Wayland výchozím desktopem. Na to jsme museli počkat tři a půl roku až na verzi 21.04, tedy pokud nepatříte k těm, kteří mají Nvidia grafickou kartu. Nvidia totiž připravila svým linuxovým uživatelům další taškařici v podobě nepodpory GBM API. To by snad od verze driverů 495.44 mělo být již vyřešeno, nicméně není. Stále jsou zde problémy. Pokud je tedy detekována grafická karta Nvidia, není Wayland výchozím rozhraním ani v Ubuntu 22.04, alespoň ne v době psaní tohoto článku (léto 2022).

Pojďme na samotný kód. Wayland používá pro handly pointery. Pro správu handlů tedy použijeme unique_ptr. K tomu, ale potřebujeme custom deleters:

// deleters
struct DisplayDeleter { void operator()(wl_display* d) { wl_display_disconnect(d); } };
struct RegistryDeleter { void operator()(wl_registry* r) { wl_registry_destroy(r); } };
struct CompositorDeleter { void operator()(wl_compositor* c) { wl_compositor_destroy(c); } };
struct XdgWmBaseDeleter { void operator()(xdg_wm_base* b) { xdg_wm_base_destroy(b); } };
struct WlSurfaceDeleter { void operator()(wl_surface* s) { wl_surface_destroy(s); } };
struct XdgSurfaceDeleter { void operator()(xdg_surface* s) { xdg_surface_destroy(s); } };
struct XdgToplevelDeleter { void operator()(xdg_toplevel* t) { xdg_toplevel_destroy(t); } };

Nyní již můžeme definovat své handly, které budou korektně uvolněny i v případě výjimky:

// globals
static unique_ptr<wl_display, DisplayDeleter> display;
static unique_ptr<wl_registry, RegistryDeleter> registry;
static unique_ptr<wl_compositor, CompositorDeleter> compositor;
static unique_ptr<xdg_wm_base, XdgWmBaseDeleter> xdgWmBase;

// objects
static unique_ptr<wl_surface, WlSurfaceDeleter> wlSurface;
static unique_ptr<xdg_surface, XdgSurfaceDeleter> xdgSurface;
static unique_ptr<xdg_toplevel, XdgToplevelDeleter> xdgToplevel;

Dále budeme potřebovat stavovou proměnnou running, několik listenerů a surface:

// state
static bool running = true;

// listeners
static wl_registry_listener registryListener;
static xdg_wm_base_listener xdgWmBaseListener;
static xdg_surface_listener xdgSurfaceListener;
static xdg_toplevel_listener xdgToplevelListener;

// Vulkan window surface
static vk::UniqueSurfaceKHR surface;

Vulkanní instanci vytvoříme obvyklým způsobem s výjimkou povolení VK_KHR_wayland_surface extension:

// Vulkan instance
instance =
   vk::createInstanceUnique(
      vk::InstanceCreateInfo{
         vk::InstanceCreateFlags(),  // flags
         &(const vk::ApplicationInfo&)vk::ApplicationInfo{
            "09-helloWindow-Wayland",  // 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
         2,           // enabled extension count
         array<const char*, 2>{  // enabled extension names
            "VK_KHR_surface",
            "VK_KHR_wayland_surface",
         }.data(),
      });

Pak si otevřeme waylandové spojení:

// open Wayland connection
display.reset(wl_display_connect(nullptr));
if(display == nullptr)
   throw runtime_error("Cannot connect to Wayland display. No Wayland server is running or invalid WAYLAND_DISPLAY variable.");

Dále budeme potřebovat globální seznam objektů, který je poskytován skrze Wayland registry a registry listener:

// registry listener
registry.reset(wl_display_get_registry(display.get()));
if(registry == nullptr)
   throw runtime_error("Cannot get Wayland registry object.");
compositor = nullptr;
registryListener = {
   .global =
      [](void* data, wl_registry* registry, uint32_t name, const char* interface, uint32_t version) {
         cout << "   " << interface << endl;
         if(strcmp(interface, wl_compositor_interface.name) == 0)
            compositor.reset(
               static_cast<wl_compositor*>(
                  wl_registry_bind(registry, name, &wl_compositor_interface, 1)));
         else if(strcmp(interface, xdg_wm_base_interface.name) == 0)
            xdgWmBase.reset(
               static_cast<xdg_wm_base*>(
                  wl_registry_bind(registry, name, &xdg_wm_base_interface, 1)));
      },
   .global_remove =
      [](void*, wl_registry*, uint32_t) {
      },
};
cout << "Wayland global registry objects:" << endl;
if(wl_registry_add_listener(registry.get(), &registryListener, nullptr))
   throw runtime_error("wl_registry_add_listener() failed.");

Jak vidíme v kódu, nové globální objekty se objevují skrz funkci global a mizí zavoláním funkce global_remove. Mnoho rozhraní (interfaces) nikdy nemizí, ale například ta, která reprezentují zařízení připojené k počítači, mohou být odpojena a pro ně uvidíme zavolání funkce global_remove. Všechna rozhraní si vypíšeme a uložíme si ty, které budeme potřebovat, tedy compositorxdg_wm_base.

Dále provedeme roundtrip k serveru, abychom si byli jisti, že nám poslal všechna rozhraní. Otestujeme, zda opravdu máme compositorxdgWmBase:

// get and init global objects
if(wl_display_roundtrip(display.get()) == -1)
   throw runtime_error("wl_display_roundtrip() failed.");
if(compositor == nullptr)
   throw runtime_error("Cannot get Wayland compositor object.");
if(xdgWmBase == nullptr)
   throw runtime_error("Cannot get Wayland xdg_wm_base object.");
xdgWmBaseListener = {
   .ping =
      [](void*, xdg_wm_base* xdgWmBase, uint32_t serial) {
         xdg_wm_base_pong(xdgWmBase, serial);
      }
};
if(xdg_wm_base_add_listener(xdgWmBase.get(), &xdgWmBaseListener, nullptr))
   throw runtime_error("xdg_wm_base_add_listener() failed.");

Jak vidíme v kódu, pokud máme xdgWmBase, tak si pro něj zaregistrujeme listener, který toho mnoho dělat nebude. Pouze na zprávu ping, odpoví pong. Tím si server ověří, že je klient naživu.

Následuje vytvoření wl_surface a xdg_surface, tedy jakéhosi „povrchu“ okna:

// create Wayland surface
wlSurface.reset(wl_compositor_create_surface(compositor.get()));
if(wlSurface == nullptr)
   throw runtime_error("wl_compositor_create_surface() failed.");
xdgSurface.reset(xdg_wm_base_get_xdg_surface(xdgWmBase.get(), wlSurface.get()));
if(xdgSurface == nullptr)
   throw runtime_error("xdg_wm_base_get_xdg_surface() failed.");
xdgSurfaceListener = {
   .configure =
      [](void* data, xdg_surface* xdgSurface, uint32_t serial) {
         cout << "surface configure" << endl;
         xdg_surface_ack_configure(xdgSurface, serial);
         wl_surface_commit(wlSurface.get());
      },
};
if(xdg_surface_add_listener(xdgSurface.get(), &xdgSurfaceListener, nullptr))
   throw runtime_error("xdg_surface_add_listener() failed.");

Kód nás překvapí tím, že vytváříme surface dvakrát. Jednak jako wl_surface a jednak jako xdg_surface. Ve skutečnosti je to ale jedno okno wl_surface, které má i xdg_surface  rozhraní. Jak jsem psal dříve – máme stabilní core protokol Wayland, ale později vznikl i protokol xdg-shell, jehož součástí je xdg_surface. Protokol xdg-shell doplnil, co bylo potřeba k plnohodnotnému uživatelskému rozhraní dnešního standardu, řečeno trochu vágně.

Nakonec zaregistrujeme listener pro xdgSurface a v něm nastavíme jedinou funkci configure. V této funkci pouze potvrdíme zkonfigurování metodou xdg_surface_ack_configure a provedeme commit nad objektem surface.

Zůstává nám poslední krok z waylandské části vytváření okna, a to vytvoření toplevel. Vytvoření toplevel bychom mohli vágně popsat jako přidání další role oknu, které tím, že má roli toplevel, se umí maximalizovat, minimalizovat, může být uživatelem přemísťováno po obrazovce a uživatel může táhnutím za jeho okraj měnit jeho velikost, atd.:

// init xdg toplevel
xdgToplevel.reset(xdg_surface_get_toplevel(xdgSurface.get()));
if(xdgToplevel == nullptr)
   throw runtime_error("xdg_surface_get_toplevel() failed.");
xdg_toplevel_set_title(xdgToplevel.get(), "Hello window!");
xdgToplevelListener = {
   .configure =
      [](void* data, xdg_toplevel* toplevel, int32_t width, int32_t height, wl_array*) -> void {
         cout << "toplevel configure (width=" << width << ", height=" << height << ")" << endl;
      },
   .close =
      [](void* data, xdg_toplevel* xdgToplevel) {
         running = false;
      },
};
if(xdg_toplevel_add_listener(xdgToplevel.get(), &xdgToplevelListener, nullptr))
   throw runtime_error("xdg_toplevel_add_listener() failed.");
wl_surface_commit(wlSurface.get());

Toplevel vytvoříme z xdgSurface  a nastavíme mu title, tedy titulek v záhlaví okna. Pak opět zaregistrujeme listener, tentokrát s funkcemi configure a close. Funkci configure budeme využívat více v následujících dílech. Teď v ní pouze vypíšeme rozměry okna. Při zavolání funkce close si nastavíme flag running na false, který pak použijeme v hlavní smyčce aplikace. Na závěr provedeme commit nad wl_surfacem.

Tím jsme hotovi s waylandím oknem. Zbývá vytvořit surface Vulkanu a pro jistotu vyflushovat buffery:

// create surface
surface =
   instance->createWaylandSurfaceKHRUnique(
      vk::WaylandSurfaceCreateInfoKHR(
         vk::WaylandSurfaceCreateFlagsKHR(),  // flags
         display.get(),  // display
         wlSurface.get()  // surface
      )
   );
if(wl_display_flush(display.get()) == -1)
   throw runtime_error("wl_display_flush() failed.");

Pak se dotážeme, která fyzická zařízení jsou kompatibilní s naším vulkanním surface. Kód je stejný jako na dvou předchozích platformách, takže jej přeskočíme.

Zbývá už jen rozjet hlavní smyčku aplikace:

// run event loop
cout << "Entering main loop." << endl;
if(wl_display_flush(display.get()) == -1)
   throw runtime_error("wl_display_flush() failed.");
while(running) {
   if(wl_display_dispatch(display.get()) == -1)
      throw runtime_error("wl_display_dispatch() failed.");
   if(wl_display_flush(display.get()) == -1)
      throw runtime_error("wl_display_flush() failed.");
}
cout << "Main loop left." << endl;

Jádro smyčky zpráv tvoří funkce wl_display_dispatch() a wl_display_flush(). Funkce wl_display_dispatch() obslouží všechny příchozí zprávy. Pokud byla fronta příchozích zpráv prázdná, tak blokuje do příchodu zprávy, kterou by obsloužila. Následné zavolání wl_display_flush() odešle na server Wayland jakékoliv požadavky zatím čekající ve frontě k odeslání. V této smyčce tedy cyklíme, dokud není flag running nastaven na false, nebo dokud nedojde k chybě.

Prvotní zavolání wl_display_flush() před vstupem do smyčky zabrání nechtěné situaci, že by nějaký požadavek na server nebyl odeslán a zůstal v bufferech, přičemž wl_display_dispatch() by marně blokoval a čekal na odpověď, protože požadavek vlastně neodešel.

Můžeme si aplikaci spustit. Ale co to? Aplikace vypsala hromadu textu, nicméně žádné okno se neobjevilo. Zde narážíme na vlastnost Waylandu: nemá ve zvyku zobrazovat nekompletní okna. Nedefinovali jsme jeho obsah – jinými slovy, zatím jsme do něj nic nevyrendrovali – takže Wayland, věrný svému designu, odmítl takovéto okno zobrazit. Tento problém vyřešíme příště. Nicméně se alespoň můžeme podívat na obsah konzole:

Wayland global registry objects:
   wl_compositor
   zwp_tablet_manager_v2
   zwp_keyboard_shortcuts_inhibit_manager_v1
   xdg_wm_base
   zwlr_layer_shell_v1
   zxdg_decoration_manager_v1
   wp_viewporter
   wl_shm
   wl_seat
   zwp_pointer_gestures_v1
   zwp_pointer_constraints_v1
   wl_data_device_manager
   zwlr_data_control_manager_v1
   zwp_primary_selection_device_manager_v1
   org_kde_kwin_idle
   zwp_idle_inhibit_manager_v1
   org_kde_plasma_shell
   org_kde_kwin_appmenu_manager
   org_kde_kwin_server_decoration_palette_manager
   org_kde_plasma_virtual_desktop_management
   org_kde_kwin_shadow_manager
   org_kde_kwin_dpms_manager
   org_kde_kwin_server_decoration_manager
   org_kde_kwin_outputmanagement
   zxdg_output_manager_v1
   wl_subcompositor
   zxdg_exporter_v2
   zxdg_importer_v2
   org_kde_kwin_outputdevice
   wl_output
   zwp_relative_pointer_manager_v1
   wl_drm
   zwp_linux_dmabuf_v1
   zwp_text_input_manager_v2
   zwp_text_input_manager_v3
   org_kde_kwin_blur_manager
   org_kde_kwin_contrast_manager
   org_kde_kwin_slide_manager
Compatible devices:
   Intel(R) UHD Graphics (CML GT2)
   llvmpipe (LLVM 12.0.1, 256 bits)
entering main loop
toplevel configure (width=0, height=0)
surface configure

Ve výpisu vidíme seznam globálních objektů Waylandu získaných z registru. Dále vidíme seznam kompatibilních zařízení, v tomto případě grafiku Intel a llvmpipe. Zmíněná llvmpipe je softwarová implementace Vulkanu, tedy softwarový rendrovač. Pak vidíme, že byla odstartována hlavní smyčka zpráv a že jsme obdrželi toplevel configure a surface configure. Protože jsme ale nevygenerovali žádný obsah okna a nepředali jej Waylandu, on díky svému designu nemohl zobrazit žádné okno. To napravíme příště. Dnes nezbývá, než zmáčknout Ctrl-C a ukončit aplikaci.

Root obecny

Shrnutí

Dnes jsme si ukázali, jak vytvořit okno na třech různých platformách – Win32, Xlib a Wayland – a jak na něm vytvořit surface. Příště vytvoříme swapchain, vyrendrujeme si obrázek na obrazovku a začneme se ponořovat do tajů rendrování do okna.

(Autorem obrázků je Jan Pečiva.)