Cikkek

Direct2D - avagy minőségi 2D hardveresen gyorsítva

kategória: Rendszer — forrás: Microsoft TechNet — dátum: 2009-09-02 — értékelés: 0Nyomtató

Bevezetés

Megmondom őszintén, eredetileg nem erről a témáról szerettem volna írni, a DirectX 11-ben debütáló Compute Shader nevű állatfaj bemutatása volt a célom. Miközben anyagot gyűjtöttem, véletlenül bukkantam rá a 2008-as Professional Developer's Conference egyik előadására, amelyben egy új 2D (Direct2D), illetve egy új szöveg API (DirectWrite)-ról hallhattunk érdekes dolgokat. Meglepődtem, hiszen egészen addig nem is hallottam, hogy új 2D API-t kapunk, ugyanakkor érdekesnek is találtam a dolgot. Elég érdekesnek ahhoz, hogy dobjam az eredeti témát, és elkezdjem kalandozásomat a hardveresen gyorsított 2D világában.

Direct2D vs. GDI/GDI+

A Windows alkalmazások túlnyomó része még ma is GDI/GDI+-t használ 2D grafikához. A GDI-t a '90-es évek hardvereihez, technológiáihoz tervezték. Persze ez nem jelenti azt, hogy ma már használhatatlan, de semmiképpen nem mondható optimális megoldásnak: például nem tudjuk vele teljesen kihasználni a grafikus gyorsítókat, nem tudunk vele élsimított primitíveket rajzolni, egy színt maximum 32 biten tudunk ábrázolni stb. Persze GDI+-szal lehetőségünk van pl. élsimított rajzolásra, de az meg lassú. A Windows Desktop Graphics csapat egy új API-ban látta ezen problémák megoldását, és így született meg ezen cikk tárgya, a Direct2D.

GDI - recés vonal

Direct2D - élsimított vonal

Az igény, hogy a 2D grafika számolásának terhét levegyük a CPU-ról, természetesen nem újkeletű, és Windows platformon sem a Direct2D az első GPU-t használó megoldás. Például .NET-fejlesztőknek ott van a WPF, ami kihasználja a grafikus gyorsítókban rejlő potenciált, vagy akár Direct3D-vel is rajzolhatnánk mindent, ami 2D.

Ezek mellet a létező megoldások mellett miért van szükség mégis egy új API-ra? Gondoljunk bele annak a fejlesztőnek a helyzetébe, akinek van egy nagy rakás 2D-renderelő kódja GDI-ban megírva, és szeretné ezt az egészet videokártyával gyorsítani. Két lehetősége van: vagy átír mindent menedzselt kódra, és WPF-et használ, vagy átír mindent DirectX-re. Nagy kódbázis esetén egyik sem tűnik járható útnak. Itt jön a képbe a Direct2D, amit úgy terveztek, hogy nagyon jól együttműködjön GDI, GDI+ (pl. könnyen rajzolhatunk vele GDI device contextbe), sőt Direct3D kódokkal is.

Az új API-val nem csak a sebesség növelése és a képminőség javítása érhető el: lehetőségünk van pl. a renderelt képet RDP-n keresztül egy távoli gépen megjeleníteni. A fejlesztő két módszer közül választhat: vagy helyben állítja elő a képet, és egy bitmapet küld el a hálózaton, vagy a képet megjelenítő gép GPU-ját használja renderelésre (primitive remoting). A második lehetőség csak akkor él, ha a célgép Windows 7-et futtat, régebbi rendszer esetén a rajzolt tartalom mindenképpen bitmapként megy át a hálózaton. Még egy fontos dolog, hogy D2D-vel service-ként is rajzolhatunk (ez pl. GDI+-szal nem lehetséges).

Hogyan működik?

