Miért probléma a shader fordítás PC-n?

A hagyományos és az explicit grafikus API-kkal sem volt jó a helyzet, holott konzolokon vajsima a kapott élmény.

Shader fordítás régen

Ha kimondjuk azt a szót, hogy shader, valószínűleg sokan a grafikára gondolnak, és teszik ezt jó okkal, ugyanakkor maguk a shaderek olyan programok, amelyek egy adott probléma megoldását célozzák a grafikus vezérlőn belül. Régebben ezeket kizárólag arra használták a fejlesztők, hogy saját transzformációs és megvilágítási modelleket írjanak, de a hardverek, illetve nem elhanyagolható módon a szoftverkörnyezet fejlődésének hála mára már általános feladatok elvégzésére is jók.

Egy ideje viszont téma PC-s körökben, hogy a shader fordítás nem túl jól megoldott, és erre időnként jön még egy-egy játék, amely akadozó működésével felerősíti a vitát. De honnan ered ez a probléma? Miért nem oldották meg az explicit API-k? Mi kellene ahhoz, hogy működjön a rendszer? Csupa kérdés, amire nem igazán ad senki választ, és különösen bosszantó lehet a méregdrága hardvereket vásárló PC-seknek, hogy konzolon ez a jelenség nem is létezik, ott vajsima élményt lehet kapni ebből a szempontból. Jelen cikkben megpróbáljuk megválaszolni, hogy mitől is történik mindez, illetve felvázolunk egy potenciális megoldást, ami a PC-t is megmentené az akadozásoktól.

Mielőtt azonban belevágnánk, érdemes kiemelni, hogy a hagyományos API-k esetében keletkező kisebb akadások nem ugyanattól vannak, mint amik az explicit API-kat sújtják. Ahhoz, hogy ezt jobban megértsük, látni kell, hogy a régebbi API-k hogyan működtek. Az OpenGL-t itt különösebben nem vizsgálnánk, mert az a shader fordítások szempontjából maga volt a katasztrófa. Nagyon sokáig a rendszer magát a shaderek forrásának szállítását követelte meg a fejlesztők részéről, amiből aztán nemhogy gyors kódot nem lehetett valós időben fordítani, de sokszor hibátlant sem, ugyanis nem volt elég egyértelmű a specifikáció az egyes esetek kezelését tekintve. Ennek eredménye az lett, hogy egy program akár ugyanazt a shader többféle gyártóhoz is hozzáigazította, és csak imádkozhattak a fejlesztők, hogy az érintett cégek ne módosítsanak shader fordítójukon, mert ha megtették, akkor onnantól kezdve volt némi esélye annak, hogy meszeltek a programfuttatásnak.

Az OpenGL ilyen szempontból játékra teljesen alkalmatlan API volt, és ezen már az sem segített, hogy időközben a Khronos Group elintézte hozzá a SPIR-V támogatását, addigra ugyanis a játékfejlesztők túlléptek ezen a poklon.

A DirectX 11 – továbbá számos korábbi verzió – már sokkal átgondoltabb rendszer volt a shader fordításokat tekintve, ugyanis a Microsoft nem engedte meg a fejlesztőknek a magas szintű HLSL kódok szállítását, ezeket le kellett fordítani úgynevezett D3D bájtkódra, kevésbé ismert néven DXBC-re. Ezt a műveletet az fxc fordítóval lehetett elvégezni, és ez egy nagyon-nagyon lassú procedúra volt, mivel a kód optimalizálása is nagyrészt ezen a ponton történt meg.

A shader fordítás vázlata DirectX 11 API-val
A shader fordítás vázlata DirectX 11 API-val [+]

Maga a D3D bájtkód egy viszonylag egyszerű fájlként jellemezhető, amely logikailag három nagyobb részre osztható. A fő adatokat a kódtörzs szolgáltatja, itt van lényegében minden olyan kód, ami magához az algoritmushoz kapcsolódik, és ennek van egy elő-, illetve egy utótagja. Maga a kódtörzs úgy lett tervezve, hogy utólag ne nagyon lehessen változtatni rajta valós időben, de az elő- és utótag már könnyen frissíthető, és ezen a ponton a grafikus eszközillesztők shader fordítói élnek különböző specifikus kiegészítésekkel, amelyek igazodnak a kódtörzsben elemzett adatokhoz, megfelelő módon fordítva le magát a D3D bájtkódot az adott grafikus vezérlő által emészthető formába. Utóbbi valós időben történik, és nem is túlságosan olcsó művelet, de valamelyik grafikus kernel szál élvégzi a munkát a processzor segítségével.

