Belefogtam egy NES emulátor leprogramozásába – ha lesz időm és kedvem, itt egy fejlesztői naplót fogok vezetni. Ezek a bejegyzések inkább ilyen vázlatszerű valamik lesznek, ezért is nem cikket írok.
Szabadidőmben gyakran játszom a PC-men konzolos játékokkal, természetesen valamilyen emulátort használva. Ezek főleg PSX, vagy PS2 játékok, ritkábban SNES, vagy NES. Programozó tanoncként mindig csodálattal néztem ezekre a programokra, hogy lehetővé teszik egy teljesen más architektúrára írt program futtatását, többnyire játszható sebességgel. Mivel szeretek új dolgokat tanulni, régóta érlelődik bennem a gondolat, hogy írok egy saját emulátort. A probléma ott volt, hogy sem a tudásom nem éreztem elegendőnek egy ilyen munkához, sem annyi szabadidőm nem volt, hogy a kutatómunka és a fejlesztés is beleférjen. Szintén hatalmas problémának éreztem, hogy csak nagyon felületesen tudtam, hogy működik a „vas”. Bár az egyetemen, a szakomon (programtervező informatikus) volt ilyen jellegű előadás, az csak arra volt elég, hogy tudjam, hogy van olyan hogy regiszter, vagy Stack Pointer, de arról csak homályos elképzelésem volt, hogy a bitek sorozatából, hogyan tudja a processzor, hogy milyen utasításokat kell végrehajtania. Tény, hogy a magas szintű nyelvek korában erre egyre kevésbé van szükség, amikor már a mikrokontrollereket is egyre gyakrabban programozzák C-ben.
Úgy alakult, hogy ebben a vizsgaidőszakban rengeteg szabadidőm volt, mivel a tárgyaim nagy része már megvan, illetve ebben a félévben főleg gyakorlati jegyes óráim voltak. Miután ráuntam a mostani játékokra, újból elővettem az ePsxe nevű PSX emulátort, és újra elkapott az érzés, hogy nekem tudnom kell, hogy ez hogy működik. Persze én is tudtam, hogy nagy fába vágom a fejszémet, ezért úgy gondoltam, hogy egy egyszerűbbel kell kezdenem. Gyerekkoromban, öcsémmel rengeteg időt ütöttünk el a TV előtt ülve, videojátékokkal játszva. Ez akkoriban az itthon csak „sárgakazettás nintendónak” nevezett szerkezetet jelentette. Arra számítottam, hogy ez már viszonylag egyszerű lesz. Pár órás kutatómunka után kiderült, hogy tévedtem.
Általánosan elmondhatjuk, hogy egy processzor működése az alábbi lépésekből áll:
• Utasítás lehívás (Fetch) – Ekkor a memóriából lehívjuk a programszámláló által mutatott utasítást
• Dekódolás – Azonosítjuk a lehívott utasítást
• Végrehajtás – A dekódolt utasításnak megfelelő műveleteket hajtjuk végre (Itt előfordulhat, hogy a memóriából további operandusokat kell lehívni.)
Ezeket a lépéseket kell megvalósítanunk. Egy emulátor feladata, hogy a futtatandó programnak olyan környezetet nyújtson, mintha az eredeti rendszeren futna. Erre többféle megközelítés létezik. Egy emulátor elkészítése során két fontos szempont van: a pontosság és a sebesség. Bár mindkettő egyformán fontos, gyakran ezek egymás ellentétei.
Ahogy említettem több megközelítés is létezik egy emulátor implementációjára. Az egyik legegyszerűbb megközelítés az interpreter. Egy ilyen program nem csinál mást, mint nyílván tartja az adott architektúra regisztereit és állapotát. A memóriából mindig kiolvassa a soron következő utasítást, majd végrehajtja az adott műveletet frissítve a megfelelő flag-eket, regisztereket és a memória tartalmát, majd lekéri a következő utasítást és minden kezdődik az elejéről.
Egy másik megközelítés a Dynamic Recompilation-nek, Dynarec-nek, vagy Binary translation-nak nevezett megoldás. Ekkor nem utasításonként történik a végrehajtás, hanem, az eredeti programkódot blokkokra tördelve, (általában az ugróutasítások, elágazások mentén) a célrendszer számára értelmezhető gépi kódra való átfordítva futás időben történik az emuláció. Ez gyakorlatilag egy JIT-telt megközelítés. Nyilvánvaló, hogy ennél a módszernél elengedhetetlen a forrás és célrendszer assembly kódjának átfogó ismerete, hogy hatékony kód készülhessen.
Az interpreter-ek előnye az egyszerűségük, és könnyű portolhatóságuk, illetve pontosságuk cserébe nagyon lassúak egy dynarec-es megoldáshoz képest. Egy dynarec megoldás bár gyors, csak nehezen portolható és a pontosságot is nehéz fenntartani. Ez főleg az időzítések betartásánál jelentkezhet. Ezen okokból én egy interpreter leprogramozása mellett döntöttem.
Kutatásaim során rátaláltam egy Chip8 nevezetű platformra, ami kvázi a kezdő emulátorprogramozók első lépése. Úgy döntöttem, hogy kiváló tanulóprojekt lesz, egy CHIP 8 emulátor elkészítése, mielőtt valami komolyabba kezdek. A CHIP 8 egy interpretált programnyelv, amit főleg a 70-es évek közepén használtak, főleg játékok elkészítésére. Mivel interpretált nyelvről beszélünk, elég volt egyszer megírni az adott programokat, és bármilyen platformon futottak, amihez volt Chip 8 értelmező.
Ez egy nagyon egyszerű platform, összesen 4096 B memóriával (Az első 512 byte az eredeti rendszereken az interpreter számára volt fenntartva, a mai emulátorok ezt karaktermemóriának tartják fenn).
Rendelkezésre áll 16 darab 8 bites regiszter – V0-VF (A VF Carry flagként funkcionál). Ezen felül van még egy 16 bites I nevű regiszterünk, ami főleg a grafikával kapcsolatos műveleteknél van használva. A stack maximum 16 mélységű. Természetesen van egy stack pointerünk (SP), illetve program számlálónk (PC). Két timer is jelen van: a delay timer az időzítésekért felel, illetve egy audio timer. Mindkettő 60Hz-es időzítéssel csökkenti az értékét. Ha az audio lejár, akkor a rendszer hangjelzést ad.
A bevitelről az eredeti rendszeren egy 16 gombos hex billentyűzet szolgált, így összesen 16 gomb áll rendelkezésre.
A megjelenítés egy 64*32-es kijelzőn történik, színből csak 2 van: fekete és fehér. A kirajzolás sprite alapú, 8bit széles és 1 és 15 pixel közötti magasságú alakzatokat rajzolhatunk.
Viszonylag kevés utasítást (opkódot) kell értelmeznünk, összesen 35 darab van. (Összehasonlításként: a NES-ben található 6502 variáns 154 opkódot ismer, illetve még néhány nem dokumentáltat.) A chip8-ban az összes utasítás 2 byte-os, ez magában foglalja a műveletekhez tartozó operandusokat is. A szokványos ugró utasításokról, elágazásokról, műveletekről beszélhetünk. Mivel ezek az utasítások 2 byte hosszúak, ezért 4 db 16-os számrendszerbeli számjeggyel írhatóak le. Például az ugróutasítás opkódja 1NNN formátumú, ahol az NNN egy memóriacím. (Ez 12 bit, pontosan 4096 byte megcímzésére alkalmas – ennyi memóriánk van).
Az opkódok könnyen értelmezhetőek, viszont mivel össze vannak vonva az utasítások a hozzájuk tartozó paraméterekkel, azokat mindig helyben kell dekódolni. Az opkódtáblázatban a következő jelölések fordulhatnak elő:
• NNN: 12 bites cím
• NN: 8-bites konstans
• N: 4-bites konstans
• X és Y: 4-bites register azonosító.
Az interneten rengeteg leírás található meg az egyes kódok jelentéséről, illetve működéséről. Talán az egyik legjobb forrás itt található, illetve a kapcsolódó Wikipedia szócikk is sok segítséget nyújt.
Az emulátorom működése a lehető legegyszerűbb módon történik. Első lépésként betölti a programot a memóriába az 512-es byte-tól kezdődően (0x200), a programszámlálót ennek megfelelően is erre a címre állítja. Ezután 2 byte-ot kiolvas a memóriából (1 Opkód), majd értelmezi, hogy melyik utasítást kell végrehajtania. Ez egy egyszerű (baromi hosszú) swich-case szerkezet. Különböző bitmaszkok segítségével a megfelelő helyre ugrik a szerkezetben, végrehajtja az adott utasítást, majd növeli a programszámlálót. (Ugró utasításnál pedig a megfelelő helyre állítja azt.) Ez nem a legszebb megoldás, de ilyen kevés opkód mellett a legkönnyebben leprogramozható.
Ahogy említettem, a grafika sprite alapú. A képet egy külön 64*32-es tömbbe mentem ki. A kirajzolandó sprite-ot össze kell XOR-ozni a jelenlegi képpel. Ennek a tömbnek a tartalmát az OpenGl API segítségével rajzolom ki. Mivel a cél az volt, hogy „csak működjön”, ezért az 1.0-ás verzió immediate módját használtam csak fel. Bár az API ezen része hivatalosan már deprecated, arra jó volt, hogy gyorsan össze tudjam dobni az egész rajzolást. A rajzolás kódja kb 20 sor. A végeredmény az alábbi képen jól látható:
Alapvetően elmondhatom, hogy a projekt első lépése sikeres volt. Sikerült bebizonyítani magamnak, hogy képes vagyok összehozni egy emulátort. A folyamat során sokat tanultam a számítógépek belső működéséről, a programok végrehajtásának módjáról. Végre a gyakorlatban is használtam a különböző bitműveleteket. Bár még lenne mit javítani a program jelenlegi állapotán, ugyanis jelenleg nem minden programot tud futtatni, és bizonyos esetekben hajlamos az összeomlásra is, a célját elérte. Elkezdem implementálni a NES emulátoromat.