2010. augusztus 28., szombat

2010. augusztus 27., péntek

Befutott a nyár utolsó Catalystja

Az AMD hivatalosan is elérhetővé tette a Catalyst legújabb kiadását, mely a Windows 7, Vista és XP operációs rendszereket támogatja. A 10.8-as verziójú, WHQL aláírással rendelkező drivercsomag a Radeon HD 2000, 3000, 4000 és 5000 szériás grafikus kártyákhoz, valamint a HD sorozatba tartozó Mobility és integrált Radeonokhoz telepíthető. Fontos megjegyezni, hogy a Toshiba, a Sony és a Panasonic nem engedélyezte a notebookokhoz tartozó terméktámogatást.
A driver a szokásos hibajavítások mellett, bevezeti az OpenGL ES 2.0-s kódok támogatását. A WebGL szabvány segítségével az erre felkészített böngészők képesek a háromdimenziós grafika gyorsítására. Ez különösen a HTML5 esetben lesz fontos, hiszen az új jelölőnyelv segítségével külső pluginek alkalmazása nélkül jeleníthető meg egy részletekben gazdag weboldal. Az AMD támogatása az összes Radeon HD szériás kártyán elérhető, továbbá Windows 7, Vista és XP alatt is működik.
A multimédiás szolgáltatások is javultak. Az új meghajtóban beállított fejlett videobeállítások mostantól alkalmazhatók az internetes videókon is. Itt érdemes megemlíteni, hogy Windows XP alatt nem érhető el az összes szolgáltatás, így a legjobb képminőség eléréséhez Windows 7 vagy Vista szükséges. A játékok szempontjából a StarCraft 2 hivatalosan is megkapta az élsimítás támogatást, továbbá az alkalmazásprofilokon keresztül növelhető a CrossFireX rendszerek teljesítménye az Eyefinity konfigurációk esetén. Pár programban kisebb sebességnövekedésekre is lehet számítani. Az AMD három játék erejéig kimérte a változásokat, amiket az alábbi felsorolás részletez:
  • Far Cry 2 – akár 2-6% Radeon HD 5700-5800 sorozat esetén és +3-8% HD 4800 mellett
  • Left 4 Dead 2 – akár 3-5% Radeon HD 5700-5800 sorozat esetén
  • Stormrise – akár 5-10% Radeon HD 5500-5600 sorozat mellett
Az augusztusi Catalyst szoftvercsomag és a hozzá tartozó dokumentáció, valamint a legfrissebb alkalmazásprofil letölthető az AMD szerveréről. A mobil termékek esetében ezt a weboldalt érdemes használni.

nakamichi

2010. augusztus 26., csütörtök

SlimDX9 03 - És legyen világosság…

Legutóbb ott hagytuk abba a SDX leckénket, hogy létrehoztunk egy csonka gúlát a háromdimenziós térben és megforgattuk azt. Mivel nem definiáltunk normál vektorokat ezért a megvilágítást ki kellet kapcsolnunk, hogy valamit lássunk a gúlánkból. Persze fények nélkül a számítógépes grafika semmit sem ér, ezért most egy egyszerű példa segítségével belekóstolunk a fények világába.

Természetesen az első lépés a megvilágítás bekapcsolása, amit a következő sorral tehetünk meg:

device.SetRenderState(RenderState.Lighting, true);

A következő fontos dolog a megfelelő Vertex formátum megadása, ahol már helyet kapnak a normál vektorok is:
  [StructLayout(LayoutKind.Sequential)]
struct Vertex
{
public Vector3 Position;
public Vector3 Normal;
public int Color;
public static readonly VertexFormat Format = VertexFormat.Position | VertexFormat.Normal | VertexFormat.Diffuse;

public Vertex(float vX, float vY, float vZ, float nX, float nY, float nZ, int color)
{
Position = new Vector3(vX, vY, vZ);
Normal = new Vector3(nX, nY, nZ);
Color = color;
}
}
Ezután a modell létrehozása következik, ami ebben az esetben egy kocka. Itt a csúcspontok mellet, a normálvektorokat és a csúcspontok színértékét is megadjuk:
    private void OnResourceLoad()
{
int c1 = Color.Red.ToArgb();
int c2 = Color.Green.ToArgb();
int c3 = Color.Blue.ToArgb();

vertices = new Vertex[]
{
// elülső oldal
new Vertex(-1.5f, +1.5f, -1.5f, 0, 0, -1, c1),
new Vertex(+1.5f, +1.5f, -1.5f, 0, 0, -1, c1),
new Vertex(-1.5f, -1.5f, -1.5f, 0, 0, -1, c1),
new Vertex(-1.5f, -1.5f, -1.5f, 0, 0, -1, c1),
new Vertex(+1.5f, +1.5f, -1.5f, 0, 0, -1, c1),
new Vertex(+1.5f, -1.5f, -1.5f, 0, 0, -1, c1),

// jobb oldal
new Vertex(+1.5f, +1.5f, -1.5f, +1, 0, 0, c2),
new Vertex(+1.5f, +1.5f, +1.5f, +1, 0, 0, c2),
new Vertex(+1.5f, -1.5f, -1.5f, +1, 0, 0, c2),
new Vertex(+1.5f, -1.5f, -1.5f, +1, 0, 0, c2),
new Vertex(+1.5f, +1.5f, +1.5f, +1, 0, 0, c2),
new Vertex(+1.5f, -1.5f, +1.5f, +1, 0, 0, c2),

// hátsó oldal
new Vertex(+1.5f, +1.5f, +1.5f, 0, 0, +1, c1),
new Vertex(-1.5f, +1.5f, +1.5f, 0, 0, +1, c1),
new Vertex(+1.5f, -1.5f, +1.5f, 0, 0, +1, c1),
new Vertex(+1.5f, -1.5f, +1.5f, 0, 0, +1, c1),
new Vertex(-1.5f, +1.5f, +1.5f, 0, 0, +1, c1),
new Vertex(-1.5f, -1.5f, +1.5f, 0, 0, +1, c1),

// bal oldal
new Vertex(-1.5f, +1.5f, +1.5f, -1, 0, 0, c2),
new Vertex(-1.5f, +1.5f, -1.5f, -1, 0, 0, c2),
new Vertex(-1.5f, -1.5f, +1.5f, -1, 0, 0, c2),
new Vertex(-1.5f, -1.5f, +1.5f, -1, 0, 0, c2),
new Vertex(-1.5f, +1.5f, -1.5f, -1, 0, 0, c2),
new Vertex(-1.5f, -1.5f, -1.5f, -1, 0, 0, c2),

// kocka teteje
new Vertex(-1.5f, +1.5f, +1.5f, 0, +1, 0, c3),
new Vertex(+1.5f, +1.5f, +1.5f, 0, +1, 0, c3),
new Vertex(-1.5f, +1.5f, -1.5f, 0, +1, 0, c3),
new Vertex(-1.5f, +1.5f, -1.5f, 0, +1, 0, c3),
new Vertex(+1.5f, +1.5f, +1.5f, 0, +1, 0, c3),
new Vertex(+1.5f, +1.5f, -1.5f, 0, +1, 0, c3),

// kocka alja
new Vertex(-1.5f, -1.5f, -1.5f, 0, -1, 0, c3),
new Vertex(+1.5f, -1.5f, -1.5f, 0, -1, 0, c3),
new Vertex(-1.5f, -1.5f, +1.5f, 0, -1, 0, c3),
new Vertex(-1.5f, -1.5f, +1.5f, 0, -1, 0, c3),
new Vertex(+1.5f, -1.5f, -1.5f, 0, -1, 0, c3),
new Vertex(+1.5f, -1.5f, +1.5f, 0, -1, 0, c3),
};
}
A normálvektor egy olyan vektor, ami az adott háromszögre merőleges. Ennek ellenére normálvektort csak csúcsponthoz rendelhetünk hozzá.

Egy háromszög és a rá merőleges normál vektor

Ezután létre kell hozni egy fényforrást, ami jelen példánkban pontszerű lesz (Point light). A fényforrásokat a Light struktúra segítségével hozhatjuk létre:
           Light light = new Light()
{
Type = LightType.Point,
Range = 10000f,
Attenuation0 = 1f,
Diffuse = Color.White,
Position = new Vector3(-2, -2, +10)
};
A pontszerű fényforrás egyenletesen sugározz, a fény minden irányban haladhat. A pontszerű fényforrás definiálásakor meg kell adni a pozíciót (Position), a fény hatótávolságát (Range), valamint a fény intenzitásának távolsággal arányos csökkenését meghatározó együtthatót (Attenuation0, Attenuation1, Attenuation2). A létrehozott fényforrást be kell állítani:

device.SetLight(0, light);

Az első szám a Light Index, míg a második az index-el hivatkozható Light struktúra. Majd a beállított fényforrást engedélyezni kell:

device.EnableLight(0, true);

A fentieket a SetupLight() metódus foglalja össze:
       private void SetupLight()
{
device.SetRenderState(RenderState.Lighting, true);

Light light = new Light()
{
Type = LightType.Point,
Range = 10000f,
Attenuation0 = 1f,
Diffuse = Color.White,
Position = new Vector3(-2, -2, +10)
};

device.SetLight(0, light);
device.EnableLight(0, true);
}
Természetesen a fényekkel később ennél sokkal részletesebben fogunk foglalkozni, ez csak egy kis ízelítő belőle. A példa többi része már ismerős és könnyedén értelmezhető, eredménye pedig itt látható:


A teljes forrás pedig letölthető innen:


Most is meg kell jegyeznem, hogy Nyisztor Károly könyvei is sokat segítettek a bejegyzés írása közben.