Mondhatjuk azt, hogy ez az egész átgondoltnak tűnik, de közel sem az. A Directx 11-ben rengeteg objektum volt megkülönböztetve, amelyeket ugyan a D3D bájtkód elő- és utótagjának frissítése által futtatási időben jól tudott kezelni a grafikus eszközillesztő, de ezek az aprócska műveletek rengeteg API hívást eredményeztek, amelyek az esetek jó részében meg is akasztották a munkavégzést, akadályozva az alkalmazás skálázását a sok maggal rendelkező processzorokon. Ráadásul annak ellenére, hogy az fxc fordító igen sokáig generálta a D3D bájtkódot, nem volt garancia arra, hogy az adott hardveren optimálisan fog futni, és időnként akár a lefordított és leszállított shadereket is lecserélték a gyártók a meghajtóból, hogy jobb legyen a sebesség. Ezek mind olyan tényezők voltak, amelyeket senki nem akart látni, mert alapvetően sok hibára adott lehetőséget, illetve az elő- és utótag frissítése, valamint a D3D bájtkódok hardver számára értelmezhető kódra való lefordítása is sok processzoridőt vett igénybe, nem kevésszer akasztva meg rövid időkre az elméletben folyamatosra tervezett játékélményt. És ezzel sajnos utólag semmit sem lehetett tenni, maga a rendszer volt így tervezve.

Shader fordítás most

A régebbi fordítási modell tehát semmiképpen sem működött, így új megoldás felé nézett az ipar. A DirectX 12-ben a Microsoft ki is dolgozott egy alternatív megoldást, amit aztán a Vulkan API is bevetett. Utóbbit nem elemezzük külön, a lényeg annyi, hogy a koncepció rendkívül hasonló, tehát ebből a szempontból a Microsoft és a Khronos Group is ugyanúgy látta a jövőt. A különbség az eltérő köztes nyelvből eredt, mivel előbbi a DXIL-t, míg utóbbi a SPIR-V-t hozta, de a lényegük ugyanaz.

A DirectX 12 legnagyobb újítása, hogy leszámolt a töménytelen mennyiségű objektummal, helyettük csak egy van: a Pipeline State Object (PSO). Ehhez kell nyúlni minden shadernél, minden futószalaglépcsőn, és ez kerül lefordításra egy bináris nagyobjektummá. Az erőforrásokat is egyszerűen a leírótáblákba kell bekötni, ami a processzor oldalán is nagyszerű, hiszen nagyon csekély mennyiségű API hívás lesz, ez pedig kíméli a processzoridőt.

Ez a fajta rendszer az optimalizálást is erőteljesen átalakítja, ugyanis az már nem a magas szintű shader kód köztes nyelvre való lefordításakor történik meg, hanem a PSO generálásakor. Ez azért hasznos, mert a hagyományos API-k esetében nagyon sok információt nem tudott maga a program, így csak egy magas szintű kódot kapott a Microsoft fxc fordítója, amiből készített egy alacsonyabb szintűt. De itt még közel sem volt optimális a helyzet, mivel a megfelelő optimalizáláshoz tudni kellett az objektumok használatakor a lehető legtöbb hardverállapotot, amit DirectX 11-ben egyszerűen nem lehetett előre látni pusztán a magas szintű kód alapján. A PSO generálásakor viszont már megvannak ezek az információk, vagyis a rendszer képes hatékonyabban optimalizálni, és ezt kihasználják például a meghajtók shader fordítói is, ugyanis számos olyan extra adatuk van, amivel a hagyományos API-knál nem rendelkeztek. Ilyen formában nagyrészt hasztalanná válik az egyes shaderek utólagos cseréje, hiszen az explicit API-k működésével általánosan is lereagálhatók a körülmények, fel lehet ismerni az egyes helyzeteket, amelyekre a meghajtó az előre beprogramozott választ tudja adni, jobb kódot biztosítva az adott PSO-khoz.

A shader fordítás vázlata DirectX 12 API-val
A shader fordítás vázlata DirectX 12 API-val [+]

