{progz}

Fixpontos számolás N-Gage-en: miért nincs float a játékciklusban

Az N-Gage és az N-Gage QD egy 104 MHz-es ARM920T processzoron fut, amelyben nincs lebegőpontos egység. Ez a C++ kódban nem látszik: a float és a double ugyanúgy lefordul, csak éppen minden szorzásból egy könyvtári függvényhívás lesz, amely egész műveletekkel utánozza a lebegőpontos számolást. Egy játékciklusban, ahol képkockánként több száz pozíciót és sebességet kell frissíteni, ez gyorsan elviszi a képkockasebességet.

A megoldás régi, a konzolos és PC-s játékfejlesztők a kilencvenes években is ezt használták: a fixpontos számolás.

A 16.16-os formátum

Egy 32 bites egész felső 16 bitje az egészrész, az alsó 16 bitje a törtrész. Az 1.0 tehát 65536, a 0.5 pedig 32768.

typedef TInt TFix;              // 16.16 fixpontos szám
const TInt KFixShift = 16;
const TFix KFixOne   = 1 << KFixShift;

inline TFix IntToFix(TInt a)    { return a << KFixShift; }
inline TInt FixToInt(TFix a)    { return a >> KFixShift; }

// Összeadás és kivonás: a sima egész művelet pont jó.
// Szorzás: az eredmény 32.32 lenne, ami nem fér el 32 biten.
inline TFix FixMulFast(TFix a, TFix b)
    {
    return (a >> 8) * (b >> 8);   // mindkettőből 8 bit pontosságot dobunk el
    }

A FixMulFast a játékokban bevett trükk. Mindkét tényezőt 8 bittel jobbra tolja, így a szorzat visszakerül a 16.16-os skálára, és a köztes eredmény 32 biten marad, amíg a két érték szorzata abszolút értékben 32 768 alatt van. Ára a pontosság: a törtrészből csak 8 bit marad. Képernyőkoordinátákhoz és sebességhez ez bőven elég, egy fizikai szimulációhoz már nem.

Szinusz táblából

A forgatáshoz szinusz és koszinusz kell. Futás közben ezeket nem számoljuk, hanem induláskor egyszer, egy táblába. A szög sem fokban van, hanem 256 lépésben egy teljes kör:

TFix iSinTable[256];   // induláskor egyszer töltjük fel

inline TFix FixSin(TInt aAngle) { return iSinTable[aAngle & 255]; }
inline TFix FixCos(TInt aAngle) { return iSinTable[(aAngle + 64) & 255]; }

// mozgás a szög irányába
iX += FixMulFast(FixCos(iAngle), iSpeed);
iY += FixMulFast(FixSin(iAngle), iSpeed);

Az & 255 miatt a körbefordulás ingyen van: a 256-os szög újra 0 lesz, a -1 pedig 255. Nincs szükség if-re és modulóra sem. A táblát egyszer, induláskor töltjük fel lebegőpontos számolással, és ez a betöltőképernyő alatt észre sem vehető.

Hol bukik el?

  • Osztás. Az ARM9-ben nincs hardveres osztás sem, tehát az egész osztás is lassú. Ahol lehet, szorozzunk reciprokkal, vagy toljunk biteket kettő hatványaival.
  • Túlcsordulás. Két 200-as érték szorzata a gyors változatban már túlcsordul. A pályakoordinátákat érdemes kisebb egységben tárolni, vagy pontosabb, lassabb szorzást használni ott, ahol kell.
  • Felhalmozódó hiba. Ha a pozíciót ezer képkockán át apró sebességekkel növeled, a 8 bites törtrész pontatlansága összeadódik. Ilyenkor a pozíciót tárold teljes 16.16-os pontossággal, és csak a sebességszorzást számold gyorsan.

Egy mérés a saját projektemből: ugyanaz a részecskeeffekt float-tal 9–10, fixpontosan 25 képkocka/másodperccel futott a QD-n. Az emulátorban mindkettő 60 fölött volt. Ezért kell mindig a készüléken mérni.