2010. augusztus 24., kedd

Frissült az OpenCL platform

Az OpenCL platformot még 2008 végén mutatta be a Khronos Group, ám most elérkezett egy apróbb frissítés ideje. Az nyílt forrású 1.1-s felület a funkcionalitás területén javított, így jobb programozhatóság és nagyobb teljesítmény várható tőle.
A platform új adattípusokat vezet be, melyek közül a legfontosabbak a háromkomponenses vektorok, illetve az új képformátumok. Az 1.1-es felület jobb együttműködést garantál az OpenGL API-val, továbbá lehetőség lesz operációkat végezni egy puffer kijelölt terültén.


Az ARM és az AMD üdvözölte a bejelentést. Sőt az utóbbi vállalat elmondta, hogy a Fusion APU-k, illetve az ATI Streammel kompatibilis hardverek támogatni fogják a megújult felületet. Az NVIDIA még nem szólalt meg az OpenCL 1.1-gyel kapcsolatban, de nyilvánvaló, hogy dolgoznak az implementáláson, így a GeForce GTX 400 család is kezelni fogja a platformot.

2010. augusztus 22., vasárnap

Cloo kedvcsináló

A most következő írás egy kezdő lecke azok számára, akik valamilyen okból kifolyólag menedzselt környezetből szeretnék használni az OpenCL API-t. Az írás sghc_toma OpenCL kedvcsináló című cikkén alapszik, ezért az azzal való egyezés nem a véletlen műve. Ezer köszönet érte sghc_toma-nak. Kezdjünk is hozzá.

Az OpenCL számolás nagy vonalakban

Az OpenCL eszköz (compute device) az a hardver, amin a párhuzamos feldolgozás történik. Ezekben a hardverekben egy, vagy több számolási egység (compute unit) van, melyek egy, vagy több feldolgozó egységet (processing element) tartalmaznak – tulajdonképpen ezek hajtják végre az utasításokat. Például egy videokártya minden stream processzora, és egy CPU minden magja is egy feldolgozó egység, az nVidia videokártyák egy multiprocesszora pedig egy számolási egység.


A videokártyán futó programrészlet neve kernel kód vagy függvény. Az egyes feldolgozó egységek a kernel kód egy példányát futtatják, mindegyikük más-más adatokon. A kerneleket OpenCL-C nyelven (ISO C99 alapokon) lehet írni, és minden eszközre külön le kell fordítani őket. Az OpenCL eszközre írt kódokat természetesen nem kell egy darab függvénybe sűríteni, a kernel hívhat más függvényeket, illetve lehet több kernel is. A kernelek, a kiegészítő függvények, illetve a kernel által használt konstansok együtt egy programot alkotnak.
A kernelek egy úgynevezett kontextusban (context), környezetben futnak, mely magába foglalja a használható eszközöket, az általuk elérhető memória objektumokat, illetve a kernelek futtatásának ütemezését végző parancslistákat (command queue). A programot, amely létrehozza a kontextusokat, előjegyzi a kernelek futtatását, host programnak nevezzük, az őt futtató hardvert pedig a host eszköznek. A videokártya szempontjából a host (gazda) a CPU. A host kódból lehet memóriát lefoglalni a kártyán, feltölteni adattal, és a kernel kód futása után a host kódból lehet elérni a számolás eredményét. A kernel kódból ez a memória hozzáférhető. A kernel kódot úgy kell elkészíteni, hogy az a feladatnak csak egy részét végzi el; hogy melyik részét, azt beépített paraméterek adják meg. A feladat felosztását mi határozzuk meg, azaz kijelöljük a GPU számára a paraméter tartományokat, amelyekben végre kell hajtani a kernelt, és a GPU saját erőforrásait figyelembe véve maga beosztja, hogy melyik rész mikor és melyik multiprocesszoron fut le.
A fentieket egy egyszerű példán át könnyedén megérthetjük. Tegyük fel, hogy egy mátrixszorzást szeretnénk elvégezni az OpenCL-el. Ehhez a kernel kódban általánosan, sor és oszlop paraméterre leírjuk azt, hogy a szorzatmátrix adott sorában és oszlopában, hogy áll elő az eredmény. A kernel számára kijelöljük, hogy a sor paraméter egytől a sorok számáig, az oszlop paraméter egytől az oszlopok számáig kell, hogy terjedjen. A GPU tehát kap egy feladatot: sor × oszlop-szor végrehajtani a kernelt. A feladat végrehajtását optimálisan a GPU határozza meg magának.

A platform modell

Az OpenCL működése leírható négy modell segítségével:
  1. a platform modell
  2. a futtatási modell
  3. a memória modell
  4. a programozási modell
Az elsővel tulajdonképpen már megismerkedtünk: a host/device kapcsolatról van szó.

A futtatási modell

Mint már említettük, az OpenCL eszköz egy feldolgozó egysége a kernel egy példányát futtatja. Egy kernel-példányt munkaegységnek (work-item) nevezünk. Mikor elindítunk egy számolást, meg kell adnunk, hogy összesen hány munkaegységre lesz szükségünk. A munkaegységek összessége az index tér (index space), mely lehet 1, 2, vagy 3 dimenziós. A munkaegységek munkacsoportokba (work-group) szervezhetők. Ez azért fontos, mert az egy munkacsoportba tartozó munkaegységek között lehetséges szinkronizáció, és mindegyikük hozzáfér a csoport lokális memóriájához (erről később bővebben), míg ez a különböző csoportba tartozó egységekről ez nem mondható el.


Minden munkaegységnek van egy úgynevezett globális azonosítója (global ID), mely egyértelműen meghatározza annak helyét az index térben. Hasonlóan, minden munkacsoportnak is van egy azonosítója (work-group ID). A munkaegységeknek ezen felül van egy helyi azonosítója (local ID), mely a munkacsoporton belüli helyét határozza meg. A fentiekből következik, hogy a munkaegység pozíciója az index térben meghatározható a csoport azonosító és a helyi azonosító kombinációjával. Az index tér dimenzióinak maximális száma, az egyes dimenziókban a maximális méret, illetve egy munkacsoport maximális mérete eszközönként eltérő lehet, ezt figyelembe kell venni programozás közben! Az OpenCL API természetesen lehetőséget nyújt ezen adatok lekérdezésére.

A memória modell

A munkaegységek/kernelek által hozzáférhető memória négy típusra van osztva. A globális memóriához (global memory) az index tér minden egyes munkaegysége hozzáfér, azt írni és olvasni is tudják. Eszköztől függően a globális memória írása/olvasása lehet cache-elt. A konstans memória (constant memory) a globális memória egy olyan része, melynek tartalma nem változik kernelfuttatás közben, azt csak a host módosíthatja.


Minden munkacsoport rendelkezik egy lokális memória (local memory) területtel, melyet minden munkaegység a csoportban képes írni/olvasni. A privát memória (private memory) minden egyes munkaegységnek a sajátja, ő írni/olvasni tudja, de más munkaegység nem fér hozzá. A CUDA-t ismerőknek: érdemes odafigyelni az elnevezésekre, mert bár a két környezet felépítése hasonló, a terminológia nagyon nem. Például a CUDA-féle lokális memória az, amit itt privátnak neveznek, és ami itt lokális memória, az a CUDA-ban megosztott (shared memory).

A programozási modell

OpenCL-t használva kétféle párhuzamosítást érhetünk el: data, illetve task parallel módon programozhatunk. Az első az OpenCL fő profilja, ez jelenti azt, hogy sok kernel példány csinálja ugyanazt az index tér más-más elemein. Ha több, más feladatot végző kernelünk van, betehetjük őket egy parancslistába, és az OpenCL megtesz minden tőle telhetőt, hogy ezek optimálisan használják ki a hardvert. Ez utóbbi a task parallel módszer, hiszen egymástól független folyamatok futnak párhuzamosan.

OpenCL a gyakorlatban

Most hogy átrágtuk magunkat a száraz tényeken, következzen egy konkrét feladat megvalósítása OpenCL-ben. Először is szükségünk van egy wrapperre amivel elérhetjük az OpenCL API függvényeit .NET környezetből. Erre jelenleg az egyik legígéretesebb projekt a Cloo és az OpenCL.NET. Jelen bejegyzésben a Cloo-val fogunk megismerkedni mivel ez objektum orientált szemlélettel készült. Töltsük le a Cloo legfrissebb változatát majd indítsuk el a Visual Studio-t. Hozzunk létre egy konzolos alkalmazást majd a referenciákhoz adjuk hozzá a Cloo.dll szerelvényt. Ezután már csak a szükséges namespace-ket kell elhelyeznünk:

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Runtime.InteropServices;
using System.Diagnostics;
using Cloo;

Első programunk rendkívül egyszerű lesz, összeadunk két vektort és az eredményt eltároljuk egy harmadikban. Persze ezt a műveletet elvégezzük, mondjuk úgy 10 milliószor. Ha nem párhuzamos megvalósításban gondolkodnánk, akkor ezt valószínűleg egy ciklussal oldanánk meg, valahogy így.

for (int index = 0; index < n; index++) c[index]= a[index] + b[index];

A számoláshoz a vektorok i-edik elemeire van szükség - tehát az egyes számolások teljesen függetlenek egymástól, így a probléma szinte felkínálja magát párhuzamosításra. A terv az, hogy írunk egy kernelt, ami elvégzi a fenti műveletet egy vektor egy elemére, majd egy (n elemű vektorokkal számolva) n elemből álló index térre rászabadítjuk ezt a kernelt. Egyszerűen hangzik, mert az is.