A Direct2D közvetlenül a Direct3D 10.1 API-ra épül, ez teszi lehetővé a mai videokártyák maximális kihasználását. Természetesen azok sem maradnak ki a jóból, akik nem rendelkeznek D3D10-képes VGA-val, ugyanis Direct3D 10 Level9-et használva DirectX 9-es driverrel is elérhető a hardveres gyorsítás. Előfordulhat, hogy nincs megfelelő eszköz a hardveres rendereléshez (pl. ha service-ként rajzolunk). A Direct2D ekkor sem esik pánikba, hiszen a szoftveres raszterizert használva ekkor is képes rajzolni. Bár konkrétan sehol sem láttam leírva, természetesnek tűnik, hogy ez a raszterizer a WARP (Windows Advanced Rasterization Platform), amelynek bétaverziója már jelen van a 2008. novemberi DirectX SDK-ban. Fontos megjegyezni, hogy ez a szoftveres megoldás is gyorsabb megjelenítést tesz lehetővé - hasonló minőség mellett - mintha GDI+-t használnánk.

Egy egyszerű példa

Ebben a bekezdésben a Direct2D programozásáról szeretnék beszélni. Természetesen nem célom egy mindenre kiterjedő leírást adni az API-ról - arra ott van az MSDN Library - csupán betekintést szeretnék nyújtani a D2D világába. Készítettem egy demóalkalmazást, amely a John Horton Conway-féle "Game of Life" nevű sejtautomata implementációja. Ennek a programnak a D2D-t érintő részeit fogom elmagyarázni. A program forráskóddal (Visual Studio 2008 project) együtt letölthető innen.

A kis kitérő után térjünk vissza a tárgyhoz! Ahhoz, hogy elkezdhessünk rajzolni Direct2D-ben, létre kell hozni egy factory-t: link

Ennek a segítségével fogjuk létrehozni a nekünk kellő erőforrásokat. Egy kis magyarázat a fenti kódrészlethez: az IFR egy makró, amit az SDK-példákból nyúltam; gyakorlatilag a hibakezelést egyszerűsíti le. A factory egy ID2D1Factory-ra mutató pointer. (Megjegyzés: a mellékelt forrásban a _COM_SMARTPTR_TYPEDEF makróval deklarált smart pointereket használok minden D2D erőforráshoz.)

Apropó, erőforrások. Mivel az egész API a Direct3D-re épül, vannak erőforrások, amelyek kötődnek a videokártyához (pl. ilyenek a rendertargetek, az ecsetek). Erről azért fontos tudni, mert ezeket bizonyos esetekben újra létre kell hoznunk (pl. lost device). Az ilyen esetekről az EndDraw függvény tájékoztat minket, erről majd kicsit később írok bővebben. A következő képen az eszközfüggő erőforrások osztálydiagramja látható: link

Már említettem, hogy a rendertargetek is eszközfüggő erőforrások. Azért nincsenek a képen, mert már így is túl sok minden van rajta. Persze nem hagyom ki őket, kaptak saját ábrát: link

Ha már így belejöttem az osztálydiagram-mutogatásba, ne maradjon ki a maradék sem. Íme az eszközfüggetlen erőforrások: link

Mivel még csak egyetlen sort magyaráztam el a forrásból, ideje lenne továbblépni. A példaprogramban három eszközfüggő D2D erőforrásra van szükség: kell egy rendertarget, hogy legyen mire rajzolnunk, illetve kell két ecset: egy a rácsozáshoz, egy pedig a sejtekhez. Hozzuk is létre őket: link

A progi tulajdonképpen egy dialógusablak, amin van egy static control - ebbe rajzolunk. Ezért a CreateHwndRenderTarget metódust használjuk, amelynek átadjuk az említett control HWND-jét. Miután ezzel megvagyunk, létrehozzuk a két ecsetet a CreateSolidColorBrush metódussal.

Biztosan feltűnt, hogy ez az egész csak akkor kerül végrehajtásra, ha a rendertargetünk nem NULL. Ha a rajzolást lezáró EndDraw metódus visszatérési értéke jelzi, hogy valami gond volt az eszközzel, akkor meg kell semmisíteni az eszközfüggő erőforrásokat (így a rendertarget is NULL lesz), majd újra létre kell őket hozni. A CreateDeviceResources függvényt minden rajzoláskor meghívjuk, így ha felszabadítottuk az erőforrásokat, újra létrehozza őket. Ha már szóba került, itt a függvény, ami kikukázza a szóban forgó erőforrásokat: link