Mindez szép és jó, de van vele egy aprócska probléma. Amíg a korábbi, DirectX 11 által alkalmazott modellben az optimalizálás előre megtörtént, addig erre itt a futtatási időben van szükség. Persze itt nagyon jó hír, hogy maga a shader fordítás extrém gyors lett. A korábbi fxc fordító szépen kihasználta az idejét a fejlesztők gépén, egy-egy shader fordításánál nyugodtan el lehetett menni kávézni, akár a város másik végén fekvő boltba is be lehetett ugrani egy csirkéért, és visszatérve még egy pörkölt megfőzése is belefért. Ez azért volt ennyire hosszadalmas, mert maga az optimalizálás a fordítás során történt meg a fejlesztők PC-jén. Az új DirectX Shader Compiler már több nagyságrenddel gyorsabb, még a kávéval is igyekezni kell, nemhogy főzésen járjon az eszünk. Ennek oka, hogy az optimalizálás a programmal szállított köztes kód kapcsán csak részben történik meg, a további finomhangolás majd futtatási időben, a shaderek végleges fordításánál lesz végrehajtva, amikor az adott grafikus vezérlő számára emészthető kód készül.

Itt viszont már nem fér bele órákig történő elmélkedés a fordító oldalán, jó esetben ezredmásodperceken belül kell az eredmény, ami persze komplexebb shaderek mellett nem lesz meg. Emiatt az egyes explicit API-t használó játékok az indításnál előre lefordítják a shadereket, ami gyengébb processzorral negyed órát vehet igénybe. Ez túlélhető, de mivel az egyes PSO-k generálása során a meghajtó is számos optimalizálással él, a keletkező kódok specifikusan az adott eszközillesztőhöz szabottak. Vagyis, ha az alkalmazás frissül vagy a felhasználó új meghajtóprogramot telepít, akkor újra le kell fordítani a shadereket.

Persze a shader fordítás történhet a tényleges játék során, vagyis amikor épp szükség van az egyes kódokra, akkor történik meg azok generálása és optimalizálása, ezáltal viszont van esélye annak, hogy akadozó játékélmény lesz a végkifejlet, amíg a fordítás be nem fejeződik. Ráadásul erre is igaz az, hogy ha frissül az alkalmazás vagy új meghajtó települ, akkor kezdődik az egész elölről.

Jó, de mi a megoldás?

Jelenleg ez a millió dolláros kérdés. Világos, hogy a DirectX 11, illetve a korábbi hagyományos API-k modellje nem volt jó, de a DirectX 12 és a Vulkan esetében is az mondható el, hogy a működésen ugyan javítottak, de nem annyit, amennyit talán kellett volna. Alapvetően az sem jó, ha a shaderek az első indításnál lesznek lefordítva, de az sem, ha ténylegesen a játék közben. Ráadásul ha ezen át is esik a felhasználó, akkor is ott van az a gond, hogy az egész kezdődik elölről az adott program, illetve a meghajtó frissítésekor.

Az egyik potenciális megoldás, amivel kíséreteznek az érintettek, az a felhasználói gyorsítótárak megosztása. Tudni kell, hogy egy adott grafikus vezérlőre levetítve, az adott programverzióval, illetve eszközillesztővel mindig ugyanolyan shaderek készülnek. Tehát, ha a programkód, a grafikus meghajtó, illetve a GPU megegyezik két gép esetén, akkor az egyik rendszer által generált állományok, vagyis a lefordított és gyorsítótárazott shaderek átmásolhatók a másikra. Meg lehet tehát azt tenni, hogy egy adott szoftver- és hardverkonfigurációt tekintve csak az első felhasználó essen át a kellemetlen élményen, és a gépe által előállított adatok mindenki másnak elérhetők lehetnek egy szerverre feltöltve.

Ennél a módszernél persze gondolni kell arra, hogy jogi értelemben a lefordított shaderek személyes adatnak számítanak, még akkor is, ha nincs bennük amúgy semmi személyhez köthető információ. A játékosoknak tehát az egyes platformokon vagy a megvásárolt játékhoz kapcsolódó felhasználói licencszerződésben vállalniuk kell ezek megosztását. És ha ez megvan, akkor is biztosítania kell a program fejlesztőjének és kiadójának a megfelelő adatkezelést, ezek esetleges kérésre történő törlését, tehát mindent, amit a fejletebb államokban a jogszabályok előírnak.

Technikailag tehát adja magát az ötlet, hogy legyen a PC-s shader fordítás közösségi szintre emelve, de jogi értelemben ez sem egy egyszerű kérdéskor.