A kernel

Első lépés a kernel kód vagy függvény elkészítése:

private static string code = @" kernel void VectorAdd(
global read_only float* a,

global read_only float* b,

global write_only float* c )

{
int index = get_global_id(0);
c[index] = a[index] + b[index]; }";

Ennyi az egész. A kernel kulcsszó - meglepő módon - azt jelzi, hogy az adott függvény egy kernel függvény. Egy kernel argumentumai a private névtérben kell legyenek, és alapértelmezetten oda is kerülnek. Ha egy argumentum mutató, megmondhatjuk, hogy a global, local és constant névterek melyikébe mutasson. A get_global_id(0) függvény az adott munkaegység globális azonosítójának első koordinátáját adja vissza. Erre azért van szükség, hogy tudjuk, hogy egy adott kernelpéldány a vektor hányadik elemének számolásáért felel. Mivel vektorokon dolgozunk, az index terünk egydimenziós, így valóban csak az első koordinátára van szükségünk. Ezután a kernel már csak elvégzi a megfelelő műveleteket a vektorok megfelelő elemein, és az eredményt visszaírja a harmadik vektor megfelelő elemébe.

Platformok

A számoláshoz szükségünk van egy OpenCL eszközre. Ahhoz, hogy létrehozhassunk egy eszközt, szükségünk lesz egy platform ID-re. Az elérhető platformok listáját a következőképpen szerezhetjük meg:

int platformNo = ComputePlatform.Platforms.Count;
ComputePlatform platform = ComputePlatform.Platforms[0];

Ez a kódrészlet az első ComputePlatform.Platforms.Count hívással kideríti az elérhető OpenCL platformok számát. A mellékelt példaprogramban az első elérhető platformot használjuk.

Eszközök

Ha megvan a platformazonosítónk (platform), lekérdezhetjük az elérhető eszközök listáját:

ReadOnlyCollection<ComputeDevice> devices = platform.Devices;

Ez nagyon hasonlít az előző kódrészlethez, a különbség annyi, hogy itt eszközökről szerzünk listát.

Környezet

Most, hogy megvan az eszköz, létre kell hozni egy környezetet a számoláshoz. Ehhez első lépésben készítünk egy környezet tulajdonság listát a platform felhasználásával:

ComputeContextPropertyList properties =
new ComputeContextPropertyList(platform);

Majd a tényleges környezet létrehozása a ComputeContext osztály segítségével történik:

ComputeContext context =
new
ComputeContext(devices, properties, null, IntPtr.Zero);

Az első paraméterben az elérhető eszközöket adjuk át, persze minden adott eszközhöz külön is kreálható környezet. Ilyenkor a ComputeDeviceTypes felsorolás tagjaiból választhatunk: Default, CPU, GPU, Accelerator, All. A következő a környezet tulajdonságait tartalmazó lista. Majd ezután, ha szükségesnek látjuk létrehozhatunk call-back függvényt amivel a hibakódokról értesülhetünk, de mi most ezt nem alkalmazzuk, ezért null.

Parancslista

Most már van környezetünk, amihez már csak hozzá kell adni egy parancslistát, ahová majd a végrehajtandó kerneleket pakoljuk.

ComputeCommandQueue commands =
new ComputeCommandQueue(context, context.Devices[0], ComputeCommandQueueFlags.None);

Ez a hívás a context.Devices[0] azonosítójú eszközhöz hoz létre egy parancslistát. Több parancslistát is létrehozhatnánk, de a kitűzött feladat elég egyszerű, egy lista is elég. Több lista esetén, amennyiben az egyes parancsok használnak közös objektumokat, figyelni kell a szinkronizálásra; erről bővebben olvashattok a OpenCL specifikációban. Még egy dolog a parancslistákkal kapcsolatban: több eszköz esetén mindegyiknek saját listára van szüksége!
Foglaljuk akkor most össze, mink van eddig. Kiválasztottunk egy OpenCL platformot, és erről a platformról egy eszközt, amin számolni fogunk. Ehhez az eszközhöz készítettünk egy környezetet, melyhez létrehoztunk egy parancslistát és írtunk egy kernel függvényt is. Most már csak pár dolgot kell végrehajtanunk: kellenek memória objektumok, amikben átadjuk a kernelnek a két vektort, illetve visszakapjuk az eredményt. A kernelfuttatáshoz szükségünk lesz egy kernel objektumra, amit csak egy, az adott eszközhöz felépített program objektumból nyerhetünk ki. Folytassuk tehát a munkát, készítsük el a program objektumot.

Program objektum

Programot készíthetünk forráskódból, illetve binárisból is. A gyorsabb inicializálás érdekében célszerű az első futtatáskor lefordítani a forrást, majd a kapott binárist (több OpenCL eszköz esetén az eltérő gépi kód miatt binárisokat) elraktározni, s következő alkalommal abból készíteni a program objektumot. Lássuk, hogy forrásból hogyan készítünk programot (a binárisból készítésre most nem térünk ki):

ComputeProgram program =
new ComputeProgram(context, new string[] { code });

Most már van egy programunk, de ez még csak a forráskódot tartalmazza, tehát le kell fordítanunk:

program.Build(null, null, null, IntPtr.Zero);

A fordítás állapotáról a program.GetBuildStatus(devices[0]) tulajdonságon keresztül kaphatunk információt. Ha minden rendben volt a ComputeProgramBuildStatus.Success enumot kapjuk vissza.

Kernel objektum

Miután sikeresen felépítettük a programot, kinyerhetjük belőle a kernel objektumot:

ComputeKernel kernel = program.CreateKernel("VectorAdd");

Mint látható, a kernel objektum gyártás elég egyszerű művelet, csupán egy lefordított programra, és a kernel függvény nevére van szükségünk hozzá.

Memória objektumok

Nos, van már kernelünk, amit tudnánk futtatni, csak egy probléma van: nincsenek adataink, amin dolgozhatunk. Ezért most létre kell hozni memória objektumokat, s feltöltjük őket a vektorainkkal. OpenCL-ben az eszköz memóriáját kétféle objektumon keresztül érhetjük el: puffer és kép (image) objektumok. A pufferekbe bármilyen adatot tehetünk, míg a kép objektumok kettő, vagy háromdimenziós képek tárolására alkalmasak. A kép objektumokkal most nem foglalkozom részletesebben, kitűzött célunk eléréséhez nincs is szükség rájuk, a vektorokat pufferekben tároljuk:

ComputeBuffer<float> a = new ComputeBuffer<float>(context, ComputeMemoryFlags.ReadOnly | ComputeMemoryFlags.CopyHostPointer, arrA);
ComputeBuffer<float> b = new ComputeBuffer<float>(context, ComputeMemoryFlags.ReadOnly | ComputeMemoryFlags.CopyHostPointer, arrB);
ComputeBuffer<float> c = new ComputeBuffer<float>(context, ComputeMemoryFlags.WriteOnly, arrC.Length);

Ez a három függvényhívás létrehozza az adott kontextusban az a, b és c vektoroknak megfelelő puffereket. Az a és b vektort csak olvasni fogjuk, így a ComputeMemoryFlags.ReadOnly flaggel hozzuk létre, míg a c vektorhoz szükség van a ComputeMemoryFlags.WriteOnly flagre, hiszen abba írjuk majd a végeredményt. Lehetőség van a puffer készítésekor megadni egy host pointert (negyedik paraméter), és az adatot, amire mutat, rögtön felmásolni a pufferbe. Ehhez szükséges megadni a ComputeMemoryFlags.CopyHostPointer flaget. Puffert létrehozhatunk a host memóriájában is (ComputeMemoryFlags.AllocateHostPointer), illetve felhasználhatunk már lefoglalt host memóriaterületet is (UseHostPointer).

Kernel futtatás

OK, már majdnem kész vagyunk, már csak futtatnunk kell a kernelt. Egyszerűen hangzik, de azért ez nem csak annyiból áll, hogy meghívunk egy függvényt, aztán fut. Be kell állítani a kernel-argumentumokat, meg kell határozni a munkacsoportok és munkaegységek számát, és elő kell jegyezni a kernelt a parancslistában.

kernel.SetMemoryArgument(0, a);
kernel.SetMemoryArgument(1, b);
kernel.SetMemoryArgument(2, c);

Ezek a függvény hívások állítják be a kernel argumentumokat. Az egyes argumentumokat a kernel függvény fejlécében elfoglalt helyük alapján azonosítjuk, ez a szám a SetMemoryArgument függvény első paramétere.

ICollection<ComputeEvent> events =
new Collection<ComputeEvent>();
commands.Execute(kernel, null, new long[] { count }, null, events);

Most már csak meg kell hívnunk a commands parancslista Execute() metódusát megfelelően felparaméterezve. Az első paraméter a kernel objektum, a második a GlobalWorkOffset amit általában null. A GlobalWorkSize vagyis a munkaegységek száma megegyezik a vektorok méretével. Azt, hogy ez hogyan van felosztva munkacsoportokra (LocalWorkSize), most teljesen mindegy (az egyes vektor elemek teljesen függetlenek egymástól, a munkaegységeknek nincs szüksége közös memóriára), ezért az OpenCL-re bízzuk.
Minden olyan függvény, ami a parancslistához ad elemeket, kér egy ComputeEvent típusú lista pointert (utolsó paraméter). Az így visszaadott ComputeEvent-tel lehet lekérdezni a parancs aktuális állapotát (pl. végzett-e?), illetve más parancsok várólistájához lehet adni a parancsot. A várólistában szereplő összes parancsnak le kell futnia, mielőtt az adott parancs lefuthatna. Nos, ha minden jól ment, ezen hívás után már fut is a kernelünk.

