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 hInstance
a windowClass
:
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 vulkaní 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 vulkaní 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 vulkaní 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 vulkaní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;
Vulkaní 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(), ®istryListener, 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 compositor
a xdg_wm_base
.
Dále provedeme roundtrip k serveru, abychom si byli jisti, že nám poslal všechna rozhraní. Otestujeme, zda opravdu máme compositor
a xdgWmBase
:
// 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 vulkaní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.
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.)