Na de a konzolok...

Az előbbiek alapján kiderülhetett, hogy akármerre lépünk, csak problémákba ütközünk, és akinek van konzolja, most felkiált, hogy márpedig ilyen jellegű akadások ezeken a gépeken nincsenek. Újabb kérdés merülhet fel: miért? Nos, ennek az oka az, hogy a konzolok fix hardverek, vagyis egy adott gépnek olyan fix specifikációja van, ami a teljes életciklusa alatt sem változik meg. Emiatt a fejlesztők itt nem is gondolkodnak köztes nyelveken vagy futtatási időben történő optimalizáláson, egyszerűen az adott shaderből előre lefordítanak egy olyan bináris állományt, ami helyből ráereszthető a grafikus vezérlőre. A program működését tekintve a fejlesztők eleve tudnak mindent arról, hogy mi várható a hardver oldalán, tehát nagyon jól lehet gépi kódra optimalizálni, és ez garantált, hogy az adott konzolon mindig futni fog, sőt, újabban már a visszafelé kompatibilitásra is figyel a Microsoft és a Sony, így akár a később érkező generációk is célozhatók lehetnek.

Látható tehát, hogy konzolokon a helyzet összehasonlíthatatlanul egyszerűbb, mint PC-ken. De működhetne ez a modell a személyi számítógépeken is? Papíron igen, de a gyakorlatban inkább nem. Az a gond, hogy a grafikus vezérők utasításarchitektúrája folyamatosan változik. Leginkább a memóriamodellhez nyúlnak hozzá a gyártók, mivel ez biztosítja a skálázhatóságot, vagyis ha PC-re lefordítanák a shadereket bináris állományba, akkor az működne a célzott hardvereken, de semmi máson. És ebbe beleértendők a később érkező grafikus vezérlők is. Ez a módszer tehát nem játszik, hiszen alapvetően fontos a fejlesztőknek, hogy ha megírnak egy programot, akkor azt több generáción át is futtatni tudják a felhasználók a PC-ken. Az egyszerűen nem felvállalható, hogy valaki megvesz egy játékot, és jön egy újabb VGA, amin az már nem indítható el, legalábbis addig, amíg le nem fordítják az utasításarchitektúrájára a shadereket.

Mi lenne az ideális alternatíva?

Az előző oldalak után azért felvázolunk egy olyan optimális megoldást, ami PC-n is működhetne, és közel azt az élményt adná, amit konzolon kap a felhasználó, ráadásul mindenféle technikai és jogi akadály nélkül.

Valószínűleg kevesen tudják, de egy grafikus API már megoldotta a shader fordítást PC-n. Ugyan az AMD már nem engedi az előző évtizedben bemutatott Mantle használatát, arra azért emlékezhetnek a felhasználók, hogy ezek a gondok az említett gyártóspecifikus explicit API-val nem voltak ennyire markánsak. Ennek az oka, hogy a Mantle-höz használt shaderek fordítása a magas szintű HLSL Ext. (módosított HLSL specifikáció) kódokból valósult meg, és a fordító közvetlenül úgynevezett AMDIL kódokat generált, ami az AMD által tervezett grafikus vezérlők virtuális utasításarchitektúrája. Eredetileg a Mantle még megkövetelte a magas szintű kódok szállítását, és az egyes pályaszakaszok betöltésekor történt meg maga a fordítás, de később már maga az AMDIL kód is mellékelhető volt az alkalmazások mellé, így a megnövekedett töltési időket is sikerült redukálni. Az eredmény pedig shader fordítások okozta akadásoktól mentes játékélmény volt.

A kérdés az, hogy lehetne-e a Mantle shader fordításra vonatkozó működéséből egy szabványos irányzat? Technikailag igen, mert minden grafikus vezérlőnél alkalmaznak a gyártók egy olyan virtuális utasításarchitektúrát, amit a különböző kódokkal lehet célozni, és az erre generált assembly jellegű kódból már elég könnyű és olcsó a valós utasításarchitektúra által emészthető kódot generálni. Ráadásul ezt nem véletlenül csinálja így minden érintett, ugyanis még egy adott cégen belül is elmondható, hogy generációnként változik az utasításarchitektúra, vagyis nem lenne célszerű ezeket egyenként kezelni, sokkal hasznosabb, ha van az összes támogatott hardver fölé húzva egy virtuális réteg, amiből aztán majd etethető az adott rendszer.