Eredmény olvasása

A számolás eredménye persze az eszköz memóriájában van, tehát ki kell olvasnunk onnan. Ehhez persze biztosnak kell lennünk benne, hogy a számolás befejeződött. Ezt a commands.Finish() függvénnyel biztosíthatjuk, ami csak akkor tér vissza, ha a paraméterként megadott parancslista minden parancsa befejeződött. Ezután még figyelembe kell vennünk azt is, hogy a host memóriájában menedzselt az az objektum amibe bele kívánjuk másolni a számítás eredményét és így meg kell védeni a GC-től.
A menedzselt objektumok nem-menedzselt kódból történő elérésének egyik módja, hogy használjuk a GCHandle struktúrát. A struktúra legfontosabb tulajdonsága, hogy akkor sem engedi a menedzselt objektumot kisöpörni a memóriából, ha már csak egy nem-menedzselt objektum hivatkozik rá (ellenkező esetben a memóriaterület felszabadul). Azt is megakadályozza, hogy az adott cím egy más szegmensre tolódjon a memórián belül. Az azonban lényeges, hogy a nem-menedzselt kódnak rendelkeznie kell a megfelelő jogosultságokkal ehhez, melyet a SecurityPermission osztály segítségével adhatunk meg.

GCHandle arrCHandle = GCHandle.Alloc(arrC, GCHandleType.Pinned);

Az eredmények host memóriába olvasása:

commands.Read(c, false, 0, count,
arrCHandle.AddrOfPinnedObject(), events);
commands.Finish();

Amikor ez lefut, a c memória objektum tartalma a host memóriában lévő arrC vektorba másolódik. Az arrCHandle AddrOfPinnedObject() metódusa kinyeri a szükséges címet. A pufferekbe írásnál kimaradt, most megemlítem: ha a második paraméter true, az olvasás/írás befejeztéig nem tért vissza a függvény, ha false, nem várja meg a műveletek befejeztét. Végül:

arrCHandle.Free();

Ezuán már a számítás eredménye elérhető az arrC tömbben és azt tehetünk vele amit csak akarunk. Végeztünk a számolással, de még nem fejeződött be a dolgunk. Illik eltakarítani magunk után, szabadítsuk fel az OpenCL erőforrásokat:

a.Dispose();
b.Dispose();
c.Dispose();
kernel.Dispose();
program.Dispose();
context.Dispose();
commands.Dispose();

Végszó

Elsőre kicsit bonyolultnak tűnhet a dolog, de higgyük el, hogy megéri a kínlódás mivel így új távlatok nyílhatnak meg előttünk. De még mielőtt elbúcsúznánk érdemes egy kis teljesítmény tesztet végrehajtani a klasszikus .NET for ciklus és az OpenCL/Cloo (GeForce 8800 GTS) páros között. Az eredmények a lenti ábrán láthatóak:

.NET for vs. Cloo

Hát igen, nem valami meggyőző. Nagy elemszámnál már a Cloo átveszi a vezetést, de nem sokkal. Mi lehet ennek az oka?! Először is a .NET nem is olyan lassú, mint hinnénk, másodszor pedig az OpenCL API-t nem tisztán értük el, hanem a Cloo wrapperen keresztül, így egy kis overhead mindig is lesz. Ez főleg akkor megy a teljesítmény rovására, ha a kernel program olyan apró és egyszerű, mint a mi VectorAdd példánk. Jóval nagyobb és összetettebb feladatok esetén már jóval nagyobb különbség is elérhető a CPU és a GPU között. Remélem érthető volt ez kis példa és talán meghozza a kedvet egy kis videokártya-programozáshoz. A teljes forrás letölthető: innen

2010. augusztus 19., csütörtök

SlimDX9 02 - Irány a 3D!

Az első SlimDX-es programunkban egy kétdimenziós háromszöget jelenítettünk meg. Megismerkedtünk a DX alapjaival, ideje hát tovább lépni a harmadik dimenzióba. Ebben a példában egy gúlát fogunk megforgatni a térben.
Fontos megjegyezni, hogy 3D-ben nem lehet csak úgy vaktában renderelni, hiszen meg kell mondanunk, hogy milyen nézőpontból, látószöggel, stb. szeretnénk lefényképezni a modellteret. Hiszen ebben a modelltérben van egy négyzet, mint mondjuk az asztalunkon a valóságban, ezt pedig egy fényképezőgéppel (ezt most mi kamerának fogjuk hívni) akárhonnan lefényképezhetjük.

A virtuális világ "lefényképezése"

De annak a fényképezőgépnek a lencséje állítható, így lehet, hogy a beállításai miatt például egy kört ellipszisnek fényképez, stb. Ezért kell nekünk is megadnunk egy kamerát a modelltérben, az elhelyezkedését, valamint a beállításait. Ezen a kamerán át látható képet fogjuk mi a monitorunkon át látni. A Direct3D ezt úgy oldja meg, hogy tárol három mátrixot: a modelltérbeli transzformáció mátrixot, a nézeti mátrixot illetve a projekciós, azaz vetítési mátrixot. Ezeken a mátrixokon a modelltérbeli pontjaink „végigfolynak”, így kerülnek a képernyőre (persze egyéb effektusok után). Így a ebben a leckében főleg ezekkel a bealításokkal fogunk foglalkozni.
DirectX-ben egy pontot három koordinátájával adjuk meg, tehát ismernünk kell a Direct3D-ben használt koordinátarendszert. Mi most a balkezes (Left-handed) koordinátarendszert fogjuk használni, ami azt jelenti, hogy az X-tengely jobbra mutat, az Y-tengely felfele, a Z-tengely pedig a képernyőbe befelé:

Bal- és jobbsodrású koordináta rendszerek

Természetesen az OpenGL jobbkezes (Right-handed) rendszert alkalmaz, ezt érdemes megjegyezni. Ekkor már csak egy kérdés maradt: hogyan tudjuk ezt a három dimenziót leképezni két dimenzióra?

Projekció-, nézet- és világtranszformáció

Elméletben már ismerjük a képszintézis főbb elemeit, most a helyi, kamera, világ nézet és a vágás végrehajtásához szükséges gyakorlati alapismereteket tekintjük át. A képszintézis szakaszaihoz tartozó transzformációkat a Direct3D mátrixokkal valósítja meg. A mátrixműveletek lehetővé teszik a transzformációk összekapcsolást, így egy mátrixban több transzformáció is megadható. A megfelelő transzformációkat a device SetTransform függvény meghívással valósíthatjuk meg. A State paraméterben megadhatjuk a transzformációt (a képszintézis lépést), a második paraméter pedig maga a transzformációs mátrix:
  • Világ: TransformState.World
  • Kamera: TransformState.View
  • Projekció: TransformState.Projection
Először is nézzük meg mit is jelentenek az említett transzformációk. Világ nézeti transzformáció (World transform), ezzel a transzformációval helyezzük el objektumainkat a jelenetünk szintetikus világában. Nézettranszformáció (View transform), ezzel megadjuk a kamera helyzetét, vagyis azt a pontot ahonnan a jelenetet szemléljük. Projekciós transzformáció (Projection transform), ezzel határozzuk meg a vetítés paramétereit, amelynek eredményeként a képernyőnk kétdimenziós felületén megjelenő objektumok háromdimenziósnak tűnnek.
A fenti transzformációk beállítására a DirectX hasznos függvényeket biztosít. Kezdjük talán a leglátványosabbal, a projekciós transzformációval. Hozzunk létre egy mátrixot, aminek az elemeit a PerspectiveFovLH() függvény segítségével kényelmesen fel tudjuk tölteni.

A nézeti gúla és paramétereinek értelmezése

A paramétereknél megadjuk a látótér szögét (Field of View), méretarányát (Aspect Ratio) valamint az elülső és a hátsó vágósíkok z-koordinátáját (z-front és z-back):

Matrix projection = Matrix.PerspectiveFovLH((float)Math.PI/4,
(float)ClientSize.Width / (float)ClientSize.Height, 1.0f, 100.0f);

Lényeges tudnunk, hogy a látószöget nem fokban, hanem radiánban kell megadni. Használjuk a Math.PI előre definiált értékét, egy elfogadható FoV (Field of View) a Math.PI / 4. A projekciós transzformációt a device.SetTransform() hívással léptethetjük életbe, amelynek első paramétere a TransformState.Projection érték, a második pedig a projekciós transzformációs mátrix:

device.SetTransform(TransformState.Projection, projection);

Az elülső vágósík koordinátája 1.0f – tehát az ennél közelebb helyezkedő objektumok nem láthatóak. Természetesen mindig a kamera pozícióhoz képest kell értelmezni a közeli és a távoli vágósík értékeit.
A háromdimenziós tér legnagyobb előnye, hogy objektumaink mozoghatnak benne, tehát be kell állítani a világ transzformációkat. Példánkban adott tengely körüli elforgatást valósítunk meg, amit szerencsére ugyancsak függvényekkel támogat a DX:

device.SetTransform(TransformState.World, Matrix.RotationAxis(
new Vector3
(
angle / ((float)Math.PI * 2.0f),
angle / ((float)Math.PI * 4.0f),
angle / ((float)Math.PI * 6.0f)
),
angle / (float)Math.PI));