OK, megvannak az erőforrásaink, rajzoljunk! Mint már említettem, a rajzoló metódus elején meghívjuk a CreateDeviceResources függvényt. Ezután megnézzük, hogy egyáltalán látható-e az ablak, amibe rajzolni akarunk - ha nem látható, nyilván nem is rajzolunk bele. Magát a rajzolást a rendertarget BeginDraw metódusával kezdjük el. Rögtön ezután le is töröljük a rendertargetet. Ez szükséges; ha nem tesszük meg, akkor az előző rajzolás eredményére fogunk rajzolni: link

Mivel a sejtek élettere görgethető, forgatható és zoomolható, be kell állítanunk a rendertarget transzformációját: link

A zoom, a rotation és a translation változók értékét a felhsaználó tudja változtatni egér-inputon keresztül. A center változó a rendertarget középpontját tartalmazza. A SetTransform metódus beállítja a rendertarget transzformációját a paraméterben adott mátrixnak megfelelően. Szerencsére a transzformációs mátrixokat nem kell kézzel kiszámolnunk, a D2D1 névtér Matrix3x2F osztályának Scale, Rotation és Translation metódusai segítségével kényelmesen megoldhatjuk ezt a feladatot.

Nos, még mindig nem rajzoltunk egy darab pixelt sem (na jó, letöröltük a rendertargetet). Egészen eddig a pillanatig, ugyanis most megrajzoljuk a rács függőleges vonalait: link

Itt a DrawLine metódus a lényeg, ami két pontot (D2D1_POINT_2F), illetve egy ecsetet kap paraméterként, és az adott ecsettel rajzol vonalat a megadott két pont között. Van még két paramétere, amik nem szerepelnek a fenti kódban, mert van alapértelmezett értékük: strokeWidth (= 1.0f), illetve a strokeStyle (= 0). Ezekkel a vonal vastaságát, illetve stílusát (folytonos, szaggatott stb.) lehet megadni. A víszintes vonalakat ugyanígy rajzoljuk, ezért azt a kódrészletet nem másolom be ide.

Már csak az aktív cellák renderelése van hátra, vágjunk is bele! Ezeket a cellák méreténél kicsivel kisebb, zöldre színezett négyzetekként jelenítjük meg. Kitöltött négyzeteket a FillRectangle metódussal rajzolhatunk. Ez kér paraméterként egy D2D_RECT_F -et, illetve egy ecsetet: link

Ezzel minden rajzolással végeztünk, már csak meg kell ezt mondanunk a rendertargetnek. Egészen eddig a rendertarget csak gyűjtögette, hogy mit kell majd kirajzolnia, de nem rajzolt semmit. Csak akkor renderel, ha meghívjuk a Flush vagy az EndDraw metódusát. Utóbbit használjuk, hiszen mást már nem akarunk rajzolni. Ez a függvény egy HRESULT értékkel tér vissza, amiből kiderül, hogy volt-e valami gond az eszközzel. Ezért megvizsgáljuk ezt az értéket, és ha egyenlő D2DERR_RECREATE_TARGET -el, megsemmisítjük az eszközfüggő erőforrásainkat: link

Most már tényleg végeztünk, gyönyörködhetünk művünkben. :)

Konklúzió

Azt hiszem, egy bemutató írásba ennél több információt nem érdemes belezsúfolni (meg hát még én is csak tanulom az újdonságokat :). Összefoglalva az eddigieket: a Windows 7-ben kapunk egy új 2D API-t, aminek segítségével az eddigieket mind sebességben, mind minőségben felülmúló kétdimenziós grafikákat renderelhetünk. Sok lehetőségre nem tértem ki az írásban (sőt, csak az alapokat érintettem): nem volt szó például sem a GDI/GDI+/Direct3D interop-ról, sem a szövegkezelést végző DirectWrite API-ról - ezekkel még nekem is el kell játszadoznom. Azért remélem kedvcsinálónak megfelelt az iromány, s néhányan kedvet kaptok a kísérletezéshez.

Köszönöm, hogy elolvastad az írást!

Szerző: Szakály Tamás

A fenti cikk a Microsoft TechNet Windows 7 pályázat egyik pályaműve, gratulálunk a szerzőnek!