PC-s szinten tehát már most elmondható, hogy az AMD-nek, az Intelnek és az NVIDIA-nak is van saját virtuális utasításarchitektúrája, amelyek egyébként folyamatosan fejlődnek, de a szoftverek felé a kompatibilitást megtartják, míg a ténylegesen kiadott hardverek a gyártói virtuális utasításarchitektúra felé maradnak kompatibilisek. Ebből ugye megvan az a híd, ami miatt egy szabványosan megírt program fut a meglévő, illetve futni fog a jövőben érkező grafikus vezérlőkön.

Lehetne ugyanakkor ezt sokkal jobban is csinálni. Az biztos, hogy a hardverek tényleges utasításarchitektúráját tekintve sosem lesz kompatibilitás a gyártók között, ezt ugye a cégek generációról generációra sem tudják garantálni házon belül, tehát irreális elvárás lenne azt kérni, hogy alkalmazkodjanak egymáshoz. De a virtuális utasításarchitektúra egészen más téma, ebből a szempontból lehetne megegyezés. Mi több, van is erre iparági példa, hiszen a HSAIL még az előző évtizedben megszületett, és azóta is van gyártói oldalon támogatottsága, elvégre bizonyos cégek részben leváltották erre saját virtuális utasításarchitektúrájukat. PC-n belül az AMD az egyes API-khoz AMDIL-t, másokhoz pedig HSAIL-t használ, míg Androidon a MediaTek, a Qualcomm, a Samsung, az ARM és az Imagination is alkalmazza utóbbit, főleg a gépi tanulással kapcsolatos feladatokhoz.

A teljes iparágon belül tehát van némi akarat arra, hogy értékelhetően működjenek ezek a dolgok, csak nem mindenki szeretne közösködni, és a PC szempontjából különösen hátrányos, hogy az Intel és az NVIDIA nem akar egységes virtuális utasításarchitektúrát. Pedig utóbbiból nem származna hátrányuk, hiszen láthatóan hat cég képes használni ugyanazt HSAIL-t, és ebből nincs egyik érintettnek sem teljesítményproblémája. Ráadásul a megegyezés szempontjából elég szabadon lehet gondolkodni, ha esetleg a HSAIL nem felel meg, akkor lehet csinálni egy ugyanilyen alternatívát, ami pont ugyanúgy működik, pont ugyanazokat a célokat szolgálja, csak ez időbe kerül, míg a HSAIL már kész van és a gyakorlatban is működik.

Valószínűleg nem azzal van a gond, hogy ne látná az iparág ezt a megoldást, inkább ott kerül homokszem a gépezetbe, hogy bizonyos cégek elvből nem akarnak közös virtuális utasításarchitektúrát. Ilyen szempontból mindegy, hogy létezik a HSAIL, akár készülhetne tucatnyi hasonló alternatíva is, pusztán az akarat hiányzik a támogatáshoz, és ennek hiányában maradnak a PC-s problémák a shader fordítással kapcsolatban. Márpedig amíg egyetlen egy, PC-piacot kiszolgáló cég is azt gondolja, hogy ezt a gondot nem kell megoldani, vagyis jó az, ha egy játékos hazaviszi a közel egymillió forintért vásárolt csúcs-VGA-ját, amely egy nagyon várt játékban akár jó ideig durván akadozó élményt ad a shader fordítások miatt, addig igazából a változás halvány szikrája sem fog felcsillanni. De ezt a gondolatmenetet akár meg is fordíthatjuk úgy, hogy amíg egy játékos hajlandó közel egymillió forintért csúcs-VGA-t vásárolni, hogy aztán egy nagyon várt címben csak akadásokat kapjon egy jó darabig, akkor nincs itt semmi gond, az érintettek felé az az üzenet megy, hogy ők mindent jól csinálnak, hiszen az eladások megtörténnek. Persze a PC-s fórumokra befutnak majd az üzenetek, hogy milyen rosszul van összerakva a PC-s port, mert konzolon nincsenek meg ugyanazok a teljesítményproblémák, csak éppen ez nem ilyen egyszerű, valójában a PC-re nem is lehet úgy összerakni a működést, mint konzolra. Pontosabban elméletben megoldható lenne, ha a gyártók csapatjátékosok lennének, de egyelőre nem azok.

Abu85

Azóta történt

Előzmények

Hirdetés