A TransformState.World paraméter megadásával jelezzük, hogy a világtranszformációt akarunk beállítani. A Matrix.RotationAxis() függvény segítségével megkaphatjuk azt a mátrixot aminek a segítésével a tetszőleges tengely körüli elforgatást valósíthatjuk meg. A tengely egy Vector3 típus definiálja, az elforgatás szögét pedig az angle változó tárolja.
Hátra van még a kamera pozíciójának beállítása, amin keresztül a jelentet szemléljük. Ez a kamera a térben bárhol lehet, és bármerre nézhet. Tehát ebből a kamerából látható kétdimenziós képre vagyunk kíváncsiak, azaz a csúcspontban lévő megfigyelő milyen képet lát a kamera síkján. A DirectX a kamerát három jellemzővel adja meg. Ez a három jellemző egy-egy vektor a modelltérben: a kamera helyzete (Eye), az a pont, amire a kamera néz (Target) és a felfele irány (Up). Ebből természetesen ki tudjuk számolni a kamera nézési irányát: Direction=Target-Eye, ami egy egyszerű kivonás vektorok közt. Ezek ismeretében már tudunk egy koordináta-rendszert illeszteni a kamerára is. A DX Matrix.LookAtLH függvény egy bal sodrású (Left Handed, LH) nézeti mátrixot tölt fel a megadott vektorok alapján:

Matrix view = Matrix.LookAtLH(
new Vector3(0, 0, zoom),
new Vector3(),
new Vector3(0, 1, 0));

device.SetTransform(TransformState.View, view);

Számunkara a kamera helyzetét meghatározó mátrix különösen fontos mivel a kamerapocizó módosításával egy fajta zoom hatást valósítunk meg. A zoom értékét a fel és a le billentyűkkel növeljük illetve, csökkentjük. A transzformációkat összefogva a void SetUpCamera(float zoom) metódus valósítja meg amit a Render() metódusban hívunk meg. Az előbbi transzformációkat együttesen a következő ábra mutatja be szemléletesen:

A DX rögzített grafikus csővezetéke

Látható, hogy az egyik oldalon belépnek a vertexek, végigfolynak a transzformációs műveleteken, majd végül a DX gépezet kiköpi a másik oldalon a rasztert, vagyis a képernyőre kerülő képet. Ezt folyamatot szépen grafikus csővezetéknek nevezik aminek csak egy eleme transzformációs modul. A hagyományos (Fixed Function Pipeline) főbb elemei a következők:
  • Transzformáló modul: A modul négy módosítható állapotregisztert tartalmaz: nézeti transzformáció mátrixa, modellezési transzformáció mátrixa, projektív transzformáció mátrixa valamint a képernyő koordináta-rendszerébe vivő transzformáció leírása (viewport). Tetszőleges vetítést (párhuzamos, perspektív) megvalósító mátrixot is támogat. Ahogyan a neve is mutatja, a geometria transzformációival foglalkozik, emiatt geometriai modulnak is hívják.
  • Árnyaló modul: Ez a modul a megvilágítással (árnyalással) és szín információkkal kapcsolatos számításokat végez. Egy veremszerű struktúrát használ fel, az aktuális fényforrások rekordjainak kezeléséhez. Ambiens, irány, pontszerű és szpot fényforrások megvalósítását támogatja.
  • Raszterizáló modul. Ez a modul az előbb említett két modul kimenetét használja fel a virtuális világ megjelenítéséhez. A raszterizáló modul végzi a 3D képszintézist (a színbufferbe rajzolást) a Direct3D-ben. Raszter műveletek, mint huzalváz, tömör kitöltési módok és textúra térképek is ebben a modulban vannak definiálva.
A hagyományos, huzalozott "transformation and lighting" (T&L) motort, rögzített műveleti sorrendű csővezetéknek nevezik, mivel a fejlesztő nem avatkozhat bele, nem módosíthatja a csővezeték működését, csupán vezérelheti. Innen ered a "fixed function pipeline” elnevezés. Felmerülhet a kérdés, hogy mi van akkor, ha a programozó nem elégedett a csővezeték működésével? A válasz egyszerű, semmi, ez van, ezt kell szeretni. Persze ez nem elfogadható válasz egy programozó számára így elég hamar (ez viszonylagos) megjelent a programozható csővezeték. És itt lépnek a képbe a shaderek, de nem menjünk ennyire elébe a dolgoknak. Ma már a merev csővezeték elavultnak számít, de ne higgye azt senki, hogy ettől el kell azt felejteni! Sőt! Igencsak hasznos, főleg kezdők számára.
Most, hogy megismerkedtünk a grafikus csővezeték fogalmával térjünk vissza pár gondolat erejéig az első példaprogramunkhoz. Első művünk egy háromszög volt képernyő koordinátarendszerben (2D) megadva. Itt elegendő lett volna egy vertexet két koordinátával megadni (x,y), de nem azt tettük, hanem négy koordinátát azaz Vector4 struktúrát használtunk. A z koordináta egyértelműen 0, míg a negyedik homogén tag (w) mindenhol 1 volt. Miért bonyolítottuk így meg a dolgokat?
Először is a DirectX alapvetően a 3D-re van felkészítve, gondoljunk csak a csővezetékre. Ezért, ha azt akarjuk, hogy vertex-ünk ne menjen keresztül a transzformációs folyamatokon, akkor azt közölni kell a DX-el. Ezért definiáltuk, úgy a vertex formátumát, hogy: VertexFormat.PositionRhw. A PositionRhw jelentése egyszerűen az, hogy ezek a vertexek már fel vannak dolgozva, azaz transzformáltak, koordinátáik pixelben értendőek. A transzformált vertexek pedig mindig homogén vágási térben helyezkednek el! Ezért szükséges a Vector4 struktúra. A struktúra x és y tagja maga a képernyő koordináta, a z koordináta természetesen teljesen jelentéktelen, csupán egy esetleges z-buffer esetén meghatározza a takarást, végül következik az rhw tag (reciprocal of homogenous w) aminek az értéke 1 szokott lenni. Ezek után jöjjön az új vertex formátumunk amivel már 3D-s vertexeket adhatunk meg:

[StructLayout(LayoutKind.Sequential)]
public struct Vertex
{
public Vector3 Position;
public int Color;
public static readonly VertexFormat Format =
VertexFormat.Position | VertexFormat.Diffuse;
}

Láthatjuk, hogy most már Vector3 struktúrát használunk és a vertex formátum simán Position vagyis térbeli és nem transzformált vertexről van szó! Ebből fogja tudni, a DX hogy milyen műveleteken kell átesnie ezeknek a vertexeknek, hogy a képernyő megjelenjenek. A pozíció mellet a vertex színét is eltároljuk. A VertexFormat.Diffuse azt jelenti, hogy minden vertex kap egy színt is. Ez azt fogja jelenteni, hogy vertex színe a megadott lesz, a primitíven belül pedig a vertexek között folyamatos színátmenet lesz. Látható, hogy több tulajdonságot ’vagy’ kapcsolattal adhatunk meg. A VertexFormat formátumot célszerű magában a Vertex struktúrában megadni Format néven, így nem kell fejben tartanunk annak megadását a renderelés során. A struktúra elején lévő attribútum [StructLayout(LayoutKind.Sequential)] azt mondja meg, hogy az adattagok folytonosan jönnek egymás után.

A jelenet geometriája

A jelenet három dimenziós testekből álló tér. Alapvetően minden testet a pontjaival (vertex) adunk meg. Hogy a megadás módja egyértelmű legyen, ezért egy nagyon egyszerű módszert használunk: minden testet háromszögekre bontunk, ez lesz egy primitív, hiszen tényleg a legegyszerűbb síkidom, aminek területe van. Ezeket a háromszögeket a pontjaival adunk meg, ami egyértelmű, hiszen a három pont akármelyik sorrendben megadva egyértelműen jellemzi a háromszöget. A háromszög pontjainak van egy jellegzetes sorrendje: ha ránézünk a háromszögre, és úgy a pontjait olyan sorrendben adtuk meg, hogy jobb irányban haladnak, akkor pozitívan irányított (DirectX-ben). Azaz pl.:

Irányított vertex megadás

Itt például 0,1,2 pozitívan irányított, viszont 0,2,1 nem. Tehát sikerült megadni egy háromszöget, megismerve az irányítását is. Ha minden háromszöget ennek megfelelően adunk meg akkor bekapcsolhatjuk a hátsó lapok eldobását, hiszen a pozitív z koordinátájú háromszög normálisok (normálvektor, ami ugye merőleges a háromszög síkjára) azt jelentik, hogy az a háromszög a test túloldalán van, azaz nem látszik. Ezért is beszéltünk a pozitív körüljárásról, hiszen csak azok a háromszögek fognak látszani, melyek az aktuális nézőpontból pozitív körüljárásúak. Tehát ez a takarási munka egyszerű:


A hátsó lapok eldobását a következő sorral kapcsolhatjuk be:

device.SetRenderState(RenderState.CullMode, Cull.Counterclockwise);

Ekkor a rendszer minden poligonhoz normálvektort számol és ennek és a nézőpontnak (nézési irány alapján) veszi a közbe zárt szögét és ez alapján dönti el, hogy hátsó lap-e. Nyilván, ha a normálvektor felénk mutat, akkor hegyes szög lesz, ha meg ellenkező irányba, akkor tompaszög. Mivel nagyjából minden második poligon hátsó lap így akár megduplázhatjuk a program sebességét.
Ezek után már csak fel kell tölteni a vertices Vertex tömböt a szükséges adatokkal (gúla csúcspontjai), amit az OnResourceLoad() metódus meg is tesz.
Mielőtt még elkezdenénk a renderelést ne felejtsük el kikapcsolni a fényeket. Ha ezt nem tennénk meg, akkor a gúlából semmi sem látszódna a fekete háttér előtt. A fények kikapcsolását a következő sor engedélyezi:

device.SetRenderState(RenderState.Lighting, false);

Ezek után már nincs más hátra, mint gyönyörködni az első valódi 3D-s DX-es programunkba, aminek az eredménye itt látható:

Hello Pyramid!

Természetesen ebben a leckében sem tértem ki mindenre mivel a forrás tartalmaz megjegyzéseket így a hiányzó részek megértése házi feladat. A példa teljes forrása itt tölthető le:


Most is meg kell jegyeznem, hogy Nyisztor Károly könyvei is sokat segítettek a bejegyzés írása közben. Valamint felhasználtam Crusader DX leckéit is.

2010. augusztus 17., kedd

A GPU-k miatt kihalnak a jelszavak?

Az egyszerűbb jelszavakat ma már egy otthoni PC-n is könnyű visszafejteni brute force módszerrel. Jelszavak helyett jelmondatok kellenek, mondják amerikai kutatók. A modern grafikus processzorok (GPU) kiemelkedő számítási kapacitása ma már nemcsak látványos 3D-s animációk megjelenítésére használható, hanem bármilyen nagy számításigényű feladat elvégzésére is. Amerikai kutatók szerint az, hogy a korábban csak szuperszámítógépekre jellemző teljesítmény az otthonokban is elérhetővé válik, kihúzhatja a talajt a jelszavas hitelesítés alól, mely ma messze a legelterjedtebb az informatikában. Az, hogy a GPU-k masszívan párhuzamos feldolgozási képessége jól hasznosítható a jelszótörésben, nem újdonság. Egy orosz vállalkozás már három évvel ezelőtt levédetett egy olyan módszert, amellyel az összes lehetséges kombinációt végigpróbáló ún. brute force támadásokhoz szükséges idő látványosan lerövidíthető. A Georgiai Műszaki Egyetem kutatói most azt mondják: a GPU-k számítási teljesítménye olyan ütemben növekszik, hogy csak hosszabb és bonyolultabb jelszavakkal érezhetjük ideig-óráig biztonságban magunkat. „Határozottan állíthatjuk, hogy ma egy hét karakterből álló jelszó reménytelenül kevés, de ahogy nő évről évre a GPU-k kapacitása, úgy nő a veszély is” – mondta Richard Boyd, a jelszavak esélyeit vizsgáló kutatás egyik vezetője.



Ma egy grafikus processzor számítási teljesítménye elérheti a két teraflopsot (billió művelet másodpercenként) – ezzel a mutatóval tíz évvel ezelőtt a világ legerősebb szuperszámítógépei közé is be lehetett volna kerülni. Bár korábban ezt a kapacitást kizárólag a háromdimenziós grafikai megjelenítés során hasznosították, néhány éve a GPU-gyártók felismerték a tudományos és műszaki számításokban rejlő potenciált, és kifejezetten ösztönzik a grafikus gyorsítóchipek ilyen célú felhasználását. Az e téren úttörő NVIDIA 2007 februárjában tette CUDA néven elérhetővé azt a standard C fejlesztői környezetet – számos könyvtárral, fordítóval és dedikált meghajtóprogramokkal –, amely ehhez megteremtette az alapot.


A jó jelszó hosszú és bonyolult

Ez a számítási kapacitás jelszótörésre is igen hatékonyan használható. A kutatók felhívják a figyelmet arra, hogy minél hosszabb jelszavat választunk, annál jobban védjük magunkat a brute force támadásokkal szemben, de fontos az is, hogy vegyesen használjunk számjegyeket, nagybetűket és különleges karaktereket. Az emberek többsége az egyszerűség kedvéért kizárólag kisbetűket választ a jelszavához, a jelszótörők is ezeket próbálják ki először.
„A billentyűzeten 95 karakter található, minden egyes karakter, amit hozzáadunk a jelszavunkhoz, exponenciálisan, 95-szörösére növeli a védettségünket” – mondja Boyd. A team szerint ha egy jelszó 12 karakternél rövidebb, ma már sebezhetőnek tekinthető. A kutatók ezért azt javasolják, hogy jelszavak helyett inkább „jelmondatokat” válasszunk, lehetőleg olyanokat, amelyekben számjegyek és szimbólumok is találhatók. A mondatokat a hosszuk miatt jóval időigényesebb visszafejteni, miközben könnyen megjegyezhetők.

Persze legyen bármilyen hosszú és bonyolult is egy jelszó, más trükkökkel (például jelszólopó programmal stb.) megszerezhető. Ezért hosszú távon a csak jelszavas hitelesítést egy olyanra kell lecserélni, amely úgy biztosít erősebb védelmet, hogy nem teszi bonyolultabbá a felhasználók életét – mondják a kutatók.

2010. augusztus 3., kedd

SlimDX9 01 - Kezdőlépések

Mit tehet az ember ha .NET programozó és megszeretne ismerkedni a DirectX API-val? Hát előveszi a MDX-t (Managed DirectX) amit azonban már 2006-ban lezártak. Így nem szerencsés túl sok energiát beleölni mivel csak a DX9-es verzióját támogatja. Pár bejegyzés erejével én is foglalkoztam vele, de már elszállt felette az idő. Helyette itt van a XNA stúdió, ami már egy komplett Framework a játékfejlesztéshez. De mi van, ha nem játékot akarunk írni, csak egyszerűen a DirectX API érdekel minket? Szerencsére nem kell lemondani a C# eleganciáról, mert itt van a SlimDX ami egy nagyon vékony wrapper a DirectX API fölé és már a DirectX 11-et is támogatja. A SlimDX az a DirectX-nek mint a Tao és az OpenTK az OpenGL-nek egy réteg, amin keresztül a natív API elérhető menedzselt környezetből. Persze az interop miatt van sebességkülönbség, de jelenleg ez egy picit sem izgat minket, mert nem DOOM 6-ot fejlesztünk! Pár FPS feláldozható a .NET környezet által biztosított kényelemért. Szerintem, megéri. Azért, hogy kedvet csináljak itt egy kis videó, hogy hozzáértő kezek mit is tudnak kivarázsolni a SlimDX-ből:



Persze a teljesítmény relatív. A C++ programozók soha nem fogják megérteni mi értelme van 3D API-t menedzselt környezetből elérni. Pedig a sebesség nem minden. A SlimDX támogatja a Direct3D9, Direct3D9Ex, Direct3D10, Direct3D 10.1, Direct3D 11, Direct2D, D3DCompiler, DirectWrite, DirectInput, DirectSound könyvtárakat és a felsorolás még nem is teljes. Egyenlőre mi is megmaradunk a DirectX 9-nél mert itt még shaderezés nélkül is kitudnunk rajzolni egy háromszöget és majd később térünk át DX10-re vagy 11-re...talán :)

A SlimDX használatbavétele


A SlimDX wrapper használatához meg kell látogatnunk a http://slimdx.org weboldalt ahonnan töltsük le a legutolsó Developer SDK-t. Fontos tudnunk, hogy a SlimDX segítségével fejlesztett programjaink csak akkor indulnak el, ha ez a library telepítve van, ezért célszer még letölteni az End User Runtime csomagot is, amit programunkhoz mellékelhetünk, ha azt a szomszédnak vagy éppenséggel a nagyvilágnak kívánjuk átadni.
A SlimDX SDK telepítése után (amivel nem lehet probléma) hozzunk létre egy új Windows Forms projektet VS-ben. A referenciákhoz adjuk hozzá a SlimDX.dll-t. A Form1-egyet nevezzük át mondjuk MainForm-ra és nézzük meg a háttérkódot. A névterekhez adjuk hozzá a következő sorokat:

using SlimDX;
using SlimDX.Direct3D9;
using SlimDX.Windows;

Amint látható a DirectX 9-es verziójával kezdünk el ismerkedni, talán egyszer a DX10-ig is eljutunk majd. Itt változtassuk meg a MainForm ősét Form-ról RenderForm-ra. A RenderForm a SlimDX.Windows névtérben lakozik, őse persze ennek is Form csak éppen a konstruktorában van egy kis trükk elrejtve, akit érdekel reflektorozza meg! Ezzel készen is vagyunk. A SlimDX.dll elérhető .NET 2.0 és 4.0 alá is, mi most a 2.0 Frameworkot igénylő szerelvényt fogjuk használni. Akkor kezdjünk el kódolni…

A Direct3D környezet lekérdezése és beállítása


Az első lépések egyike, hogy inicializáljuk a Direct3D környezetet. Ehhez létrehozunk egy példányt a Direct3D osztályból.

private Direct3D direct3D = new Direct3D ();

A direct3D példány segítségével elérhetőek azok a függvények amelyekkel a rendelkezésre álló megjelenítő hardvereszköz(ök) képességeit lekérdezhetjük. Ezt az objektumot kell legelőször létrehozni és persze ez is szűnik meg utoljára.
Természetesen ahhoz, hogy megjelenítsünk bármit is szükség van megjelenítő eszközre vagy adapterre. Az elérhető megjelenítő eszközöket a direct3D.Adapters tulajdonság segítségével kaphatjuk meg, amit egy AdapterCollection listában tárolunk:

AdapterCollection adapters = direct3D.Adapters;

A lista elemei tárolják az elérhető eszközök tulajdonságait amik az AdapterInformation osztályból származnak. Egy megjelenítő részletes leírását pedig a Details tulajdonságon keresztül érhetjük el, ami pedig az AdapterDetails osztály egy példánya. A Details osztálynak számtalan tulajdonsága van ami keresztül információkat kaphatunk, ezek közül a legfontosabbak:
  • Descripton – Eszköz leírása szövegesen
  • DriverName – Meghajtó
  • DeviceName – Logikai Név
  • DriverVersion – Meghajtó verzió
  • VendorID – Gyártóazonosító
  • DeviceIdentifier – Eszközazonosító
  • WHQLLevel (Windows Hardware Quality Labs (WHQL)) – Digitális aláírás
A WHQL a Microsoft tesztlaboratóriuma, amelynek feladata a Windows operációs rendszerhez szánt eszközök és eszközmeghajtó programok kompatibilitásának ellenőrzése, és erről a megfelelő tanúsítványok kiállítása. Összefoglalva így kérdezhetjük le az elérhető megjelenítőket:
 AdapterCollection adapters = direct3D.Adapters;

foreach (AdapterInformation adapter in adapters)
{
DisplayModeCollection modes = adapter.GetDisplayModes(Format.X8R8G8B8);
DisplayMode current = adapter.CurrentDisplayMode;
AdapterDetails detail = adapter.Details;
}
A CurrentDisplayMode tulajdonság egy DisplayMode struktúrát ad vissza, aminek adattagjai tartalmazzák a megjelenítő szélességét (Width) és magasságát (Height), valamint ezek egymáshoz viszonyított arányát (AspectRatio). A Format a pixelformátumot, azaz a képpontok memóriabeli leképezésének módját, a RefreshRate pedig a képfrissítés gyakoriságát tartalmazza. A DisplayModeCollection egy lista amiben a videokártya által támogatott üzemmódokat tárolhatjuk el. A listát az adapter GetDisplayModes metódusának meghívásával kérdezhetjük le. Paraméternek az ellenőrizni kívánt RGBA, mélység vagy stencil puffer formátumot kell átadnunk. Az alapértelmezett megjelenítő sorszámát a következőképpen kaphatjuk meg:

private int adapter = direct3D.Adapters.DefaultAdapter.Adapter;

A legtöbb esetben adapter = 0. Az adapter sorszámú megjelenítőhöz tartozó képernyő beállításokat egy DisplayMode struktúrában tároljuk el:

private DisplayMode mode =
= direct3D.GetAdapterDisplayMode(adapter);

A következő teendőnk felmérni az eszköz csúcspontfeldolgozó képességét. Ehhez a az IDirect3D9 interfész GetDeviceCaps() függvényét hívjuk meg, amely a végrehajtás során feltölt egy Capabilities típust az eszköz jellemzőivel:

private Capabilities caps =
= direct3D.GetDeviceCaps(adapter, DeviceType.Hardware);

A függvény első paramétere a megjelenítő eszköz sorszáma (adapter) – mi az elsődleges (alapértelmezett) megjelenítő eszköz képességeit szeretnénk felmérni. A második paraméter pedig az eszköz típusa, ami lehet (Hardware, Software, Reference, NullReference). Az eszköz képességeit a caps fogja tartalmazni. A hardweres csúcspontfeldolgozás lekérdezése a következő képpen valósítható meg:

CreateFlags createFlags = CreateFlags.None;

if ((caps.DeviceCaps & DeviceCaps.HWTransformAndLight) != 0)
createFlags = CreateFlags.HardwareVertexProcessing;
else
createFlags = CreateFlags.SoftwareVertexProcessing;

Amennyiben eszközünk támogatja a hardveres T&L műveleteket, leendő eszközünk csúcspontfeldolgozással kapcsolatos létrehozási paramétereit hardveres megoldásra állítjuk (CreateFlags.HardwareVertexProcessing), ellenkező esetben a szoftveres vertex feldolgozást választjuk. Érdemes megemlíteni, hogy vegyes csúcspont feldolgozás is választható (CreateFlags.MixedVertexProcessing). Amennyiben van hardveres vertex-támogatás, úgy ez az opció elérhető. Az alkalmazásban a szoftveres és a hardveres csúcspontfeldolgozás között az alábbi függvény segítségével váltogathatunk:

bool Device.SoftwareVertexProcessing

Vagyis a létrehozott device példány SoftwareVertexProcessing tulajdonságának szoftveres vertex feldolgozáshoz TRUE, hardveres feldolgozáshoz pedig FALSE értéket kell beállítani.
Természetesen fontos, hogy a kiválasztott képpontformátum támogatottságát is leellenörizzük, az első és a hátsó színpuffer esetében egyaránt, alblakos és szükség szerint teljes képernyős üzemmodókban is. Az ellenörzés a CheckDeviceType függvény segítségével történik:

bool check = direct3D.CheckDeviceType(adapter,
DeviceType.Hardware, mode.Format, mode.Format, false);

Az első paraméter az Adapter ami a megjelenítő eszköz sorszáma. A második paraméter a DeviceType ami az eszköz típusát adja meg. A DeviceType felsorolás egy eleme lehet, a leggyakoribb a DeviceType.Hardware, hiszen ez biztosítja a hardveres gyorsítást. Az AdapterFormat az elsődleges képernyőpuffer tesztelendő pixelformátuma, a BackBufferFormat a hátsó lapra szánt pixelformátum, mindegyik a Format felsorolás valamelyik tagja. Az utolsó paraméter pedig a Windowed ami TRUE, ha ablakos, és FALSE a teljes képernyős alkalmazás esetén. A megkülönböztetés azért fontos mert egyes pixelformátumok kizárólag teljes képernyős üzemmódban működnek, míg mások csakis ablakos alkalmazásokban. Példaprogramjainkban mind az első, mind a hátsó színpuffer esetén az asztal mindenkori pixelformátumát fogjuk alkalmazni, aminek a beállításait a mode változó tárolja.

A létrehozandó eszköz paramétereinek beállítása

A létrehozandó eszköz paramétereit egy „struktúra” tartalmazza, amelynek a helyes feltöltése létfontosságú. Hozzunk létre egy példányt a PresentParameters osztályból:

private PresentParameters presentParameters =
= new PresentParameters();

Ez a példány fogja tartalmazni az eszköz paramétereit, ezért most tekintsük át a PresentParameters adattagjait:
  • BackBufferWidth: a hátsó képernyőpuffer szélessége pixelben. Ablakos alkalmazások esetében a zérus érték azt jelenti, hogy a kliensablak szélességét érvényesítjük.
  • BackBufferHeight: a hátsó képernyőpuffer magassága pixelben. Ablakos alkalmazások esetében a zérus érték azt jelenti, hogy a kliensablak szélességét érvényesítjük.
  • BackBufferFormat: a hátsó lap pixelformátuma. Ablakos alkalmazások esetében használható a Format.Unknown érték, amivel azt jelezzük, hogy az asztal aktuális pixelformátumát szeretnénk átvenni.
  • BackBufferCount: a hátsó pufferek száma, általában kettő (dupla pufferelés)
  • Multisample: az elsimítás típusa, ami a MultisampleType felsorolás egy tagja. Az alapértelmezett érték a MultisampleType.None, viszont ha szeretnénk élsímitást a lapváltást SwapEffect.Discard értékre kell állítani.
  • MultisampleQuality: az élsimítás minősége, értéke nulla és CheckDeviceMultisampleType függvény által visszaadott (QualityLevels - 1) érték közötti tartományba tartozik.
  • SwapEffect: a lapváltás módja ami a SwapEffect felsorolás értékkészletéből kerül ki. A SwapEffect.Discard a legáltalánosabb és egyben a leghatékonyabb beállítás, nem használunk swappet, egyből megjelenítünk mindent.
  • DeviceWindowHandle: az alkalmazás ablakkezelője, beazonosítja az ablakot, ahol megjelenítjük a színteret.
  • Windowed: a TRUE értékkel azt jelezzük, hogy alkalmazásunk ablakos módban fut, ellenlező esetben pedig teljes képernyős alkalmazásról van szó.
  • EnableAutoDepthStencil: amennyiban TRUE értékre állítjuk akkor maga a Direct3D hozza létre és kezeli a mélység- és a stencilpuffert. Ellenkező esetben magunknak kell gondoskodnunk mindenről.
  • AutoDepthStencilFormat: a mélységpuffer pixelformátuma a Format felsorolás tagja. Kizárólag EnableAutoDepthStencil = true beállítás mellett érvényesül.
  • PresentFlags: további beállítások amik a PresentFlags felsorolás tagjai.
    FullScreenRefreshRateInHertz: a képernyő vízszintes frissítési frekvenciája. Ablakos üzemmódban értéke kötelezően nulla, teljes képernyős alkalmazás esetében pedig használjuk az mode.RefreshRate értékét.
  • PresentationInterval: a lapváltás maximális gyakorisáága. A leggyakoribb érték a PresentInterval.Default amikor is a függőleges visszatérés (vertical retrace) alatt történik meg a lapváltás. A PresentInterval.Immediate beállításnál azonnal végrehajtódik a lapváltás. A PresentInterval.One hasonlóan a Default értékhez a függőleges visszatérésre vár, de attól eltérően nem a rendszeridőt hanem az annál pontosabb és számításigényesebb timeBeginPeriod függvényt hívja meg.
Az eszköz elkészítése

Lassan de biztosan elérkeztünk a lényeghez, létrehozzuk a az eszközünket amelyen keresztül elérhetjük a videokártya szolgáltatásait. Az eszköz készítése a Device osztály példányosításából áll:

private Device device = new Device(direct3D, adapter,
DeviceType.Hardware, this.Handle, createFlags, presentParameters);

A Device konstruktorának át kell adni egy példányt a Direct3D osztályból, az adapter a megjelenítő sorszáma, DeviceType a már szokásos hardware, a controlHandle az ablak kezelője amelyhez eszközünket társítjuk. Ha a PresentParameters típus DeviceWindowHandle tagját érvényes értékre állítottuk akkor NULL értéket is állíthatunk ablakos alkalmazások esetében. A CreateFlags a leendő eszköz létrehozási opciói, érvényes értékeit CreateFlags felsorolás tartalmazza. A vertex-feldolgozás módja mellett itt jelezhető az is, ha az alkalmazást többszálú futtatásra szeretnénk felkészíteni (CreateFlags.Multithreaded). Végül a PresentParameters típus egy feltöltött példányát kell átadni ami az eszköz jellemzőit tárolja. A típus egyes adattagjai megváltozhatnak a hívás során. Ilyen általában a BackBufferWidth, a BackBufferHeight, a BackBufferCount valamint a BackBufferFormat.
Miután elkészültünk a DirectX eszközzel, nincs más hátra mint kirajzolni valamit a képernyőre ami nem is lehet más mint egy háromszög 2D-ben. Tudom, elég unalmas…

Rajzolás! Hurrá!

A rajzolás előkészítéseként definiálni kell egy struktúrát amiben eltároljuk a csúcspontok tulajdonságait azaz az attribútumokat. Az egyik legfontosabb attribútum a pozíció (x, y, z) ez triviális, de persze ennél sokkal több információ is csatolható egy vertexhez. Gondoljunk csak a színre, vagy a textúra koordinátára, stb. Nyilván való az is, miden feladatnak más-más vertex formátum felel meg ezért azt a programozónak szabadon kell tudnia változtatni. Nos, mi most egy elég alap szerkezetet hozunk létre, a pocizó mellet csak a vertex színét vesszük fel az attribútumok közé:

[StructLayout(LayoutKind.Sequential)]
public struct Vertex
{
public
Vector4 Position;
public int
Color;
}

A vertex struktúra felépítéséről viszont értesíteni kell a DirectX eszközt is, hiszen egyébként nem tudja helyesen értelmezni a kapott adatokat:

device.VertexFormat =
= VertexFormat.PositionRhw | VertexFormat.Diffuse;

A fenti beállítás azt mondja meg a Direct3D-nek, hogy a vertexeken nem kell már végrehajtani a transzformációs műveleteket (pl. forgatás, eltolás, stb) mivel eleve képernyő-koordinátákban szerepel a pozíció (PositionRhw). A VertexFormat Diffuse tagja pedig azt fejezi ki, hogy a pozíció mellett a vertexek színértéke foglal helyet. Röviden a definiált vertex formátumot a TransformedColored névvel illethetnénk. Persze ennél sokkal rugalmasabban is definiálhatunk vertex formátumot de erről majd később lesz majd csak szó. A Vertex struktúra felhasználásával hozzunk létre egy tömböt és töltsük is fel azt:
  private Vertex[] vertices = new Vertex[3];

private void OnResourceLoad()
{
vertices[0].Position = new Vector4((float)(0.50f * Width), (float)(0.25f * Height), 0f, 1f);
vertices[0].Color = Color.Red.ToArgb();

vertices[1].Position = new Vector4((float)(0.75f * Width), (float)(0.75f * Height), 0f, 1f);
vertices[1].Color = Color.Green.ToArgb();

vertices[2].Position = new Vector4((float)(0.25f * Width), (float)(0.75f * Height), 0f, 1f);
vertices[2].Color = Color.Blue.ToArgb();
}
Ezek után már valóban csak a rajzolás lépései vannak hátra ami azért elég hasonló az OpenGL-hez:

device.Clear(ClearFlags.Target | ClearFlags.ZBuffer,
Color.Black, 1.0f, 0);

A fenti sor törli a render Target-et és a Z-puffert, a háttérszínt pedig feketére állítja, vagyis a szín- és Z-puffer inicializálása történik meg. Meg kell adni még a Z-puffer kezdőértékét végül pedig a stencil puffer kitöltési értékét is, jelen esetben nem használjuk.

device.SetRenderState(RenderState.FillMode, FillMode.Solid);

A SetRenderState metódus segítségével beállítjuk a megjelenítés módját, vagyis, hogy pont, drótváz vagy kitöltött megjelenítést szeretnénk. Ehhez előszór a RenderState felsorolás FillMode tagját választjuk ki, majd a FillMode enum Solid elemét adjuk át. Ezután indítsuk el a jelenetet, vagyis a renderelést:
            //A jelenet indítása - függöny fel
device.BeginScene();
{
device.VertexFormat = VertexFormat.PositionRhw | VertexFormat.Diffuse;
device.DrawUserPrimitives(PrimitiveType.TriangleList, 1, vertices);
}
//A jelenet vége - függöny le
device.EndScene();
A device DrawUserPrimitives metóduson keresztül adjuk meg, hogy milyen primitív típust alkalmazzon a megjelenítés során (PrimitiveType.TriangleList). Példánkban a vertexeket háromszögek csúcspontjaként fogja kezelni a DX. Ezt követi a primitívek száma, majd a tömb ami a vertexeket tartalmazza. Ezután lezárjuk a jelenetet:

device.EndScene();

Majd következik a lapváltás - a hátsó puffer tartalma az elsődleges pufferre kerül, azaz megjelenik a képernyőn a háromszögünk:

device.Present();

Egyenlőre ennyiből tevődik össze a Render() metódusunk. Már csak meg kell hívnunk, tehát írjuk felül a Form OnPaint metódusát. Ez egy gyakori megoldás…de nem most!

Az alkalmazás főciklusa

Miután létrehozunk a Direct3D eszközt és megírtuk a Render() metódust következhet a megjelenítés. DirectX alkalmazások esetében a megjelenítést felelős Render() függvény a Run() függvényből hívódik meg folyamatosan, az alkalmazás futása alatt. A kérdés az, hogy miként valósítsuk meg a Run() metódust .NET-ben.
A legtöbb ablakos rendszert elsősorban a felhasználói reakciók fogadására és lekezelésére készítették fel és ez persze kihat az újrarajzolás és frissítés megvalósítására is. Egyszóval az OnPaint metódust nem arra tervezték, hogy folyamatosan hívogassuk és így valósítsuk meg a Run() metódust. Pedig a DirectX azt szereti, ha az adatok egyfolytában rendelkezésre állnak mivel a jelenetet folyamatosan megjeleníti függetlenül attól, hogy változott-e valami a színtérben vagy sem. A lényeg a folyamatosság és a gyorsaság! Ezt az ellentmondást nehéz feloldani. Az egyik lehetséges megoldás az, ha közvetlenül érjük el a Win32 szolgáltatásokat a menedzselt oldal teljes megkerülésével és így valósítjuk meg a Run() metódust. Szerencsére erre a SlimDX tartalmaz egy vékony wrappert amit a MessagePump osztályban találhatunk. Használata rendkívül egyszerű. Nyissuk meg a Program.cs fájlt és a Main() metódusban helyezzük el a következő sorokat:

var form = new MainForm();
MessagePump.Run(form, form.Render);

A MessagePump Run metódusának át kell adni az ablakunk egy példányát majd azt a metódust, ami a megjelenítést megvalósítja. Ezzel készen is vagyunk. Ha az ablakunkat bezárjuk illik felszabadítani (explicite) a létrehozott Direct3D és Device objektumokat, amit a következő megoldással automatikusan meg is tehetünk:

foreach
(var item in ObjectTable.Objects)
{ item.Dispose(); }

Ezzel meg is volnánk. A fenti megoldás egyszerű, gyors és takarékos, ráadásul kiválóan működik. Felmerülhet a kérdés, hogy mi is van a MessagePump osztályban? A "trükköt" az ötlet kitalálójától érdemes elolvasni amit meg is tehetünk itt: Tom Miller's Blog

Amikor az ablak mérete megváltozik…

Ha ablakos alkalmazást készítünk, akkor számolnunk kell azzal, hogy a felhasználó átméretezi az ablakot. Ez természetes, mi ezzel a gond? Csupán az, hogy a hátsó puffer méreteit az ablak méreteihez igazítottuk mivel arra rajzolunk. Amikor viszont az ablak mérete megváltozik, az nem vonja magával a hátsó puffer automatikus megváltoztatását így elég furcsa kép szörnyed ránk. A megoldás az, hogy ha az ablak mérete változik, újra beállítjuk a hátsó puffer szélesség-magasság tulajdonságait majd a DirectX eszközt reseteljük az új beállításokkal:
        protected override void OnResize(EventArgs e)
{
if (this.WindowState == FormWindowState.Minimized || isFullScreen == true) return;

OnResourceLoad();

if (device != null)
{
presentParameters.BackBufferWidth = this.ClientSize.Width;
presentParameters.BackBufferHeight = this.ClientSize.Height;

device.Reset(presentParameters);
}
}
Az ablakos és a teljes képernyős üzemmód között az ALT+ENTER billentyűkombináció segítségével válthatunk. A megvalósítás nem bonyolult mindenki implementálhatja maga is a fenti leírás alapján. A példa teljes forrása itt tölthető le:


És itt a végeredmény, amikor is megpendítjük a DirectX szörny bajszát a .NET Framework biztonsági ketrece mögül:

Amikor a SlimDX munkába lendül

Lassan végére érünk ennek a bejegyzésnek is. Sok mindennel nem foglalkoztunk, legfőbbként a DirectX eszköz elvesztésének esetével és annak lekezelésével, de majd mindenre sor kerül idővel. Remélem tudtam segíteni kicsit az elinduláshoz és itt meg kell jegyeznem, hogy Nyisztor Károly könyvei is sokat segítettek a bejegyzés írása közben.